當前位置:
首頁 > 最新 > Linux 內核 BPF 簡介

Linux 內核 BPF 簡介

本篇文章介紹了Linux內核中的組件BPF,分別從系統架構和應用實現上進行了說明。

回顧上篇文章:Hadoop 集群基準測試

前言

BPF(Berkeley Packet Filter) 是 Linux 內核中一種十分靈活有效的,類似虛擬機的組件。利用它,可以在保證安全的情況下在多種 hook 點執行二進位的代碼。現在,內核中有多個子系統在使用該組件,最主要的方面,就是網路,追蹤以及系統安全方面[1]。

[1] 這裡的安全主要涉及 sandboxing(沙盒)。

BPF 在 1992 年就已經在內核中有所使用,而目前我們說 BPF,多數是在講現代的 BPF。現代 BPF,主要是指在內核版本 3.18 之後引入的擴展的 BPF(eBPF)。相對的,在該版本之前的 BPF,就被稱為 cBPF(classic BPF)。傳統的 BPF 主要用於網路方面。典型應用就是 tcpdump。而現在,eBPF 擴展了 cBPF,但是可以直接透明的使用 cBPF。

要對 BPF 程序有一個直觀的了解,可以使用 tcpdump 來生成 BPF 的機器指令 (類似 x86 彙編指令)。

儘管從 BPF 的名字上是包過濾器。可是,該工具使用的指令集卻是通用的,並且不限於網路包。這也是為何 BPF 可以使用在網路子系統以外的內核組件中。

BPF 系統構架

上文講到,BPF 並不僅僅是用於網路子系統包過濾方面。同樣,BPF 也不僅僅是提供了一組指令集,其更在該指令集的基礎上,提供了一系列基礎設施,比如類似 k/v map,以及相關的 helper functions,用以與內核交互。在最新的 eBPF 特性中,還包含了從 BPF 函數到 BPF 函數的調用方法,以及代碼的動態載入和卸載。

BPF 具體使用過程中,內核執行的是 BPF 的二進位碼。因此,使用 BPF 的過程中,就需要將人類可讀的文字編譯成機器可執行的二進位碼。目前,LLVM 提供了生成 BPF 的編譯器後端。這樣,使用 LLVM 提供的類似 clang 的軟體,就可以將 C 程序,編譯成為 BPF 的二進位目標碼。BPF 本身與內核結合緊密,同時提供了十分完備的編程方法,通過這些方法,可以將對內核的影響降低到最低限度。

從整體上,BPF 程序,執行流程以及與內核交互流程如下圖:

BPF 程序運行在內核中,其本身不提供內核的追蹤和系統事件統計功能。但由於其運行在內核態,BPF 可以有效的使用 tracepoint,kprobes,uprobes 以及 perf 的相關功能[1]。

指令集

BPF 提供的指令集屬於 RISC 指令集。該指令集最開始是為將 C 語言的一個子集編譯為 BPF 指令而設計。生成 BPF 的二進位代碼之後,通過 bpf 系統調用注入進內核,然後使用內核的 BPF 的 JIT 具體執行指令。BPF 採用這種方式的優勢有:

1)不需要將數據在用戶態和內核態之間拷貝,同時執行流程也不需要頻繁地在內核態和用戶態之間進行切換。

2)BPF 程序可以將不需要的程序特性在編譯時完全刪除,一方面減小程序大小,另一方面,也提高 BPF 二進位碼的效率。

3)支持熱升級,BPF 程序可以在不重啟機器,不影響系統或者容器功能,不中斷網路數據流的情況下升級程序。

4)BPF 與內核結合緊密,並且,提供穩定的應用程序介面,可以廣泛的用於不同的內核版本。

5)BPF 程序,在內核中有相應的檢測器。以確保 BPF 程序不會導致系統崩潰。

在執行上,BPF 程序在內核中執行,始終是事件驅動的。也就是說,只有在相應的內核事件發生時,才會執行 BPF 程序。例如,網路數據包的接收時,可以觸發相應的 BPF 程序執行。

BPF 限制

