構建並運行 eBPF 應用 - Part 1
本文將介紹如何使用 C 和 Golang 編寫第一個 eBPF 程序。我們將在第一部分介紹實際的 eBPF 程序,在第二部分介紹用戶空間應用程序。
準備工作
本文開發所運行的操作系統是:
OS: Ubuntu 22.04
Linux Header Version: 6.5.0–14-generic
還通過 apt 安裝了一些依賴項:
sudo apt-get -y install libbpf bpfcc-tools
完成 ebpf 的代碼除了需要 Go 的知識之外,讀者對 C 語言編程需要熟悉。
什麼是 eBPF[1]?
有許多博客 / 網站對 eBPF 進行了深入探討(請查看資源部分),但爲了簡單起見,我們姑且認爲 eBPF 是一種在不修改內核源代碼的情況下使用模塊擴展 Linux 內核的方法。
筆者認爲 eBPF 就是是內核的一個鉤子,允許在內核空間運行邏輯。
User Space vs Kernel Space
當我們談論內核空間時,通常指的是操作系統。這是一個特權區域,可以完全訪問硬件和軟件資源。當我們談論用戶空間時,這裏通常是你運行日常程序(如谷歌瀏覽器)的地方。用戶空間的訪問權限是有限制的。
選擇要掛鉤的事件
這裏給大家推薦一個學習上手開發 eBPF 的好的項目:kepler[2]Kepler 的一項工作是通過 CPU 計劃切換,計算每個進程(由 PID 標識)使用 CPU 的時間。
CPU 調度 [3] 是指在正在執行的進程之間進行切換,以便更好地利用處理能力(當一個進程受阻時,CPU 會暫停處理該進程,並切換到另一個進程)。
因此,如果我們想複製這一功能,我們可以這樣做:
-
知道某個進程何時將開始使用 CPU
-
知道某個進程何時停止使用 CPU
-
計算這兩個時刻之間的時間
這樣我們就能大致估算出每個流程需要多少時間,同時要記住,一個流程會被安排多次。
瞭解 event 的格式
在 BPF 事件中,每個事件在運行函數時都會包含一些稱爲 "上下文" 的內容。這些上下文實質上就是事件發出的信息。我們需要定義一個 C 結構來保存這些信息,但首先,我們需要獲得該結構的格式。我們可以通過運行下面的代碼來實現:
$ sudo cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
name: sched_switch
ID: 327
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:char prev_comm[16]; offset:8; size:16; signed:0;
field:pid_t prev_pid; offset:24; size:4; signed:1;
field:int prev_prio; offset:28; size:4; signed:1;
field:long prev_state; offset:32; size:8; signed:1;
field:char next_comm[16]; offset:40; size:16; signed:0;
field:pid_t next_pid; offset:56; size:4; signed:1;
field:int next_prio; offset:60; size:4; signed:1;
print fmt: "prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d", REC->prev_comm, REC->prev_pid, REC->prev_prio, (REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1)) ? __print_flags(REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1), "|", { 0x00000001, "S" }, { 0x00000002, "D" }, { 0x00000004, "T" }, { 0x00000008, "t" }, { 0x00000010, "X" }, { 0x00000020, "Z" }, { 0x00000040, "P" }, { 0x00000080, "I" }) : "R", REC->prev_state & (((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) ? "+" : "", REC->next_comm, REC->next_pid, REC->next_prio
要處理的信息相當多,但爲了簡單起見,我們可以說,在這個用例中,我們不需要關心任何以 common_ 爲前綴的字段。這樣我們就有了以下字段:
char prev_comm[16];
pid_t prev_pid;
int prev_prio;
long prev_state;
char next_comm[16];
pid_t next_pid;
int next_prio;
然後我們就可以利用這些信息創建下面的 C 結構:
struct sched_switch_args {
char prev_comm[16];
int prev_pid;
int prev_prio;
long prev_state;
char next_comm[16];
int next_pid;
int next_prio;
};
現在,首先要注意的是,我將 pid_t 類型改爲 int。這是因爲 pid_t 的底層類型是 int。我們可以使用 pid_t 類型,但需要依賴 sys/types.h,而在本例中我們並不需要。有關這方面的更多信息,可以閱讀這篇文章 [4]。
創建 BPF Map
要從內核空間收集數據並在用戶空間訪問這些數據,我們需要使用一種叫做 BPF 映射的東西。BPF 映射是推送到用戶空間的數據結構。在本例中,我們將使用基於 PID 的哈希表類型。這需要我們創建三個結構,即:
- 通過 key struct 識別數據
struct key_t {
// This is the process ID
// which we will use to identify
// in the hash map
__u32 pid;
};
- 存儲數據的方式是 value struct
struct val_t {
// used to understand the start time of the process
__u64 start_time;
// used to store the elapsed time of the process
__u64 elapsed_time;
};
- eBpf HashMap 將他們聯繫在一起
struct {
// The type of BPF map we are creating
__uint(type, BPF_MAP_TYPE_HASH);
// specifying the type to be used for the key
__type(key, struct key_t);
// specifying the type to be used as the value
__type(value, struct val_t);
// max amount of entries to store in the map
__uint(max_entries, 10240);
// name of the map as well as a section macro
// from the bpf lib to designate this type
// as a BPF map
} process_time_map SEC(".maps");
我已經添加了註釋,解釋這些結構中每一行的作用。
創建 eBPF 函數
最後一步是創建 eBPF 函數。爲此,我們需要一個 eBPF 程序。這是一個 C 語言函數,帶有一些宏標識,這樣我們就可以使用先前定義的類型進行交互,例如:
SEC("tracepoint/sched/sched_switch")
int cpu_processing_time(struct sched_switch_args *ctx) {
// get the current time in ns
__u64 ts = bpf_ktime_get_ns();
// we need to check if the process is in our map
struct key_t prev_key = {
.pid = ctx->prev_pid,
};
struct val_t *val = bpf_map_lookup_elem(&process_time_map, &prev_key);
// if the previous PID does not exist it means that we just started
// watching or we missed the start somehow
// so we ignore it for now
if (val) {
// Calculate and store the elapsed time for the process and we reset the
// start time so we can measure the next cycle of that process
__u64 elapsed_time = ts - val->start_time;
struct val_t new_val = {.start_time = ts, .elapsed_time = elapsed_time};
bpf_map_update_elem(&process_time_map, &prev_key, &new_val, BPF_ANY);
return 0;
};
// we need to check if the next process is in our map
// if it's not we need to set initial time
struct key_t next_key = {
.pid = ctx->next_pid,
};
struct val_t *next_val = bpf_map_lookup_elem(&process_time_map, &prev_key);
if (!next_val) {
struct val_t next_new_val = {.start_time = ts};
bpf_map_update_elem(&process_time_map, &next_key, &next_new_val, BPF_ANY);
return 0;
}
return 0;
}
下面的宏指定了該函數要連接的事件。
SEC("tracepoint/sched/sched_switch")
這一行就是我們查找 BPF 映射數據的方法。我們使用一個唯一的鍵,並將其傳遞給 bpf_map_lookup_elem 函數,該函數將返回一個 val_t 類型的值(我們之前定義的)。如果該鍵下沒有值,該函數將返回 NULL,請注意我們需要將 BPF 映射類型作爲 &process_time_map 傳遞。
struct val_t *val = bpf_map_lookup_elem(&process_time_map, &prev_key);
這一行是我們向 BPF 地圖添加數據的過程。我們將傳遞鍵(本例中爲 &prev_key)和鍵值(&new_val),後者將把該值存儲到 BPF 映射中。請再次注意,我們傳遞的是映射類型。BPF_ANY 用於將鍵更新爲新值,或者在鍵不存在時創建新值(參見文檔)。
bpf_map_update_elem(&process_time_map, &prev_key, &new_val, BPF_ANY);
這樣,我們就完成了功能,不過,我們還需要在代碼中添加最後一行:
char _license[] SEC("license") = "Dual MIT/GPL";
由於 eBPF 是以 GPL 許可的,這意味着所有集成軟件也需要與 GPL 兼容。如果沒有這一行,就無法將代碼加載到內核中。因此,我們的最終代碼片段如下(我添加了需要包含的 C 頭文件):
#include <linux/sched.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <stddef.h>
#ifndef TASK_COMM_LEN
#define TASK_COMM_LEN 16
#endif
struct key_t {
__u32 pid;
};
struct val_t {
__u64 start_time;
__u64 elapsed_time;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct key_t);
__type(value, struct val_t);
__uint(max_entries, 10240);
} process_time_map SEC(".maps");
// this is the structure of the sched_switch event
struct sched_switch_args {
char prev_comm[TASK_COMM_LEN];
int prev_pid;
int prev_prio;
long prev_state;
char next_comm[TASK_COMM_LEN];
int next_pid;
int next_prio;
};
SEC("tracepoint/sched/sched_switch")
int cpu_processing_time(struct sched_switch_args *ctx) {
// get the current time in ns
__u64 ts = bpf_ktime_get_ns();
// we need to check if the process is in our map
struct key_t prev_key = {
.pid = ctx->prev_pid,
};
struct val_t *val = bpf_map_lookup_elem(&process_time_map, &prev_key);
// if the previous PID does not exist it means that we just started
// watching or we missed the start somehow
// so we ignore it for now
if (val) {
// Calculate and store the elapsed time for the process and we reset the
// start time so we can measure the next cycle of that process
__u64 elapsed_time = ts - val->start_time;
struct val_t new_val = {.start_time = ts, .elapsed_time = elapsed_time};
bpf_map_update_elem(&process_time_map, &prev_key, &new_val, BPF_ANY);
return 0;
};
// we need to check if the next process is in our map
// if it's not we need to set initial time
struct key_t next_key = {
.pid = ctx->next_pid,
};
struct val_t *next_val = bpf_map_lookup_elem(&process_time_map, &prev_key);
if (!next_val) {
struct val_t next_new_val = {.start_time = ts};
bpf_map_update_elem(&process_time_map, &next_key, &next_new_val, BPF_ANY);
return 0;
}
return 0;
}
char _license[] SEC("license") = "Dual MIT/GPL";
這樣,我們就完成了 BPF 內核側的程序。在本系列的下一篇文章中,我將介紹用 GO 語言編寫用戶空間程序,並使用名爲 bpf2go[5] 的工具來幫助我們實現程序綁定。
參考資料
[1]
what-is-ebpf: https://ebpf.io/what-is-ebpf/#what-is-ebpf
[2]
kepler github: https://github.com/sustainable-computing-io/kepler/tree/main
[3]
cpu-scheduling: https://www.studytonight.com/operating-system/cpu-scheduling
[4]
glibc-223: https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_554.html#:~:text=The%20pid_t%20data%20type%20is,Function%3A%20pid_t%20getppid%20%28void%29
[5]
bpf2go: https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/o-5Cf83RyFoMuI6Itsre5w