eBPF 概述:第 3 部分:軟件開發(fā)生態(tài)
1. 前言
在本系列的第 1 部分和第 2 部分中,我們對 eBPF 虛擬機進行了簡潔的深入研究。閱讀上述部分并不是理解第 3 部分的必修課,盡管很好地掌握了低級別的基礎(chǔ)知識確實有助于更好地理解高級別的工具。為了理解這些工具是如何工作的,我們先定義一下 eBPF 程序的高層次組件:
后端:這是在內(nèi)核中加載和運行的 eBPF 字節(jié)碼。它將數(shù)據(jù)寫入內(nèi)核 map 和環(huán)形緩沖區(qū)的數(shù)據(jù)結(jié)構(gòu)中。
加載器:它將字節(jié)碼后端加載到內(nèi)核中。通常情況下,當加載器進程終止時,字節(jié)碼會被內(nèi)核自動卸載。
前端:從數(shù)據(jù)結(jié)構(gòu)中讀取數(shù)據(jù)(由后端寫入)并將其顯示給用戶。
數(shù)據(jù)結(jié)構(gòu):這些是后端和前端之間的通信手段。它們是由內(nèi)核管理的 map 和環(huán)形緩沖區(qū),可以通過文件描述符訪問,并需要在后端被加載之前創(chuàng)建。它們會持續(xù)存在,直到?jīng)]有更多的后端或前端進行讀寫操作。
在第 1 部分和第 2 部分研究的?sock_example.c?中,所有的組件都被放置在一個 C 文件中,所有的動作都由用戶進程完成。
第 40-45 行創(chuàng)建 map數(shù)據(jù)結(jié)構(gòu)。
第 47-61 行定義后端。
第 63-76 行在內(nèi)核中加載后端
第 78-91 行是前端,負責將從 map 文件描述符中讀取的數(shù)據(jù)打印給用戶。
eBPF 程序可以更加復雜:多個后端可以由一個(或單獨的多個?。?span style="font-weight: bolder;">加載器進程加載,寫入多個數(shù)據(jù)結(jié)構(gòu),然后由多個前端進程讀取,所有這些都可以發(fā)生在一個跨越多個進程的用戶 eBPF 應(yīng)用程序中。

2. 層級 1:容易編寫的后端:LLVM eBPF 編譯器
我們在前面的文章中看到,在內(nèi)核中編寫原始的 eBPF 字節(jié)碼是不僅困難而且低效,這非常像用處理器的匯編語言編寫程序,所以很自然地開發(fā)了一個能夠?qū)?LLVM 中間表示編譯成 eBPF 程序的模塊,并從 2015 年的 v3.7 開始發(fā)布(GCC 到現(xiàn)在為止仍然不支持 eBPF)。這使得多種高級語言如 C、Go 或 Rust 的子集可以被編譯到 eBPF。最成熟和最流行的是基于 C 語言編寫的方式,因為內(nèi)核也是用 C 寫的,這樣就更容易復用現(xiàn)有的內(nèi)核頭文件。
LLVM 將 “受限制的 C” 語言(記住,沒有無界循環(huán),最大 4096 條指令等等,見第 1 部分開始)編譯成 ELF 對象文件,其中包含特殊區(qū)塊(section),并可基于 bpf()系統(tǒng)調(diào)用,使用 libbpf 等庫加載到內(nèi)核中。這種設(shè)計有效地將后端定義從加載器和前端中分離出來,因為 eBPF 字節(jié)碼包含在 ELF 文件中。
內(nèi)核還在?samples/bpf/?下提供了使用這種模式的例子:*_kern.c 文件被編譯為 *_kern.o(后端代碼),被 *_user.c(裝載器和前端)加載。
將本系列第 1 和第 2 部分的?sock_exapmle.c 原始字節(jié)碼?轉(zhuǎn)換為 “受限的 C” 代碼“?sockex1_kern.c,這比原始字節(jié)碼更容易理解和修改。
#include?
#include?
#include?
#include?
#include?"bpf_helpers.h"
struct?bpf_map_def?SEC("maps")?my_map?=?{
????.type?=?BPF_MAP_TYPE_ARRAY,
????.key_size?=?sizeof(u32),
????.value_size?=?sizeof(long),
????.max_entries?=?256,
};
SEC("socket1")
int?bpf_prog1(struct?__sk_buff?*skb)
{
????int?index?=?load_byte(skb,?ETH_HLEN?+?offsetof(struct?iphdr,?protocol));
????long?*value;
????value?=?bpf_map_lookup_elem(&my_map,?&index);
????if?(value)
????????__sync_fetch_and_add(value,?skb->len);
????return?0;
}
char?_license[]?SEC("license")?=?"GPL";產(chǎn)生的 eBPF ELF 對象 sockex1_kern.o,包含了分離的后端和數(shù)據(jù)結(jié)構(gòu)定義。加載器和前端sockex1_user.c,用于解析 ELF 文件、創(chuàng)建所需的 map 和加載字節(jié)碼中內(nèi)核函數(shù) bpf_prog1(),然后前端像以前一樣繼續(xù)運行。
引入這個 “受限的 C” 抽象層所做的權(quán)衡是使 eBPF后端代碼更容易用高級語言編寫,代價是增加加載器的復雜性(現(xiàn)在需要解析 ELF 對象),而前端大部分不受影響。
3. 層級 2:自動化后端/加載器/前端的交互:BPF 編譯器集合(BCC)
并不是每個人手頭都有內(nèi)核源碼,特別是在生產(chǎn)中,而且一般來說,將基于 eBPF 工具與特定的內(nèi)核源碼版本捆綁在一起并不是一個好主意。設(shè)計和實現(xiàn) eBPF 程序的后端,前端,加載器和數(shù)據(jù)結(jié)構(gòu)之間的相互作用可能是非常復雜,這也比較容易出錯和耗時(特別是在 C 語言中),這被認為是一種危險的低級語言。除了這些風險之外,開發(fā)人員還經(jīng)常為常見問題重新造輪子,會造成無盡的設(shè)計變化和實現(xiàn)。為了減輕這些痛苦,社區(qū)創(chuàng)建了 BCC 項目:其為編寫、加載和運行 eBPF 程序提供了一個易于使用的框架,除了上面舉例的 “限制性 C” 之外,還可以通過編寫簡單的 python 或 lua 腳本來實現(xiàn)。
BCC 項目有兩個部分。
編譯器集合(BCC 本身):這是用于編寫 BCC 工具的框架,也是我們文章的重點。請繼續(xù)閱讀。
BCC-tools:這是一個不斷增長的基于 eBPF 且經(jīng)過測試的程序集,提供了使用的例子和手冊。更多信息見本教程。
BCC 的安裝包很大:它依賴于 LLVM/clang 將 “受限的 C”、python/lua 等編譯成 eBPF,它還包含像 libbcc(用 C++ 編寫)、libbpf 等庫實現(xiàn)【譯者注:原文 python/lua 順序有錯,另外 libcc 是 BCC 項目,libbpf 目前已經(jīng)是內(nèi)核代碼一部分】。部分內(nèi)核代碼的也被復制到 BCC 代碼中,所以它不需要基于完整的內(nèi)核源(只需要頭文件)進行構(gòu)建。它可以很容易地占用數(shù)百 MB 的空間,這對于小型嵌入式設(shè)備來說不友好,我們希望這些設(shè)備也可以從 eBPF 的力量中受益。探索嵌入式設(shè)備由于大小限制問題的解決方案,將是我們在第 4 部分的重點。
eBPF 程序組件在 BCC 組織方式如下:
后端和數(shù)據(jù)結(jié)構(gòu):用 “限制性 C” 編寫。可以在單獨的文件中,或直接作為多行字符串存儲在加載器/前端的腳本中,以方便使用。參見:語言參考?!咀g者注:在 BCC 實現(xiàn)中,后端代碼采用面向?qū)ο蟮淖龇?,真正生成字?jié)碼的時候,BCC 會進行一次預處理,轉(zhuǎn)換成真正的 C 語言代碼方式,這也包括 map 等數(shù)據(jù)結(jié)構(gòu)的定義方面】。
加載器和前端:可用非常簡單的高級 python/lua 腳本編寫。參見:語言參考。
因為 BCC 的主要目的是簡化 eBPF 程序的編寫,因此它盡可能地標準化和自動化:在后臺完全自動化地通過 LLVM 編譯 “受限的 C”后端,并產(chǎn)生一個標準的 ELF 對象格式類型,這種方式允許加載器對所有 BCC 程序只實現(xiàn)一次,并將其減少到最小的 API(2 行 python)。它還將數(shù)據(jù)結(jié)構(gòu)的 API 標準化,以便于通過前端訪問。簡而言之,它將開發(fā)者的注意力集中在編寫前端上,而不必擔心較低層次的細節(jié)問題。
為了最好地說明它是如何工作的,我們來看一個簡單的具體例子,它是對前面文章中的?sock_example.c?的重新實現(xiàn)。該程序統(tǒng)計回環(huán)接口上收到了 TCP、UDP 和 ICMP 數(shù)據(jù)包的數(shù)量。