在 BPF 的設計上,eBPF 的可執行程序的大小最大只能時 4K 個指令。儘管如此,系統卻對 BPF 程序的數量沒有太多限制。另外,處於代碼安全考慮,BPF 程序禁止使用循環和遞歸調用,所有指令,形成一個有向無環圖 (DAG),會刪除無效代碼和異常跳轉。同時,也會對 BPF 代碼進行全面的靜態檢查,追蹤所有堆棧和寄存器狀態,以及檢查調用參數的有效性。所有這些操作,都是為了保證,BPF 的代碼,在載入內核執行的過程中,不會引起內核崩潰等嚴重問題。

正是因為這些嚴格的檢查機制,其一方面可以保證 BPF 代碼能夠在內核中安全運行而不引起系統崩潰,另一方面,也增加了 BPF 程序的編寫難度。

BPF 應用

由於 BPF 具有的這些特性,在網路子系統中,有不少項目。

1)Cilium, 這個是目前大量使用 BPF,為容器提供強大有效的網路,系統安全以及 3 層到 7 層的負載均衡。

2)Facebook,開發了一套基於 BPF/XDP 的負載均衡系統,旨在替換 IPVS。這套系統,同樣具有預防 DDoS 的功能。

3)Netflix,主要是 Brendan Gregg,基於 BPF 在系統追蹤和統計方面的處理優勢,開發的 bcc 項目,用以提供基於 BPF 的系統追蹤工具。同時,Brendan Gregg 使用 BPF 實現了 perf record 的火焰圖功能。

4)谷歌也有類似 bcc 的項目。他們開發的 BPFd,用於系統事件追蹤,以及對遠程系統的事件追蹤。同時,也在將不同的內部工具轉換為 BPF。

5)Suricata,一個入侵檢測 (IDS) 供應商,開始開發基於 BPF 的入侵檢測系統,用以替換原有的基於 nfqueue 和 iptables 檢測數據包的設施。

6)Open vSwitch,也在做基於 BPF 的數據包轉發處理方面的工作。

對於 BPF 的使用,還有很多其他項目。而從上面提到的項目中,BPF 的應用,不僅在其誕生的領域 (數據包處理) 方面有更成熟的應用。在內核的其他部分也在逐步獲得越來越廣泛的應用。

在內核社區,內核包過濾機制中,目前主要有 iptables 和 nftables。iptables 已經在內核中和系統中使用了 20 年左右。nftables,作為 iptables 的替代者,在設計上注意了 iptables 中存在的問題。在 2014 年,內核 3.13 版本引入內核之後,現有的 iptables 用戶向新介面遷移的速度很慢。在生產領域,原有 iptables 規則遷移到新的 nftables 規則,需要承擔不小的遷移和測試工作量,還有相應的風險。而在人們提出使用 BPF 實現 nftables 相應的功能時,很少有人提出反對。這樣,一部分由 BPF 實現的 nftables 的功能就被合併進內核主線。

使用方法

BPF 有著豐富的使用場景,但是其使用方法卻十分簡單。從上面的圖片可以看到 BPF 與內核交互的方法。內核提供了一個專門的系統調用 bpf() 用於裝載 BPF 程序。

結合上面的圖片,BPF 與內核交互需要 3 個步驟。那麼按照這 3 個步驟,使用 BPF,需要:

1)load

該步驟的核心是調用 bpf() 系統調用,將 BPF 二進位程序裝載進入內核。然而在此之前,我們需要由實現我們功能的 BPF 程序。就需要編寫 BPF 程序。BPF 程序不同於一般的 linux x86 程序,其二進位格式與 x86 指令集不同。因此,BPF 程序,我們一方面可以用彙編語言編寫,另一方面,可以使用現代成熟的編譯器,將 C 語言編譯成 BPF 指令。

使用編譯器的時候,由於 BPF 是一種新的指令集,gcc 編譯器目前沒有 BPF 指令的編譯器後端。目前能夠進行 BPF 程序編譯的編譯器是 llvm 的 clang 編譯器。

2)attach

這一步,內核的實現上,是在 setsockopt 中添加了一個標記,用以對 socket 附加 BPF 程序。同時將 sock 上的 BPF 程序激活。在執行該步驟之後,sock 接收到的數據包,都會經過 BPF 程序的處理。

3)copy/get results

獲取結果,核心也是通過 bpf 系統調用,將相應的結構指針傳給系統調用,內核將處理好的數據返回給用戶態程序。

