eBPF 概述:第 1 部分:介紹
1. 前言
有興趣了解更多關(guān)于 eBPF 技術(shù)的底層細(xì)節(jié)?那么請(qǐng)繼續(xù)移步,我們將深入研究 eBPF 的底層細(xì)節(jié),從其虛擬機(jī)機(jī)制和工具,到在遠(yuǎn)程資源受限的嵌入式設(shè)備上運(yùn)行跟蹤。
注意:本系列博客文章將集中在 eBPF 技術(shù),因此對(duì)于我們來(lái)講,文中 BPF 和 eBPF 等同,可相互使用。BPF 名字/縮寫(xiě)已經(jīng)沒(méi)有太大的意義,因?yàn)檫@個(gè)項(xiàng)目的發(fā)展遠(yuǎn)遠(yuǎn)超出了它最初的范圍。BPF 和 eBPF 在該系列中會(huì)交替使用。
第 1 部分和第 2 部分?為新人或那些希望通過(guò)深入了解 eBPF 技術(shù)棧的底層技術(shù)來(lái)進(jìn)一步了解 eBPF 技術(shù)的人提供了深入介紹。
第 3 部分是對(duì)用戶空間工具的概述,旨在提高生產(chǎn)力,建立在第 1 部分和第 2 部分中介紹的底層虛擬機(jī)機(jī)制之上。
第 4 部分側(cè)重于在資源有限的嵌入式系統(tǒng)上運(yùn)行 eBPF 程序,在嵌入式系統(tǒng)中完整的工具鏈技術(shù)棧(BCC/LLVM/python 等)是不可行的。我們將使用占用資源較小的嵌入式工具在 32 位 ARM 上交叉編譯和運(yùn)行 eBPF 程序。只對(duì)該部分感興趣的讀者可選擇跳過(guò)其他部分。
第 5 部分是關(guān)于用戶空間追蹤。到目前為止,我們的努力都集中在內(nèi)核追蹤上,所以是時(shí)候我們關(guān)注一下用戶進(jìn)程了。
如有疑問(wèn)時(shí),可使用該流程圖:

