一文看懂 eBPF|eBPF 的簡單使用

eBPF(extended Berkeley Packet Filter) 可謂 Linux 社區的新寵,很多大公司都開始投身於 eBPF 技術,如 Goole、Facebook、Twitter 等。

eBPF 究竟有什麼魅力讓大家都關注它呢?

這是因爲 eBPF 增加了內核的可擴展性,讓內核變得更加靈活和強大。

如果大家玩過 樂高積木 的話就會深有體會,樂高積木就是通過不斷向主體添加積木來組合出更龐大的模型。

而 eBPF 就像樂高積木一樣,可以不斷向內核添加 eBPF 模塊來增強內核的功能。

本文分爲 3 篇:

  1. eBPF 的簡單使用

  2. eBPF 的實現原理

  3. kprobes 在 eBPF 中的實現原理

看完這 3 篇文章,估計對 eBPF 也有較深的理解了。

什麼是 eBPF

eBPF 全稱 extended Berkeley Packet Filter,中文意思是 擴展的伯克利包過濾器。一般來說,要向內核添加新功能,需要修改內核源代碼或者編寫 內核模塊 來實現。而 eBPF 允許程序在不修改內核源代碼,或添加額外的內核模塊情況下運行。

從 eBPF 的名字看,好像是專門爲過濾網絡包而創造的。其實,eBPF 是從 BPF(也稱爲 cBPF:classic Berkeley Packet Filter)發展而來的,BPF 是專門爲過濾網絡數據包而創造的。

但隨着 eBPF 不斷完善和加強,現在的 eBPF 已經不再限於過濾網絡數據包了。

eBPF 架構

我們先來看看 eBPF 的架構,如下圖所示:

下面用文字來描述一下:

用戶態

  1. 用戶編寫 eBPF 程序,可以使用 eBPF 彙編或者 eBPF 特有的 C 語言來編寫。

  2. 使用 LLVM/CLang 編譯器,將 eBPF 程序編譯成 eBPF 字節碼。

  3. 調用 bpf() 系統調用把 eBPF 字節碼加載到內核。

內核態

  1. 當用戶調用 bpf() 系統調用把 eBPF 字節碼加載到內核時,內核先會對 eBPF 字節碼進行安全驗證。

  2. 使用 JIT(Just In Time)技術將 eBPF 字節編譯成本地機器碼(Native Code)。

  3. 然後根據 eBPF 程序的功能,將 eBPF 機器碼掛載到內核的不同運行路徑上(如用於跟蹤內核運行狀態的 eBPF 程序將會掛載在 kprobes 的運行路徑上)。當內核運行到這些路徑時,就會觸發執行相應路徑上的 eBPF 機器碼。

如果大家使用過 Java 編寫程序的話,會發現 eBPF 與 Java 的 AOP(Aspect Oriented Programming 面向切面編程)概念很像。

爲了讓有 Java 經驗的同學更容易接受 eBPF 技術。我們先介紹一下 Java 中的 AOP 概念。

在 AOP 概念中,有兩個很重要的角色:切點 和 攔截器

  1. 切點:程序中某個具體的業務點(方法)。

  2. 攔截器:攔截器其實是一段 Java 代碼,用於攔截切點在執行前(或執行後),先運行這段 Java 代碼。

eBPF 程序就像 AOP 中的攔截器,而內核的某個運行路徑就像 AOP 中的切點。

根據掛載點功能的不同,大概可以分爲以下幾個模塊:

  1. 性能跟蹤

  2. 網絡

  3. 容器

  4. 安全

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 程序的步驟如下:

  1. 使用 C 語言編寫 eBPF 程序的內核態功能(也就是運行在內核態的 eBPF 程序)。

  2. 使用 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()

下面我們來看看每一行代碼的具體含義:

運行 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