天天看點

BPF的可移植性和CO-RE (Compile Once – Run Everywhere)

BPF的可移植性和CO-RE (Compile Once – Run Everywhere)

在上一篇文章中介紹了提高socket性能的幾個socket選項,其中給出了幾個源于核心源碼樹中的例子,如果選擇使用核心樹中的Makefile進行編譯的話,可能會出現與本地頭檔案沖突的情況,如重複定義變量,結構體類型不對等錯誤。這些問題大大影響了BPF程式的可移植性。

本文将介紹BPF可移植性存在的問題,以及如何使用BPF CO-RE(Compile Once – Run Everywhere)解決這些問題。

BPF:最前沿的技術

自BPF成立以來,BPF社群将盡可能簡化BPF應用程式的開發作為工作重點,目的是将BPF的使用變得與使用者空間的應用一樣簡單明了。伴随着BPF可程式設計性的穩步發展,BPF程式的開發也越來越簡單。

盡管BPF提升了使用上的便利性,但卻忽略了BPF程式開發中的一個方面:可移植性。"BPF可移植性"意味着什麼?我們将BPF可移植性定義為成功編寫并通過核心驗證的一個BPF程式,且跨核心版本可用,無需針對特定的核心重新編譯。

本文描述了BPF的可移植性問題以及解決方案:BPF CO-RE(Compile Once – Run Everywhere)。首先會調研BPF本身的可移植性問題,描述為什麼這是個問題,以及為什麼解決它很重要。然後,我們将介紹解決方案中的進階元件:BPF CO-RE,并簡要介紹實作這一目标所需要解決的難題。最後,我們将以各種教程作為結尾,介紹BPF CO-RE方法的使用者API,并提供相關示例。

BPF可移植性的問題

BPF程式是使用者提供的一部分代碼,這些代碼會直接注入到核心,一旦經過加載和驗證,BPF程式就可以在核心上下文中運作。這些程式運作在核心的記憶體空間中,并能夠通路所有可用的核心内部狀态,功能非常強大,這也是為什麼BPF技術成功落地到多個應用中的原因。然而,在使用其強大的能力的同時也帶來了一些負擔:BPF程式無法控制周圍核心環境的記憶體布局,是以必須依賴獨立開發,編譯和部署的核心。

此外,核心類型和資料結構會不斷變化。不同的核心版本會在結構體内部混用結構體字段,甚至會轉移到新的内部結構體中。結構體中的字段可能會被重命名或删除,類型可能會改變(變為微相容或完全不同的類型)。結構體和其他類型可以被重命名,被條件編譯(取決于核心配置),或直接從核心版本中移除。

換句話講,不同核心釋出版本中的所有内容都有可能發生變化,BPF應用開發者應該能夠預料到這個問題。考慮到不斷變化的核心環境,那麼該如何利用BPF做有用的事?有如下幾點原因:

首先,并不是所有的BPF程式都需要通路内部的核心資料結構。一個例子是

opensnoop

工具,該工具依靠kprobes /tracepoints來跟蹤哪個程序打開了哪些檔案,僅需要捕獲少量的系統調用就可以工作。由于系統調用提供了穩定的ABI,不會随着核心版本而變化,是以不用考慮這類BPF程式的可移植性。不幸的是,這類應用非常少,且這類應用的功能也會大大受限。

此外,核心内部的BPF機器提供了有限的“穩定接口”集,BPF程式可以依靠這些穩定接口在核心間保持穩定。事實上,不同版本的核心的底層結構和機制是會發生變化的,但BPF提供的穩定接口從使用者程式中抽象了這些細節。

例如,網絡應用會通過檢視少量的

sk_buff

(即封包資料)中的屬性來獲得非常有用且通用的資訊。為此,BPF校驗器提供了一個穩定的

__sk_buff

視圖(注意前面的下劃線),該視圖為BPF程式屏蔽了

struct sk_buff

結構體的變更。所有對

__sk_buff

字段通路都可以透明地重寫為對實際

sk_buff

的通路(有時非常複雜-在擷取最終請求的字段之前需要追蹤一堆内部指針)。類似的機制同樣适用于不同的BPF程式類型,通過BPF校驗器來識别特定類型的BPF上下文。如果使用這類上下文開發BPF程式,就可以不用擔心可移植性問題。

但有時候需要通路原始的核心資料(如經常會通路到的

struct task_struct

,表示一個程序或線程,包含大量程序資訊),此時就隻能靠自己了。跟蹤,監視和分析應用程式通常是這種情況,這些應用程式是一類非常有用的BPF程式。

