eBPF perf buffer 的使用
1. 什麼是 perf buffer?
eBPF 中提供了內核和用戶空間之間高效地交換數據的機制 --perf buffer,它是一種 per-cpu 的環形緩衝區,當我們需要將 eBPF 收集到的數據發送到用戶空間記錄或者處理時,就可以用 perf buffer 來完成。它還有如下特點:
-
能夠記錄可變長度數據記;
-
能夠通過內存映射的方式在用戶態讀取讀取數據,而無需通過系統調用陷入到內核去拷貝數據;
-
實現 epoll 通知機制
eBPF 提供了專門的 map 和 helper function 來使用 perf buffer:
-
map:BPF_MAP_TYPE_PERF_EVENT_ARRAY /* 此類型 map 專門用於 perfbuffer */
-
helper function:bpf_perf_event_output /* 用於通知用戶態拷貝數據 */
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 的使用一般有以下幾步:
-
ebpf 在代碼通過 SEC(".maps") 聲明一個 BPF_MAP_TYPE_PERF_EVENT_ARRAY 類型的 map my_map;
-
ebpf 在 sys_write 這個內核函數上定義一個 kprobe,函數名爲 bpf_prog1;
-
skeleton 框架藉助 libbpf 調用 xxx_bpf__open() 解析上面定義在 ".maps" section 的 map my_map,並調用 xxx_bpf__load() 在內核中創建這個 map;
-
用戶態調用 libbpf 提供的庫函數 perf_buffer__new() 創建 struct perf_buffer *pb;
-
skeleton 框架藉助 libbpf 調用 xxx_bpf__attach() 將前面的 kprobe 函數 bpf_prog1 attach 到 sys_write 上;
-
用戶態調用 perf_buffer__poll(pb, xxx) 監聽 ebpf 上 perf_buffer 的事件;
-
一旦內核有發生 sys_write() 調用,bpf_prog1() 就調用 bpf_perf_event_output() 通知 poll 等等 perf buffer 的任務;
-
在 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