eBPF 入門開發實踐教程一:介紹 eBPF 的基本概念、常見的開發工具

推薦語:最近,龍蜥社區 eBPF 技術探索 SIG 在陳莉君教授帶領下,榮獲了年度最佳 SIG,eBPF 技術繼續保持高熱度,同時,SIG 所有成員歡欣鼓舞,激發了新一輪學習 eBPF 的熱潮,爲了便於初學者入門,從本週起本公衆號將連載 SIG Maintainer 鄭昱笙同學的 eBPF 入門和進階文章,歡迎持續關注。

1. eBPF 簡介:安全和有效地擴展內核

eBPF 是一項革命性的技術,起源於 Linux 內核,可以在操作系統的內核中運行沙盒程序。它被用來安全和有效地擴展內核的功能,而不需要改變內核的源代碼或加載內核模塊。eBPF 通過允許在操作系統內運行沙盒程序,應用程序開發人員可以在運行時,可編程地向操作系統動態添加額外的功能。然後,操作系統保證安全和執行效率,就像在即時編譯(JIT)編譯器和驗證引擎的幫助下進行本地編譯一樣。eBPF 程序在內核版本之間是可移植的,並且可以自動更新,從而避免了工作負載中斷和節點重啓。

今天,eBPF 被廣泛用於各類場景:在現代數據中心和雲原生環境中,可以提供高性能的網絡包處理和負載均衡;以非常低的資源開銷,做到對多種細粒度指標的可觀測性,幫助應用程序開發人員跟蹤應用程序,爲性能故障排除提供洞察力;保障應用程序和容器運行時的安全執行,等等。可能性是無窮的,而 eBPF 在操作系統內核中所釋放的創新纔剛剛開始 [3]。

eBPF 的未來:內核的 JavaScript 可編程接口

對於瀏覽器而言,JavaScript 的引入帶來的可編程性開啓了一場巨大的革命,使瀏覽器發展成爲幾乎獨立的操作系統。現在讓我們回到 eBPF:爲了理解 eBPF 對 Linux 內核的可編程性影響,對 Linux 內核的結構以及它如何與應用程序和硬件進行交互有一個高層次的理解是有幫助的 [4]。

Linux 內核的主要目的是抽象出硬件或虛擬硬件,並提供一個一致的 API(系統調用),允許應用程序運行和共享資源。爲了實現這個目的,我們維護了一系列子系統和層,以分配這些責任 [5]。每個子系統通常允許某種程度的配置,以考慮到用戶的不同需求。如果不能配置所需的行爲,就需要改變內核,從歷史上看,改變內核的行爲,或者讓用戶編寫的程序能夠在內核中運行,就有兩種選擇:

79VQpk

實際上,兩種方案都不常用,前者成本太高,後者則幾乎沒有可移植性。

有了 eBPF,就有了一個新的選擇,可以重新編程 Linux 內核的行爲,而不需要改變內核的源代碼或加載內核模塊,同時保證在不同內核版本之間一定程度上的行爲一致性和兼容性、以及安全性 [6]。爲了實現這個目的,eBPF 程序也需要有一套對應的 API,允許用戶定義的應用程序運行和共享資源 --- 換句話說,某種意義上講 eBPF 虛擬機也提供了一套類似於系統調用的機制,藉助 eBPF 和用戶態通信的機制,Wasm 虛擬機和用戶態應用也可以獲得這套“系統調用” 的完整使用權,一方面能可編程地擴展傳統的系統調用的能力,另一方面能在網絡、文件系統等許多層次實現更高效的可編程 IO 處理。

new-os

正如上圖所示,當今的 Linux 內核正在向一個新的內核模型演化:用戶定義的應用程序可以在內核態和用戶態同時執行,用戶態通過傳統的系統調用訪問系統資源,內核態則通過 BPF Helper Calls 和系統的各個部分完成交互。截止 2023 年初,內核中的 eBPF 虛擬機中已經有 220 多個 Helper 系統接口,涵蓋了非常多的應用場景。

值得注意的是,BPF Helper Call 和系統調用二者並不是競爭關係,它們的編程模型和有性能優勢的場景完全不同,也不會完全替代對方。對 Wasm 和 Wasi 相關生態來說,情況也類似,專門設計的 wasi 接口需要經歷一個漫長的標準化過程,但可能在特定場景能爲用戶態應用獲取更佳的性能和可移植性保證,而 eBPF 在保證沙箱本質和可移植性的前提下,可以提供一個快速靈活的擴展系統接口的方案。