調用之後,attr 中,會包含所需結果。從這個方面看,BPF 在內核態與用戶態之間拷貝數據的數據量很小,這個避免了將大量數據拷貝到用戶態的系統開銷。相比傳統的 tcpdump,這個數據量可以稱作小家碧玉了。

用法示例

C 語言示例

samples/bpf/sock_sample.c

1. 首先通知內核申請 BPF 的存儲空間。

我們對內核中的信息進行收集或者處理,需要使用內核態存儲空間,因此,就需要在使用內核存儲空間之前,通知內核分配所需大小的空間。如下代碼:

該空間是內核態空間,空間的使用是在 bpf 程序中操作。在用戶態編寫 BPF 程序時,操作對象是 map_fd。

2. 編寫 BPF 程序。

BPF 程序中,能夠操作的存儲空間,就是上面通過系統調用分配的內核態存儲空間。另外,對於不同的 BPF 掛載點,還有其特定的內核上下文。這些上下文,就是 BPF 獲取信息的來源。

其內部的掛載點,大概分為如下幾類:

kprobes or uprobes

socket filters (original tcpdump use case)

seccomp

tc filters or actions, either ingress or egress

XDP (NEW)

C 語言編寫的 bpf 程序如下:

該部分,就是我們需要自己編寫的 BPF 程序。上面這段代碼,是使用 BPF 彙編的方式進行編寫的。其中的宏經過替換之後,實際上形成的是類似上面 tcpdump 生成的代碼。這裡需要注意的是,tcpdump 生成的代碼,BPF 程序的 hook 點固定為 socket。其產生的代碼,不能 attach 到其他 hook 點[2]。可以看出,該代碼,實際上組成一個數組,我們需要的是這個數組。使用 BPF 彙編編寫程序,程序的可讀性不高,一般情況下,會使用 C 編寫程序,llvm 編譯器,編譯為二進位的 BPF 程序,然後嵌入上面的數組形式,調用 bpf 系統調用,載入內核。

該段代碼,是使用的 socket 的 filter,其在內核態執行的上下文為:

其中 * filter->bpf_func,就是 BPF 程序經過 jit 編譯之後的 image 地址,可以看到,BPF 程序接收到的參數中,存在一個與上下文相關的參數。

3. 將 BPF 程序載入進入內核。

將 BPF 程序載入如內核,只是將用戶態程序拷貝到內核態,並使用內核的 BPF jit 編譯機進行編譯。將 BPF 二進位碼翻譯成目標機器的可執行代碼。(在 load 過程中翻譯,並且還會減小 BPF 代碼的大小)。在翻譯過程中,還會對代碼再次進行檢測,確保代碼不會引起系統崩潰。

4.激活 BPF 程序。

前期,通過 bpf 系統調用將 BPF 程序載入進入內核之後,我們還需要將 BPF 程序激活。

該操作,將 BPF 程序附加到 sock 上,對於 sock 上的 BPF 實現,其附加點是 sock 的 sk_filter 成員。該成員調用點在 packet_rcv 函數。也就是 PF_PACKET 類型 sock,數據包接收路徑上。因此,該操作對象,應該是 raw 類型的 sock。

執行該步驟之後,內核就已經開始執行 BPF 程序開始按照 BPF 程序執行操作了。

5. 獲取統計結果。

編寫 BPF 程序,使用 BPF 功能,是為了獲取內核的相關信息。從 BPF 獲取內核的信息,可以使用:

獲取相應的結果。該函數,實際上也是執行 bpf() 系統調用。將指定 key 在內核存儲空間中對應的值保存到 tcp_cnt 中。

Python 示例

恩,還是 python 簡潔明了。Python 有其他人實現的 bcc 編譯庫可以直接調用。這個就省去 C 語言中的分配內存,編譯為 BPF 二進位碼,然後 load,attach 的操作。

上述步驟,簡要介紹簡單使用 BPF 的步驟和方法。可以看到,BPF 程序完全實現在用戶態,而能夠安全,有效的獲取到內核態的相關信息。

[1] tracepoint 和 perf 的相關功能主要是讀取內核有關數據,已進行統計和觀測。kprobes 可以實現更強大的功能,不過,也更容易引起系統崩潰。

[2] 這裡需要了解,tcpdump 實際使用的是 libpcap 庫進行包處理的,BPF 代碼也是該庫編譯。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 小米運維 的精彩文章:

nginx+lua 入門

TAG:小米運維 |