eBPF 入門教程

語言: CN / TW / HK

作者:Adrian Ratiu

譯者:狄衛華

注:本文已取得作者本人的翻譯授權!

原文連結: https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/

1. 前言

有興趣瞭解更多關於 eBPF 技術的底層細節?那麼請繼續移步,我們將深入研究 eBPF 的底層細節,從其虛擬機器機制和工具,到在遠端資源受限的嵌入式裝置上執行跟蹤。

注意:本系列部落格文章將集中在 eBPF 技術,因此對於我們來講,文中 BPF 和 eBPF 等同,可相互使用。BPF 名字/縮寫已經沒有太大的意義,因為這個專案的發展遠遠超出了它最初的範圍。BPF 和 eBPF 在該系列中會交替使用。

  • 第 1 部分 [1]第 2 部分 [2] 為新人或那些希望通過深入瞭解 eBPF 技術棧的底層技術來進一步瞭解 eBPF 技術的人提供了深入介紹。

  • 第 3 部分 [3] 是對使用者空間工具的概述,旨在提高生產力,建立在第 1 部分和第 2 部分中介紹的底層虛擬機器機制之上。

  • 第 4 部分 [4] 側重於在資源有限的嵌入式系統上執行 eBPF 程式,在嵌入式系統中完整的工具鏈技術棧(BCC/LLVM/python 等)是不可行的。我們將使用佔用資源較小的嵌入式工具在 32 位 ARM 上交叉編譯和執行 eBPF 程式。只對該部分感興趣的讀者可選擇跳過其他部分。

  • 第 5 部分 [5] 是關於使用者空間追蹤。到目前為止,我們的努力都集中在核心追蹤上,所以是時候我們關注一下使用者程序了。

如有疑問時,可使用該流程圖:

2. eBPF 是什麼?

eBPF 是一個基於暫存器的虛擬機器,使用自定義的 64 位 RISC 指令集,能夠在 Linux 核心內執行即時本地編譯的 "BPF 程式",並能訪問核心功能和記憶體的一個子集。這是一個完整的虛擬機器實現,不要與基於核心的虛擬機器(KVM)相混淆,後者是一個模組,目的是使 Linux 能夠作為其他虛擬機器的管理程式。eBPF 也是主線核心的一部分,所以它不像其他框架那樣需要任何第三方模組( LTTng [6]SystemTap [7] ),而且幾乎所有的 Linux 發行版都預設啟用。熟悉 DTrace 的讀者可能會發現 DTrace/BPFtrace 對比 [8] 非常有用。

在核心內執行一個完整的虛擬機器主要是考慮便利和安全。雖然 eBPF 程式所做的操作都可以通過正常的核心模組來處理,但直接的核心程式設計是一件非常危險的事情 - 這可能會導致系統鎖定、記憶體損壞和程序崩潰,從而導致安全漏洞和其他意外的效果,特別是在生產裝置上(eBPF 經常被用來檢查生產中的系統),所以通過一個安全的虛擬機器執行本地 JIT 編譯的快速核心程式碼對於安全監控和沙盒、網路過濾、程式跟蹤、效能分析和除錯都是非常有價值的。部分簡單的樣例可以在這篇優秀的 eBPF 參考 [9] 中找到。