目前的 eBPF 仍然處於早期階段,但是藉助當前 eBPF 提供的內核接口和用戶態交互的能力,經由 Wasm-bpf 的系統接口轉換,Wasm 虛擬機中的應用已經幾乎有能力獲取內核以及用戶態任意一個函數調用的數據和返回值(kprobe,uprobe...);以很低的代價收集和理解所有系統調用,並獲取所有網絡操作的數據包和套接字級別的數據(tracepoint,socket...);在網絡包處理解決方案中添加額外的協議分析器,並輕鬆地編程任何轉發邏輯(XDP,TC...),以滿足不斷變化的需求,而無需離開 Linux 內核的數據包處理環境。

不僅如此,eBPF 還有能力往用戶空間任意進程的任意地址寫入數據(bpf_probe_write_user[7]),有限度地修改內核函數的返回值(bpf_override_return[8]),甚至在內核態直接執行某些系統調用 [9];所幸的是,eBPF 在加載進內核之前對字節碼會進行嚴格的安全檢查,確保沒有內存越界等操作,同時,許多可能會擴大攻擊面、帶來安全風險的功能都是需要在編譯內核時明確選擇啓用才能使用的;在 Wasm 虛擬機將字節碼加載進內核之前,也可以明確選擇啓用或者禁用某些 eBPF 功能,以確保沙箱的安全性。

2. 關於如何學習 eBPF 相關的開發的一些建議

本文不會對 eBPF 的原理做更詳細的介紹,不過這裏有一個學習規劃和參考資料,也許會有一些價值:

eBPF 入門(5-7h)

推薦:

回答三個問題:

  1. 瞭解 eBPF 是什麼東西?爲啥要有這個玩意,不能用內核模塊?

  2. 它有什麼功能?能在 Linux 內核裏面完成哪些事情?有哪些 eBPF 程序的類型和 helper(不需要知道全部,但是需要知道去哪裏找)?

  3. 能拿來做什麼?比如說在哪些場景中進行運用?網絡、安全、可觀測性?

瞭解如何開發 eBPF 程序(10-15h)

瞭解並嘗試一下 eBPF 開發框架:

其他開發框架:Go 語言或者 Rust 語言,請自行搜索並且嘗試(0-2h)

有任何問題或者想了解的東西,不管是不是和本項目相關,都可以在本項目的 discussions 裏面開始討論。

回答一些問題,並且進行一些嘗試(2-5h):

  1. 如何開發一個最簡單的 eBPF 程序?

  2. 如何用 eBPF 追蹤一個內核功能或函數?有很多種方法,舉出對應的代碼;

  3. 有哪些方案能通過用戶態和內核態通信?如何從用戶態向內核態傳送信息?如何從內核態向用戶態傳遞信息?舉出代碼示例;

  4. 編寫一個你自己的 eBPF 程序,實現一個功能;

  5. eBPF 程序的整個生命週期裏面,分別在用戶態和內核態做了哪些事情?

3. 如何使用 eBPF 編程

原始的 eBPF 程序編寫是非常繁瑣和困難的。爲了改變這一現狀,llvm 於 2015 年推出了可以將由高級語言編寫的代碼編譯爲 eBPF 字節碼的功能,同時,eBPF 社區將 bpf() 等原始的系統調用進行了初步地封裝,給出了libbpf庫。這些庫會包含將字節碼加載到內核中的函數以及一些其他的關鍵函數。在 Linux 的源碼包的samples/bpf/目錄下,有大量 Linux 提供的基於libbpf的 eBPF 樣例代碼。

一個典型的基於 libbpf 的 eBPF 程序具有*_kern.c*_user.c兩個文件,*_kern.c中書寫在內核中的掛載點以及處理函數,*_user.c中書寫用戶態代碼,完成內核態代碼注入以及與用戶交互的各種任務。更爲詳細的教程可以參考該視頻然而由於該方法仍然較難理解且入門存在一定的難度,因此現階段的 eBPF 程序開發大多基於一些工具,比如:

以及還有比較新的工具,例如: eunomia-bpf 以及 Coolbpf.

編寫 eBPF 程序

eBPF 程序由內核態部分和用戶態部分構成。內核態部分包含程序的實際邏輯,用戶態部分負責加載和管理內核態部分。使用 eunomia-bpf 開發工具,只需編寫內核態部分的代碼。

內核態部分的代碼需要符合 eBPF 的語法和指令集。eBPF 程序主要由若干個函數組成,每個函數都有其特定的作用。可以使用的函數類型包括:

BCC