在這種情況下,如果某些核心在需要采集的字段(如從

struct task_struct

開始的第16個位元組的偏移處)前添加了一個新的字段,那麼此時如何保證不會讀取到垃圾資料?如果一個字段重命名了又如何處理(如核心4.6和4.7的

thread_struct

fs

字段的名稱是不同的)?或者如果需要基于一個核心的兩種配置來運作程式,其中一個配置會禁用某些特性,并編譯出部分結構(一種常見的場景是解釋字段,這些字段是可選的,但如果存在則非常有用)?所有這些條件意味着無法使用本地開發伺服器上的頭檔案編譯出一個BPF程式,然後分發到其他系統上運作。這是因為不同核心版本的頭檔案中的資料的記憶體布局可能是不同的。

迄今為止,人們編譯這類BPF程式會依賴BCC (BPF Compiler Collection)。使用BCC,可以将BPF程式的C代碼以字元串的形式嵌入到使用者空間的程式中,當程式最終部署并運作在目标主機上後,BCC會喚醒其嵌入的Clang/LLVM,提取本地核心頭檔案(必須確定已從正确的kernel-devel軟體包中将其安裝在系統上),并即時進行編譯。通過這種方式來確定BPF程式期望的記憶體布局和主機運作的核心的記憶體布局是相同的。如果需要處理一些選項和核心編譯出來的潛在産物,則可以在自己的源代碼中添加

#ifdef

/

#else

來适應重命名字段、不同的數值語義或目前配置導緻的不可用内容等帶來的風險。嵌入的Clang會移除代碼中無關的内容,并調整BPF程式代碼,以比對到特定的核心。

這種方式聽起來很不錯,但實際并非沒有缺點:

  • Clang/LLVM組合是一個很大的庫,導緻釋出的應用的庫會比較大;
  • Clang/LLVM組合使用的資源比較多,是以當編譯的BPF代碼啟動時會消耗大量資源,可能會推翻已均衡的生産負載;
  • 這樣做其實也是在賭目标系統将存在核心頭檔案,大多數情況下這不是問題,但有時可能會引起很多麻煩。這也是核心開發人員感到特别麻煩的點,因為他們經常必須在開發過程中建構和部署自定義的核心。如果沒有自定義建構的核心頭檔案包,則基于BCC的應用将無法在這種核心上運作,進而剝奪了開發人員用于調試和監視的工具集;
  • BPF程式的測試和開發疊代也相當痛苦,因為一旦重新編譯并重新開機使用者空間控制應用程式,甚至會在運作時遇到各種瑣碎的編譯錯誤。這無疑會增加難度,且無益于快速疊代。

總之, BCC是一個很好的工具,尤其适合快速原型制作,實驗和小型工具,但在用于廣泛部署的生産BPF應用程式時,它無疑具有很多缺點。

我們正在使用BPF CO-RE來增強BPF的可移植性,并相信這是未來BPF程式開發的趨勢,尤其對于複雜的實際應用的BPF程式。

進階BFP CO-RE機制

BPF CO-RE在軟體堆棧的各個級别彙集了必要的功能和資料:核心,使用者空間的BPF加載器庫(libbpf),和編譯器(Clang)。通過這些元件來支援編寫可移植的BPF程式,使用相同的預編譯的BPF程式來處理不同核心之間的差異。BPF CO-RE需要以下元件的內建和合作:

  • BTF類型資訊,用于允許擷取關于核心和BPF程式類型和代碼的關鍵資訊,進而為解決BPF CO-RE的其他難題提供了可能性;
  • 編譯器(Clang)為BPF程式C代碼提供了表達意圖和記錄重定位資訊的方法;
  • BPF加載器(libbpf)将核心和BPF程式中的BTF綁定在一起,用于将編譯後的BPF代碼調整為目标主機上的特定核心代碼;
  • 核心,在完全不依賴BPF CO-RE的情況下,提供了進階BPF功能來啟用某些更進階的場景。

這些元件可以內建到一起工作,提供了前所未有的便捷性,适應性和表達性(來開發可移植BPF程式,以前隻能在運作時通過BCC編譯BPF程式的C代碼來實作),而無需像BCC一樣付出高昂的代價。

BTF