基於設計,eBPF 虛擬機器和其程式有意地設計為 不是 圖靈完備的:即不允許有迴圈(正在進行的工作是支援有界迴圈【譯者注:已經支援有界迴圈,#pragma unroll 指令】),所以每個 eBPF 程式都需要保證完成而不會被掛起、所有的記憶體訪問都是有界和型別檢查的(包括暫存器,一個 MOV 指令可以改變一個暫存器的型別)、不能包含空解引用、一個程式必須最多擁有 BPF_MAXINSNS 指令(預設 4096)、"主"函式需要一個引數(context)等等。當 eBPF 程式被載入到核心中,其指令被驗證模組解析為有向環狀圖,上述的限制使得正確性可以得到簡單而快速的驗證。

譯者注:BPF_MAXINSNS 這個限制已經被放寬至 100 萬條指令(BPF_COMPLEXITY_LIMIT_INSNS),但是非特權執行的 BPF 程式這個限制仍然會保留。

歷史上,eBPF (cBPF) 虛擬機器只在核心中可用,用於過濾網路資料包,與使用者空間程式沒有互動,因此被稱為 "伯克利資料包過濾器"【譯者注:早期的 BPF 實現被稱為經典 cBPF】。從核心 v3.18(2014 年)開始,該虛擬機器也通過 bpf() syscall [10]uapi/linux/bpf.h [11] 暴露在使用者空間,這導致其指令集在當時被凍結,成為公共 ABI,儘管後來仍然可以(並且已經)新增新指令。

因為核心內的 eBPF 實現是根據 GPLv2 授權的,它不能輕易地被非 GPL 使用者重新分發,所以也有一個替代的 Apache 授權的使用者空間 eBPF 虛擬機器實現,稱為 "uBPF"。撇開法律條文不談,基於使用者空間的實現對於追蹤那些需要避免核心-使用者空間上下文切換成本的效能關鍵型應用很有用。

3. eBPF 是怎麼工作的?

eBPF 程式在事件觸發時由核心執行,所以可以被看作是一種函式掛鉤或事件驅動的程式設計形式。從使用者空間執行按需 eBPF 程式的價值較小,因為所有的按需使用者呼叫已經通過正常的非 VM 核心 API 呼叫("syscalls")來處理,這裡 VM 位元組碼帶來的價值很小。事件可由 kprobes/uprobes、tracepoints、dtrace probes、socket 等產生。這允許在核心和使用者程序的指令中鉤住(hook)和檢查任何函式的記憶體、攔截檔案操作、檢查特定的網路資料包等等。一個比較好的參考是 Linux 核心版本對應的 BPF 功能 [12]

如前所述,事件觸發了附加的 eBPF 程式的執行,後續可以將資訊儲存至 map 和環形緩衝區(ringbuffer)或呼叫一些特定 API 定義的核心函式的子集。一個 eBPF 程式可以連結到多個事件,不同的 eBPF 程式也可以訪問相同的 map 以共享資料。一個被稱為 "program array" 的特殊讀/寫 map 儲存了對通過 bpf() 系統呼叫載入的其他 eBPF 程式的引用,在該 map 中成功的查詢則會觸發一個跳轉,而且並不返回到原來的 eBPF 程式。這種 eBPF 巢狀也有限制,以避免無限的遞迴迴圈。

執行 eBPF 程式的步驟:

  1. 使用者空間將位元組碼和程式型別一起傳送到核心,程式型別決定了可以訪問的核心區域【譯者注:主要是 BPF 幫助函式的各種子集】。

  2. 核心在位元組碼上執行驗證器,以確保程式可以安全執行(kernel/bpf/verifier.c)。

  3. 核心將位元組碼編譯為原生代碼,並將其插入(或附加到)指定的程式碼位置。【譯者注:如果啟用了 JIT 功能,位元組碼編譯為原生代碼】。

  4. 插入的程式碼將資料寫入環形緩衝區或通用鍵值 map。

  5. 使用者空間從共享 map 或環形緩衝區中讀取結果值。

map 和環形緩衝區結構是由核心管理的(就像管道和 FIFO 一樣),獨立於掛載的 eBPF 或訪問它們的使用者程式。對 map 和環形緩衝區結構的訪問是非同步的,通過檔案描述符和引用計數實現,可確保只要有至少一個程式還在訪問,結構就能夠存在。載入的 JIT 後代碼通常在載入其的使用者程序終止時被刪除,儘管在某些情況下,它仍然可以在載入程序的生命期之後繼續存在。

為了方便編寫 eBPF 程式和避免進行原始的 bpf()系統呼叫,核心提供了方便的 libbpf 庫 [13] ,包含系統呼叫函式包裝器,如 bpf_load_program [14] 和結構定義(如 bpf_map [15] ),在 LGPL 2.1 和 BSD 2-Clause 下雙重許可,可以靜態連結或作為 DSO。核心程式碼也提供了一些使用 libbpf 簡潔的例子,位於目錄 samples/bpf/ [16] 中。

4. 樣例學習

核心開發者非常可憐,因為核心是一個獨立的專案,因而沒有使用者空間諸如 Glibc、LLVM、JavaScript 和 WebAssembly 諸如此類的好東西! - 這就是為什麼核心中 eBPF 例子中會包含原始位元組碼或通過 libbpf 載入預組裝的位元組碼檔案。我們可以在 sock_example.c [17] 中看到這一點,這是一個簡單的使用者空間程式,使用 eBPF 來計算環回介面上統計接收到  TCP、UDP 和 ICMP 協議包的數量。

我們跳過微不足道的的 main [18]open_raw_sock [19] 函式,而專注於神奇的程式碼 test_sock [20]

static int test_sock(void)
{
int sock = -1, map_fd, prog_fd, i, key;
long long value = 0, tcp_cnt, udp_cnt, icmp_cnt;

map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 256, 0);
if (map_fd < 0) {printf("failed to create map'%s'\n", strerror(errno));
goto cleanup;
}

struct bpf_insn prog[] = {BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),
BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol) /* R0 = ip->proto */),
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */
BPF_LD_MAP_FD(BPF_REG_1, map_fd),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2),
BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */
BPF_RAW_INSN(BPF_STX | BPF_XADD | BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* xadd r0 += r1 */
BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */
BPF_EXIT_INSN(),};
size_t insns_cnt = sizeof(prog) / sizeof(struct bpf_insn);