與此前直接用 C 語言編寫的方式不同,用 BCC 實現(xiàn)具有以下優(yōu)勢:
忘掉原始字節(jié)碼:你可以用更方便的 “限制性 C” 編寫所有后端。
不需要維護任何 LLVM 的 “限制性 C” 構(gòu)建邏輯。代碼被 BCC 在腳本執(zhí)行時直接編譯和加載。
沒有危險的 C 代碼:對于編寫前端和加載器來說,Python 是一種更安全的語言,不會出現(xiàn)像空解引用(null dereferences)的錯誤。
代碼更簡潔,你可以專注于應(yīng)用程序的邏輯,而不是具體的機器問題。
腳本可以被復制并在任何地方運行(假設(shè)已經(jīng)安裝了 BCC),它不會被束縛在內(nèi)核的源代碼目錄中。
等等。
在上面的例子中,我們使用了 BPF.SOCKET_FILTER 程序類型,其結(jié)果是我們掛載的 C 函數(shù)得到一個網(wǎng)絡(luò)數(shù)據(jù)包緩沖區(qū)作為 context 上下文參數(shù)【譯者注:本例中為 struct _sk_buff *skb】。我們還可以使用 BPF.KPROBE 程序類型來探測任意的內(nèi)核函數(shù)。我們繼續(xù)優(yōu)化,不再使用與上面相同的接口,而是使用一個特殊的 kprobe_* 函數(shù)名稱前綴,以描述一個更高級別的 BCC API。

