Linux 核心使用參考計數機制來管理 eBPF 程式和 Map 的生命週期。每當有新的參考建立時(例如透過 BPF_PROG_LOAD
或 BPF_MAP_CREATE
命令),參考計數會增加。當參考計數降到零時,核心會自動釋放這些資源。
這種機制確保了 eBPF 資源的有效管理,避免資源洩漏。例如,當一個載入 eBPF 程式的使用者空間程式結束時,如果沒有其他參考,該 eBPF 程式會被自動釋放。
固定 BPF 物件與 BPF 連結
為了讓 eBPF 程式和 Map 在原始程式結束後仍能存續,Linux 提供了兩種主要機制:
固定(Pinning):將 BPF 物件固定到 BPF 檔案系統(通常是
/sys/fs/bpf/
),建立一個持久的參考。BPF 連結(BPF Links):建立額外的參考指向 eBPF 程式,使其與特定事件的關聯能夠獨立存在。
這兩種機制讓 eBPF 程式和 Map 能夠在原始載入程式結束後繼續運作,實作了更靈活的佈署和管理方式。
Map 操作的核心命令
除了前面提到的 BPF_MAP_GET_NEXT_KEY
和 BPF_MAP_LOOKUP_ELEM
,bpf()
系統呼叫還提供了一系列用於操作 Map 的命令:
BPF_MAP_UPDATE_ELEM
:向 Map 中新增或更新元素BPF_MAP_LOOKUP_ELEM
:查詢 Map 中特定鍵對應的值BPF_MAP_DELETE_ELEM
:從 Map 中刪除特定鍵值對
這些命令構成了使用者空間程式與 eBPF Map 互動的基礎。透過它們,程式可以動態地修改 Map 內容,實作與 eBPF 程式的資料交換。
Map 更新範例
以下是一個使用 BPF_MAP_UPDATE_ELEM
更新 Map 的簡化範例:
// 準備鍵值對
int key = 501;
struct config_value value = { .message = "Hi user 501!" };
// 更新 Map
struct bpf_attr attr = {
.map_fd = map_fd,
.key = ptr_to_u64(&key),
.value = ptr_to_u64(&value),
.flags = BPF_ANY,
};
int result = syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
這段程式碼展示瞭如何使用 BPF_MAP_UPDATE_ELEM
命令更新 Map:
- 首先定義要更新的鍵值對
- 然後準備
bpf_attr
結構,指定 Map 的檔案描述符、鍵值對的記憶體位置以及操作旗標 - 最後呼叫
bpf()
系統呼叫執行更新操作
其中 BPF_ANY
旗標表示無論鍵是否已存在,都執行更新操作。如果只想在鍵不存在時新增,可以使用 BPF_NOEXIST
;如果只想在鍵已存在時更新,則使用 BPF_EXIST
。
eBPF 程式的附加機制
將 eBPF 程式附加到特定事件是實作系統觀測和程式化控制的關鍵步驟。不同型別的 eBPF 程式有不同的附加方法:
kprobe 事件附加
對於 kprobe 事件(用於追蹤核心函式),附加過程通常涉及 perf_event_open()
和 ioctl()
系統呼叫:
// 建立 perf 事件
struct perf_event_attr attr = {
.type = PERF_TYPE_TRACEPOINT,
.config = tracepoint_id,
// 其他設定...
};
int pfd = syscall(__NR_perf_event_open, &attr, pid, cpu, -1, PERF_FLAG_FD_CLOEXEC);
// 附加 eBPF 程式
ioctl(pfd, PERF_EVENT_IOC_SET_BPF, prog_fd);
其他附加方法
不同型別的 eBPF 程式使用不同的附加方法:
- cgroup 程式:使用
bpf(BPF_PROG_ATTACH)
系統呼叫 - 原始追蹤點(raw tracepoint):使用
bpf(BPF_RAW_TRACEPOINT_OPEN)
系統呼叫 - XDP 程式:通常透過
netlink
通訊或專用的 API 附加到網路介面
這種多樣性反映了 eBPF 的廣泛應用範圍和靈活性,能夠適應不同的系統觀測和控制需求。
BTF 資料與 eBPF 可攜性
在追蹤 bpf()
系統呼叫時,我們可能會注意到 BTF(BPF Type Format)資料的載入。BTF 是一種用於描述資料結構格式的元資料,它在 eBPF 生態系統中扮演著重要角色:
幫助工具理解資料結構:例如,
bpftool
使用 BTF 資料來理解 Map 中儲存的資料結構,從而能夠以人類可讀的方式顯示這些資料。提升 eBPF 程式可攜性:BTF 使得 eBPF 程式能夠在不同版本的核心上執行,而無需重新編譯,這大提升了 eBPF 應用的佈署便利性。
BTF 資料的詳細格式和使用方式是一個複雜的主題,它為 eBPF 程式提供了類別似於傳統程式的除錯資訊,使得工具能夠更好地理解和處理 eBPF 程式和資料。
實際操作:探索與實驗
對於想要深入瞭解 bpf()
系統呼叫的開發者,以下是一些值得嘗試的實驗:
驗證指令數量:確認
BPF_PROG_LOAD
系統呼叫中的insn_cnt
欄位是否與使用bpftool
輸出的 eBPF 位元組碼指令數量相符。觀察多 Map 行為:執行兩個具有相同 Map 名稱的程式例項,然後使用
bpftool map dump name config
觀察其行為。透過strace
追蹤系統呼叫,可以看到工具如何處理多個同名 Map。動態更新 Map:在程式執行時使用
bpftool map update
修改 Map 內容,然後驗證 eBPF 程式是否能夠感知這些變更。固定 eBPF 程式:在程式執行時,使用
bpftool prog pin
命令將 eBPF 程式固定到 BPF 檔案系統,然後終止原始程式並確認 eBPF 程式仍然在核心中執行。使用原始追蹤點:嘗試將程式從使用 kprobe 轉換為使用原始追蹤點,體驗不同的附加機制。
這些實驗能夠幫助開發者更好地理解 eBPF 系統的內部運作機制,為開發更複雜的 eBPF 應用打下基礎。
深入理解 eBPF 系統呼叫的重要性
深入理解 bpf()
系統呼叫對於 eBPF 開發者來說至關重要,原因有以下幾點:
底層機制掌握:瞭解系統呼叫如何工作,能夠幫助開發者更好地診斷和解決問題。
效能最佳化:知道核心如何處理 eBPF 相關操作,有助於設計更高效的程式和資料存取模式。
高階功能開發:某些高階功能可能需要直接使用系統呼叫,而不是依賴高層次的抽象函式庫。
安全性考量:瞭解系統呼叫如何驗證和限制 eBPF 程式,有助於開發更安全的應用。
透過本文的探索,玄貓希望能為開發者揭開 eBPF 系統呼叫的神秘面紗,展示這個強大機制的內部運作方式。隨著 eBPF 技術的不斷發展,深入理解其基礎機制將變得越來越重要。
在下一篇文章中,我們將探討 BTF 資料的詳細結構和使用方式,以及它如何使 eBPF 程式在不同核心版本間保持可攜性。這是 eBPF 生態系統中另一個關鍵而復雜的主題,對於開發生產級 eBPF 應用至關重要。
透過這一系列的探索,我們不僅能夠掌握 eBPF 的使用技巧,還能深入理解其內部運作機制,為開發強大與高效的系統工具打下堅實基礎。
eBPF 工具實戰:使用 BCC 的 opensnoop 工具探索 BPF 連結
在深入瞭解 CO-RE、BTF 和 Libbpf 前,讓我們先透過一個實用的案例來觀察 eBPF 程式在系統中的運作方式。BCC 工具集中的 opensnoop 能追蹤系統中的檔案開啟操作,是瞭解 eBPF 實際應用的好例子。
執行 opensnoop 並檢視 BPF 連結
當執行 opensnoop 應用程式時,它會建立數個 BPF 連結。這些連結可以透過 bpftool
工具觀察:
$ bpftool link list
116: perf_event prog 1849
bpf_cookie 0
pids opensnoop(17711)
117: perf_event prog 1851
bpf_cookie 0
pids opensnoop(17711)
這個輸出顯示了兩個 BPF 連結,分別對應到程式 ID 1849 和 1851。我們可以進一步確認這些程式的詳細資訊:
$ bpftool prog list
...
1849: tracepoint name tracepoint__syscalls__sys_enter_openat
tag 8ee3432dcd98ffc3 gpl run_time_ns 95875
run_cnt 121
loaded_at 2023-01-08T15:49:54+0000 uid 0
xlated 240B jited 264B memlock 4096B map_ids 571,568
btf_id 710
pids opensnoop(17711)
1851: tracepoint name tracepoint__syscalls__sys_exit_openat
tag 387291c2fb839ac6 gpl run_time_ns 8515669
run_cnt 120
loaded_at 2023-01-08T15:49:54+0000 uid 0
xlated 696B jited 744B memlock 4096B map_ids 568,571,569
btf_id 710
pids opensnoop(17711)
這段輸出揭示了重要訊息:opensnoop 工具利用了兩個 eBPF 程式,分別附加到系統呼叫的進入點 (sys_enter_openat
) 和結束點 (sys_exit_openat
)。這種設計能捕捉檔案開啟操作的完整生命週期 - 從請求開始到操作完成,包括任何錯誤訊息。
固定 BPF 連結
在 opensnoop 執行過程中,我們可以嘗試將其中一個連結「固定」到檔案系統中:
$ bpftool link pin id 116 /sys/fs/bpf/mylink
這個操作會將連結 ID 116 固定到 /sys/fs/bpf/mylink
路徑。有趣的是,即使終止 opensnoop 程式,這個連結和對應的程式仍會保留在核心中,因為它們已被固定到檔案系統。
使用 libbpf 的 BPF 連結建立
如果檢視後續章節的範例程式碼,會發現 hello-buffer-config.py
的 libbpf 版本自動設定了 BPF 連結。使用 strace
工具可以檢視它進行的系統呼叫:
$ strace -e bpf ./hello-buffer-config
這將顯示 bpf(BPF_LINK_CREATE)
系統呼叫,展示 libbpf 如何自動處理連結建立過程。
CO-RE、BTF 與跨核心可移植性問題
為何需要 CO-RE?
eBPF 程式經常需要存取核心資料結構,這就要求 eBPF 程式設計師在編譯時包含相關的 Linux 標頭檔,以便正確定位這些資料結構中的欄位。然而,由於 Linux 核心持續開發,內部資料結構可能在不同核心版本間發生變化。
這導致一個關鍵問題:如果將在一台機器上編譯的 eBPF 物件檔案載入到具有不同核心版本的機器上,無法保證資料結構保持一致。這就是 CO-RE(Compile Once, Run Everywhere,一次編譯,到處執行)技術試圖解決的核心問題。
BCC 的可移植性方法及其限制
在 CO-RE 出現之前,BCC 專案採用了一種不同的方法來處理核心可移植性問題。BCC 的策略是在目標機器上執行時編譯 eBPF 程式碼,但這種方法存在多個問題:
- 環境依賴:每台目標機器都需要安裝編譯工具鏈和核心標頭檔,而後者並非總是預設存在
- 啟動延遲:每次工具啟動時都需要等待編譯完成,可能導致數秒的延遲
- 資源浪費:在大量相同機器上執行時,在每台機器上重複編譯是對計算資源的浪費
- 容器封裝限制:即使將 eBPF 原始碼和工具鏈封裝到容器映像中,仍然無法解決確保核心標頭檔存在的問題
- 資源受限裝置不適用:嵌入式裝置可能沒有足夠的記憶體資源來執行編譯步驟
雖然 BCC 對於學習 eBPF 基本概念很有幫助,特別是因為其 Python 使用者空間程式碼簡潔易讀,但對於現代嚴肅的 eBPF 開發而言,它不是最佳選擇。
CO-RE 技術深度解析
CO-RE 方法提供了一個更優雅的解決方案,能夠在不同核心版本間實作 eBPF 程式的可移植性。它由幾個關鍵元素組成:
BTF (BPF Type Format)
BTF 是表達資料結構佈局和函式簽名的格式。在 CO-RE 中,它用於確定編譯時和執行時使用的結構之間的差異。BTF 也被 bpftool 等工具用來以人類可讀的格式匯出資料結構。Linux 5.4 之後的核心支援 BTF。
Kernel Headers (核心標頭)
Linux 核心原始碼包含描述其使用的資料結構的標頭檔,這些標頭可能在不同版本的 Linux 之間發生變化。eBPF 程式設計師可以選擇包含單獨的標頭檔,或者使用 bpftool 從執行中的系統生成一個名為 vmlinux.h
的標頭檔,其中包含 BPF 程式可能需要的所有核心資料結構資訊。
編譯器支援
Clang 編譯器被增強,當它使用 -g
標誌編譯 eBPF 程式時,會包含所謂的 CO-RE 重定位資訊,這些資訊是從描述核心資料結構的 BTF 資訊中衍生出來的。GCC 編譯器也在 12 版本中為 BPF 目標增加了 CO-RE 支援。
資料結構重定位的函式庫支援
當使用者空間程式將 eBPF 程式載入到核心時,CO-RE 方法要求根據編譯到物件中的 CO-RE 重定位資訊調整位元組碼,以補償編譯時存在的資料結構與即將執行的目標機器上的資料結構之間的任何差異。
有幾個函式庫可以處理這個過程:
- libbpf:最初的 C 函式庫,包含這種重定位能力
- Cilium eBPF 函式庫:為 Go 程式設計師提供相同的能力
- Aya:為 Rust 程式設計師提供類別似功能
實作 CO-RE:從理論到實踐
讓我們看如何在實際開發中應用 CO-RE 技術。這裡我將展示一個完整的工作流程,從環境準備到程式執行。
環境準備
首先,需要確保系統支援 BTF。檢查方法如下:
$ ls -la /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 4276302 Jan 10 09:32 /sys/kernel/btf/vmlinux
如果此檔案存在,表示你的系統支援 BTF。
生成 vmlinux.h
接下來,使用 bpftool 從執行的核心生成包含所有資料結構定義的標頭檔:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
這個命令會建立一個包含當前核心所有資料型別定義的 C 標頭檔。
編寫 CO-RE 相容的 eBPF 程式
以下是一個簡單的 CO-RE 相容程式範例,用於追蹤程式建立:
// process_trace.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
struct event {
pid_t pid;
char comm[16];
};
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct event event = {};
// 使用 BPF_CORE_READ 進行安全的資料存取
event.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&event.comm, sizeof(event.comm));
// 傳送事件到使用者空間
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
這個程式使用 CO-RE 機制追蹤程式執行事件。關鍵點包括:
- 包含
vmlinux.h
以取得核心資料結構定義 - 使用
BPF_CORE_READ
巨集安全地存取資料結構,即使在不同核心版本間欄位置發生變化 - 透過 perf event 地圖將資料從核心傳送到使用者空間
編譯 CO-RE 程式
使用 Clang 編譯上述程式:
$ clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I. -c process_trace.bpf.c -o process_trace.bpf.o
這個命令使用 -g
標誌生成帶有除錯資訊的 BPF 物件檔案,其中包含 CO-RE 重定位資訊。
使用者空間程式與 Libbpf
下面是一個使用 libbpf 載入和執行上述 eBPF 程式的使用者空間程式:
// process_trace.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "process_trace.skel.h"
static volatile bool running = true;
static void sig_handler(int sig)
{
running = false;
}
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
printf("EXEC: PID = %d, COMM = %s\n", e->pid, e->comm);
return 0;
}
int main(int argc, char **argv)
{
struct process_trace_bpf *skel;
struct perf_buffer *pb = NULL;
int err;
// 設定訊號處理
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
// 開啟和載入 BPF 程式
skel = process_trace_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// 附加 BPF 程式
err = process_trace_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
// 設定 perf buffer
pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 8, handle_event, NULL, NULL, NULL);
if (!pb) {
err = -1;
fprintf(stderr, "Failed to create perf buffer\n");
goto cleanup;
}
printf("Successfully started! Please run some commands in another terminal...\n");
// 持續處理事件
while (running) {
err = perf_buffer__poll(pb, 100);
if (err < 0 && err != -EINTR) {
fprintf(stderr, "Error polling perf buffer: %d\n", err);
goto cleanup;
}
}
cleanup:
perf_buffer__free(pb);
process_trace_bpf__destroy(skel);
return err != 0;
}
這個使用者空間程式負責載入和管理 eBPF 程式的執行:
- 使用 libbpf 的骨架功能(skeleton)開啟並載入 eBPF 物件
- 附加 eBPF 程式到對應的追蹤點
- 設定 perf buffer 用於接收來自核心的事件
- 處理接收到的事件並輸出結果
在這個過程中,libbpf 函式庫會自動處理 CO-RE 重定位,確保 eBPF 程式能在不同的核心版本上正確執行。
libbpf 與 BPF 骨架生成
libbpf 提供了一個強大的功能,可以從 BPF 物件檔案生成 C 骨架頭檔,大幅簡化了使用者空間程式的編寫。這是如何實作的:
$ bpftool gen skeleton process_trace.bpf.o > process_trace.skel.h
生成的骨架頭檔包含了所有必要的結構和函式,使得使用者空間程式能夠方便地與 eBPF 程式互動。這種方法的優點包括:
- 型別安全:骨架提供型別安全的 API 來存取 BPF 地圖和全域變數
- 自動化管理:自動處理 BPF 物件的生命週期(載入、附加、解除安裝)
- 簡化程式碼:減少重複性程式碼,使用者空間程式更簡潔
- CO-RE 透明處理:自動處理所有 CO-RE 相關的重定位工作
BTF 的深入理解
BTF 不僅是 CO-RE 的基礎,它還為 eBPF 生態系統帶來了其他重要好處:
BTF 的結構與功能
BTF 是一種高效的型別資訊格式,專為 eBPF 程式設計。它有幾個關鍵特性:
- 緊湊表示:BTF 資料通常比 DWARF 等傳統除錯資訊格式小得多
- 自描述性:BTF 包含完整的型別系統資訊,包括結構、聯合、列舉等
- 核心整合:現代 Linux 核心直接嵌入 BTF 資訊,使其可透過
/sys/kernel/btf/vmlinux
存取
BTF 的實際應用
除了支援 CO-RE 外,BTF 還啟用了許多強大功能:
- 增強的除錯能力:工具可以使用 BTF 資訊將原始位元組碼轉換為人類可讀的格式
- 程式驗證改進:核心驗證器可以利用 BTF 資訊進行更精確的檢查
- 全域變數支援:BTF 使 eBPF 程式能夠安全地存取全域變數
- 結構標籤檢查:確保程式只存取它們被允許存取的資料結構部分
檢查系統的 BTF 支援
可以使用以下命令檢查 BTF 資訊:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c | head -20
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/* Copyright (C) 2020 Facebook */
#ifndef __VMLINUX_H__
#define __VMLINUX_H__
#ifndef BPF_NO_PRESERVE_ACCESS_INDEX
#pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record)
#endif
typedef _Bool bool;
typedef __signed__ char __s8;
typedef unsigned char __u8;
typedef __signed__ short __s16;
typedef unsigned short __u16;
typedef __signed__ int __s32;
typedef unsigned int __u32;
typedef __signed__ long __s64;
typedef unsigned long __u64;
libbpf 函式庫深度解析
libbpf 是 CO-RE 生態系統中的核心元件,負責處理 eBPF 物件的載入、重定位和管理。
libbpf 的關鍵功能
- BPF 物件載入:處理 eBPF 程式的載入和驗證
- CO-RE 重定位:自動進行資料結構佈局調整,確保程式在不同核心版本上執行
- BPF 地圖管理:建立和管理 BPF 地圖,處理地圖存取
- 程式附加:將 eBPF 程式附加到各種掛鉤點(如追蹤點、kprobes 等)
- 環相扣(ring buffer)支援:提供高效的資料傳輸機制
- 骨架生成:與 bpftool 配合生成型別安全的程式骨架
libbpf 的使用模式
使用 libbpf 開發 eBPF 應用程式通常遵循以下模式:
- 編寫 eBPF 程式:使用 C 語言編寫 eBPF 程式,包含
vmlinux.h
取得核心定義 - 編譯 eBPF 程式:使用 Clang 編譯 eBPF 程式,生成物件檔案
- 生成骨架:使用 bpftool 從物件檔案生成骨架頭檔
- 編寫使用者空間程式:使用生成的骨架開發使用者空間程式
- 編譯最終應用程式:將使用者空間程式與 libbpf 函式庫一起編譯
libbpf-bootstrap:快速入門範本
對於初學者,libbpf-bootstrap 專案提供了一組範本和構建系統,簡化了使用 libbpf 和 CO-RE 開發 eBPF 應用程式的過程:
$ git clone https://github.com/libbpf/libbpf-bootstrap.git
$ cd libbpf-bootstrap/examples/c
$ make
這些範本展示了不同型別的 eBPF 程式,從簡單的追蹤程式到複雜的網路過濾器,是學習 libbpf 的絕佳起點。
CO-RE 與 libbpf 實戰:追蹤系統呼叫
讓我們實作一個完整的範例,展示如何使用 CO-RE 和 libbpf 追蹤系統呼叫。
eBPF 程式 (syscall_trace.bpf.c)
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
struct event {
u32 pid;
u32 tgid;
u64 timestamp;
u64 syscall_nr;
char comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
char LICENSE[] SEC("license") = "GPL";
SEC("tracepoint/raw_syscalls/sys_enter")
int handle_sys_enter(struct trace_event_raw_sys_enter *ctx)
{
u64 id = bpf_get_current_pid_tgid();
u32 pid = id >> 32;
u32 tgid = id;
// 跳過核心執行緒
if (pid == 0)
return 0;
struct event *e;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
e->pid = pid;
e->tgid = tgid;
e->timestamp = bpf_ktime_get_ns();
e->syscall_nr = ctx->id;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
這個 eBPF 程式追蹤所有系統呼叫的進入點:
- 使用
tracepoint/raw_syscalls/sys_enter
追蹤點捕捉所有系統呼叫 - 收集程式 ID、執行緒 ID、時間戳、系統呼叫編號和程式名稱
- 使用環形緩衝區(ringbuf)將事件資料高效傳輸到使用者空間
- 跳過 PID 為 0 的核心執行緒,減少不必要的資料收集
使用者空間程式 (syscall_trace.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include "syscall_trace.skel.h"
static volatile bool running = true;
static void sig_handler(int sig)
{
running = false;
}
static const char *syscall_names[] = {
[0] = "read",
[1] = "write",
[2] = "open",
[3] = "close",
// ... 更多系統呼叫名稱 ...
};
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
char ts[32];
time_t t;
struct tm *tm;
t = e->timestamp / 1000000000;
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
const char *syscall_name = "unknown";
if (e->syscall_nr < sizeof(syscall_names) / sizeof(syscall_names[0]) &&
syscall_names[e->syscall_nr]) {
syscall_name = syscall_names[e->syscall_nr];
}
printf("[%s] %s (PID: %u) syscall: %s (%llu)\n",
ts, e->comm, e->pid, syscall_name, e->syscall_nr);
return 0;
}
int main(int argc, char **argv)
{
struct syscall_trace_bpf *skel;
struct ring_buffer *rb = NULL;
int err;
// 設定訊號處理
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
// 開啟和載入 BPF 程式
skel = syscall_trace_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// 附加 BPF 程式
err = syscall_trace_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
// 設定 ring buffer
rb = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
printf("Successfully started tracing syscalls. Press Ctrl+C to stop.\n");
// 持續處理事件
while (running) {
err = ring_buffer__poll(rb, 100);
if (err < 0 && err != -EINTR) {
fprintf(stderr, "Error polling ring buffer: %d\n", err);
goto cleanup;
}
}
cleanup:
ring_buffer__free(rb);
syscall_trace_bpf__destroy(skel);
return err != 0;
}
這個使用者空間程式處理來自 eBPF 程式的系統呼叫事件:
- 使用 libbpf 骨架 API 載入和附加 eBPF 程式
- 設定環形緩衝區接收事件資料
- 將系統呼叫編號轉換為可讀的系統呼叫名稱
- 格式化輸出,顯示時間戳、程式名稱、PID 和系統呼叫資訊
- 處理訊號以優雅地終止程式
#與最佳實踐
CO-RE、BTF 和 libbpf 共同解決了 eBPF 程式跨核心可移植性的挑戰,為開發者提供了一種高效與可靠的方法來開發和佈署 eBPF 應用程式。相比 BCC 的執行時編譯方法,CO-RE 方法具有顯著優勢:更快的啟動時間、更少的依賴關係、更好的資源利用率和更廣泛的裝置支援。
在實際開發中,建議遵循以下最佳實踐:
- 使用 vmlinux.h:依賴從目標系統生成的 vmlinux.h 而非手動包含特定核心標頭
- **採用
BPF CO-RE 的骨架與基礎架構
在開發 BPF 程式時,一個常見的挑戰是如何確保程式能夠在不同版本的 Linux 核心上執行。這就是 BPF CO-RE (Compile Once – Run Everywhere) 技術誕生的原因。透過 CO-RE,我們可以編寫一次 BPF 程式,然後在各種不同版本的核心上執行,而無需為每個核心版本重新編譯。
BPF 骨架 (Skeleton) 的作用
BPF 骨架是 CO-RE 生態系統中的一個關鍵元素。它是從編譯好的 BPF 物件檔案自動生成的,包含了一系列方便的函式,這些函式可以幫助使用者空間程式管理 BPF 程式的生命週期,包括:
- 將 BPF 程式載入核心
- 將程式附加到特定事件
- 管理程式的執行狀態
- 存取程式中定義的對映(maps)
如果你使用 C 語言編寫使用者空間程式,可以透過 bpftool gen skeleton
命令生成骨架。這些生成的函式提供了更高層次的抽象,比直接使用底層函式庫(如 libbpf、cilium/ebpf 等)更加方便。
BPF 骨架本質上是一個自動生成的 C 語言頭檔案,它封裝了與 BPF 程式互動所需的所有底層操作。當我開發大型 BPF 應用時,骨架大幅簡化了程式碼,特別是在處理多個 BPF 程式和對映時。骨架提供了型別安全的 API,這意味著許多錯誤可以在編譯時被捕捉,而不是在執行時才發現。
CO-RE 的核心參考資源
在深入 CO-RE 之前,有幾個關鍵資源值得一提:
- Andrii Nakryiko 的部落格文章詳細描述了 CO-RE 的背景、工作原理和使用方法
- BPF CO-RE 參考 - 這是開發者必讀的權威資源
- libbpf-bootstrap - 展示瞭如何從頭開始使用 CO-RE + libbpf + 骨架構建 eBPF 應用
現在我們已經對 CO-RE 的元素有了基本瞭解,接下來讓我們探討它的工作原理,從 BTF (BPF Type Format) 開始。
BPF 型別格式 (BTF) 深度解析
BTF 訊息描述了資料結構和程式碼在記憶體中的佈局方式。這些訊息可以用於多種不同的用途,是 CO-RE 技術的根本。
BTF 的核心使用案例
結構佈局調整
BTF 在 CO-RE 中的主要作用是識別 eBPF 程式編譯環境與執行環境之間的結構佈局差異。當程式載入核心時,系統會根據這些差異自動進行適當的調整。這個重定位過程是 CO-RE 技術的核心,我將在後面詳細討論。
人類可讀的資料展示
BTF 使得以人類可讀的形式優雅地顯示結構內容成為可能。例如,從電腦的角度來看,字串只是一系列位元組,但將這些位元組轉換為字元使得字串更容易被人類理解。在前面的章節中,我們已經看到 bpftool
如何使用 BTF 訊息來格式化對映轉儲的輸出。
BTF 訊息還包括行號和函式訊息,使 bpftool
能夠在翻譯或 JIT 編譯的程式轉儲輸出中插入原始碼。在驗證器日誌輸出中,你也會看到原始碼訊息,這些同樣來自 BTF 訊息。
BPF 自旋鎖支援
BTF 訊息對於 BPF 自旋鎖也是必需的。自旋鎖用於防止兩個 CPU 核心同時存取同一個對映值。鎖必須是對映值結構的一部分,如下所示:
struct my_value {
... <其他欄位>
struct bpf_spin_lock lock;
... <其他欄位>
};
在核心中,eBPF 程式使用 bpf_spin_lock()
和 bpf_spin_unlock()
輔助函式來取得和釋放鎖。這些輔助函式只有在有 BTF 訊息描述鎖欄位在結構中的位置時才能使用。
自旋鎖是在核心版本 5.1 中增加的,使用它們有很多限制:它們只能用於雜湊或陣列對映型別,並且不能在追蹤或通訊端過濾器型別的 eBPF 程式中使用。自旋鎖的關鍵優勢是它們允許在多個 CPU 之間安全地分享資料,而不需要將資料複製到使用者空間再回傳。在高效能場景中,這可以顯著提高效率。
使用 bpftool 列出 BTF 訊息
與程式和對映一樣,我們可以使用 bpftool
工具來顯示 BTF 訊息。以下命令列出了載入到核心中的所有 BTF 資料:
bpftool btf list
輸出範例:
1: name [vmlinux] size 5843164B
2: name [aes_ce_cipher] size 407B
3: name [cryptd] size 3372B
...
149: name <anon> size 4372B prog_ids 319 map_ids 103
pids hello-buffer-co(7660)
155: name <anon> size 37100B
pids bpftool(7784)
列表中的第一個條目是 vmlinux
,它對應於持有當前執行內核的 BTF 訊息的 vmlinux 檔案。
在執行 hello-buffer-config
範例程式時,我們可以看到描述該程式使用的 BTF 訊息的條目:
149: name <anon> size 4372B prog_ids 319 map_ids 103
pids hello-buffer-co(7660)
這行告訴我們:
- 這塊 BTF 訊息的 ID 是 149
- 它是一個約 4KB 大小的匿名 BTF 訊息塊
- 它被 ID 為 319 的 BPF 程式和 ID 為 103 的 BPF 對映使用
- 它也被 PID 為 7660 的程式使用,該程式執行 hello-buffer-config 可執行檔案
這些程式、對映和 BTF 識別符號與 bpftool
顯示的 hello-buffer-config
的 hello
程式的輸出相比對:
bpftool prog show name hello
輸出:
319: kprobe name hello tag a94092da317ac9ba gpl
loaded_at 2022-08-28T14:13:35+0000 uid 0
xlated 400B jited 428B memlock 4096B map_ids
103,104
btf_id 149
pids hello-buffer-co(7660)
唯一看起來不完全比對的是程式參照了一個額外的對映 ID 104。這是效能事件緩衝區對映,它不使用 BTF 訊息,因此不會出現在 BTF 相關的輸出中。
BTF 型別探討
就像 bpftool
可以轉儲程式和對映的內容一樣,它也可以用來檢視 BTF 型別訊息。知道 BTF 訊息的 ID 後,我們可以使用 bpftool btf dump id <id>
命令檢查其內容。
讓我們看一個實際範例,分析一個簡單的 BPF 程式中的 BTF 型別定義。假設我們有以下原始碼:
struct user_msg_t {
char message[12];
};
BPF_HASH(config, u32, struct user_msg_t);
這個雜湊表有 u32
型別的鍵和 struct user_msg_t
型別的值。該結構包含一個 12 位元組的 message 欄位。
當我們使用 bpftool btf dump id 149
檢查 BTF 訊息時,前幾行輸出如下:
[1] TYPEDEF 'u32' type_id=2
[2] TYPEDEF '__u32' type_id=3
[3] INT 'unsigned int' size=4 bits_offset=0 nr_bits=32 encoding=(none)
讓我們詳細解析這三種型別:
- 型別 1 定義了一個名為
u32
的型別,其型別由 type_id 2 定義,即以[2]
開頭的行。 - 型別 2 的名稱為
__u32
,其型別由 type_id 3 定義。 - 型別 3 是一個名為
unsigned int
的整數型別,長度為 4 位元組。
這三種型別都是 32 位無符號整數型別的同義詞。在 C 語言中,整數的長度是平台相關的,所以 Linux 定義了像 u32
這樣的型別來明確定義特定長度的整數。
接下來的 BTF 輸出看起來像這樣:
[4] STRUCT 'user_msg_t' size=12 vlen=1
'message' type_id=6 bits_offset=0
[5] INT 'char' size=1 bits_offset=0 nr_bits=8 encoding=(none)
[6] ARRAY '(anon)' type_id=5 index_type_id=7 nr_elems=12
[7] INT '__ARRAY_SIZE_TYPE__' size=4 bits_offset=0 nr_bits=32 encoding=(none)
這些型別定義與 config
對映中使用的 user_msg_t
結構相關:
- 型別 4 是
user_msg_t
結構本身,總共 12 位元組。它有一個名為 “message” 的欄位,其型別由 type_id 6 定義。 - 型別 5 是
char
型別,大小為 1 位元組。 - 型別 6 是一個匿名陣列,其元素型別是 type_id 5(即
char
),索引型別是 type_id 7,元素數量是 12。 - 型別 7 是
__ARRAY_SIZE_TYPE__
,這是一個 4 位元組的整數型別,用於表示陣列大小。
從這個分析中,我們可以看到 BTF 如何精確描述資料結構的佈局,包括每個欄位的型別、大小和偏移量。這些訊息使得 CO-RE 能夠在不同核心版本之間正確調整結構佈局,確保 BPF 程式能夠正確存取資料。