BPF的可移植性和CO-RE (Compile Once – Run Everywhere)
BPF的可移植性和CO-RE (Compile Once – Run Everywhere)
在上一篇文章中介紹了提高socket性能的幾個socket選項(xiàng),其中給出了幾個源于內(nèi)核源碼樹中的例子,如果選擇使用內(nèi)核樹中的Makefile進(jìn)行編譯的話,可能會出現(xiàn)與本地頭文件沖突的情況,如重復(fù)定義變量,結(jié)構(gòu)體類型不對等錯誤。這些問題大大影響了BPF程序的可移植性。
本文將介紹BPF可移植性存在的問題,以及如何使用BPF CO-RE(Compile Once – Run Everywhere)解決這些問題。
BPF:最前沿的技術(shù)
自BPF成立以來,BPF社區(qū)將盡可能簡化BPF應(yīng)用程序的開發(fā)作為工作重點(diǎn),目的是將BPF的使用變得與用戶空間的應(yīng)用一樣簡單明了。伴隨著BPF可編程性的穩(wěn)步發(fā)展,BPF程序的開發(fā)也越來越簡單。
盡管BPF提升了使用上的便利性,但卻忽略了BPF程序開發(fā)中的一個方面:可移植性。"BPF可移植性"意味著什么?我們將BPF可移植性定義為成功編寫并通過內(nèi)核驗(yàn)證的一個BPF程序,且跨內(nèi)核版本可用,無需針對特定的內(nèi)核重新編譯。
本文描述了BPF的可移植性問題以及解決方案:BPF CO-RE(Compile Once – Run Everywhere)。首先會調(diào)研BPF本身的可移植性問題,描述為什么這是個問題,以及為什么解決它很重要。然后,我們將介紹解決方案中的高級組件:BPF CO-RE,并簡要介紹實(shí)現(xiàn)這一目標(biāo)所需要解決的難題。最后,我們將以各種教程作為結(jié)尾,介紹BPF CO-RE方法的用戶API,并提供相關(guān)示例。
BPF可移植性的問題
BPF程序是用戶提供的一部分代碼,這些代碼會直接注入到內(nèi)核,一旦經(jīng)過加載和驗(yàn)證,BPF程序就可以在內(nèi)核上下文中運(yùn)行。這些程序運(yùn)行在內(nèi)核的內(nèi)存空間中,并能夠訪問所有可用的內(nèi)核內(nèi)部狀態(tài),功能非常強(qiáng)大,這也是為什么BPF技術(shù)成功落地到多個應(yīng)用中的原因。然而,在使用其強(qiáng)大的能力的同時(shí)也帶來了一些負(fù)擔(dān):BPF程序無法控制周圍內(nèi)核環(huán)境的內(nèi)存布局,因此必須依賴獨(dú)立開發(fā),編譯和部署的內(nèi)核。
此外,內(nèi)核類型和數(shù)據(jù)結(jié)構(gòu)會不斷變化。不同的內(nèi)核版本會在結(jié)構(gòu)體內(nèi)部混用結(jié)構(gòu)體字段,甚至?xí)D(zhuǎn)移到新的內(nèi)部結(jié)構(gòu)體中。結(jié)構(gòu)體中的字段可能會被重命名或刪除,類型可能會改變(變?yōu)槲⒓嫒莼蛲耆煌念愋?。結(jié)構(gòu)體和其他類型可以被重命名,被條件編譯(取決于內(nèi)核配置),或直接從內(nèi)核版本中移除。
換句話講,不同內(nèi)核發(fā)布版本中的所有內(nèi)容都有可能發(fā)生變化,BPF應(yīng)用開發(fā)者應(yīng)該能夠預(yù)料到這個問題。考慮到不斷變化的內(nèi)核環(huán)境,那么該如何利用BPF做有用的事?有如下幾點(diǎn)原因:
首先,并不是所有的BPF程序都需要訪問內(nèi)部的內(nèi)核數(shù)據(jù)結(jié)構(gòu)。一個例子是opensnoop工具,該工具依靠kprobes /tracepoints來跟蹤哪個進(jìn)程打開了哪些文件,僅需要捕獲少量的系統(tǒng)調(diào)用就可以工作。由于系統(tǒng)調(diào)用提供了穩(wěn)定的ABI,不會隨著內(nèi)核版本而變化,因此不用考慮這類BPF程序的可移植性。不幸的是,這類應(yīng)用非常少,且這類應(yīng)用的功能也會大大受限。
此外,內(nèi)核內(nèi)部的BPF機(jī)器提供了有限的“穩(wěn)定接口”集,BPF程序可以依靠這些穩(wěn)定接口在內(nèi)核間保持穩(wěn)定。事實(shí)上,不同版本的內(nèi)核的底層結(jié)構(gòu)和機(jī)制是會發(fā)生變化的,但BPF提供的穩(wěn)定接口從用戶程序中抽象了這些細(xì)節(jié)。
例如,網(wǎng)絡(luò)應(yīng)用會通過查看少量的sk_buff(即報(bào)文數(shù)據(jù))中的屬性來獲得非常有用且通用的信息。為此,BPF校驗(yàn)器提供了一個穩(wěn)定的__sk_buff 視圖(注意前面的下劃線),該視圖為BPF程序屏蔽了struct sk_buff結(jié)構(gòu)體的變更。所有對__sk_buff字段訪問都可以透明地重寫為對實(shí)際sk_buff的訪問(有時(shí)非常復(fù)雜-在獲取最終請求的字段之前需要追蹤一堆內(nèi)部指針)。類似的機(jī)制同樣適用于不同的BPF程序類型,通過BPF校驗(yàn)器來識別特定類型的BPF上下文。如果使用這類上下文開發(fā)BPF程序,就可以不用擔(dān)心可移植性問題。
但有時(shí)候需要訪問原始的內(nèi)核數(shù)據(jù)(如經(jīng)常會訪問到的 struct task_struct,表示一個進(jìn)程或線程,包含大量進(jìn)程信息),此時(shí)就只能靠自己了。跟蹤,監(jiān)視和分析應(yīng)用程序通常是這種情況,這些應(yīng)用程序是一類非常有用的BPF程序。
在這種情況下,如果某些內(nèi)核在需要采集的字段(如從struct task_struct開始的第16個字節(jié)的偏移處)前添加了一個新的字段,那么此時(shí)如何保證不會讀取到垃圾數(shù)據(jù)?如果一個字段重命名了又如何處理(如內(nèi)核4.6和4.7的thread_struct的fs字段的名稱是不同的)?或者如果需要基于一個內(nèi)核的兩種配置來運(yùn)行程序,其中一個配置會禁用某些特性,并編譯出部分結(jié)構(gòu)(一種常見的場景是解釋字段,這些字段是可選的,但如果存在則非常有用)?所有這些條件意味著無法使用本地開發(fā)服務(wù)器上的頭文件編譯出一個BPF程序,然后分發(fā)到其他系統(tǒng)上運(yùn)行。這是因?yàn)椴煌瑑?nèi)核版本的頭文件中的數(shù)據(jù)的內(nèi)存布局可能是不同的。
迄今為止,人們編譯這類BPF程序會依賴BCC (BPF Compiler Collection)。使用BCC,可以將BPF程序的C代碼以字符串的形式嵌入到用戶空間的程序中,當(dāng)程序最終部署并運(yùn)行在目標(biāo)主機(jī)上后,BCC會喚醒其嵌入的Clang/LLVM,提取本地內(nèi)核頭文件(必須確保已從正確的kernel-devel軟件包中將其安裝在系統(tǒng)上),并即時(shí)進(jìn)行編譯。通過這種方式來確保BPF程序期望的內(nèi)存布局和主機(jī)運(yùn)行的內(nèi)核的內(nèi)存布局是相同的。如果需要處理一些選項(xiàng)和內(nèi)核編譯出來的潛在產(chǎn)物,則可以在自己的源代碼中添加#ifdef/#else來適應(yīng)重命名字段、不同的數(shù)值語義或當(dāng)前配置導(dǎo)致的不可用內(nèi)容等帶來的風(fēng)險(xiǎn)。嵌入的Clang會移除代碼中無關(guān)的內(nèi)容,并調(diào)整BPF程序代碼,以匹配到特定的內(nèi)核。
這種方式聽起來很不錯,但實(shí)際并非沒有缺點(diǎn):
- Clang/LLVM組合是一個很大的庫,導(dǎo)致發(fā)布的應(yīng)用的庫會比較大;
- Clang/LLVM組合使用的資源比較多,因此當(dāng)編譯的BPF代碼啟動時(shí)會消耗大量資源,可能會推翻已均衡的生產(chǎn)負(fù)載;
- 這樣做其實(shí)也是在賭目標(biāo)系統(tǒng)將存在內(nèi)核頭文件,大多數(shù)情況下這不是問題,但有時(shí)可能會引起很多麻煩。這也是內(nèi)核開發(fā)人員感到特別麻煩的點(diǎn),因?yàn)樗麄兘?jīng)常必須在開發(fā)過程中構(gòu)建和部署自定義的內(nèi)核。如果沒有自定義構(gòu)建的內(nèi)核頭文件包,則基于BCC的應(yīng)用將無法在這種內(nèi)核上運(yùn)行,從而剝奪了開發(fā)人員用于調(diào)試和監(jiān)視的工具集;
- BPF程序的測試和開發(fā)迭代也相當(dāng)痛苦,因?yàn)橐坏┲匦戮幾g并重啟用戶空間控制應(yīng)用程序,甚至?xí)谶\(yùn)行時(shí)遇到各種瑣碎的編譯錯誤。這無疑會增加難度,且無益于快速迭代。
總之, BCC是一個很好的工具,尤其適合快速原型制作,實(shí)驗(yàn)和小型工具,但在用于廣泛部署的生產(chǎn)BPF應(yīng)用程序時(shí),它無疑具有很多缺點(diǎn)。
我們正在使用BPF CO-RE來增強(qiáng)BPF的可移植性,并相信這是未來BPF程序開發(fā)的趨勢,尤其對于復(fù)雜的實(shí)際應(yīng)用的BPF程序。
高級BFP CO-RE機(jī)制
BPF CO-RE在軟件堆棧的各個級別匯集了必要的功能和數(shù)據(jù):內(nèi)核,用戶空間的BPF加載器庫(libbpf),和編譯器(Clang)。通過這些組件來支持編寫可移植的BPF程序,使用相同的預(yù)編譯的BPF程序來處理不同內(nèi)核之間的差異。BPF CO-RE需要以下組件的集成和合作:
- BTF類型信息,用于允許獲取關(guān)于內(nèi)核和BPF程序類型和代碼的關(guān)鍵信息,進(jìn)而為解決BPF CO-RE的其他難題提供了可能性;
- 編譯器(Clang)為BPF程序C代碼提供了表達(dá)意圖和記錄重定位信息的方法;
- BPF加載器(libbpf)將內(nèi)核和BPF程序中的BTF綁定在一起,用于將編譯后的BPF代碼調(diào)整為目標(biāo)主機(jī)上的特定內(nèi)核代碼;
- 內(nèi)核,在完全不依賴BPF CO-RE的情況下,提供了高級BPF功能來啟用某些更高級的場景。
這些組件可以集成到一起工作,提供了前所未有的便捷性,適應(yīng)性和表達(dá)性(來開發(fā)可移植BPF程序,以前只能在運(yùn)行時(shí)通過BCC編譯BPF程序的C代碼來實(shí)現(xiàn)),而無需像BCC一樣付出高昂的代價(jià)。
BTF
整個BPF CO-RE方法的關(guān)鍵推動因素之一是BTF。BTF (BPF Type Format) 是作為一個更通用,更詳細(xì)的DWARF調(diào)試信息的替代品而出現(xiàn)的。BTF是一種節(jié)省空間,緊湊但依然具有足夠表達(dá)能力的格式,可以描述C程序的所有類型信息。由于其簡單性和使用的重復(fù)數(shù)據(jù)刪除算法,與DWARF相比,BTF的大小可減少多達(dá)100倍。現(xiàn)在,已經(jīng)可以在內(nèi)核運(yùn)行時(shí)顯示地嵌入BPF類型信息:只需要啟用CONFIG_DEBUG_INFO_BTF=y內(nèi)核選項(xiàng)即可。內(nèi)核本身可以使用BTF功能,用于增強(qiáng)BPF驗(yàn)證程序自身的功能。
關(guān)于BPF CO-RE更重要的是,內(nèi)核還通過/sys/kernel/btf/vmlinux上的sysfs公開了這種自描述的權(quán)威BTF信息(定義了確切的結(jié)構(gòu)布局)。嘗試如下命令:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c
某些unix系統(tǒng)下安裝的bpftool默認(rèn)不支持btf命令選項(xiàng),可以在linux內(nèi)核源碼的
/tools/bpf/bpftool目錄下執(zhí)行make命令進(jìn)行編譯。如果遇到linux/if.h和net/if.h頭文件定義沖突的話,可以將/tools/bpf/bpftool/net.c中的這一行注釋掉再編譯:
#include <linux/if.h>
目前很多內(nèi)核默認(rèn)并不會打開BTF內(nèi)核選項(xiàng),因此需要自己編譯內(nèi)核。基本步驟如下:
首先升級gcc;
編譯帶BTF選項(xiàng)的內(nèi)核前需要安裝pahole,可以從github官方下載源碼編譯即可。需要注意的是,該編譯過程需要依賴
git,因此需要通過git clone代碼編譯,而不能下載源碼壓縮包編譯;按照官方編譯步驟直接執(zhí)行make時(shí)可能會遇到錯誤"Performing Test HAVE_REALLOCARRAY_SUPPORT - Failed",其實(shí)僅需要執(zhí)行make pahole編譯出pahole即可。導(dǎo)出當(dāng)前內(nèi)核配置:
$ cd linux-5.10.1 $ cp -v /boot/config-$(uname -r) .config在linux-5.10.1目錄中使用
make menuconfig命令修改系統(tǒng)配置文件,并保存。可以使用"/"直接查找需要修改的內(nèi)核選項(xiàng);編譯并創(chuàng)建內(nèi)核鏡像,如果僅需要vmlinux的話,在編譯完之后執(zhí)行
make vmlinux即可$ make #可以使用多核方式加速編譯,指定使用4個核 $ make -j 4 #使用nproc命令獲取到的核數(shù) $ make -j $(nproc)安裝內(nèi)核:
$ sudo make modules_install安裝內(nèi)核:
$ sudo make install更新 grub config文件
$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg $ sudo grubby --set-default /boot/vmlinuz-5.6.9重啟
通過上述命令可以獲得到一個可兼容的C頭文件(即"vmlinux.h"),包含所有的內(nèi)核類型("所有"意味著包含那些不會通過kernel-devel包暴露的頭文件)。
編譯器支持
為了啟用BPF CO-RE,并讓BPF加載程序(即libbpf)將BPF程序調(diào)整為在目標(biāo)主機(jī)上運(yùn)行的特定內(nèi)核,Clang擴(kuò)展了一些內(nèi)置功能,通過這些擴(kuò)展功能可以發(fā)出BTF重定位,捕獲有關(guān)BPF程序代碼打算讀取哪些信息的高級描述。例如要讀取task_struct->pid字段,Clang會記錄一個名為"pid"的字段,類型為"pid_t",位于struct task_struct中。這樣,即使目標(biāo)內(nèi)核的task_struct結(jié)構(gòu)中的"pid"字段在task_struct結(jié)構(gòu)體內(nèi)部發(fā)生了偏移(如,由于"pid"字段前面添加了額外的字段),或即使該字段轉(zhuǎn)移到了某個嵌套的匿名結(jié)構(gòu)或聯(lián)合體中,這樣也能夠通過其名稱和類型信息找到它。這種方式稱為字段偏移量重定位。
通過這種方式可以捕獲不僅一個字段的偏移量,也可以捕獲字段的其他屬性,如字段的存在性或大小。即使對于比特字段(眾所周知,它們是C語言中“拒絕合作”的數(shù)據(jù)類型),也能夠捕獲足夠多的數(shù)據(jù)來重定位這些字段,所有這些對BPF程序開發(fā)人員都是透明的。
BPF加載器(libbpf)
前面的所有數(shù)據(jù)最終會集合到一起,由libbpf進(jìn)行處理,libbpf作為BPF程序的加載器。它會使用編譯好的BPF ELF文件,必要時(shí)對其進(jìn)行后處理,配置各種內(nèi)核對象(maps,programs等),然后觸發(fā)BPF程序的加載和驗(yàn)證。
libbpf知道如何將BPF程序代碼匹配到特定的內(nèi)核。它會查看程序記錄的BTF類型和重定位信息,然后將這些信息與內(nèi)核提供的BTF信息進(jìn)行匹配。libbpf解析并匹配所有的類型和字段,更新必要的偏移以及重定位數(shù)據(jù),確保BPF程序能夠正確地運(yùn)行在特定的內(nèi)核上。如果一切順利,則BPF應(yīng)用開發(fā)人員會獲得一個BPF程序,這種方式可以針對目標(biāo)主機(jī)上的內(nèi)核進(jìn)行“量身定制”,就好像程序是專門針對這個內(nèi)核編譯的,但無需在應(yīng)用程序中分發(fā)Clang以及在目標(biāo)主機(jī)上的運(yùn)行時(shí)中執(zhí)行編譯,就可以實(shí)現(xiàn)所有這些目標(biāo)。
內(nèi)核
令人驚奇的是,內(nèi)核無需太多變動就可以支持BPF CO-RE。歸功于一個好的關(guān)注點(diǎn)分離(separation of concerns,SOC),當(dāng)libbpf處理完BPF程序代碼之后,在內(nèi)核看來,它與其他有效的BPF程序代碼一樣,與使用最新內(nèi)核頭文件在主機(jī)上直接編譯的BPF程序并沒有區(qū)別,這意味著BPF CO-RE的許多功能都不需要先進(jìn)的內(nèi)核功能,因此可以更廣泛,更迅速地進(jìn)行調(diào)整。
有可能在某些場景下需求較新內(nèi)核的支持,但這種情況很少。在下一部分中,我們將在解釋BPF CO-RE面向用戶的機(jī)制時(shí)討論這種情況,其中將詳細(xì)介紹BPF CO-RE面向用戶的API。
BPF CO-RE:面向用戶的體驗(yàn)
現(xiàn)在我們將看一下BPF應(yīng)用的一些典型場景,以及如何通過BPF CO-RE解決兼容性問題。下面可以看到,一些可移植性問題(如兼容結(jié)構(gòu)體布局差異)可以透明地進(jìn)行處理,但其他一些場景則需要更加顯示地處理,如if/else條件判斷(與編譯時(shí)BCC程序中的#ifdef/#else構(gòu)造相反)和BPF CO-RE提供的一些額外機(jī)制。
擺脫對內(nèi)核頭文件的依賴
除了使用內(nèi)核的BTF信息進(jìn)行字段的重定位意外,還可以將BTF信息生成一個大(基于5.10.1版本生成的長度有106382行)的頭文件("vmlinux.h"),其中包含了所有的內(nèi)核內(nèi)部類型,可以避免對系統(tǒng)范圍的內(nèi)核頭文件的依賴。可以使用如下方式生成vmlinux.h:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
當(dāng)使用了vmlinux.h,此時(shí)就不需要依賴像#include <linux/sched.h>, #include <linux/fs.h>這樣的頭文件,僅需要#include "vmlinux.h"即可。該頭文件包含了所有的內(nèi)核類型:暴露了UAPI,通過kernel-devel提供的內(nèi)部類型,以及其他一些更加內(nèi)部的內(nèi)核類型。
不幸的是,BTF(即DWARF)不會記錄#define宏,因此在vmlinux.h中丟失一些常用的宏。但大多數(shù)通常不存在的宏可以通過libbpf的bpf_helpers.h(即libbpf提供的內(nèi)核側(cè)的庫)頭文件提供。
讀取內(nèi)核結(jié)構(gòu)體字段
大多數(shù)場景下會從某個內(nèi)核結(jié)構(gòu)中讀取一個字段。假設(shè)我們期望讀取task_struct結(jié)構(gòu)體的pid字段。使用BCC時(shí)非常簡單:
pid_t pid = task->pid;
BCC會將task->pid重寫為對bpf_probe_read()的調(diào)用,非常方便(雖然有時(shí)候不會成功,具體取決于使用的表達(dá)式的復(fù)雜度)。當(dāng)使用libbpf時(shí),由于它沒有BCC的代碼重寫功能,因此需要使用其他方式來得到相同的結(jié)果。
如果添加了BTF_PROG_TYPE_TRACING 程序,那么就可以輕松掌握BPF驗(yàn)證程序,允許理解和跟蹤BTF類型的本質(zhì),并允許使用指針直接讀取內(nèi)核內(nèi)存,避免使用bpf_probe_read()調(diào)用。
Libbpf + BPF_PROG_TYPE_TRACING 方式:
pid_t pid = task->pid;
將該功能與BPF CO-RE配合使用,可以支持可移植(即可重定位)的字段讀取,此時(shí)需要將此代碼封裝到編譯器內(nèi)置的__builtin_preserve_access_index中
BPF_PROG_TYPE_TRACING + BPF CO-RE 方式:
pid_t pid = __builtin_preserve_access_index(({ task->pid; }));
這種方式能夠正常工作,同時(shí)也支持不同內(nèi)核版本間的可移植性。但鑒于BPF_PROG_TYPE_TRACING的前沿性,因此必須顯式地使用bpf_probe_read()。
非CO-RE libbpf方式:
pid_t pid;
bpf_probe_read(&pid, sizeof(pid), &task->pid);
現(xiàn)在,使用CO-RE+libbpf,我們有兩種方式來實(shí)現(xiàn)訪問pid字段的值。一種是直接使用bpf_core_read()替換bpf_probe_read():
pid_t pid;
bpf_core_read(&pid, sizeof(pid), &task->pid);
bpf_core_read()是一個簡單的宏,它會將所有的參數(shù)直接傳遞給bpf_probe_read(),但也會使Clang通過__builtin_preserve_access_index()記錄第三個參數(shù)(&task->pid)的字段的偏移量。
bpf_probe_read(&pid, **sizeof**(pid), __builtin_preserve_access_index(&task->pid));
但像bpf_probe_read()/bpf_core_read()這樣的調(diào)用方式很快就會變得難以維護(hù),特別是獲取通過指針連在一起的結(jié)構(gòu)體時(shí)。例如,獲取當(dāng)前進(jìn)程的可執(zhí)行文件的inode號時(shí),可以使用BCC獲取:
u64 inode = task->mm->exe_file->f_inode->i_ino;
當(dāng)使用 bpf_probe_read()/bpf_core_read()時(shí),將會變?yōu)?個調(diào)用,并使用一個臨時(shí)變量來保存這些中間指針,才能最終獲得i_ino字段。當(dāng)使用BPF CO-RE時(shí),我們可以使用一個輔助宏來使用類似BCC的方式獲得該字段的值:
BPF CO-RE方式:
u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode, i_ino);
此外,如果想要使用一個變量保存內(nèi)容,則可以使用如下方式,避免使用額外的中間變量:
u64 inode;
BPF_CORE_READ_INTO(&inode, task, mm, exe_file, f_inode, i_ino);
還有一個對應(yīng)的 bpf_core_read_str(),可以直接替換bpf_probe_read_str();還有一個BPF_CORE_READ_STR_INTO()宏,其工作方式與BPF_CORE_READ_INTO()類似,但會對最后一個字段執(zhí)行bpf_probe_read_str()調(diào)用。
可以通過bpf_core_field_exists()宏校驗(yàn)?zāi)繕?biāo)內(nèi)核是否存在某個字段,并以此作相應(yīng)的處理。
pid_t pid = bpf_core_field_exists(task->pid) ? BPF_CORE_READ(task, pid) : -1;
此外,可以通過bpf_core_field_size()宏捕獲任意字段的大小,以此來保證不同內(nèi)核版本間的字段大小沒有發(fā)生變化。
u32 comm_sz = bpf_core_field_size(task->comm); /* will set comm_sz to 16 */
除此之外,在某些情況下,當(dāng)讀取一個內(nèi)核結(jié)構(gòu)體的比特位字段時(shí),可以使用特殊的BPF_CORE_READ_BITFIELD() (使用直接內(nèi)存讀取) 和BPF_CORE_READ_BITFIELD_PROBED() (依賴bpf_probe_read() 調(diào)用)宏。它們抽象了提取比特位字段繁瑣而痛苦的細(xì)節(jié),同時(shí)保留了跨內(nèi)核版本的可移植性:
struct tcp_sock *s = ...;
/* with direct reads */
bool is_cwnd_limited = BPF_CORE_READ_BITFIELD(s, is_cwnd_limited);
/* with bpf_probe_read()-based reads */
u64 is_cwnd_limited;
BPF_CORE_READ_BITFIELD_PROBED(s, is_cwnd_limited, &is_cwnd_limited);
字段重定位和相關(guān)的宏是BFP CO-RE提供的主要能力。它涵蓋了很多實(shí)際的使用案例。
處理內(nèi)核版本和配置差異
在一些場景下,BPF程序不得不處理內(nèi)核間的差異。如某些字段名稱的變更導(dǎo)致其變?yōu)榱艘粋€完全不同的字段(但具有相同的意義)。反之亦然,當(dāng)字段不變,但其含義發(fā)生了變化。如在內(nèi)核4.6之后,task_struct結(jié)構(gòu)體的utime和stime字段從以秒為單位換為以納秒為單位,這種情況下,不得不進(jìn)行一些轉(zhuǎn)換工作。有時(shí),需要提取的數(shù)據(jù)存在于某些內(nèi)核配置中,但已在其他內(nèi)核配置中進(jìn)行了編譯。還有在很多其他場景下,不可能有一個適合所有內(nèi)核的通用類型。
為了處理上述問題,BPF CO-RE提出了兩種補(bǔ)充方案:libbpf提供了extern Kconfig variables 和struct flavors.
Libbpf提供的外部變量很簡單。BPF程序可以使用一個知名名稱(如LINUX_KERNEL_VERSION,用于獲取允許的內(nèi)核的版本)定義一個外部變量,或使用Kconfig的鍵(如CONFIG_HZ,用于獲取內(nèi)核的HZ值),libbpf會使BPF程序可以將這類外部變量用作任何其他全局變量。這些變量具有正確的值,與執(zhí)行BPF程序的活動內(nèi)核相匹配。此外,BPF校驗(yàn)器會跟蹤這些變量,并能夠使用它們進(jìn)行高級控制流分析和消除無效代碼。查看如下例子,了解如何使用BPF CO-RE抽取線程的CPU用戶時(shí)間:
extern u32 LINUX_KERNEL_VERSION __kconfig;
extern u32 CONFIG_HZ __kconfig;
u64 utime_ns;
if (LINUX_KERNEL_VERSION >= KERNEL_VERSION(4, 11, 0))
utime_ns = BPF_CORE_READ(task, utime);
else
/* convert jiffies to nanoseconds */
utime_ns = BPF_CORE_READ(task, utime) * (1000000000UL / CONFIG_HZ);
其他機(jī)制,如struct flavors,可以用于不同內(nèi)核間類型不兼容的場景。這種場景下,無法使用一個通用的結(jié)構(gòu)體定義來為多個內(nèi)核提供相同的BPF程序。下面是一個人為構(gòu)造的例子,看下struct flavors如何抽取fs/fsbase(已經(jīng)重命名)來作一些線程本地?cái)?shù)據(jù)的處理:
/* up-to-date thread_struct definition matching newer kernels */
struct thread_struct {
...
u64 fsbase;
...
};
/* legacy thread_struct definition for <= 4.6 kernels */
struct thread_struct___v46 { /* ___v46 is a "flavor" part */
...
u64 fs;
...
};
extern int LINUX_KERNEL_VERSION __kconfig;
...
struct thread_struct *thr = ...;
u64 fsbase;
if (LINUX_KERNEL_VERSION > KERNEL_VERSION(4, 6, 0))
fsbase = BPF_CORE_READ((struct thread_struct___v46 *)thr, fs);
else
fsbase = BPF_CORE_READ(thr, fsbase);
本例中,BPF應(yīng)用將<= 4.6內(nèi)核的“舊版” thread_struct定義為struct thread_struct___v46。類型名稱中的三個下劃線以及其后的所有內(nèi)容均被視為此結(jié)構(gòu)的“flavor”。libbpf會忽略這個flavor部分,即在執(zhí)行重定位時(shí),該類型定義會匹配到實(shí)際運(yùn)行的內(nèi)核的struct thread_struct。這樣的約定允許在一個C程序中具有可替代(且不兼容)的定義,并在運(yùn)行時(shí)選擇最合適的定義(例如,上面示例中的特定于內(nèi)核版本的處理邏輯),然后使用類型強(qiáng)轉(zhuǎn)為struct flavor來提取必要的字段。
如果沒有structural flavors,則不能實(shí)現(xiàn)編譯一次就可以在多個內(nèi)核上運(yùn)行的目標(biāo),否則就需要將#ifdef源代碼編譯成兩個單獨(dú)的BPF程序,并在運(yùn)行時(shí)由控制應(yīng)用程序手動選擇適當(dāng)?shù)腂PF程序,這些操作增加了復(fù)雜度和維護(hù)的成本。盡管不是透明的,但BPF CO-RE甚至可以使用這種高級方案,通過熟悉的C代碼構(gòu)造來解決此問題。
根據(jù)用戶提供的配置變更行為
有時(shí)候,在BPF程序了解內(nèi)核版本和配置之后仍然無法決定如何從內(nèi)核獲取數(shù)據(jù)。這種情況下,用戶空間的控制程序可能是唯一知道確切需要做什么的一方,以及需要啟用或禁用那些特性。通常是通過某種配置數(shù)據(jù)進(jìn)行通信,在用戶空間和BPF程序之間共享數(shù)據(jù)。現(xiàn)今,一種不需要依賴BPF CO-RE的實(shí)現(xiàn)方式是使用BPF map作為配置數(shù)據(jù)的容器。BPF程序通過查找BPF map來抽取配置,并根據(jù)配置變更控制流,但這種方法有很多缺點(diǎn):
- BPF程序每次進(jìn)行map查詢配置值時(shí)都會造成運(yùn)行時(shí)開銷。這部分開銷可能會快速增大,某些高性能BPF應(yīng)用禁止這種方式。
- 配置值是不變的,且在BPF程序啟動之后是只讀的,但這部分?jǐn)?shù)據(jù)仍然在BPF校驗(yàn)器在校驗(yàn)階段仍然被認(rèn)為是黑盒數(shù)據(jù)。意味著校驗(yàn)器無法清理無用代碼以及執(zhí)行其他高級代碼分析,使得無法使用BPF程序邏輯的可配置部分(這部分功能是最前沿的功能,僅在新內(nèi)核中支持,當(dāng)運(yùn)行在老內(nèi)核上時(shí)不會破壞該程序)。由于BPF驗(yàn)證程序必須悲觀地認(rèn)為配置可以是任何東西,且有可能會使用該"未知"的功能(盡管用戶明確配置不會發(fā)生這種情況)。
解決此類(公認(rèn)復(fù)雜)場景的方法是使用只讀全局?jǐn)?shù)據(jù)。在BPF程序加載到內(nèi)核之前由控制應(yīng)用進(jìn)行設(shè)置。從BPF程序側(cè)看,這部分?jǐn)?shù)據(jù)就像訪問普通的全局變量。由于全局變量使用直接內(nèi)存訪問方式,因此不會產(chǎn)生BPF map查詢的開銷。控制語言側(cè)需要在BPF程序加載之前設(shè)置初始的配置值,這樣當(dāng)BPF校驗(yàn)器進(jìn)行程序校驗(yàn)時(shí),會將配置值認(rèn)為是只讀的,這樣BPF校驗(yàn)器會將這部分內(nèi)容認(rèn)為是已知的常量,并使用高級控制流分析來執(zhí)行無用代碼的刪除。
上例中,在老版本的BPF校驗(yàn)器下,將不會使用未知的BPF輔助功能,且這部分代碼會被移除。在新版本BPF校驗(yàn)器下,應(yīng)用提供不同的配置后,允許使用新的BPF輔助功能,這部分邏輯會通過BPF校驗(yàn)器的校驗(yàn)。下面BPF代碼例子很好地展示了這種行為:
/* global read-only variables, set up by control app */
const bool use_fancy_helper;
const u32 fallback_value;
...
u32 value;
if (use_fancy_helper)
value = bpf_fancy_helper(ctx);
else
value = bpf_default_helper(ctx) * fallback_value;
從用戶空間看,應(yīng)用程序?qū)⒛軌蛲ㄟ^BPF框架輕松地提供此配置。BPF框架討論不在本文討論范圍之內(nèi),請參閱內(nèi)核代碼庫中的runqslower 工具來展示如何使用它來簡化BPF應(yīng)用程序。
回顧
BPF CO-RE的目標(biāo)是幫助BPF開發(fā)者使用一個簡單的方式解決簡單的可移植性問題(如讀取結(jié)構(gòu)體字段),并使用它來定位復(fù)雜的可移植性問題(如不兼容的數(shù)據(jù)結(jié)構(gòu),復(fù)雜的用戶空間控制條件等)。使得開發(fā)者的BPF程序能夠"一次編譯–隨處運(yùn)行", 這是通過結(jié)合一些上述的BPF CO-RE構(gòu)建塊來實(shí)現(xiàn)的:
vmlinux.h消除了對內(nèi)核頭文件的依賴;- 字段重定位(字段偏移,存在性,大小等)使得可以從內(nèi)核中抽取數(shù)據(jù);
- libbpf提供的
Kconfig外部變量允許BPF程序適應(yīng)各種內(nèi)核版本以及特定配置的更改; - 當(dāng)上述都不適合時(shí),app提供了只讀的配置和
struct flavors,作為解決任何應(yīng)用程序必須處理的復(fù)雜場景的最終大錘。
不需要CO-RE功能也可以成功編寫,部署和維護(hù)可以支持的BPF程序,但在需要時(shí),BPF CO-RE可提供最簡單的方式來幫助解決問題。所有這些功能仍然提供了良好的可用性和熟悉的工作流程,可將C代碼編譯為二進(jìn)制文件,并進(jìn)行輕量級的分發(fā)。不再需要繁瑣的編譯器庫并為運(yùn)行時(shí)編譯付出寶貴的運(yùn)行時(shí)資源。 同樣,也不再需要在運(yùn)行時(shí)捕獲瑣碎的編譯錯誤。
TIPS
- 相關(guān)信息可以參見官方說明:BPF CO-RE (Compile Once – Run Everywhere)
- libbpf的頭文件位于內(nèi)核源碼的
/tools/lib/bpf目錄下
參考
- BPF CO-RE presentation from LSF/MM2019 conference: summary, slides.
- Arnaldo Carvalho de Melo’s presentation "BPF: The Status of BTF" dives deep into BPF CO-RE and dissects the runqslower tool quite nicely.
- BTF deduplication algorithm
本文來自博客園,作者:charlieroro,轉(zhuǎn)載請注明原文鏈接:http://www.rzrgm.cn/charlieroro/p/14206214.html

浙公網(wǎng)安備 33010602011771號