整個BPF CO-RE方法的關鍵推動因素之一是BTF。BTF (BPF Type Format) 是作為一個更通用,更詳細的DWARF調試資訊的替代品而出現的。BTF是一種節省空間,緊湊但依然具有足夠表達能力的格式,可以描述C程式的所有類型資訊。由于其簡單性和使用的重複資料删除算法,與DWARF相比,BTF的大小可減少多達100倍。現在,已經可以在核心運作時顯示地嵌入BPF類型資訊:隻需要啟用

CONFIG_DEBUG_INFO_BTF=y

核心選項即可。核心本身可以使用BTF功能,用于增強BPF驗證程式自身的功能。

關于BPF CO-RE更重要的是,核心還通過

/sys/kernel/btf/vmlinux

上的sysfs公開了這種自描述的權威BTF資訊(定義了确切的結構布局)。嘗試如下指令:

$ bpftool btf dump file /sys/kernel/btf/vmlinux format c           
某些unix系統下安裝的bpftool預設不支援btf指令選項,可以在linux核心源碼的

/tools/bpf/bpftool

目錄下執行

make

指令進行編譯。如果遇到

linux/if.h

net/if.h

頭檔案定義沖突的話,可以将

/tools/bpf/bpftool/net.c

中的這一行注釋掉再編譯:

#include <linux/if.h>

目前很多核心預設并不會打開BTF核心選項,是以需要自己編譯核心。基本步驟如下:
  1. 首先更新gcc;
  2. 編譯帶BTF選項的核心前需要安裝pahole,可以從github官方下載下傳源碼編譯即可。需要注意的是,該編譯過程需要依賴

    git

    ,是以需要通過

    git clone

    代碼編譯,而不能下載下傳源碼壓縮包編譯;按照官方編譯步驟直接執行

    make

    時可能會遇到錯誤"Performing Test HAVE_REALLOCARRAY_SUPPORT - Failed",其實僅需要執行

    make pahole

    編譯出pahole即可。
  3. 導出目前核心配置:
    $ cd linux-5.10.1     $ cp -v /boot/config-$(uname -r) .config           
  4. 在linux-5.10.1目錄中使用

    make menuconfig

    指令修改系統配置檔案,并儲存。可以使用"/"直接查找需要修改的核心選項;
  5. 編譯并建立核心鏡像,如果僅需要vmlinux的話,在編譯完之後執行

    make vmlinux

    即可
    $ make     #可以使用多核方式加速編譯,指定使用4個核     $ make -j 4     #使用nproc指令擷取到的核數     $ make -j $(nproc)           
  6. 安裝核心:
    $ sudo make modules_install           
  7. $ sudo make install           
  8. 更新 grub config檔案
    $ sudo grub2-mkconfig -o /boot/grub2/grub.cfg     $ sudo grubby --set-default /boot/vmlinuz-5.6.9           
  9. 重新開機

通過上述指令可以獲得到一個可相容的C頭檔案(即"vmlinux.h"),包含所有的核心類型("所有"意味着包含那些不會通過

kernel-devel

包暴露的頭檔案)。

編譯器支援

為了啟用BPF CO-RE,并讓BPF加載程式(即libbpf)将BPF程式調整為在目标主機上運作的特定核心,Clang擴充了一些内置功能,通過這些擴充功能可以發出BTF重定位,捕獲有關BPF程式代碼打算讀取哪些資訊的進階描述。例如要讀取

task_struct->pid

字段,Clang會記錄一個名為"pid"的字段,類型為"pid_t",位于

struct task_struct

中。這樣,即使目标核心的

task_struct

結構中的"pid"字段在

task_struct

結構體内部發生了偏移(如,由于"pid"字段前面添加了額外的字段),或即使該字段轉移到了某個嵌套的匿名結構或聯合體中,這樣也能夠通過其名稱和類型資訊找到它。這種方式稱為字段偏移量重定位。

通過這種方式可以捕獲不僅一個字段的偏移量,也可以捕獲字段的其他屬性,如字段的存在性或大小。即使對于比特字段(衆所周知,它們是C語言中“拒絕合作”的資料類型),也能夠捕獲足夠多的資料來重定位這些字段,所有這些對BPF程式開發人員都是透明的。

BPF加載器(libbpf)

前面的所有資料最終會集合到一起,由libbpf進行處理,libbpf作為BPF程式的加載器。它會使用編譯好的BPF ELF檔案,必要時對其進行後處理,配置各種核心對象(maps,programs等),然後觸發BPF程式的加載和驗證。

