gobpf 使用示例:使用 perf event 保存数据

前言

前面 hello 示例中我们是通过 sudo cat  /sys/kernel/debug/tracing/trace_pipe 的方式来查看数据的, 本文简单讲述如何通过 perf event 来保存 eBPF 数据然后再在 Go 程序中读取 perf event 中保存的数据。

使用 perf event 保存数据

还是前面的那个 hello.c ,不过这次会使用 perf event 来保存 open 的文件名称

eBPF C 代码如下(hello.c):

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

struct bpf_map_def SEC("maps/open_event") open_event = {
    .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    .key_size = sizeof(int),
    .value_size = sizeof(__u32),
    .max_entries = 128,
    .pinning = 0,
    .namespace = "",
};

struct data_t {
    __u32 pid;
    char file_name[256];
};

SEC("kprobe/do_sys_open")
int kprobe__do_sys_open(struct pt_regs *ctx) {
        struct data_t data = {};

        data.pid = bpf_get_current_pid_tgid() >> 32;
        __u32 cpu = bpf_get_smp_processor_id();

        bpf_probe_read(&data.file_name, sizeof(data.file_name), PT_REGS_PARM2(ctx));

        bpf_perf_event_output(ctx, &open_event, cpu, &data, sizeof(data));

        return 0;
}

char _license[] SEC("license") = "GPL";

简单来说就是定义一个 type 为 BPF_MAP_TYPE_PERF_EVENT_ARRAYbpf_map_def 变量 open_event, 然后通过 bpf_perf_event_output 函数将数据写入到定义的 open_event 中。

在 Go 程序中读取 perf event 数据

package main

import (
    "fmt"
    "os"
    "unsafe"

    "github.com/iovisor/gobpf/elf"
)

/*
#include <linux/types.h>
struct data_t {
    __u32 pid;
    char file_name[256];
};
*/
import "C"

type Event struct {
    Pid      uint32
    FileName string
}

func main() {
    mod := elf.NewModule("hello.o")
    err := mod.Load(nil)
    if err != nil {
        panic(err)
    }
    defer mod.Close()
    err = mod.EnableKprobes(128)
    if err != nil {
        panic(err)
    }

    channel := make(chan []byte)
    lost := make(chan uint64)

    perfMap, err := elf.InitPerfMap(mod, "open_event", channel, lost)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed to init perf map: %s\n", err)
        os.Exit(1)
    }
    go func() {
        for {
            l := <-lost
            fmt.Println(l)
        }
    }()
    perfMap.PollStart()
    defer perfMap.PollStop()

    for {
        var event Event
        data := <-channel
        event = openEventToGo(&data)
        fmt.Printf("pid %d open file %s\n", event.Pid, event.FileName)
    }
}

func openEventToGo(data *[]byte) (event Event) {
    eventC := (*C.struct_data_t)(unsafe.Pointer(&(*data)[0]))

    event.Pid = uint32(eventC.pid)
    event.FileName = C.GoString(&eventC.file_name[0])

    return
}

通过 elf.InitPerfMap 指定要读取的 perf event map 的变量的名称 (跟 eBPF 程序中定义的 open_event 相对应),然后使用方法的 channel 读取数据。

同时会使用 cgo 来将 c 中的数据类型转换为 go 中定义的结构体

编译并运行:

$ make
$ make run
...
pid 364 open file /proc/493/status
pid 364 open file /proc/493/status
pid 364 open file /proc/493/comm
...

可以看到已经可以在 go 程序中读取到 eBPF 程序保存的数据了。

P.S. 本文的所有代码在 Github 上都有一份完整版: https://github.com/mozillazg/gobpf-examples/tree/master/2-perf-event


Comments