一文看懂 eBPF|eBPF 的簡單使用
eBPF(extended Berkeley Packet Filter)
可謂 Linux 社區的新寵,很多大公司都開始投身於 eBPF
技術,如 Goole、Facebook、Twitter 等。
eBPF 究竟有什麼魅力讓大家都關注它呢?
這是因爲 eBPF 增加了內核的可擴展性,讓內核變得更加靈活和強大。
如果大家玩過 樂高積木
的話就會深有體會,樂高積木就是通過不斷向主體添加積木來組合出更龐大的模型。
而 eBPF 就像樂高積木一樣,可以不斷向內核添加 eBPF 模塊來增強內核的功能。
本文分爲 3 篇:
-
eBPF 的簡單使用
-
eBPF 的實現原理
-
kprobes 在 eBPF 中的實現原理
看完這 3 篇文章,估計對 eBPF 也有較深的理解了。
什麼是 eBPF
eBPF 全稱 extended Berkeley Packet Filter,中文意思是 擴展的伯克利包過濾器
。一般來說,要向內核添加新功能,需要修改內核源代碼或者編寫 內核模塊
來實現。而 eBPF 允許程序在不修改內核源代碼,或添加額外的內核模塊情況下運行。
從 eBPF 的名字看,好像是專門爲過濾網絡包而創造的。其實,eBPF 是從 BPF(也稱爲 cBPF:classic Berkeley Packet Filter)發展而來的,BPF 是專門爲過濾網絡數據包而創造的。
但隨着 eBPF 不斷完善和加強,現在的 eBPF 已經不再限於過濾網絡數據包了。
eBPF 架構
我們先來看看 eBPF 的架構,如下圖所示:
下面用文字來描述一下:
用戶態
-
用戶編寫 eBPF 程序,可以使用 eBPF 彙編或者 eBPF 特有的 C 語言來編寫。
-
使用 LLVM/CLang 編譯器,將 eBPF 程序編譯成 eBPF 字節碼。
-
調用
bpf()
系統調用把 eBPF 字節碼加載到內核。
內核態
-
當用戶調用
bpf()
系統調用把 eBPF 字節碼加載到內核時,內核先會對 eBPF 字節碼進行安全驗證。 -
使用
JIT(Just In Time)
技術將 eBPF 字節編譯成本地機器碼(Native Code)。 -
然後根據 eBPF 程序的功能,將 eBPF 機器碼掛載到內核的不同運行路徑上(如用於跟蹤內核運行狀態的 eBPF 程序將會掛載在
kprobes
的運行路徑上)。當內核運行到這些路徑時,就會觸發執行相應路徑上的 eBPF 機器碼。
如果大家使用過 Java 編寫程序的話,會發現 eBPF 與 Java 的 AOP(Aspect Oriented Programming 面向切面編程)概念很像。
爲了讓有 Java 經驗的同學更容易接受 eBPF 技術。我們先介紹一下 Java 中的 AOP 概念。
在 AOP 概念中,有兩個很重要的角色:切點
和 攔截器
。
-
切點
:程序中某個具體的業務點(方法)。 -
攔截器
:攔截器其實是一段 Java 代碼,用於攔截切點在執行前(或執行後),先運行這段 Java 代碼。
eBPF 程序就像 AOP 中的攔截器,而內核的某個運行路徑就像 AOP 中的切點。
根據掛載點功能的不同,大概可以分爲以下幾個模塊:
-
性能跟蹤
-
網絡
-
容器
-
安全
eBPF 使用
在介紹 eBPF 的實現前,我們先來介紹一下如何使用 eBPF 來跟蹤 fork()
系統調用的運行情況。
編寫 eBPF 程序有多種方式,比如使用原生 eBPF 彙編來編寫,但使用原生 eBPF 彙編編寫程序的難度較大,所以一般不建議。
也可以使用 eBPF 受限的 C 語言來編寫,難度比使用原生 eBPF 彙編簡單些,但對初學者來說也不是十分友好。
最簡單是使用 BCC 工具來編寫,BCC 工具幫我們簡化了很多繁瑣的工作,比如不用編寫加載器。
下面我們將使用 BCC 工具來介紹怎麼編寫一個 eBPF 程序。
注意:由於 eBPF 對內核的版本有較高的要求,不同版本的內核對 eBPF 的支持可能有所不相同。所以使用 eBPF 時,最好使用最新版本的內核。
本文使用
Ubuntu 20.20
(內核版本爲 5.8.1)作爲解說。
1. BCC 工具安裝
在 Ubuntu 系統中安裝 BCC 工具是比較簡單的,可以使用以下命令:
$ sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
BCC 工具可以讓你使用 Python 和 C 語言組合來編寫 eBPF 程序。
安裝完成後,可以使用命令 bcc -v
來測試是否安裝成功。如果安裝失敗,可以參考官網安裝文檔,如下:
https://github.com/iovisor/bcc/blob/master/INSTALL.md
2. 編寫 eBPF 版的 hello world
一般編程課的第一步都是編寫著名的 hello world
程序,所以我們也以編寫 hello world
程序作爲第一步吧。
使用 BCC 編寫 eBPF 程序的步驟如下:
-
使用 C 語言編寫 eBPF 程序的內核態功能(也就是運行在內核態的 eBPF 程序)。
-
使用 Python 編寫加載代碼和用戶態功能。
爲什麼不能全部使用 Python 編寫呢?這是因爲 LLVM/Clang 只支持將 C 語言編譯成 eBPF 字節碼,而不支持將 Python 代碼編譯成 eBPF 字節碼。
所以,eBPF 內核態程序只能使用 C 語言編寫。而 eBPF 的用戶態程序可以使用 Python 進行編寫,這樣就能簡化編寫難度。
所以,第一步就是編寫 eBPF 內核態程序。
使用 C 編寫 eBPF 程序
新建一個 hello.c 文件,並輸入下面的內容:
int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}
使用 Python 和 BCC 工具開發一個用戶態程序
新建一個 hello.py 文件,並輸入下面的內容:
#!/usr/bin/env python3
# 1) 加載 BCC 庫
from bcc import BPF
# 2) 加載 eBPF 內核態程序
b = BPF(src_file="hello.c")
# 3) 將 eBPF 程序掛載到 kprobe
b.attach_kprobe(event="do_sys_openat2", fn_)
# 4) 讀取並且打印 eBPF 內核態程序輸出的數據
b.trace_print()
下面我們來看看每一行代碼的具體含義:
-
導入了 BCC 庫的 BPF 模塊,以便接下來調用。
-
調用 BPF() 函數加載 eBPF 內核態程序(也就是我們編寫的 hello.c)。
-
將 eBPF 程序掛載到內核探針(簡稱 kprobe),其中
do_sys_openat2()
是系統調用openat()
在內核中的實現。 -
讀取內核調試文件
/sys/kernel/debug/tracing/trace_pipe
的內容(bpf_trace_printk()
函數會將信息寫入到此文件),並打印到標準輸出中。
運行 eBPF 程序
用戶態程序開發完成之後,最後一步就是執行它了。需要注意的是,eBPF 程序需要以 root
用戶來運行:
$ sudo python3 hello.py
運行後,可以看到如下輸出:
$ sudo python3 hello.py
b' python3-31683 [001] .... 614653.225903: 0: Hello, World!'
b' python3-31683 [001] .... 614653.226093: 0: Hello, World!'
b' python3-31683 [001] .... 614653.226606: 0: Hello, World!'
b' <...>-31684 [000] .... 614654.387288: 0: Hello, World!'
b' irqbalance-669 [000] .... 614658.232433: 0: Hello, World!'
...
到了這裏,我們已經成功開發並運行了第一個 eBPF 程序。當然,這個程序很簡單,並且也沒有實際的用途。
但通過這個程序,我們大概可以知道使用 BCC 開發一個 eBPF 程序的步驟。
因爲本系列文章並不是介紹如何開發 eBPF 程序,而是介紹 eBPF 的原理和實現。如果大家有興趣學習如何開發 eBPF 程序,那麼建議大家看看《BPF 性能之巔》這本書,這本書詳細地介紹瞭如何開發 eBPF 程序。
在下篇文章中,我們將介紹 eBPF 的實現原理,敬請期待。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/V-5k1mX5JRA0lWLXJ2AxpA