标签 Linux 下的文章

如何像gitlab-runner那样将Go应用安装为系统服务

本文永久链接 – https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner

《让reviewdog支持gitlab-push-commit,守住代码质量下限》一文中,gitlab-runner(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤):

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等,我们暂不care),系统服务就是由systemd管理的daemon process(守护进程)。

systemd是什么?linux主机上电后,os内核被加载并启动,os内核完成初始化以后,由内核第一个启动的程序是init程序,其PID(进程ID)为1,它为系统里所有进程的“祖先”,systemd便是主流新版linux中的那个init程序,它负责在主机启动后拉起所有安装为系统服务的程序

这些被systemd拉起的服务程序以守护进程(daemon process)的形式运行,那什么又是守护进程呢?《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》一书中是这样定义的:

Daemons are processes that live for a long time. They are often started when the system is bootstrapped and terminate only when the system is shut down. Because they don’t have a controlling terminal, we say that they run in the background. UNIX systems have numerous daemons that perform day-to-day activities.

守护进程是长期存在的进程。它们通常在系统启动时被启动,并在系统关闭时才终止。因为它们没有控制终端,我们说它们是在后台运行的。UNIX系统有许多执行日常活动的守护进程。

该书还提供了一个用户层应用程序将自己变为守护进程的标准步骤(编码规则(coding rules)),并给出了一个C语言示例:

#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void
daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    /*
     * Clear file creation mask.
     */
    umask(0);
    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can’t get file limit", cmd);
    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();

    /*
     * Ensure future opens won’t allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can’t ignore SIGHUP", cmd);
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    /*
     * Change the current working directory to the root so
     * we won’t prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can’t change directory to /", cmd);
    /*
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; i++)
        close(i);
    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}

那么,Go应用程序是否可以参考上面的转换步骤将自己转换为一个守护进程呢?很遗憾!Go团队说很难做到。Go社区倒是有很多第三方的方案,比如像go-daemon这样的第三方实现,不过我并没有验证过这些方案,不保证完全ok。

Go团队推荐通过像systemd这样的init system来实现Go程序的守护进程转换。gitlab-runner就是将自己安装为system服务,并由systemd对其进行管理的。

题外话:其实,自从有了容器技术(比如:docker)后,daemon service(守护进程服务)的需求似乎减少了。因为使用-d选项运行容器,应用本身就运行于后台,使用–restart=always/on-failure选项,容器引擎(比如docker engine)会帮我们管理service,并在service宕掉后重启service。

那么,我们如何像gitlab-runner那样将自己安装为一个systemd service呢?我们继续向下看。

注意:这里只是将Go应用安装成一个systemd service,并不是自己将自己转换为守护进程,安装为systemd service本身是可行的,也是安全的。

翻看gitlab-runner源码,你会发现gitlab-runner将自己安装为系统服务全依仗于github.com/kardianos/service这个Go包,这个包是Go标准库database包维护者之一Daniel Theophanes开源的系统服务操作包,该包屏蔽了os层的差异,为开发人员提供了相对简单的Service操作接口,包括下面这些控制动作:

// github.com/kardianos/service/blob/master/service.go
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}

好了,下面我们就用一个例子myapp来介绍一下如何利用kardianos/service包让你的Go应用具备将自己安装为system service的能力

myapp是一个http server,它在某个端口上提供服务,当收到请求时,返回”Welcome”字样的应答:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

func run(config string) error {
    ... ...

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("[%s]: receive a request from: %s\n", c.Server.Addr, r.RemoteAddr)
        w.Write([]byte("Welcome"))
    })
    fmt.Printf("listen on %s\n", c.Server.Addr)
    return http.ListenAndServe(c.Server.Addr, nil)
}

现在我们要为myapp增加一些能力,让它支持将自己安装为systemd service,并可以通过subcommand启动(start)、停止(stop)和卸载(uninstall)systemd service。

我们首先通过os包和flag包为该程序增加subcommand和其参数的解析能力。我们不使用第三方命令行参数解析包,只是用标准库的flag包。由于myapp支持subcommand,我们需要为每个带命令行参数的subcommand单独申请一个FlagSet实例,如下面代码中的installCommand和runCommand。每个subcommand的命令行参数也要绑定到各自subcommand对应的FlagSet实例上,比如下面代码init函数体中的内容。

另外由于使用了subcommand,默认的flag.Usage不再能满足我们的要求了,我们需要自己实现一个usage函数并赋值给flag.Usage:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

var (
    installCommand = flag.NewFlagSet("install", flag.ExitOnError)
    runCommand     = flag.NewFlagSet("run", flag.ExitOnError)
    user           string
    workingdir     string
    config         string
)

const (
    defaultConfig = "/etc/myapp/config.ini"
)

func usage() {
    s := `
USAGE:
   myapp command [command options] 

COMMANDS:
     install               install service
     uninstall             uninstall service
     start                 start service
     stop                  stop service
     run                   run service

OPTIONS:
     -config string
        config file of the service (default "/etc/myapp/config.ini")
     -user string
        user account to run the service
     -workingdir string
        working directory of the service`

    fmt.Println(s)
}

func init() {
    installCommand.StringVar(&user, "user", "", "user account to run the service")
    installCommand.StringVar(&workingdir, "workingdir", "", "working directory of the service")
    installCommand.StringVar(&config, "config", "/etc/myapp/config.ini", "config file of the service")
    runCommand.StringVar(&config, "config", defaultConfig, "config file of the service")
    flag.Usage = usage
}

func main() {
    var err error
    n := len(os.Args)
    if n <= 1 {
        fmt.Printf("invalid args\n")
        flag.Usage()
        return
    }

    subCmd := os.Args[1] // the second arg

    // get Config
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }
... ...
}

这些都完成后,我们在getServiceConfig函数中获取即将安装为systemd service的本服务的元配置信息:

// https://github.com/bigwhite/experiments/blob/master/system-service/config.go

func getServiceConfig(subCmd string) (*service.Config, error) {
    c := service.Config{
        Name:             "myApp",
        DisplayName:      "Go Daemon Service Demo",
        Description:      "This is a Go daemon service demo",
        Executable:       "/usr/local/bin/myapp",
        Dependencies:     []string{"After=network.target syslog.target"},
        WorkingDirectory: "",
        Option: service.KeyValue{
            "Restart": "always", // Restart=always
        },
    }   

    switch subCmd {
    case "install":
        installCommand.Parse(os.Args[2:])
        if user == "" {
            fmt.Printf("error: user should be provided when install service\n")
            return nil, errors.New("invalid user")
        }
        if workingdir == "" {
            fmt.Printf("error: workingdir should be provided when install service\n")
            return nil, errors.New("invalid workingdir")
        }
        c.UserName = user
        c.WorkingDirectory = workingdir

        // arguments
        // ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"
        c.Arguments = append(c.Arguments, "run", "-config", config)
    case "run":
        runCommand.Parse(os.Args[2:]) // parse config
    }   

    return &c, nil
}

这里要注意的是service.Config中的Option和Arguments,前者用于在systemd service unit配置文件中放置任意的键值对(比如这里的Restart=always),而Arguments则会被组成为ExecStart键的值,该值会在start service时传入使用。

接下来,我们便利用service包基于加载的Config创建操作服务的实例(srv),然后将它和subCommand一并传入runServiceControl实现对systemd service的控制(如下面代码)。

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go
func main() {

    // ... ...
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }

    prg := &NullService{}
    srv, err := service.New(prg, c)
    if err != nil {
        fmt.Printf("new service error: %s\n", err)
        return
    }

    err = runServiceControl(srv, subCmd)
    if err != nil {
        fmt.Printf("%s operation error: %s\n", subCmd, err)
        return
    }

    fmt.Printf("%s operation ok\n", subCmd)
    return
}

func runServiceControl(srv service.Service, subCmd string) error {
    switch subCmd {
    case "run":
        return run(config)
    default:
        return service.Control(srv, subCmd)
    }
}

好了,代码已经完成!现在让我们来验证一下myapp的能力。

我们先来完成编译和二进制程序的安装:

$make
go build -o myapp main.go config.go

$sudo make install
cp ./myapp /usr/local/bin
$sudo make install-cfg
mkdir -p /etc/myapp
cp ./config.ini /etc/myapp

接下来,我们就来将myapp安装为systemd的服务:

$sudo ./myapp install -user tonybai -workingdir /home/tonybai
install operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead)

我们看到安装后,myApp已经成为了myApp.service,并处于inactive状态,其systemd unit文件/etc/systemd/system/myApp.service内容如下:

$sudo cat /etc/systemd/system/myApp.service
[Unit]
Description=This is a Go daemon service demo
ConditionFileIsExecutable=/usr/local/bin/myapp

After=network.target syslog.target 

[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"

WorkingDirectory=/home/tonybai
User=tonybai

Restart=always

RestartSec=120
EnvironmentFile=-/etc/sysconfig/myApp

[Install]
WantedBy=multi-user.target

接下来,我们来启动一下该服务:

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 5s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.3M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432

我们看到myApp服务成功启动,并在65432这个端口上监听!

我们利用curl向这个端口发送一个请求:

$curl localhost:65432
Welcome                                                                         

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 1min 27s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348

我们看到myApp服务运行正常并返回预期应答结果。

现在我们利用stop subcommand停掉该服务:

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2022-09-09 23:33:03 CST; 3s ago
    Process: 623859 ExecStart=/usr/local/bin/myapp run -config /etc/myapp/config.ini (code=killed, signal=TERM)
   Main PID: 623859 (code=killed, signal=TERM)

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348
Sep 09 23:33:03 tonybai systemd[1]: Stopping This is a Go daemon service demo...
Sep 09 23:33:03 tonybai systemd[1]: myApp.service: Succeeded.
Sep 09 23:33:03 tonybai systemd[1]: Stopped This is a Go daemon service demo.

修改配置/etc/myapp/config.ini(将监听端口从65432改为65431),然后再重启该服务:

$sudo cat /etc/myapp/config.ini
[server]
addr=":65431"

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:34:38 CST; 3s ago
   Main PID: 624046 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─624046 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:34:38 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:34:38 tonybai myapp[624046]: listen on :65431

从systemd的状态日志中我们看到myApp服务启动成功,并改为监听65431端口,我们访问一下该端口:

$curl localhost:65431
Welcome                                                                                                                      

$curl localhost:65432
curl: (7) Failed to connect to localhost port 65432: Connection refused

从上述结果可以看出,我们的配置更新和重启都是成功的!

我们亦可以使用myapp的uninstall功能从systemd中卸载该服务:

$sudo ./myapp uninstall
uninstall operation ok
$sudo systemctl status myApp
Unit myApp.service could not be found.

好了,到这里我们看到:在文章开始处提出的给Go应用增加将自己安装为systemd service的能力的目标已经顺利实现了。

最后小结一下:service包让我们的程序有了将自己安装为system service的能力。它也可以让你开发出将其他程序安装为一个system service的能力,不过这个作业就留给大家了:)。大家如有问题,欢迎在评论区留言。

本文涉及的代码可以在这里下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用Go语言实现eBPF程序内核态与用户态的双向数据交换

本文永久链接 – https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go

在之前的两篇文章中,无论是使用C语言开发eBPF程序,还是使用Go开发的eBPF程序,都是hello world级别的,可能有用,但谈不上十分实用。

通常来说,一个实用的eBPF程序,它的内核态部分与用户态部分是有数据交换的,有了这种数据交换,eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性,eBPF MAP是绕不过去的机制

在这一篇有关eBPF程序开发的文章中,我们就来看看如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换

一. why BPF MAP?

永远不要忘记BPF字节码是运行于OS内核态的代码,这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据,通常仅能通过系统调用陷入内核态来实现。因此,在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。

那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢?用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢?

Linux内核BPF开发者于是就引入了BPF MAP机制BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道。同时由于bpf map存储在内核分配的内存空间,处于内核态,可以被运行于在内核态的多个BPF程序所共享,同样可以作为多个BPF程序交换和共享数据的机制。

二. BPF MAP不是狭义的map数据结构

BPF MAP究竟是什么呢?它不是我们狭义理解的哈希映射表的数据结构,而是一种通用数据结构,可以存储不同类型数据的通用数据结构。用著名内核BPF开发者Andrii Nakryiko的话来说,MAP就是BPF中代表抽象数据容器(abstract data container)的一个概念

截至目前,内核BPF支持的MAP类型已经有20+种,下面是libbpf中bpf.h中列出的当前支持的MAP类型:

// libbpf/include/uapi/linux/bpf.h
enum bpf_map_type {
    BPF_MAP_TYPE_UNSPEC,
    BPF_MAP_TYPE_HASH,
    BPF_MAP_TYPE_ARRAY,
    BPF_MAP_TYPE_PROG_ARRAY,
    BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    BPF_MAP_TYPE_PERCPU_HASH,
    BPF_MAP_TYPE_PERCPU_ARRAY,
    BPF_MAP_TYPE_STACK_TRACE,
    BPF_MAP_TYPE_CGROUP_ARRAY,
    BPF_MAP_TYPE_LRU_HASH,
    BPF_MAP_TYPE_LRU_PERCPU_HASH,
    BPF_MAP_TYPE_LPM_TRIE,
    BPF_MAP_TYPE_ARRAY_OF_MAPS,
    BPF_MAP_TYPE_HASH_OF_MAPS,
    BPF_MAP_TYPE_DEVMAP,
    BPF_MAP_TYPE_SOCKMAP,
    BPF_MAP_TYPE_CPUMAP,
    BPF_MAP_TYPE_XSKMAP,
    BPF_MAP_TYPE_SOCKHASH,
    BPF_MAP_TYPE_CGROUP_STORAGE,
    BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
    BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
    BPF_MAP_TYPE_QUEUE,
    BPF_MAP_TYPE_STACK,
    BPF_MAP_TYPE_SK_STORAGE,
    BPF_MAP_TYPE_DEVMAP_HASH,
    BPF_MAP_TYPE_STRUCT_OPS,
    BPF_MAP_TYPE_RINGBUF,
    BPF_MAP_TYPE_INODE_STORAGE,
    BPF_MAP_TYPE_TASK_STORAGE,
    BPF_MAP_TYPE_BLOOM_FILTER,
};

这里数据结构类型众多,但不是本文的重点,我们不一一介绍了。其中的BPF_MAP_TYPE_HASH类型是BPF支持的第一种MAP数据结构,这个类型可以理解为我们日常接触的hash映射表,通过键值对的形式索引数据。在后续的例子中我们将使用这种类型的MAP。

那么BPF MAP是如何可以在内核态与用户态共享数据的?原理是什么呢?

bpf这个系统调用的说明中,我们能找到端倪。下面是bpf系统调用的函数原型:

// https://man7.org/linux/man-pages/man2/bpf.2.html

#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

从bpf的原型来看,似乎比较简单。但bpf其实是一个“富调用”,即不止能干一件事,通过cmd传入的值不同,它可以围绕BPF完成很多事情。最主要的功能是加载bpf程序(cmd=BPF_PROG_LOAD),其次是围绕MAP的一系列操作,包括创建MAP(cmd=BPF_MAP_CREATE)、MAP元素查询(cmd=BPF_MAP_LOOKUP_ELEM)、MAP元素值更新(cmd=BPF_MAP_UPDATE_ELEM)等。

当cmd=BPF_MAP_CREATE时,即bpf执行创建MAP的操作后,bpf调用会返回一个文件描述符fd,通过该fd后续可以操作新创建的MAP。通过fd访问map,这个很unix

当然这么底层的系统调用,一般BPF用户态开发人员无需接触到,像libbpf就包装了一系列的map操作函数,这些函数不会暴露map fd给用户,简化了使用方法,提升了使用体验。

下面我们先来看一下如何用C语言实现基于map的BPF用户态与内核态的数据交换。

三. 使用C基于libbpf使用map的示例

这个示例改造自helloworld示例。原helloworld示例在execve这个系统调用被调用时输出一条内核日志(在/sys/kernel/debug/tracing/trace_pipe中可以查看到),用户态程序并没有与内核态程序做任何数据交换。

在这个新示例(execve_counter)中,我们依然跟踪系统调用execve,不同的是我们对execve进行调用计数,并将技术存储在BPF MAP中。而用户态部分程序则读取该MAP中的计数并定时输出计数值。

我们先来看看BPF内核态部分的源码:

// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter/execve_counter.bpf.c

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

typedef __u64 u64;
typedef char stringkey[64];

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 128);
    //__type(key, stringkey);
    stringkey* key;
    __type(value, u64);
} execve_counter SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
  stringkey key = "execve_counter";
  u64 *v = NULL;
  v = bpf_map_lookup_elem(&execve_counter, &key);
  if (v != NULL) {
    *v += 1;
  }
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

和helloworld示例不同,我们在新示例中定义了一个map结构execve_counter,通过SEC宏将其标记为BPF MAP变量。

这个map结构有四个字段:

  • type: 使用的BPF MAP类型(参见前面的bpf_map_type枚举类型),这里我们使用BPF_MAP_TYPE_HASH,即一个hash散列表结构;
  • max_entries:map内的key-value对的最大数量;
  • key: 指向key内存空间的指针。这里我们自定义了一个类型stringkey(char[64])来表示每个key元素的类型;
  • value: 指向value内存空间的指针,这里value元素的类型为u64,一个64位整型。

内核态函数bpf_prog的实现也比较简单:在上面的map中查询”execve_counter”这个key,如果查到了,则将得到的value指针指向的内存中的值加1。

我们再来看看execve_counter这个示例的用户态部分的程序源码:

// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve_counter/execve_counter.c

#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <linux/bpf.h>
#include "execve_counter.skel.h"

typedef __u64 u64;
typedef char stringkey[64];

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct execve_counter_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = execve_counter_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    /* Load & verify BPF programs */
    err = execve_counter_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* init the counter */
    stringkey key = "execve_counter";
    u64 v = 0;
    err = bpf_map__update_elem(skel->maps.execve_counter, &key, sizeof(key), &v, sizeof(v), BPF_ANY);
    if (err != 0) {
        fprintf(stderr, "Failed to init the counter, %d\n", err);
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = execve_counter_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    for (;;) {
            // read counter value from map
            err = bpf_map__lookup_elem(skel->maps.execve_counter, &key, sizeof(key), &v, sizeof(v), BPF_ANY);
            if (err != 0) {
               fprintf(stderr, "Lookup key from map error: %d\n", err);
               goto cleanup;
            } else {
               printf("execve_counter is %llu\n", v);
            }

            sleep(5);
    }

cleanup:
    execve_counter_bpf__destroy(skel);
    return -err;
}