這個例子來自于?bcc/examples/tracing/bitehist.py。它通過掛載在 blk_account_io_completion() 內(nèi)核函數(shù)來打印一個 I/O 塊大小的直方圖。
請注意:eBPF 的加載是根據(jù)?kprobe__blk_account_io_completion() 函數(shù)的名稱自動發(fā)生的(加載器隱含實現(xiàn))! 【譯者注:kprobe__ 前綴會被 BCC 編譯代碼過程中自動識別并轉(zhuǎn)換成對應(yīng)的附加函數(shù)調(diào)用】從用 libbpf 在 C 語言中編寫和加載字節(jié)碼以來,我們已經(jīng)走了很遠。
4. 層級 3:Python 太低級了:BPFftrace
在某些用例中,BCC 仍然過于底層,例如在事件響應(yīng)中檢查系統(tǒng)時,時間至關(guān)重要,需要快速做出決定,而編寫 python/“限制性 C” 會花費太多時間,因此 BPFtrace 建立在 BCC 之上,通過特定領(lǐng)域語言(受 AWK 和 C 啟發(fā))提供更高級別的抽象。根據(jù)聲明帖,該語言類似于 DTrace 語言實現(xiàn),也被稱為 DTrace 2.0,并提供了良好的介紹和例子。
BPFtrace 在一個強大而安全(但與 BCC 相比仍有局限性)的語言中抽象出如此多的邏輯,是非常讓人驚奇的。這個單行 shell 程序統(tǒng)計了每個用戶進程系統(tǒng)調(diào)用的次數(shù)(訪問內(nèi)置變量、map 函數(shù)?和count()文檔獲取更多信息)。
bpftrace?-e?'tracepoint:raw_syscalls:sys_enter?{@[pid,?comm]?=?count();}'BPFtrace 在某些方面仍然是一個正在進行的工作。例如,目前還沒有簡單的方法來定義和運行一個套接字過濾器來實現(xiàn)像我們之前所列舉的 sock_example 這樣的工具。它可能通過在 BPFtrace 中用 kprobe:netif_receive_skb 鉤子完成,但這種情況下 BCC 仍然是一個更好的套接字過濾工具。在任何情況下(即使在目前的狀態(tài)下),BPFTrace 對于在尋求 BCC 的全部功能之前的快速分析/調(diào)試仍然非常有用。
5. 層級 4:云環(huán)境中的 eBPF:IOVisor
IOVisor?是 Linux 基金會的一個合作項目,基于本系列文章中介紹的 eBPF 虛擬機和工具。它使用了一些非常高層次的熱門概念,如 “通用輸入/輸出”,專注于向云/數(shù)據(jù)中心開發(fā)人員和用戶提供 eBPF 技術(shù)。
內(nèi)核 eBPF 虛擬機成為 “IO Visor 運行時引擎”
編譯器后端成為 “IO Visor 編譯器后端”
一般的 eBPF 程序被重新命名為 “IO 模塊”
實現(xiàn)包過濾器的特定 eBPF 程序成為 “IO 數(shù)據(jù)平面模塊/組件”
等等。
考慮到原來的名字(擴展的伯克利包過濾器),并沒有代表什么意義,也許所有這些重命名都是受歡迎和有價值的,特別是如果它能使更多的行業(yè)利用 eBPF 的力量。
IOVisor 項目創(chuàng)建了?Hover 框架,也被稱為 “IO 模塊管理器”,它是一個管理 eBPF 程序(或 IO 模塊)的用戶空間后臺服務(wù)程序,能夠?qū)?IO 模塊推送和拉取到云端,這類似于 Docker daemon 發(fā)布/獲取鏡像的方式。它提供了一個 CLI,Web-REST 接口,也有一個花哨的 Web UI。Hover 的重要部分是用 Go 編寫的,因此,除了正常的 BCC 依賴性外,它還依賴于 Go 的安裝,這使得它體積變得很大,這并不適合我們最終在第 4 部分中的提及的小型嵌入式設(shè)備。
6. 總結(jié)
在這一部分,我們研究了建立在 eBPF 虛擬機之上的用戶空間生態(tài)系統(tǒng),以提高開發(fā)人員的工作效率和簡化 eBPF 程序部署。這些工具使得使用 eBPF 非常容易,用戶只需 “apt-get install bpftrace” 就可以運行單行程序,或者使用 Hover 守護程序?qū)?eBPF 程序(IO 模塊)部署到 1000 臺機器上。然而,所有這些工具,盡管它們給開發(fā)者和用戶提供了所有的力量,但卻需要很大的磁盤空間,甚至可能無法在 32 位 ARM 系統(tǒng)上運行,這使得它們不是很適合小型嵌入式設(shè)備,所以這就是為什么在第 4 部分我們將探索其他項目,試圖緩解運行針對嵌入式設(shè)備生態(tài)系統(tǒng)的 eBPF 程序。
原文地址:
https://www.collabora.com/news-and-blog/blog/2019/04/26/an-ebpf-overview-part-3-walking-up-the-software-stack/
