1. 程式人生 > >BPF的可移植性和CO-RE (Compile Once – Run Everywhere)

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

## BPF的可移植性和CO-RE (Compile Once – Run Everywhere) 在上一篇[文章](https://www.cnblogs.com/charlieroro/p/14140343.html)中介紹了提高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](https://github.com/iovisor/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](https://github.com/libbpf/libbpf))將核心和BPF程式中的BTF繫結在一起,用於將編譯後的BPF程式碼調整為目標主機上的特定核心程式碼; - 核心,在完全不依賴BPF CO-RE的情況下,提供了高階BPF功能來啟用某些更高階的場景。 這些元件可以整合到一起工作,提供前所未有的便捷性,適應性和表達性(來開發可移植BPF程式,以前只能在執行時通過BCC編譯BPF程式的C程式碼來實現),而無需像BCC一樣付出高昂的代價。 #### BTF 整個BPF CO-RE方法的關鍵推動因素之一是BTF。BTF ([BPF Type Format](https://www.kernel.org/doc/html/latest/bpf/btf.html)) 是作為一個更通用,更詳細的DWARF除錯資訊的替代品而建立的。BTF是一種節省空間,緊湊但仍具有足夠表達能力的格式,可以描述C程式的所有型別資訊。由於其簡單性和使用的[重複資料刪除演算法](https://facebookmicrosites.github.io/bpf/blog/2018/11/14/btf-enhancement.html),與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`中的這一行註釋掉再編譯: > >`#