map是在execve_counter_bpf__load中完成的创建,跟踪代码你会发现(参考libbpf源码),最终会调用bpf系统调用创建map。

和helloworld示例不同的是,我们在attach handler之前,先使用libbpf封装的bpf_map__update_elem初始化了bpf map中的key(初始化为0,如果没有这一步,第一次bpf程序执行时,会提示找不到key)。

然后attach handler后,我们在一个循环中每隔5s通过bpf_map__lookup_elem查询一下key=”execve_counter”的值并输出到控制台。

用户态程序之所以可以直接使用map,是因为bpftool基于execve_counter.bpf.c生成的execve_counter.skel.h中包含了map的各种信息。

接下来我们执行make编译一下这个ebpf程序,然后执行并观察输出:

$sudo ./execve_counter
libbpf: loading object 'execve_counter_bpf' from buffer
libbpf: elf: section(3) tracepoint/syscalls/sys_enter_execve, size 192, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 24 insns (192 bytes)
libbpf: elf: section(4) .reltracepoint/syscalls/sys_enter_execve, size 16, link 22, flags 0, type=9
libbpf: elf: section(5) .rodata, size 64, link 0, flags 2, type=1
libbpf: elf: section(6) .maps, size 32, link 0, flags 3, type=1
libbpf: elf: section(7) license, size 13, link 0, flags 3, type=1
libbpf: license of execve_counter_bpf is Dual BSD/GPL
libbpf: elf: section(13) .BTF, size 898, link 0, flags 0, type=1
libbpf: elf: section(15) .BTF.ext, size 176, link 0, flags 0, type=1
libbpf: elf: section(22) .symtab, size 744, link 1, flags 0, type=2
libbpf: looking for externs among 31 symbols...
libbpf: collected 0 externs total
libbpf: map 'execve_counter': at sec_idx 6, offset 0.
libbpf: map 'execve_counter': found type = 1.
libbpf: map 'execve_counter': found key [9], sz = 64.
libbpf: map 'execve_counter': found value [13], sz = 8.
libbpf: map 'execve_counter': found max_entries = 128.
libbpf: map 'execve_c.rodata' (global data): at sec_idx 5, offset 0, flags 480.
libbpf: map 1 is "execve_c.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(3) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #15 against 'execve_counter'
libbpf: prog 'bpf_prog': found map 0 (execve_counter, sec 6, off 0) for insn #15
libbpf: map 'execve_counter': created successfully, fd=4
libbpf: map 'execve_c.rodata': created successfully, fd=5
execve_counter is 0
execve_counter is 0
execve_counter is 9
execve_counter is 23
... ...

