摘要: 本系列為《Learning eBPF》一書的翻譯系列。 (內容並非機翻,部分夾帶私貨)筆者學習自用,歡迎大家討論學習。 ...
前一章講了 eBPF 為什麼這麼弔,不理解沒關係,現在開始,我們通過一個 “Hello world” 例子,來真正入門一下。
BCC Python
框架是上手 eBPF 的最友好方式。來看。
2.1 BCC 的 Hello World
下麵的程式是一段 BCC 框架的 Hello World 程式。
#!/usr/bin/python3
from bcc import BPF
program = r"""
int hello(void *ctx) {
bpf_trace_printk("Hello World!\n");
return 0;
}
"""
b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
b.trace_print()
這段程式包含了兩部分:
- 運行在內核態的 eBPF 程式本身(
hello()
); - 運行在用戶態的,用於載入 eBPF 程式到內核空間並讀取它生成的 trace 控製程序(
hello.py
)。
下圖顯示了這段代碼運行時的狀態。
下麵來逐行解釋這段代碼。
第一行告訴你,這是一個 Python 程式。實際上 #!/usr/bin/python3
是指定預設的 Python 解釋器。
eBPF 程式本身是 C 語言編寫的。這部分代碼為:
int hello(void *ctx) {
bpf_trace_printk("Hello World!");
return 0;
}
其中,bpf_trace_printk()
是 eBPF 輔助函數,用於列印一條消息。有關輔助函數的更多討論,見第 5 章。
這段 eBPF 程式是以靜態字元串 program
的形式被定義在 Python 腳本中,並作為參數,傳遞給 BPF 對象:
b = BPF(text=program)
當然,C 程式最終會由 BCC 框架負責編譯執行。
eBPF 程式需要綁定到一個事件上。在這個例子中,我們選擇的事件為 execve
系統調用。當有任何應用程式運行時,都會調用 execve()
,從而觸發我們綁定的 eBPF。然而,execve
系統調用在不同架構的 Linux 上可能會有不同的實現方式。但是,eBPF 提供了一種非常方便的方式(通過名稱)來尋找當前支持的系統調用,就像這樣:
syscall = b.get_syscall_fnname("execve")
現在,變數 syscall
指代了系統調用。接下來,使用一個探針 kprobe
(詳見第 1 章)來將 hello()
函數綁定到 execve
事件上。
b.attach_kprobe(event=syscall, fn_name="hello")
此時,eBPF 程式已經被成功載入到內核,並完成了綁定。那麼,當有一個進程被執行時,將觸發這段 hello()
程式,完成一條消息的列印。剩下的工作,就是去讀取 trace 的輸出,並列印到標準輸出中。
b.trace_print()
trace_print()
函數將進入無限迴圈,直到你鍵入Ctrl+C
終止這段 eBPF 程式。
下麵這張圖顯示了這段 eBPF 程式的運行原理:
根據這張圖回顧一下整個流程。
1)這段 Python 程式編譯了 C 代碼,載入內核,並與 execve()
完成綁定。
2)當有其他進程運行時,執行 execve()
系統調用,觸發 eBPF 中的 hello()
程式段,列印一行輸出(在 pipe 中,後文會再次提到)。
3)用戶態的程式讀取這些輸出,並列印到屏幕上。
2.2 運行 Hello World
運行這段程式,其結果取決於你當前的運行環境正在或即將運行的進程。
如果這段代碼啥也沒輸出,請再起一個終端,手動執行一個程式。eBPF 將列印一行行的 Hello world 消息。
書里沒有提到,但是很重要,運行 BCC 框架的 eBPF 程式,需要先安裝 bcc-python 庫。譯者使用 REHL8-x86 操作系統,因此通過 yum 包管理器來安裝:
yum install -y python3-bcc.x86_64
。
這裡書中再次強調,eBPF 程式是立即生效的。首先是不需要重啟,其次是對應用程式無侵入(已經重覆很多遍了)。這是因為,eBPF 所綁定的是 execve()
系統調用,因此和應用程式沒關係。即使你寫了一個腳本,手動調用這個系統調用,那麼,這個 eBPF 也會觸發。
列印輸出除了 “Hello World” 字元串以外,還有其他信息。例如,執行 execve
的進程 ID 為 5412,並使用了 bash
命令等等。Python 程式從哪裡讀取這個輸出信息的呢?實際上,bpf_trace_printk()
輔助函數會把列印寫入 /sys/kernel/debug/tracing/trace_pipe
文件中。你可以通過 cat
指令來查看(需要 root 許可權)。
eBPF 程式使用這種方式列印信息,雖然簡單,但卻有下麵兩點局限性:
- 僅支持字元串類型的輸出。你想傳結構體類型?沒門。
trace_pipe
文件只有這一個。也就是說,所有正在運行的 eBPF 都會把輸出寫入到這裡。難受吧!
那麼,有沒有一種更好的方式傳遞數據呢??答案就是:eBPF 映射(maps
)。
2.3 eBPF 映射:maps
映射 maps
是 eBPF 的擴展功能,它是一類可以讓 eBPF 程式和用戶態程式訪問的數據結構。
maps
支持內核態 eBPF 之間的通信,也支持 eBPF 到用戶態程式之間的通信。主要的作用包括以下幾種:
- 用戶空間寫入需要由 eBPF 程式檢索的配置信息。
- 一個 eBPF 存儲狀態,以供另一個 eBPF 程式(或者同一 eBPF 的後續指令)使用。
- eBPF 程式將數據寫入
maps
,以供用戶空間應用程式讀取,從而列印結果。
eBPF maps
有很多種類型,在 uapi/linux/bpf.h
文件中可以查看,內核文檔中也有相關的介紹。
通常,eBPF maps
都是鍵值對類型結構,但具體 key
和 value
的指代和形式又有所區別。本章,將主要介紹 hash
、perf
、 ring buffer
以及 eBPF 程式數組
。
誠然,eBPF maps
不止這些。
有些 map
,形似數組,但其 key
小得僅有 4 位元組;【array】
有些 map
,如哈希表,key
的種類能夠包羅萬象;【hash】
有些 map
,便利操作,或 FIFO
列隊而伺,或 FILO
作棧而生;或 LRU
行冷熱數據分離,或 LPM
做最長首碼匹配;【queue、stack、lru、lpm、Bloom filter】
有些 map
,特殊對象專用,拓寬網路和尾調用的技術;【sockmaps、devmaps、program array、map-of-map】
有些 map
,對應CPU核心,尋求併發操作的可能性。【cpu-*】
接下來的例子,我們來看一下使用哈希表類型的 map
基本用法。
2.3.1 哈希表 map
在上一個給出的例子中,我們的 eBPF 程式綁定了 execve()
系統調用。接下來,要用哈希表 HASH
做一下改編,key
用來存儲用戶 ID,value
用來記錄某個用戶下的進程執行調用 execve()
的次數。這個程式統計了不同的用戶分別運行了多少個程式。
來看這個 eBPF 程式的 C 代碼。
BPF_HASH(counter_table); // A
int hello(void *ctx) {
u64 uid;
u64 counter = 0;
u64 *p;
uid = bpf_get_current_uid_gid() & 0xFFFFFFFF; // B
p = counter_table.lookup(&uid); // C
if (p != 0) { // D
counter = *p;
}
counter++; // E
counter_table.update(&uid, &counter); // F
return 0;
}
代碼解釋:
【A】BPF_HASH()
是一個 BCC
巨集聲明的哈希表。
【B】bpf_get_current_uid_gid()
是一個輔助函數,用來獲取當前進程的用戶 ID。這個輔助函數返回值是一個 64 位的值,其中,用戶 ID 存儲在低 32 位(高 32 位為用戶組 ID)。
【C】通過 key
查找哈希表中的 value
。這裡是通過 uid
查找 p
。返回一個指針。
【D】如果指定的 uid
,在哈希表中存在一個 p
,將哈希表中的 p
值設置給 counter
;若哈希表中不存在對應 uid
的 p
,counter
的值將為預設值 0
。
【E】無論 counter
值為多少,在這裡都對其進行自增操作。
【F】使用新的 counter
值,更新對應 uid
的哈希表。
我們仔細看一下這兩行代碼。首先是查找哈希表 value
:
p = counter_table.lookup(&uid);
然後是更新哈希表:
counter_table.update(&uid, &counter);
你可能會有點疑問了:C 語言能這麼寫?結構體可以直接調用成員函數?不對吧?實際上,你是對的,C 語言確實不支持在結構體中定義這樣的函數。但是,BCC 框架中的 C,實際上是一種不嚴格的 C。BCC 在真正執行 C 代碼的編譯前,會重寫這些不嚴格的語法(實際上是通過若幹個 BCC 巨集來實現的)。
接下來,和前面的例子一樣,將這段 C 程式聲明為一個 program
字元串,然後通過 BCC 將其編譯載入內核,並綁定在 execve()
系統調用上。
b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
但這次,還需要一些額外的工作,在用戶態中讀取哈希表的內容。
while true: # A
sleep(2)
s = ""
for k, v in b["counter_table"].items(): # B
s += f"ID {k.value}: {v.value}\t"
print(s)
代碼解釋:
【A】無限迴圈。每隔 2s 列印輸出。
【B】BCC 框架會自動創建一個 Python 對象來指代哈希表。這個迴圈將會遍歷 eBPF 定義的 counter_table
哈希表中的所有鍵值對,然後完成列印。
運行這段程式,你需要兩個終端。終端 1 運行 eBPF 程式,終端 2 運行指令。
可以看到,每 2s 輸出一行。我們關註最後一行的兩個鍵值對:
key = 501, value = 5
key = 0, value = 2
在第二個終端里,作者的用戶 ID 為 501。當運行 ls
命令時,值為 501 的 uid
計數器自增 1。而當運行 sudo ls
時,發生了兩次 execve()
。第一次是在 501 用戶下的 sudo
命令,第二次是在 root
用戶下的 ls
命令。
這個例子給出了使用哈希表 map
從內核態向用戶態傳遞數據的方式。當然,你也可以使用數組類型的 map
來實現這個功能(因為 key
為整數)。
Linux 內核中存在一個 名為perf
的子系統,也可以傳遞內核態數據到用戶空間,eBPF 剛好也支持這種方式。我們來看一下。
2.3.2 Perf 和 Ring buffer map
在這一小節中,我們再來看一種更複雜的 “Hello World” BCC 程式,它使用了 Perf
環形緩衝區,用來向用戶態傳遞自定義的數據結構。
環形緩衝區:內核 5.8 版本才引入的結構,在這之前為普通的基於共用記憶體的緩衝區。實際上
perf
環形緩衝區更有優勢,具體可以參考 Andrii Nakryiko 的這篇博客: https://nakryiko.com/posts/bpf-ringbuf/
那麼,問題來了,什麼是環形緩衝區?
環形緩衝區
是一種數據結構,它不是 eBPF 獨有的。環形緩衝區實際上是一段記憶體空間,其空間中的地址在邏輯上首尾相連成環。環形緩衝區包括兩個工作指針,一個負責讀,一個負責寫,二者同向移動。寫指針指向的位置就是下個數據被寫入的位置(數據可以任意長度,其長度信息包含在數據頭中),同理,讀指針指向的位置就是下一個需要讀取的數據開頭(根據數據頭中的長度,控制讀指針移動距離)。
下圖直觀的展示了環形緩衝區的樣貌。
讀指針和寫指針始終朝著一個方向運動。若在某一時刻,讀指針追上了寫指針,則說明緩衝區沒數據可讀了。相反,若寫指針追上了讀指針,則說明緩衝區沒空間可寫了,那麼此時,需要寫入的數據就會被丟棄(丟棄計數器會增加)。如果你控制的好,讀寫指針以相同的速率運動,始終不會相遇,那麼恭喜你,你便擁有了一個無限大的迴圈緩衝區可以使用。
瞭解了環形緩衝區的概念後,我們再來改進一下之前綁定到 execve()
的 eBPF 程式,來實時列印運行進程的簡單信息。
BPF_PERF_OUTPUT(output); // A
struct data_t { // B
int pid;
int uid;
char command[16];
char message[12];
};
int hello(void *ctx) {
struct data_t data = {}; // C
char message[12] = "Hello World";
dara.pid = bpf_get_current_pid_tgid() >> 32; // D
data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF; // E
bpf_get_current_comm(&data.command, sizeof(data.command)); // F
bpf_probe_read_kernel(&data.message, sizeof(data.message), message); // G
output.perf_submit(ctx, &data, sizeof(data)); // H
return 0;
}
代碼解釋:
【A】BCC 框架聲明瞭一個巨集定義 BPF_PERF_OUTPUT
,用來創建一個 perf
映射區域,以便內核態可以向用戶態傳遞消息。這裡定義為 output
。
【B】每次 hello()
運行之時,都會填充一個結構體來存儲關鍵欄位。這是結構體定義,包括進程 ID、用戶 ID、當前運行指令名稱以及 message
信息。
【C】data
被定義為本地變數,message
被賦值為 "Hello world"
字元串。
【D】bpf_get_current_pid_tgid()
,輔助函數,用於獲取觸發當前 eBPF 程式的進程 ID。該函數返回一個 64 位的值,高 32 位是進程 ID(低 32 位為線程組 ID,對於單線程的進程,同為進程 ID)。
【E】bpf_get_current_uid_gid()
,輔助函數,前文介紹過,用於獲取用戶 ID。
【F】bpf_get_current_comm()
,輔助函數,用於獲取當前執行的指令名稱。
在 C 語言中,你不可以直接使用
"="
賦值字元串,你需要傳入一個待寫入字元串的地址。
【G】這個例子中,message = "Hello World"
,bpf_probe_read_kernel()
輔助函數會將它拷貝到 data
結構體的對應位置。
【H】此時,data
結構體中已經填充了 pid
、uid
、command[]
以及 message[]
。這裡調用 output.perf_submit()
將 data
結構體提交到 map
中。
接下來,與第一個 “Hello World” 程式類似,這一段 C 程式將被定義為一段字元串 program
,下麵是 Python 代碼。
b = BPF(text=program) # A
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
def print_event(cpu, data, size): # B
data = b["output"].event(data)
print(f"{data.pid} {data.uid} {data.command.decode()} {data.message.decode()}")
b["output"].open_perf_buffer(print_event) # C
while True: ![image](uploading...)# D
b.perf_buffer_poll()
代碼解釋:
【A】編譯、載入、綁定 eBPF C程式。不再贅述。
【B】print_event()
是一個回調函數,用於將 data
的內容列印到屏幕上。BCC 已經做了很多復繁重的工作,因此你只需要簡單的 b["output"].event()
來從內核態 map
中獲取數據。
【C】b["output"].open_perf_buffer()
用於打開 perf ring buffer
。該函數接收 print_event
參數,是將其聲明為一個回調。即,當 perf ring buffer
中有數據時,觸發回調,列印這個數據。
【D】無限迴圈,調用 perf_buffer_poll()
拉取 perf ring buffer
內容。
運行這段程式,你能得到以下輸出:
和以前一樣,你可能需要另起一個終端,執行命令,來驗證你的程式。
這個例子和最初的 “Hello World” 程式最大的不同就是,我們不再使用有限的 trace pipe
傳遞數據,而使用了 perf ring buffer
。執行原理通先前也有了些許區別,如下圖所示。
通過環形緩衝區傳遞數據會不會仍然使用了 trace pipe
呢?你可以運行一下命令檢驗一下:
cat /sys/kernel/debug/tracing/trace_pipe
這個例子還給出了一些輔助函數的使用示例,第 7 章我們會更加詳細討論。
這些輔助函數主要輔助於檢索事件觸發時的上下文信息,輔助函數的合理使用,能夠極大提高性能。因為這些上下文信息產生於內核、收集於內核、最後仍然應用於內核。這減少了很多不必要的內核態和用戶態的切換。
2.3.3 函數調用
能否在 eBPF 程式的 C 代碼中將重覆代碼塊抽象成函數,並執行函數調用?這個看似簡單的動作,在早先的 eBPF版本中並不支持(僅支持調用輔助函數)。如果你非要調用自定義函數,有沒有方法呢?當然有,你可以將其聲明為內聯函數。就像下麵這樣。
static __always_inline void my_function(void *ctx, int val)
__always_inline
修飾符會在編譯期間,對當前函數進行優化。
那麼,普通函數和內聯函數有什麼區別呢?我們可以用一張圖來加以說明:
對於普通函數(上圖左側),當函數 F 被調用時,順序執行的指令會跳轉到函數 F 的起始地址(函數調用實際上就是地址切換),執行 F 的指令序列。當函數 F 執行完畢,return
語句會再次跳轉回函數 F 調用前的位置,接續進行。
對於內聯函數(上圖右側),並沒有地址跳轉,因為編譯時這個函數會完全編譯到順序執行的指令序列中。
但是,內聯函數是有局限性的。如果你在多個位置調用了同一個內聯函數,那麼在最終的可執行文件中,必然會產生該函數的多個指令副本。(這也是為啥通過 kprobe
探針無法綁定到內核內聯函數的原因,我們第 7 章再來看這個問題)
直到 4.16 版本的內核以及 6.0 版本的 LLVM,eBPF 中內聯函數的限制才被取消。因此,在這之後,你可以放心地定義函數調用(但必須是 static
的)。
2.3.4 尾調用
尾調用是什麼?引用 ebpf.io 網站的一句介紹:“尾調用允許 eBPF 調用和執行另一個 eBPF 並替換執行上下文,類似於一個進程執行 execve()
系統調用的方式。”
換句話說,尾調用之後,函數不會再返回給調用者了。
Tail calls can call and execute another eBPF program and replace the execution context, similar to how the execve() system call operates for regular processes.
尾調用也不是 eBPF 獨有的思想。eBPF 為什麼要使用尾調用呢?這是因為,eBPF 的運行棧太有限了(僅有 512 位元組),在遞歸調用函數時(實際上是向運行棧中一節一節地添加棧幀),很容易導致棧溢出。而尾調用恰恰允許在不增加堆棧的情況下,調用一系列函數。這是非常有效且實用的。
你可以使用下麵的輔助函數來增加一個尾調用:
long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)
其三個參數的含義分別是:
ctx
向被調用者傳遞當前 eBPF 程式的上下文信息。prog_array_map
是一個程式數組(BPF_MAP_TYPE_PROG_ARRAY
)類型的 eBPFmap
,用於記錄一組 eBPF 程式的文件描述符。index
為程式數組中需要調用的 eBPF 程式索引。
這個輔助函數一旦運行成功,就不會返回了。因為調用者的運行棧已經被下一個 eBPF 程式的運行棧替換了。當然,如果指定 index
的 eBPF 程式不存在,該輔助函數也會執行失敗,此時調用者繼續執行。無事發生。
需要註意的是,若使用尾調用,所有需要執行的 eBPF 程式需要同時載入到內核中。而且還需要設置好程式數組 map
。
使用 BCC 框架如何進行尾調用呢?可以使用下麵簡單的方式:
prog_array_map.call(ctx, index)
在編譯它之前,BCC 框架會自動將其轉換為標準的尾調用輔助函數:
bpf_tail_call(ctx, prog_array_map, index)
下麵來看一個使用尾調用的 BCC 框架的具體例子。
BPF_PROG_ARRAY(syscall, 300); // A
int hello(struct bpf_raw_tracepoint_args *ctx) { // B
int opcode = ctx->args[1]; // C
syscall.call(ctx, opcode); // D
bpf_trace_printk("Another syscall: %d", opcode); // E
return 0;
}
int hello_execve(void *ctx) { // F
bpf_trace_printk("Executing a program");
return 0;
}
int hello_timer(struct bpf_raw_tracepoint_args *ctx) { // G
if (ctx->args[1] == 222) {
bpf_trace_printk("Creating a timer");
} else if (ctx->args[1] == 226) {
bpf_trace_printk("Deleting a timer");
} else {
bpf_trace_printk("Some other timer operation");
}
return 0;
}
int ignore_opcode(void *ctx) { // H
return 0;
}
代碼解釋:
【A】BPF_PROG_ARRAY
巨集定義,對應映射類型 BPF_MAP_TYPE_PROG_ARRAY
。在這裡,命名為 syscall
,容量為 300。
【B】即將被用戶態代碼綁定在 sys_enter
類別的 Tracepoint
上,即當有任何系統調用被執行時,都會觸發這個函數。bpf_raw_tracepoint_args
類型的結構體 ctx
存放上下文信息。
譯者註:
sys_enter
是raw_syscalls
類型的Tracepoint
;同族還有sys_exit
。詳細信息可查看文件:
/sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/format
【C】對於 sys_enter
類型的追蹤點,其參數第 2 項為操作碼,即指代即將執行的系統調用號。這裡賦值給變數 opcode
。
【D】這一步,我們把 opcode
作為索引,進行尾調用,執行下一個 eBPF 程式。
再次提醒,這裡的寫法是 BCC 優化,在真正編譯前,BCC 最終會將其重寫為
bpf_tail_call
輔助函數。
【E】如果尾調用成功,這一行將永遠不會被執行。添加這一行的原因是保底輸出,防止程式數組 map
沒有命中。
【F】hello_execve()
,程式數組的一項,對應 execve()
系統調用。經由尾調用觸發。
【G】hello_timer()
,程式數組的一項,對應計時器相關的系統調用。經由尾調用觸發。
【H】ignore_opcode()
,程式數組的一項,用於忽略我們不關心的系統調用。經由尾調用觸發。
現在,我們來看一下用戶態的程式(重點,如何載入和設置尾調用)。
b = BPF(text=program)
b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello") # A
ignore_fn = b.load_func("ignore_opcode", BPF.RAW_TRACEPOINT) # B
exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)
timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)
prog_array = b.get_table("syscall") # C
prog_array[ct.c_int(59)] = ct.c_int(exec_fn.fd)
prog_array[ct.c_int(222)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(223)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(224)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(225)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(226)] = ct.c_int(timer_fn.fd)
# Ignore same syscalls that come up a lot # D
prog_array[ct.c_int(21)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(22)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(25)] = ct.c_int(ignore_fn.fd)
...
b.trace_print() # E
代碼解釋:
【A】與前文綁定到 kprobe
不同,這次用戶態將 hello()
主 eBPF 程式綁定到 sys_enter
追蹤點(Tracepoint
)上.
【B】這些 load_func()
方法用來將每個尾調用函數載入內核,並返回尾調用函數的文件描述符
。尾調用需要和父調用保持相同的程式類型(這裡是 BPF.RAW_TRACEPOINT
)。
一定不要混淆,每個尾調用程式本身就是一個 eBPF 程式。
【C】接下來,向我們創建好的 syscall
程式數組中添充條目。大可不必全部填滿,如果執行時遇到空的,那也沒啥影響。同樣的,將多個 index
指向同一個尾調用也是可以的(事實上這段程式就是這樣做的,將計時器相關的系統調用指向同一個 eBPF 尾調用)。
譯者註:這裡的
ct.c_int()
來自 Python 的 ctypes 庫,用於 Python 到 C 的類型轉換。
【D】由於一些系統調用會頻繁地被執行,所以使用 ignore_opcode()
尾調用將他們忽略掉。
【E】不斷列印輸出,直到用戶終止程式。
運行這段程式,獲得下麵的輸出:
當遇到尾調用沒匹配上的系統調用時,會輸出 “Another syscall”。
內核 4.2 版本才開始支持尾調用,然而在很長的一段時間內,尾調用和 BPF 的編譯過程不太相容(尾調用需要 JIT 編譯器的支持)。直到 5.10 版本才解決了這個問題。
你可以最多鏈接 33 個尾調用(而每個 eBPF 程式的指令複雜度最大支持 100w)。這樣一來,eBPF 才能真正發揮出巨大潛力來了。
2.4 小結
本章給出了 eBPF BCC 框架實現的 “Hello World” 程式,以及它的一些變體。同時,也介紹了 eBPF maps 在內核和用戶態交互之間的應用。
BCC 框架為我們提供了很好的封裝,我們不需要瞭解程式具體要如何編譯、如何載入內核以及如何綁定事件,即可成功運行我們的自定義邏輯。
但作為學習者,僅瞭解這些是不夠的。eBPF 程式到底怎麼執行?看來要深入地剖析了。且聽下回分解。