BCC 全稱爲 BPF Compiler Collection,該項目是一個 python 庫, 包含了完整的編寫、編譯、和加載 BPF 程序的工具鏈,以及用於調試和診斷性能問題的工具。

自 2015 年發佈以來,BCC 經過上百位貢獻者地不斷完善後,目前已經包含了大量隨時可用的跟蹤工具。其官方項目庫提供了一個方便上手的教程,用戶可以快速地根據教程完成 BCC 入門工作。

用戶可以在 BCC 上使用 Python、Lua 等高級語言進行編程。相較於使用 C 語言直接編程,這些高級語言具有極大的便捷性,用戶只需要使用 C 來設計內核中的 BPF 程序,其餘包括編譯、解析、加載等工作在內,均可由 BCC 完成。

然而使用 BCC 存在一個缺點便是在於其兼容性並不好。基於 BCC 的 eBPF 程序每次執行時候都需要進行編譯,編譯則需要用戶配置相關的頭文件和對應實現。在實際應用中, 相信大家也會有體會,編譯依賴問題是一個很棘手的問題。也正是因此,在本項目的開發中我們放棄了 BCC, 選擇了可以做到一次編譯 - 多次運行的 libbpf-bootstrap 工具。

eBPF Go library

eBPF Go 庫提供了一個通用的 eBPF 庫,它解耦了獲取 eBPF 字節碼的過程和 eBPF 程序的加載和管理,並實現了類似 libbpf 一樣的 CO- 功能。eBPF 程序通常是通過編寫高級語言創建的,然後使用 clang/LLVM 編譯器編譯爲 eBPF 字節碼。

libbpf

libbpf-bootstrap是一個基於libbpf庫的 BPF 開發腳手架,從其 github 上可以得到其源碼。

libbpf-bootstrap綜合了 BPF 社區過去多年的實踐,爲開發者提了一個現代化的、便捷的工作流,實 現了一次編譯,重複使用的目的。

基於libbpf-bootstrap的 BPF 程序對於源文件有一定的命名規則, 用於生成內核態字節碼的 bpf 文件以.bpf.c結尾,用戶態加載字節碼的文件以.c結尾,且這兩個文件的 前綴必須相同。

基於libbpf-bootstrap的 BPF 程序在編譯時會先將*.bpf.c文件編譯爲 對應的.o文件,然後根據此文件生成skeleton文件,即*.skel.h,這個文件會包含內核態中定義的一些 數據結構,以及用於裝載內核態代碼的關鍵函數。在用戶態代碼include此文件之後調用對應的裝載函數即可將 字節碼裝載到內核中。同樣的,libbpf-bootstrap也有非常完備的入門教程,用戶可以在該處得到詳細的入門操作介紹。

eunomia-bpf

開發、構建和分發 eBPF 一直以來都是一個高門檻的工作,使用 BCC、bpftrace 等工具開發效率高、可移植性好,但是分發部署時需要安裝 LLVM、Clang 等編譯環境,每次運行的時候執行本地或遠程編譯過程,資源消耗較大;使用原生的 CO-RE libbpf 時又需要編寫不少用戶態加載代碼來幫助 eBPF 程序正確加載和從內核中獲取上報的信息,同時對於 eBPF 程序的分發、管理也沒有很好地解決方案。

eunomia-bpf 是一個開源的 eBPF 動態加載運行時和開發工具鏈,是爲了簡化 eBPF 程序的開發、構建、分發、運行而設計的,基於 libbpf 的 CO-RE 輕量級開發框架。

使用 eunomia-bpf ,可以:

eunomia-bpf 由一個編譯工具鏈和一個運行時庫組成, 對比傳統的 BCC、原生 libbpf 等框架,大幅簡化了 eBPF 程序的開發流程,在大多數時候只需編寫內核態代碼,即可輕鬆構建、打包、發佈完整的 eBPF 應用,同時內核態 eBPF 代碼保證和主流的 libbpf, libbpfgo, libbpf-rs 等開發框架的 100% 兼容性。需要編寫用戶態代碼的時候,也可以藉助 Webassembly 實現通過多種語言進行用戶態開發。和 bpftrace 等腳本工具相比, eunomia-bpf 保留了類似的便捷性, 同時不僅侷限於 trace 方面, 可以用於更多的場景, 如網絡、安全等等。

  • eunomia-bpf 項目 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf

  • gitee 鏡像: https://gitee.com/anolis/eunomia

Coolbpf 請參考:https://gitee.com/anolis/coolbpf。

參考資料

完整的教程和源代碼已經全部開源,可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/g9aSYMNOQ26sGOltDZ5sKg