注:如果不知道如何编译execve_counter这个示例,请先移步《使用C语言从头开发一个Hello World级别的eBPF程序》了解其构建原理。

bpftool工具提供了查看map的特性,我们可以通过它查看示例创建的map:

$sudo bpftool map
114: hash  name execve_counter  flags 0x0
    key 64B  value 8B  max_entries 128  memlock 20480B
    btf_id 120
116: array  name execve_c.rodata  flags 0x80
    key 4B  value 64B  max_entries 1  memlock 4096B
    frozen

我们还可以dump一下整个map:

$sudo bpftool map dump id 114
[{
        "key": "execve_counter",
        "value": 23
    }
]

我们看到,整个map中就一个键值对(key=”execve_counter”),其值与示例的用户态部分程序输出的一致。

好了,有了C示例作为基础,我们再来看看如何基于Go来实现这个示例。

四. 使用Go基于cilium/ebpf实现execve-counter示例

使用Go开发BPF用户态部分程序要容易的多,cilium/ebpf提供了的包用起来很简单。如果还不知道如何用Go开发ebpf用户态部分的套路,请先移步《使用Go语言开发eBPF程序》一文了解一下。

Go语言示例的必不可少的原料是execve_counter.bpf.c,这个C源码文件与上面的execve_counter示例中的execve_counter.bpf.c的唯一差别就是include的头文件改成了common.h:

$diff execve_counter.bpf.c ../execve-counter/execve_counter.bpf.c
1,2c1,2
<
< #include "common.h"
---
> #include <linux/bpf.h>
> #include <bpf/bpf_helpers.h>

基于原料execve_counter.bpf.c,bpf2go工具会生成用户态部分所需的Go源码,比如:bpfObject中包含的bpf map实例:

// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
    ExecveCounter *ebpf.Map `ebpf:"execve_counter"`
}

最后,我们在main包main函数中直接使用这些生成的与bpf objects相关的Go函数即可,下面是main.go部分源码:

// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf execve_counter.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    // init the map element
    var key [64]byte
    copy(key[:], []byte("execve_counter"))
    var val int64 = 0
    if err := objs.bpfMaps.ExecveCounter.Put(key, val); err != nil {
        log.Fatalf("init map key error: %s", err)
    }

    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            if err := objs.bpfMaps.ExecveCounter.Lookup(key, &val); err != nil {
                log.Fatalf("reading map error: %s", err)
            }
            log.Printf("execve_counter: %d\n", val)

        case <-stopper:
            // Wait for a signal and close the perf reader,
            // which will interrupt rd.Read() and make the program exit.
            log.Println("Received signal, exiting program..")
            return
        }
    }
}

在main函数,我们通过objs.bpfMaps.ExecveCounter直接访问map实例,并通过其Put和Lookup方法可以直接操作map。这里要注意的是key的类型必须与execve_counter.bpf.c中的key类型(char[64])保持内存布局一致,不能直接用string类型,否则会在执行时报下面错误:

init map key error: can't marshal key: string doesn't marshal to 64 bytes

编译和执行execve-counter-go和helloworld-go别无二致:

$make
$go run -exec sudo main.go bpf_bpfel.go

2022/07/17 16:59:52 execve_counter: 0
2022/07/17 16:59:57 execve_counter: 14
^C2022/07/17 16:59:59 Received signal, exiting program..

五. 小结

本文介绍了eBPF内核态部分与用户态部分进行数据交换的主要方法:BPF MAP机制。这里的MAP不是狭义的一种hash散列表,而是一个抽象数据结构容器,目前支持二十几种数据结构,大家可以根据自己的需求挑选适当的结构(可查询手册了解各种数据结构的特点)。

MAP本质上也是由bpf系统调用创建的,bpf程序只需要声明map的key、value、type等组成信息即可。用户态可以通过bpf系统调用返回的fd操作map,libbpf和cilium/ebpf等封装了对fd的操作,这样简化了API的使用。

内核中map的update操作不是原子的,因此当有多个bpf程序并发访问一个map时,需要同步操作。bpf提供了bpf_spin_lock来实现对map操作的同步。我们可以在value类型中加入bpf_spin_lock来同步对value的修改,就像下面的例子(例子来自《Linux Observability with BPF》一书):

struct concurrent_element {
    struct bpf_spin_lock semaphore;
    int count;
}

struct bpf_map_def SEC("maps") concurrent_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(int),
    .value_size = sizeof(struct concurrent_element),
    .max_entries = 100,
};

int bpf_program(struct pt_regs *ctx) {
      intkey=0;
      struct concurrent_element init_value = {};
      struct concurrent_element *read_value;
      bpf_map_create_elem(&concurrent_map, &key, &init_value, BPF_NOEXIST);
      read_value = bpf_map_lookup_elem(&concurrent_map, &key);
      bpf_spin_lock(&read_value->semaphore);
      read_value->count += 100;
      bpf_spin_unlock(&read_value->semaphore);
}

本文涉及代码可以在这里下载。

六. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats