eBPF perf buffer 的使用

1. 什麼是 perf buffer?

eBPF 中提供了內核和用戶空間之間高效地交換數據的機制 --perf buffer,它是一種 per-cpu 的環形緩衝區,當我們需要將 eBPF 收集到的數據發送到用戶空間記錄或者處理時,就可以用 perf buffer 來完成。它還有如下特點:

eBPF 提供了專門的 map 和 helper function 來使用 perf buffer:

2.BPF 中如何使用?

前面提到了這麼多的概念可能比較迷糊,現在我們來實際操作一把,show me the code。

2.1 內核態 eBPF 程序

首先,在 eBPF 中定義一個 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map;這個 map 可以不用指定 max_entries,因爲在 libbpf 中會默認設置 max_entries 爲系統 cpu 個數。

/* BPF perfbuf map */
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(u32));
} my_map SEC(".maps");

然後,我們再構建一個用於通知用戶態拷貝的 prog。這個 prog 在 sys_write 掛接了一個 kprobe 處理程序,程序調用 bpf_perf_event_output() 來通知用戶態拷貝數據 data。

SEC("kprobe/sys_write")
int bpf_prog1(struct pt_regs *ctx)
{
        struct eb {
                u64 pid;
                u64 cookie;
        } data;

        data.pid = bpf_get_current_pid_tgid();
        data.cookie = 0x12345678;

        bpf_perf_event_output(ctx, &my_map, 0, &data, sizeof(data));

        return 0;
}

2.2 用戶態程序

這部分代碼是基於 libbpf-bootstrap 的 BPF skeleton 框架來編寫的。我們基於 Coolbpf 會有比較簡潔的寫法,做了一些封裝。但是爲了大家從源碼處易於理解,我們採用 libbpf-bootstrap 示例一下。

struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
struct perfbuf_output_bpf *skel;
    ...
pb_opts.sample_cb = handle_event;
pb = perf_buffer__new(bpf_map__fd(skel->maps.my_map), 8 /* 32KB per CPU */, &pb_opts);
if (libbpf_get_error(pb)) {
        err = -1;
        fprintf(stderr, "Failed to create perf buffer\n");
        goto cleanup;
}
  ...
while (!exiting) {
        err = perf_buffer__poll(pb, 100 /* timeout, ms */);
        /* Ctrl-C will cause -EINTR */
        if (err == -EINTR) {
            err = 0;
            break;
        }
        if (err < 0) {
            printf("Error polling perf buffer: %d\n", err);
            break;
        }
}

上面程序通過 perf_buffer_new 這個 libbpf 提供的庫函數來創建一個 struct perf_buffer;此外還通過 pb_opts.samble_cb = handle_event 設置了 perf_buffer 的通知回調處理函數。

然後通過 perf_buffer__poll() 等待內核的通知事件;當內核側 eBPF 程序調用 bpf_perf_event_output() 發起通知時,用戶態 poll 等待的任務就會被喚醒從而調用 pb_opts.samble_cb 指向的回調函數 handle_event.

#define MAX_CNT 1000000000ll
struct eventbuf {
        __u64 pid;
        __u64 cookie;
} ;
void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
        static __u64 cnt;
        const struct eventbuf *eb = data;
        if (eb->cookie !=  0x12345678) {
                printf("BUG pid %llx cookie %llx sized %d\n", eb->pid, eb->cookie, data_sz);
                return;
        }
        cnt++;
        if (cnt == MAX_CNT) {
                printf("recv %lld events per sec\n",
                        MAX_CNT * 1000000000ll / (time_get_ns() - start_time));
        }
        return;
}

3. 整體邏輯梳理

從前面的幾個步驟可知,perf buffer 的使用一般有以下幾步:

  1. ebpf 在代碼通過 SEC(".maps") 聲明一個 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map my_map;

  2. ebpf 在 sys_write 這個內核函數上定義一個 kprobe,函數名爲 bpf_prog1;

  3. skeleton 框架藉助 libbpf 調用 xxx_bpf__open() 解析上面定義在 ".maps" section 的 map my_map,並調用 xxx_bpf__load() 在內核中創建這個 map;

  4. 用戶態調用 libbpf 提供的庫函數 perf_buffer__new() 創建 struct perf_buffer *pb;

  5. skeleton 框架藉助 libbpf 調用 xxx_bpf__attach() 將前面的 kprobe 函數 bpf_prog1 attach 到 sys_write 上;

  6. 用戶態調用 perf_buffer__poll(pb, xxx) 監聽 ebpf 上 perf_buffer 的事件;

  7. 一旦內核有發生 sys_write() 調用,bpf_prog1() 就調用 bpf_perf_event_output() 通知 poll 等等 perf buffer 的任務;

  8. 在 perf buffer 上 perf_buffer__poll 等待的任務收到內核發來的通知,調用回調函數 handle_event() 拷貝數據並做處理。

今天我們比較簡單的介紹了 perf buffer 編寫 eBPF 代碼的內核態和用戶態部分的處理流程,在下一篇文章中,我們將介紹 perf buffer 用到的 map:BPF_MAP_TYPE_PERF_EVENT_ARRAY 的具體實現。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/F9x4JosCVOnq0omQQxS0Lw