prog_fd = bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, prog, insns_cnt,
"GPL", 0, bpf_log_buf, BPF_LOG_BUF_SIZE);
if (prog_fd < 0) {printf("failed to load prog'%s'\n", strerror(errno));
goto cleanup;
}

sock = open_raw_sock("lo");

if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0) {printf("setsockopt %s\n", strerror(errno));
goto cleanup;
}

首先,通過 libbpf API 建立一個 BPF map,該行為就像一個最大 256 個元素的固定大小的陣列。按 IPROTO_* [21] 定義的鍵索引網路協議(2 位元組的 word),值代表各自的資料包計數(4 位元組大小)。除了陣列,eBPF 對映還實現了 其他資料結構型別 [22] ,如棧或佇列。

接下來,eBPF 的位元組碼指令陣列使用方便的 核心巨集 [23] 進行定義。在這裡,我們不會討論位元組碼的細節(這將在第 2 部分描述機器後進行)。更高的層次上,位元組碼從資料包緩衝區中讀取協議字,在 map 中查詢,並增加特定的資料包計數。

然後 BPF 位元組碼被載入到核心中,並通過 libbpf 的 bpf_load_program 返回 fd 引用來驗證正確/安全。呼叫指定了 eBPF 程式型別 [24] ,這決定了它可以訪問哪些核心子集。因為樣例是一個 SOCKET_FILTER 型別,因此提供了一個指向當前網路包的引數。最後,eBPF 的位元組碼通過套接字層被附加到一個特定的原始套接字上,之後在原始套接字上接受到的每一個數據包執行 eBPF 位元組碼,無論協議如何。

剩餘的工作就是讓使用者程序開始輪詢共享 map 的資料。

 for (i = 0; i < 10; i++) {
key = IPPROTO_TCP;
assert(bpf_map_lookup_elem(map_fd, &key, &tcp_cnt) == 0);

key = IPPROTO_UDP;
assert(bpf_map_lookup_elem(map_fd, &key, &udp_cnt) == 0);

key = IPPROTO_ICMP;
assert(bpf_map_lookup_elem(map_fd, &key, &icmp_cnt) == 0);

printf("TCP %lld UDP %lld ICMP %lld packets\n",
tcp_cnt, udp_cnt, icmp_cnt);
sleep(1);
}
}

5. 總結

第 1 部分介紹了 eBPF 的基礎知識,我們通過如何載入位元組碼和與 eBPF 虛擬機器通訊的例子進行了講述。由於篇幅限制,編譯和執行例子作為留給讀者的練習。我們也有意不去分析具體的 eBPF 位元組碼指令,因為這將是第 2 部分的重點。在我們研究的例子中,使用者空間通過 libbpf 直接用 C 語言從核心虛擬機器中讀取 eBPF map 值(使用 10 次 1 秒的睡眠!),這很笨重,而且容易出錯,而且很快就會變得很複雜,所以在第 3 部分,我們將研究更高級別的工具,通過指令碼或特定領域的語言自動與虛擬機器互動。

繼續閱讀(eBPF 概述,第 2 部分:機器和位元組碼)[25]...。

引用連結

[1]

第 1 部分: https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/

[2]

第 2 部分: https://www.collabora.com/news-and-blog/blog/2019/04/15/an-ebpf-overview-part-2-machine-and-bytecode/

[3]

第 3 部分: https://www.collabora.com/news-and-blog/blog/2019/04/26/an-ebpf-overview-part-3-walking-up-the-software-stack/

[4]

第 4 部分: https://www.collabora.com/news-and-blog/blog/2019/05/06/an-ebpf-overview-part-4-working-with-embedded-systems/

[5]

第 5 部分: https://www.collabora.com/news-and-blog/blog/2019/05/14/an-ebpf-overview-part-5-tracing-user-processes/

[6]

LTTng: https://lttng.org/docs/v2.10/#doc-lttng-modules

[7]

SystemTap: https://kernelnewbies.org/SystemTap

[8]

DTrace/BPFtrace 對比: http://www.brendangregg.com/blog/2018-10-08/dtrace-for-linux-2018.html

[9]

eBPF 參考: http://www.brendangregg.com/ebpf.html

[10]

bpf() syscall: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf

[11]

uapi/linux/bpf.h: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h

[12]

Linux 核心版本對應的 BPF 功能: https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md

[13]

libbpf 庫: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf

[14]

bpf_load_program: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf/bpf.c#L214

[15]

bpf_map: https://github.com/torvalds/linux/blob/v4.20/tools/lib/bpf/libbpf.c#L157

[16]

samples/bpf/: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/

[17]

sock_example.c: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c

[18]

main: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L98

[19]

open_raw_sock: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.h#L13

[20]

test_sock: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/sock_example.c#L35

[21]

IPROTO_*: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/in.h#L28

[22]

其他資料結構型別: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L113

[23]

核心巨集: https://github.com/torvalds/linux/blob/v4.20/samples/bpf/bpf_insn.h

[24]

程式型別: https://github.com/torvalds/linux/blob/v4.20/include/uapi/linux/bpf.h#L138

[25]

繼續閱讀(eBPF 概述,第 2 部分:機器和位元組碼): https://www.collabora.com/news-and-blog/blog/2019/04/15/an-ebpf-overview-part-2-machine-and-bytecode/

關於 KubeSphere

KubeSphere (https://kubesphere.io)是在 Kubernetes 之上構建的 開源容器混合雲 ,提供全棧的 IT 自動化運維的能力,簡化企業的 DevOps 工作流。

KubeSphere 已被  Aqara 智慧家居、杭州數跑科技、本來生活、新浪、華夏銀行、四川航空、國藥集團、微眾銀行、紫金保險、中通、 中國人保壽險、中國太平保險、 中移金科、Radore、ZaloPay  等海內外數千家企業採用。KubeSphere 提供了開發者友好的嚮導式操作介面和豐富的企業級功能,包括 多雲與多叢集管理、Kubernetes 資源管理、DevOps (CI/CD)、應用生命週期管理、微服務治理 (Service Mesh)、多租戶管理、監控日誌、告警通知、審計事件、儲存與網路管理、GPU support  等功能,幫助企業快速構建一個強大和功能豐富的容器雲平臺。

 :sparkles: GitHub :https://github.com/kubesphere

 :computer: 官網(中國站) :https://kubesphere.com.cn

:man:‍:computer:‍  微信群: 請搜尋新增群助手微訊號  kubesphere