libbpf知道如何将BPF程式代碼比對到特定的核心。它會檢視程式記錄的BTF類型和重定位資訊,然後将這些資訊與核心提供的BTF資訊進行比對。libbpf解析并比對所有的類型和字段,更新必要的偏移以及重定位資料,確定BPF程式能夠正确地運作在特定的核心上。如果一切順利,則BPF應用開發人員會獲得一個BPF程式,這種方式可以針對目标主機上的核心進行“量身定制”,就好像程式是專門針對這個核心編譯的,但無需在應用程式中分發Clang以及在目标主機上的運作時中執行編譯,就可以實作所有這些目标。

核心

令人驚奇的是,核心無需太多變動就可以支援BPF CO-RE。歸功于一個好的關注點分離(separation of concerns,SOC),當libbpf處理完BPF程式代碼之後,在核心看來,它與其他有效的BPF程式代碼一樣,與使用最新核心頭檔案在主機上直接編譯的BPF程式并沒有差別,這意味着BPF CO-RE的許多功能都不需要先進的核心功能,是以可以更廣泛,更迅速地進行調整。

有可能在某些場景下需求較新核心的支援,但這種情況很少。在下一部分中,我們将在解釋BPF CO-RE面向使用者的機制時讨論這種情況,其中将詳細介紹BPF CO-RE面向使用者的API。

BPF CO-RE:面向使用者的體驗

現在我們将看一下BPF應用的一些典型場景,以及如何通過BPF CO-RE解決相容性問題。下面可以看到,一些可移植性問題(如相容結構體布局差異)可以透明地進行處理,但其他一些場景則需要更加顯示地處理,如

if

else

條件判斷(與編譯時BCC程式中的

#ifdef

#else

構造相反)和BPF CO-RE提供的一些額外機制。

擺脫對核心頭檔案的依賴

除了使用核心的BTF資訊進行字段的重定位意外,還可以将BTF資訊生成一個大(基于5.10.1版本生成的長度有106382行)的頭檔案("vmlinux.h"),其中包含了所有的核心内部類型,可以避免對系統範圍的核心頭檔案的依賴。可以使用如下方式生成vmlinux.h:

$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h           

當使用了vmlinux.h,此時就不需要依賴像

#include <linux/sched.h>

,

#include <linux/fs.h>

這樣的頭檔案,僅需要

#include "vmlinux.h"

即可。該頭檔案包含了所有的核心類型:暴露了UAPI,通過

kernel-devel

提供的内部類型,以及其他一些更加内部的核心類型。

不幸的是,BTF(即DWARF)不會記錄

#define

宏,是以在vmlinux.h中丢失一些常用的宏。但大多數通常不存在的宏可以通過libbpf的bpf_helpers.h(即libbpf提供的核心側的庫)頭檔案提供。

讀取核心結構體字段

大多數場景下會從某個核心結構中讀取一個字段。假設我們期望讀取

task_struct

結構體的pid字段。使用BCC時非常簡單:

pid_t pid = task->pid;           

BCC會将task->pid重寫為對

bpf_probe_read()

的調用,非常友善(雖然有時候不會成功,具體取決于使用的表達式的複雜度)。當使用libbpf時,由于它沒有BCC的代碼重寫功能,是以需要使用其他方式來得到相同的結果。

如果添加了

BTF_PROG_TYPE_TRACING

程式,那麼就可以輕松掌握BPF驗證程式,允許了解和跟蹤BTF類型的本質,并允許使用指針直接讀取核心記憶體,避免使用

bpf_probe_read()

調用。

Libbpf + BPF_PROG_TYPE_TRACING 方式:

pid_t pid = task->pid;           

将該功能與BPF CO-RE配合使用,可以支援可移植(即可重定位)的字段讀取,此時需要将此代碼封裝到編譯器内置的

__builtin_preserve_access_index

BPF_PROG_TYPE_TRACING + BPF CO-RE 方式:

pid_t pid = __builtin_preserve_access_index(({ task->pid; }));           

這種方式能夠正常工作,同時也支援不同核心版本間的可移植性。但鑒于

BPF_PROG_TYPE_TRACING

的前沿性,是以必須顯式地使用

bpf_probe_read()

非CO-RE libbpf方式:

pid_t pid;     bpf_probe_read(&pid, sizeof(pid), &task->pid);           

現在,使用CO-RE+libbpf,我們有兩種方式來實作通路pid字段的值。一種是直接使用

bpf_core_read()

替換

bpf_probe_read()

:

pid_t pid;     bpf_core_read(&pid, sizeof(pid), &task->pid);           

bpf_core_read()

是一個簡單的宏,它會将所有的參數直接傳遞給

bpf_probe_read()

,但也會使Clang通過