2. eBPF 是什么?
eBPF 是一個(gè)基于寄存器的虛擬機(jī),使用自定義的 64 位 RISC 指令集,能夠在 Linux 內(nèi)核內(nèi)運(yùn)行即時(shí)本地編譯的 “BPF 程序”,并能訪問(wèn)內(nèi)核功能和內(nèi)存的一個(gè)子集。這是一個(gè)完整的虛擬機(jī)實(shí)現(xiàn),不要與基于內(nèi)核的虛擬機(jī)(KVM)相混淆,后者是一個(gè)模塊,目的是使 Linux 能夠作為其他虛擬機(jī)的管理程序。eBPF 也是主線內(nèi)核的一部分,所以它不像其他框架那樣需要任何第三方模塊(LTTng?或?SystemTap),而且?guī)缀跛械?Linux 發(fā)行版都默認(rèn)啟用。熟悉 DTrace 的讀者可能會(huì)發(fā)現(xiàn)?DTrace/BPFtrace 對(duì)比非常有用。
在內(nèi)核內(nèi)運(yùn)行一個(gè)完整的虛擬機(jī)主要是考慮便利和安全。雖然 eBPF 程序所做的操作都可以通過(guò)正常的內(nèi)核模塊來(lái)處理,但直接的內(nèi)核編程是一件非常危險(xiǎn)的事情 - 這可能會(huì)導(dǎo)致系統(tǒng)鎖定、內(nèi)存損壞和進(jìn)程崩潰,從而導(dǎo)致安全漏洞和其他意外的效果,特別是在生產(chǎn)設(shè)備上(eBPF 經(jīng)常被用來(lái)檢查生產(chǎn)中的系統(tǒng)),所以通過(guò)一個(gè)安全的虛擬機(jī)運(yùn)行本地 JIT 編譯的快速內(nèi)核代碼對(duì)于安全監(jiān)控和沙盒、網(wǎng)絡(luò)過(guò)濾、程序跟蹤、性能分析和調(diào)試都是非常有價(jià)值的。部分簡(jiǎn)單的樣例可以在這篇優(yōu)秀的?eBPF 參考中找到。
基于設(shè)計(jì),eBPF 虛擬機(jī)和其程序有意地設(shè)計(jì)為不是圖靈完備的:即不允許有循環(huán)(正在進(jìn)行的工作是支持有界循環(huán)【譯者注:已經(jīng)支持有界循環(huán),#pragma unroll 指令】),所以每個(gè) eBPF 程序都需要保證完成而不會(huì)被掛起、所有的內(nèi)存訪問(wèn)都是有界和類型檢查的(包括寄存器,一個(gè) MOV 指令可以改變一個(gè)寄存器的類型)、不能包含空解引用、一個(gè)程序必須最多擁有 BPF_MAXINSNS 指令(默認(rèn) 4096)、“主"函數(shù)需要一個(gè)參數(shù)(context)等等。當(dāng) eBPF 程序被加載到內(nèi)核中,其指令被驗(yàn)證模塊解析為有向環(huán)狀圖,上述的限制使得正確性可以得到簡(jiǎn)單而快速的驗(yàn)證。
譯者注:BPF_MAXINSNS 這個(gè)限制已經(jīng)被放寬至 100 萬(wàn)條指令(BPF_COMPLEXITY_LIMIT_INSNS),但是非特權(quán)執(zhí)行的 BPF 程序這個(gè)限制仍然會(huì)保留。
歷史上,eBPF (cBPF) 虛擬機(jī)只在內(nèi)核中可用,用于過(guò)濾網(wǎng)絡(luò)數(shù)據(jù)包,與用戶空間程序沒(méi)有交互,因此被稱為 “伯克利數(shù)據(jù)包過(guò)濾器”(譯者注:早期的 BPF 實(shí)現(xiàn)被稱為經(jīng)典 cBPF)。從內(nèi)核 v3.18(2014 年)開(kāi)始,該虛擬機(jī)也通過(guò)?bpf() syscall?和uapi/linux/bpf.h?暴露在用戶空間,這導(dǎo)致其指令集在當(dāng)時(shí)被凍結(jié),成為公共 ABI,盡管后來(lái)仍然可以(并且已經(jīng))添加新指令。
因?yàn)閮?nèi)核內(nèi)的 eBPF 實(shí)現(xiàn)是根據(jù) GPLv2 授權(quán)的,它不能輕易地被非 GPL 用戶重新分發(fā),所以也有一個(gè)替代的 Apache 授權(quán)的用戶空間 eBPF 虛擬機(jī)實(shí)現(xiàn),稱為 “uBPF”。撇開(kāi)法律條文不談,基于用戶空間的實(shí)現(xiàn)對(duì)于追蹤那些需要避免內(nèi)核-用戶空間上下文切換成本的性能關(guān)鍵型應(yīng)用很有用。
3. eBPF 是怎么工作的?
eBPF 程序在事件觸發(fā)時(shí)由內(nèi)核運(yùn)行,所以可以被看作是一種函數(shù)掛鉤或事件驅(qū)動(dòng)的編程形式。從用戶空間運(yùn)行按需 eBPF 程序的價(jià)值較小,因?yàn)樗械陌葱栌脩粽{(diào)用已經(jīng)通過(guò)正常的非 VM 內(nèi)核 API 調(diào)用(“syscalls”)來(lái)處理,這里 VM 字節(jié)碼帶來(lái)的價(jià)值很小。事件可由 kprobes/uprobes、tracepoints、dtrace probes、socket 等產(chǎn)生。這允許在內(nèi)核和用戶進(jìn)程的指令中鉤住(hook)和檢查任何函數(shù)的內(nèi)存、攔截文件操作、檢查特定的網(wǎng)絡(luò)數(shù)據(jù)包等等。一個(gè)比較好的參考是?Linux 內(nèi)核版本對(duì)應(yīng)的 BPF 功能。
如前所述,事件觸發(fā)了附加的 eBPF 程序的執(zhí)行,后續(xù)可以將信息保存至 map 和環(huán)形緩沖區(qū)(ringbuffer)或調(diào)用一些特定 API 定義的內(nèi)核函數(shù)的子集。一個(gè) eBPF 程序可以鏈接到多個(gè)事件,不同的 eBPF 程序也可以訪問(wèn)相同的 map 以共享數(shù)據(jù)。一個(gè)被稱為 “program array” 的特殊讀/寫(xiě) map 存儲(chǔ)了對(duì)通過(guò) bpf() 系統(tǒng)調(diào)用加載的其他 eBPF 程序的引用,在該 map 中成功的查找則會(huì)觸發(fā)一個(gè)跳轉(zhuǎn),而且并不返回到原來(lái)的 eBPF 程序。這種 eBPF 嵌套也有限制,以避免無(wú)限的遞歸循環(huán)。
運(yùn)行 eBPF 程序的步驟:
用戶空間將字節(jié)碼和程序類型一起發(fā)送到內(nèi)核,程序類型決定了可以訪問(wèn)的內(nèi)核區(qū)域(譯者注:主要是 BPF 輔助函數(shù)的各種子集)。
內(nèi)核在字節(jié)碼上運(yùn)行驗(yàn)證器,以確保程序可以安全運(yùn)行(kernel/bpf/verifier.c)。
內(nèi)核將字節(jié)碼編譯為本地代碼,并將其插入(或附加到)指定的代碼位置。(譯者注:如果啟用了 JIT 功能,字節(jié)碼編譯為本地代碼)。
插入的代碼將數(shù)據(jù)寫(xiě)入環(huán)形緩沖區(qū)或通用鍵值 map。
用戶空間從共享 map 或環(huán)形緩沖區(qū)中讀取結(jié)果值。
map 和環(huán)形緩沖區(qū)結(jié)構(gòu)是由內(nèi)核管理的(就像管道和 FIFO 一樣),獨(dú)立于掛載的 eBPF 或訪問(wèn)它們的用戶程序。對(duì) map 和環(huán)形緩沖區(qū)結(jié)構(gòu)的訪問(wèn)是異步的,通過(guò)文件描述符和引用計(jì)數(shù)實(shí)現(xiàn),可確保只要有至少一個(gè)程序還在訪問(wèn),結(jié)構(gòu)就能夠存在。加載的 JIT 后代碼通常在加載其的用戶進(jìn)程終止時(shí)被刪除,盡管在某些情況下,它仍然可以在加載進(jìn)程的生命期之后繼續(xù)存在。
為了方便編寫(xiě) eBPF 程序和避免進(jìn)行原始的 bpf()系統(tǒng)調(diào)用,內(nèi)核提供了方便的?libbpf 庫(kù),包含系統(tǒng)調(diào)用函數(shù)包裝器,如bpf_load_program?和結(jié)構(gòu)定義(如?bpf_map),在 LGPL 2.1 和 BSD 2-Clause 下雙重許可,可以靜態(tài)鏈接或作為 DSO。內(nèi)核代碼也提供了一些使用 libbpf 簡(jiǎn)潔的例子,位于目錄?samples/bpf/?中。
4. 樣例學(xué)習(xí)
內(nèi)核開(kāi)發(fā)者非常可憐,因?yàn)閮?nèi)核是一個(gè)獨(dú)立的項(xiàng)目,因而沒(méi)有用戶空間諸如 Glibc、LLVM、JavaScript 和 WebAssembly 諸如此類的好東西! - 這就是為什么內(nèi)核中 eBPF 例子中會(huì)包含原始字節(jié)碼或通過(guò) libbpf 加載預(yù)組裝的字節(jié)碼文件。我們可以在?sock_example.c?中看到這一點(diǎn),這是一個(gè)簡(jiǎn)單的用戶空間程序,使用 eBPF 來(lái)計(jì)算環(huán)回接口上統(tǒng)計(jì)接收到 TCP、UDP 和 ICMP 協(xié)議包的數(shù)量。
我們跳過(guò)微不足道的的?main?和?open_raw_sock?函數(shù),而專注于神奇的代碼?test_sock。
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;
?????}首先,通過(guò) libbpf API 創(chuàng)建一個(gè) BPF map,該行為就像一個(gè)最大 256 個(gè)元素的固定大小的數(shù)組。按?IPROTO_*?定義的鍵索引網(wǎng)絡(luò)協(xié)議(2 字節(jié)的 word),值代表各自的數(shù)據(jù)包計(jì)數(shù)(4 字節(jié)大小)。除了數(shù)組,eBPF 映射還實(shí)現(xiàn)了其他數(shù)據(jù)結(jié)構(gòu)類型,如棧或隊(duì)列。
接下來(lái),eBPF 的字節(jié)碼指令數(shù)組使用方便的內(nèi)核宏進(jìn)行定義。在這里,我們不會(huì)討論字節(jié)碼的細(xì)節(jié)(這將在第 2 部分描述機(jī)器后進(jìn)行)。更高的層次上,字節(jié)碼從數(shù)據(jù)包緩沖區(qū)中讀取協(xié)議字,在 map 中查找,并增加特定的數(shù)據(jù)包計(jì)數(shù)。
然后 BPF 字節(jié)碼被加載到內(nèi)核中,并通過(guò) libbpf 的 bpf_load_program 返回 fd 引用來(lái)驗(yàn)證正確/安全。調(diào)用指定了 eBPF 是什么程序類型,這決定了它可以訪問(wèn)哪些內(nèi)核子集。因?yàn)闃永且粋€(gè) SOCKET_FILTER 類型,因此提供了一個(gè)指向當(dāng)前網(wǎng)絡(luò)包的參數(shù)。最后,eBPF 的字節(jié)碼通過(guò)套接字層被附加到一個(gè)特定的原始套接字上,之后在原始套接字上接受到的每一個(gè)數(shù)據(jù)包運(yùn)行 eBPF 字節(jié)碼,無(wú)論協(xié)議如何。
剩余的工作就是讓用戶進(jìn)程開(kāi)始輪詢共享 map 的數(shù)據(jù)。
????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. 總結(jié)
第 1 部分介紹了 eBPF 的基礎(chǔ)知識(shí),我們通過(guò)如何加載字節(jié)碼和與 eBPF 虛擬機(jī)通信的例子進(jìn)行了講述。由于篇幅限制,編譯和運(yùn)行例子作為留給讀者的練習(xí)。我們也有意不去分析具體的 eBPF 字節(jié)碼指令,因?yàn)檫@將是第 2 部分的重點(diǎn)。在我們研究的例子中,用戶空間通過(guò) libbpf 直接用 C 語(yǔ)言從內(nèi)核虛擬機(jī)中讀取 eBPF map 值(使用 10 次 1 秒的睡眠!),這很笨重,而且容易出錯(cuò),而且很快就會(huì)變得很復(fù)雜,所以在第 3 部分,我們將研究更高級(jí)別的工具,通過(guò)腳本或特定領(lǐng)域的語(yǔ)言自動(dòng)與虛擬機(jī)交互。
原文:
https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/