__builtin_preserve_access_index()

記錄第三個參數(

&task->pid

)的字段的偏移量。

bpf_probe_read(&pid, **sizeof**(pid), __builtin_preserve_access_index(&task->pid));           

但像

bpf_probe_read()

bpf_core_read()

這樣的調用方式很快就會變得難以維護,特别是擷取通過指針連在一起的結構體時。例如,擷取目前程序的可執行檔案的

inode

号時,可以使用BCC擷取:

u64 inode = task->mm->exe_file->f_inode->i_ino;           

當使用

bpf_probe_read()

bpf_core_read()

時,将會變為4個調用,并使用一個臨時變量來儲存這些中間指針,才能最終獲得

i_ino

字段。當使用BPF CO-RE時,我們可以使用一個輔助宏來使用類似BCC的方式獲得該字段的值:

BPF CO-RE方式:

u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode, i_ino);           

此外,如果想要使用一個變量儲存内容,則可以使用如下方式,避免使用額外的中間變量:

u64 inode;     BPF_CORE_READ_INTO(&inode, task, mm, exe_file, f_inode, i_ino);           

還有一個對應的

bpf_core_read_str()

,可以直接替換

bpf_probe_read_str()

;還有一個

BPF_CORE_READ_STR_INTO()

宏,其工作方式與

BPF_CORE_READ_INTO()

類似,但會對最後一個字段執行

bpf_probe_read_str()

可以通過

bpf_core_field_exists()

宏校驗目标核心是否存在某個字段,并以此作相應的處理。

pid_t pid = bpf_core_field_exists(task->pid) ? BPF_CORE_READ(task, pid) : -1;           

此外,可以通過

bpf_core_field_size()

宏捕獲任意字段的大小,以此來保證不同核心版本間的字段大小沒有發生變化。

u32 comm_sz = bpf_core_field_size(task->comm); /* will set comm_sz to 16 */           

除此之外,在某些情況下,當讀取一個核心結構體的比特位字段時,可以使用特殊的

BPF_CORE_READ_BITFIELD()

(使用直接記憶體讀取) 和

BPF_CORE_READ_BITFIELD_PROBED()

(依賴

bpf_probe_read()

調用)宏。它們抽象了提取比特位字段繁瑣而痛苦的細節,同時保留了跨核心版本的可移植性:

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);           

字段重定位和相關的宏是BFP CO-RE提供的主要能力。它涵蓋了很多實際的使用案例。

處理核心版本和配置差異

在一些場景下,BPF程式不得不處理核心間的差異。如某些字段名稱的變更導緻其變為了一個完全不同的字段(但具有相同的意義)。反之亦然,當字段不變,但其含義發生了變化。如在核心4.6之後,

task_struct

結構體的

utime

stime

字段從以秒為機關換為以納秒為機關,這種情況下,不得不進行一些轉換工作。有時,需要提取的資料存在于某些核心配置中,但已在其他核心配置中進行了編譯。還有在很多其他場景下,不可能有一個适合所有核心的通用類型。

為了處理上述問題,BPF CO-RE提出了兩種補充方案:libbpf提供了extern Kconfig variables 和struct flavors.

Libbpf提供的外部變量很簡單。BPF程式可以使用一個知名名稱(如LINUX_KERNEL_VERSION,用于擷取允許的核心的版本)定義一個外部變量,或使用Kconfig的鍵(如CONFIG_HZ,用于擷取核心的HZ值),libbpf會使BPF程式可以将這類外部變量用作任何其他全局變量。這些變量具有正确的值,與執行BPF程式的活動核心相比對。此外,BPF校驗器會跟蹤這些變量,并能夠使用它們進行進階控制流分析和消除無效代碼。檢視如下例子,了解如何使用BPF CO-RE抽取線程的CPU使用者時間:

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);           

其他機制,如struct flavors,可以用于不同核心間類型不相容的場景。這種場景下,無法使用一個通用的結構體定義來為多個核心提供相同的BPF程式。下面是一個人為構造的例子,看下struct flavors如何抽取

fs

fsbase

(已經重命名)來作一些線程本地資料的處理:

/* 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應用将<= 4.6核心的“舊版”

thread_struct

定義為

struct thread_struct___v46

。類型名稱中的三個下劃線以及其後的所有内容均被視為此結構的“flavor”。libbpf會忽略這個flavor部分,即在執行重定位時,該類型定義會比對到實際運作的核心的

struct thread_struct

。這樣的約定允許在一個C程式中具有可替代(且不相容)的定義,并在運作時選擇最合适的定義(例如,上面示例中的特定于核心版本的處理邏輯),然後使用類型強轉為struct flavor來提取必要的字段。

如果沒有structural flavors,則不能實作編譯一次就可以在多個核心上運作的目标,否則就需要将

#ifdef

源代碼編譯成兩個單獨的BPF程式,并在運作時由控制應用程式手動選擇适當的BPF程式,這些操作增加了複雜度和維護的成本。盡管不是透明的,但BPF CO-RE甚至可以使用這種進階方案,通過熟悉的C代碼構造來解決此問題。

根據使用者提供的配置變更行為

有時候,在BPF程式了解核心版本和配置之後仍然無法決定如何從核心擷取資料。這種情況下,使用者空間的控制程式可能是唯一知道确切需要做什麼的一方,以及需要啟用或禁用那些特性。通常是通過某種配置資料進行通信,在使用者空間和BPF程式之間共享資料。現今,一種不需要依賴BPF CO-RE的實作方式是使用BPF map作為配置資料的容器。BPF程式通過查找BPF map來抽取配置,并根據配置變更控制流,但這種方法有很多缺點:

  • BPF程式每次進行map查詢配置值時都會造成運作時開銷。這部分開銷可能會快速增大,某些高性能BPF應用禁止這種方式。
  • 配置值是不變的,且在BPF程式啟動之後是隻讀的,但這部分資料仍然在BPF校驗器在校驗階段仍然被認為是黑盒資料。意味着校驗器無法清理無用代碼以及執行其他進階代碼分析,使得無法使用BPF程式邏輯的可配置部分(這部分功能是最前沿的功能,僅在新核心中支援,當運作在老核心上時不會破壞該程式)。由于BPF驗證程式必須悲觀地認為配置可以是任何東西,且有可能會使用該"未知"的功能(盡管使用者明确配置不會發生這種情況)。

解決此類(公認複雜)場景的方法是使用隻讀全局資料。在BPF程式加載到核心之前由控制應用進行設定。從BPF程式側看,這部分資料就像通路普通的全局變量。由于全局變量使用直接記憶體通路方式,是以不會産生BPF map查詢的開銷。控制語言側需要在BPF程式加載之前設定初始的配置值,這樣當BPF校驗器進行程式校驗時,會将配置值認為是隻讀的,這樣BPF校驗器會将這部分内容認為是已知的常量,并使用進階控制流分析來執行無用代碼的删除。

上例中,在老版本的BPF校驗器下,将不會使用未知的BPF輔助功能,且這部分代碼會被移除。在新版本BPF校驗器下,應用提供不同的配置後,允許使用新的BPF輔助功能,這部分邏輯會通過BPF校驗器的校驗。下面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;           

從使用者空間看,應用程式将能夠通過BPF架構輕松地提供此配置。BPF架構讨論不在本文讨論範圍之内,請參閱核心代碼庫中的runqslower 工具來展示如何使用它來簡化BPF應用程式。

回顧

BPF CO-RE的目标是幫助BPF開發者使用一個簡單的方式解決簡單的可移植性問題(如讀取結構體字段),并使用它來定位複雜的可移植性問題(如不相容的資料結構,複雜的使用者空間控制條件等)。使得開發者的BPF程式能夠"一次編譯–随處運作", 這是通過結合一些上述的BPF CO-RE建構塊來實作的:

  • vmlinux.h

    消除了對核心頭檔案的依賴;
  • 字段重定位(字段偏移,存在性,大小等)使得可以從核心中抽取資料;
  • libbpf提供的

    Kconfig

    外部變量允許BPF程式适應各種核心版本以及特定配置的更改;
  • 當上述都不适合時,app提供了隻讀的配置和

    struct flavors

    ,作為解決任何應用程式必須處理的複雜場景的最終大錘。

不需要CO-RE功能也可以成功編寫,部署和維護可以支援的BPF程式,但在需要時,BPF CO-RE可提供最簡單的方式來幫助解決問題。所有這些功能仍然提供了良好的可用性和熟悉的工作流程,可将C代碼編譯為二進制檔案,并進行輕量級的分發。不再需要繁瑣的編譯器庫并為運作時編譯付出寶貴的運作時資源。 同樣,也不再需要在運作時捕獲瑣碎的編譯錯誤。

TIPS

  • 相關資訊可以參見官方說明:BPF CO-RE (Compile Once – Run Everywhere)
  • libbpf的頭檔案位于核心源碼的

    /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