全面介紹eBPF-概念
前面介紹了BCC可觀測性和BCC網路,但對底層使用的eBPF的介紹相對較少,且官方欠缺對網路方面的介紹。下面對eBPF進行全面介紹。
BPF概述
下面內容來自Linux官方文件:
eBPF的演進
最初的[Berkeley Packet Filter (BPF) PDF]是為捕捉和過濾符合特定規則的網路包而設計的,過濾器為執行在基於暫存器的虛擬機器上的程式。
在核心中執行使用者指定的程式被證明是一種有用的設計,但最初BPF設計中的一些特性卻並沒有得到很好的支援。例如,虛擬機器的指令集架構(ISA)相對落後,現在處理器已經使用64位的暫存器,併為多核系統引入了新的指令,如原子指令XADD。BPF提供的一小部分RISC指令已經無法在現有的處理器上使用。
因此Alexei Starovoitov在eBPF的設計中介紹瞭如何利用現代硬體,使eBPF虛擬機器更接近當代處理器,eBPF指令更接近硬體的ISA,便於提升效能。其中最大的變動之一是使用了64位的暫存器,並將暫存器的數量從2提升到了10個。由於現代架構使用的暫存器遠遠大於10個,這樣就可以像本機硬體一樣將引數通過eBPF虛擬機器暫存器傳遞給對應的函式。另外,新增的BPF_CALL
指令使得呼叫核心函式更加便利。
將eBPF對映到本機指令有助於實時編譯,提升效能。3.15核心中新增的eBPF補丁使得x86-64上執行的eBPF相比老的BPF(cBPF)在網路過濾上的效能提升了4倍,大部分情況下會保持1.5倍的效能提升。很多架構 (x86-64, SPARC, PowerPC, ARM, arm64, MIPS, and s390)已經支援即時(JIT)編譯。
使用eBPF可以做什麼?
一個eBPF程式會附加到指定的核心程式碼路徑中,當執行該程式碼路徑時,會執行對應的eBPF程式。鑑於它的起源,eBPF特別適合編寫網路程式,將該網路程式附加到網路socket,進行流量過濾,流量分類以及執行網路分類器的動作。eBPF程式甚至可以修改一個已建鏈的網路socket的配置。XDP工程會在網路棧的底層執行eBPF程式,高效能地進行處理接收到的報文。從下圖可以看到eBPF支援的功能:
BPF對網路的處理可以分為tc/BPF和XDP/BPF,它們的主要區別如下(參考該文件):
- XDP的鉤子要早於tc,因此效能更高:tc鉤子使用
sk_buff
結構體作為引數,而XDP使用xdp_md
結構體作為引數,sk_buff
中的資料要遠多於xdp_md
,但也會對效能造成一定影響,且報文需要上送到tc鉤子才會觸發處理程式。由於XDP鉤子位於網路棧之前,因此XDP使用的xdp_buff
(即xdp_md
)無法訪問sk_buff後設資料。struct xdp_buff { /* Linux 5.8*/ void *data; void *data_end; void *data_meta; void *data_hard_start; struct xdp_rxq_info *rxq; struct xdp_txq_info *txq; u32 frame_sz; /* frame size to deduce data_hard_end/reserved tailroom*/ }; struct xdp_rxq_info { struct net_device *dev; u32 queue_index; u32 reg_state; struct xdp_mem_info mem; } ____cacheline_aligned; /* perf critical, avoid false-sharing */ struct xdp_txq_info { struct net_device *dev; };
data
指向page中的資料包的其實位置,data_end
指向資料包的結尾。由於XDP允許headroom
(見下文),data_hard_start
指向page中headroom
的起始位置,即,當對報文進行封裝時,data
會通過bpf_xdp_adjust_head()
向data_hard_start
移動。相同的BPF輔助函式也可以用以解封裝,此時data
會遠離data_hard_start
。
data_meta
一開始指向與data
相同的位置,但bpf_xdp_adjust_meta()
能夠將其朝著data_hard_start
移動,進而給使用者後設資料提供空間,這部分空間對核心網路棧是不可見的,但可以被tc BPF程式讀取( tc 需要將它從 XDP 轉移到skb
)。反之,可以通過相同的BPF程式將data_meta
遠離data_hard_start
來移除或減少使用者後設資料大小。data_meta
還可以單純地用於在尾呼叫間傳遞狀態,與tc BPF程式訪問的skb->cb[]控制塊類似。對於
struct xdp_buff
中的報文指標,有如下關係 :data_hard_start
<=data_meta
<=data
<data_end
。
rxq
欄位指向在ring啟動期間填充的額外的與每個接受佇列相關的後設資料。BPF程式可以檢索
queue_index
,以及網路裝置上的其他資料(如ifindex
等)。
tc能夠更好地管理報文:tc的BPF輸入上下文是一個
sk_buff
,不同於XDP使用的xdp_buff
,二者各有利弊。當核心的網路棧在XDP層之後接收到一個報文時,會分配一個buffer,解析並儲存報文的後設資料,這些後設資料即sk_buff
。該結構體會暴露給BPF的輸入上下文,這樣tc ingress層的tc BPF程式就能夠使用網路棧從報文解析到的後設資料。使用sk_buff
,tc可以更直接地使用這些後設資料,因此附加到tc BPF鉤子的BPF程式可以讀取或寫入skb的mark,pkt_type, protocol, priority, queue_mapping, napi_id, cb[] array, hash, tc_classid 或 tc_index, vlan metadata等,而XDP能夠傳輸使用者的後設資料以及其他資訊。tc BPF使用的struct __sk_buff
定義在linux/bpf.h標頭檔案中。xdp_buff 的弊端在於,其無法使用sk_buff中的資料,XDP只能使用原始的報文資料,並傳輸使用者後設資料。XDP的能夠更快地修改報文:sk_buff包含很多協議相關的資訊(如GSO階段的資訊),因此其很難通過簡單地修改報文資料達到切換協議的目的,原因是網路棧對報文的處理主要基於報文的後設資料,而非每次訪問資料包內容的開銷。因此,BPF輔助函式需要正確處理內部
sk_buff
的轉換。而xdp_buff
則不會有這種問題,因為XDP的處理時間早於核心分配sk_buff的時間,因此可以簡單地實現對任何報文的修改(但管理起來要更加困難)。tc/ebpf和xdp可以互補:如果使用者需要修改報文,同時對資料進行比較複雜的管理,那麼,可以通過執行兩種型別的程式來彌補每種程式型別的侷限性。XDP程式位於ingress,可以修改完整的報文,並將使用者後設資料從XDP BPF傳遞給tc BPF,然後tc可以使用XDP的後設資料和
sk_buff
欄位管理報文。tc/eBPF可以作用於ingress和egress,但XDP只能作用於ingress:與XDP相比,tc BPF程式可以在ingress和egress的網路資料路徑上觸發,而XDP只能作用於ingress。
tc/BPF不需要改變硬體驅動,而XDP通常會使用native驅動模式來獲得更高的效能。但tc BPF程式的處理仍作用於早期的核心網路資料路徑上(GRO處理之後,協議處理和傳統的iptables防火牆的處理之前,如iptables PREROUTING或nftables ingress鉤子等)。而在egress上,tc BPF程式在將報文傳遞給驅動之前進行處理,即在傳統的iptables防火牆(如iptables POSTROUTING)之後,但在核心的GSO引擎之前進行處理。一個特殊情況是,如果使用了offloaded的tc BPF程式(通常通過SmartNIC提供),此時Offloaded tc/eBPF接近於Offloaded XDP的效能。
從下圖可以看到TC和XDP的工作位置,可以看到XDP對報文的處理要先於TC:
核心執行的另一種過濾型別是限制程式可以使用的系統呼叫。通過seccomp BPF實現。
eBPF也可以用於通過將程式附加到tracepoints
, kprobes
,和perf events
的方式定位核心問題,以及進行效能分析。因為eBPF可以訪問核心資料結構,開發者可以在不編譯核心的前提下編寫並測試程式碼。對於工作繁忙的工程師,通過該方式可以方便地除錯一個線上執行的系統。此外,還可以通過靜態定義的追蹤點除錯使用者空間的程式(即BCC除錯使用者程式,如Mysql)。
使用eBPF有兩大優勢:快速,安全。為了更好地使用eBPF,需要了解它是如何工作的。
核心的eBPF校驗器
在核心中執行使用者空間的程式碼可能會存在安全和穩定性風險。因此,在載入eBPF程式前需要進行大量校驗。首先通過對程式控制流的深度優先搜尋保證eBPF能夠正常結束,不會因為任何迴圈導致核心鎖定。嚴禁使用無法到達的指令;任何包含無法到達的指令的程式都會導致載入失敗。
第二個階段涉及使用校驗器模擬執行eBPF程式(每次執行一個指令)。在每次指令執行前後都需要校驗虛擬機器的狀態,保證暫存器和棧的狀態都是有效的。嚴禁越界(程式碼)跳躍,以及訪問越界資料。
校驗器不會檢查程式的每條路徑,它能夠知道程式的當前狀態是否是已經檢查過的程式的子集。由於前面的所有路徑都必須是有效的(否則程式會載入失敗),當前的路徑也必須是有效的,因此允許驗證器“修剪”當前分支並跳過其模擬階段。
校驗器有一個"安全模式",禁止指標運算。當一個沒有CAP_SYS_ADMIN
特權的使用者載入eBPF程式時會啟用安全模式,確保不會將核心地址洩露給非特權使用者,且不會將指標寫入記憶體。如果沒有啟用安全模式,則僅允許在執行檢查之後進行指標運算。例如,所有的指標訪問時都會檢查型別,對齊和邊界衝突。
無法讀取包含未初始化內容的暫存器,嘗試讀取這類暫存器中的內容將導致載入失敗。R0-R5的暫存器內容在函式呼叫期間被標記未不可讀狀態,可以通過儲存一個特殊值來測試任何對未初始化暫存器的讀取行為;對於讀取堆疊上的變數的行為也進行了類似的檢查,確保沒有指令會寫入只讀的幀指標暫存器。
最後,校驗器會使用eBPF程式型別(見下)來限制可以從eBPF程式呼叫哪些核心函式,以及訪問哪些資料結構。例如,一些程式型別可以直接訪問網路報文。
bpf()系統呼叫
使用bpf()
系統呼叫和BPF_PROG_LOAD
命令載入程式。該系統呼叫的原型為:
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
bpf_attr
允許資料在核心和使用者空間傳遞,具體型別取決於cmd
引數。
cmd可以是如下內容:
BPF_MAP_CREATE
Create a map and return a file descriptor that refers to the
map. The close-on-exec file descriptor flag (see fcntl(2)) is
automatically enabled for the new file descriptor.
BPF_MAP_LOOKUP_ELEM
Look up an element by key in a specified map and return its
value.
BPF_MAP_UPDATE_ELEM
Create or update an element (key/value pair) in a specified
map.
BPF_MAP_DELETE_ELEM
Look up and delete an element by key in a specified map.
BPF_MAP_GET_NEXT_KEY
Look up an element by key in a specified map and return the
key of the next element.
BPF_PROG_LOAD
Verify and load an eBPF program, returning a new file descrip‐
tor associated with the program. The close-on-exec file
descriptor flag (see fcntl(2)) is automatically enabled for
the new file descriptor.
size
引數給出了bpf_attr
聯合體物件的位元組長度。
BPF_PROG_LOAD
載入的命令可以用於建立和修改eBPF maps,maps是普通的key/value資料結構,用於在eBPF程式和核心空間或使用者空間之間通訊。其他命令允許將eBPF程式附加到一個控制組目錄或socket檔案描述符上,迭代所有的maps和程式,以及將eBPF物件固定到檔案,這樣在載入eBPF程式的程式結束後不會被銷燬(後者由tc分類器/操作程式碼使用,因此可以將eBPF程式持久化,而不需要載入的程式保持活動狀態)。完整的命令可以參考bpf()幫助文件。
雖然可能存在很多不同的命令,但大體可以分為兩類:與eBPF程式互動的命令,與eBPF maps互動的命令,或同時與程式和maps互動的命令(統稱為物件)。
eBPF 程式型別
使用BPF_PROG_LOAD
載入的程式型別確定了四件事:附加的程式的位置,驗證器允許呼叫的核心輔助函式,是否可以直接訪問網路資料包文,以及傳遞給程式的第一個引數物件的型別。實際上,程式型別本質上定義了一個API。建立新的程式型別甚至純粹是為了區分不同的可呼叫函式列表(例如,BPF_PROG_TYPE_CGROUP_SKB
和BPF_PROG_TYPE_SOCKET_FILTER
)。
當前核心支援的eBPF程式型別為:
BPF_PROG_TYPE_SOCKET_FILTER
: a network packet filterBPF_PROG_TYPE_KPROBE
: determine whether a kprobe should fire or notBPF_PROG_TYPE_SCHED_CLS
: a network traffic-control classifierBPF_PROG_TYPE_SCHED_ACT
: a network traffic-control actionBPF_PROG_TYPE_TRACEPOINT
: determine whether a tracepoint should fire or notBPF_PROG_TYPE_XDP
: a network packet filter run from the device-driver receive pathBPF_PROG_TYPE_PERF_EVENT
: determine whether a perf event handler should fire or notBPF_PROG_TYPE_CGROUP_SKB
: a network packet filter for control groupsBPF_PROG_TYPE_CGROUP_SOCK
: a network packet filter for control groups that is allowed to modify socket optionsBPF_PROG_TYPE_LWT_*
: a network packet filter for lightweight tunnelsBPF_PROG_TYPE_SOCK_OPS
: a program for setting socket parametersBPF_PROG_TYPE_SK_SKB
: a network packet filter for forwarding packets between socketsBPF_PROG_CGROUP_DEVICE
: determine if a device operation should be permitted or not
隨著新程式型別的增加,核心開發人員也會發現需要新增新的資料結構。
eBPF 資料結構
eBPF使用的主要的資料結構是eBPF map,這是一個通用的資料結構,用於在核心或核心和使用者空間傳遞資料。其名稱"map"也意味著資料的儲存和檢索需要用到key。
使用bpf()
系統呼叫建立和管理map。當成功建立一個map後,會返回與該map關聯的檔案描述符。關閉相應的檔案描述符的同時會銷燬map。每個map定義了4個值:型別,元素最大數目,數值的位元組大小,以及key的位元組大小。eBPF提供了不同的map型別,不同型別的map提供了不同的特性。
BPF_MAP_TYPE_HASH
: a hash tableBPF_MAP_TYPE_ARRAY
: an array map, optimized for fast lookup speeds, often used for countersBPF_MAP_TYPE_PROG_ARRAY
: an array of file descriptors corresponding to eBPF programs; used to implement jump tables and sub-programs to handle specific packet protocolsBPF_MAP_TYPE_PERCPU_ARRAY
: a per-CPU array, used to implement histograms of latencyBPF_MAP_TYPE_PERF_EVENT_ARRAY
: stores pointers tostruct perf_event
, used to read and store perf event countersBPF_MAP_TYPE_CGROUP_ARRAY
: stores pointers to control groupsBPF_MAP_TYPE_PERCPU_HASH
: a per-CPU hash tableBPF_MAP_TYPE_LRU_HASH
: a hash table that only retains the most recently used itemsBPF_MAP_TYPE_LRU_PERCPU_HASH
: a per-CPU hash table that only retains the most recently used itemsBPF_MAP_TYPE_LPM_TRIE
: a longest-prefix match trie, good for matching IP addresses to a rangeBPF_MAP_TYPE_STACK_TRACE
: stores stack tracesBPF_MAP_TYPE_ARRAY_OF_MAPS
: a map-in-map data structureBPF_MAP_TYPE_HASH_OF_MAPS
: a map-in-map data structureBPF_MAP_TYPE_DEVICE_MAP
: for storing and looking up network device referencesBPF_MAP_TYPE_SOCKET_MAP
: stores and looks up sockets and allows socket redirection with BPF helper functions
所有的map都可以通過eBPF或在使用者空間的程式中使用 bpf_map_lookup_elem()
和bpf_map_update_elem()
函式進行訪問。某些map型別,如socket map,會使用其他執行特殊任務的eBPF輔助函式。
eBPF的更多細節可以參見官方幫助文件。
注:
在Linux4.4之前,
bpf()
要求呼叫者具有CAP_SYS_ADMIN
capability許可權,從Linux 4.4.開始,非特權使用者可以使用BPF_PROG_TYPE_SOCKET_FILTER
型別和相應的map建立受限的程式,然而這類程式無法將核心指標儲存到map中,僅限於使用如下輔助函式:* get_random * get_smp_processor_id * tail_call * ktime_get_ns
可以通過sysctl禁用非特權訪問:
/proc/sys/kernel/unprivileged_bpf_disabled
eBPF物件(maps和程式)可以在不同的程式間共享。例如,在fork之後,子程式會繼承引用eBPF物件的檔案描述符。此外,引用eBPF物件的檔案描述符可以通過UNIX域socket傳輸。引用eBPF物件的檔案描述符可以通過
dup(2)
和類似的呼叫進行復制。當所有引用物件的檔案描述符關閉後,才會釋放eBPF物件。eBPF程式可以使用受限的C語言進行編寫,並使用clang編譯器編譯為eBPF位元組碼。受限的C語言會禁用很多特性,如迴圈,全域性變數,浮點數以及使用結構體作為函式引數。可以在核心原始碼的samples/bpf/*_kern.c 檔案中檢視例子。
核心中的just-in-time (JIT)可以將eBPF位元組碼轉換為機器碼,提升效能。在Linux 4.15之前,預設會禁用JIT,可以通過修改
/proc/sys/net/core/bpf_jit_enable
啟用JIT。
- 0 禁用JIT
- 1 正常編譯
- 2 dehub模式。
從Linux 4.15開始,核心可能會配置
CONFIG_BPF_JIT_ALWAYS_ON
選項,這種情況下,會啟用JIT編譯器,bpf_jit_enable
會被設定為1。如下架構支援eBPF的JIT編譯器:* x86-64 (since Linux 3.18; cBPF since Linux 3.0); * ARM32 (since Linux 3.18; cBPF since Linux 3.4); * SPARC 32 (since Linux 3.18; cBPF since Linux 3.5); * ARM-64 (since Linux 3.18); * s390 (since Linux 4.1; cBPF since Linux 3.7); * PowerPC 64 (since Linux 4.8; cBPF since Linux 3.1); * SPARC 64 (since Linux 4.12); * x86-32 (since Linux 4.18); * MIPS 64 (since Linux 4.18; cBPF since Linux 3.16); * riscv (since Linux 5.1).
eBPF輔助函式
可以參考官方幫助文件檢視libbpf庫提供的輔助函式。
官方文件給出了現有的eBPF輔助函式。更多的例項可以參見核心原始碼的samples/bpf/
和tools/testing/selftests/bpf/
目錄。
在官方幫助文件中有如下補充:
由於在編寫幫助文件的同時,也同時在進行eBPF開發,因此新引入的eBPF程式或map型別可能沒有及時新增到幫助文件中,可以在核心原始碼樹中找到最準確的描述:
include/uapi/linux/bpf.h:主要的BPF標頭檔案。包含完整的輔助函式列表,以及對輔助函式使用的標記,結構體和常量的描述
net/core/filter.c:包含大部分與網路有關的輔助函式,以及使用的程式型別列表
kernel/trace/bpf_trace.c:包含大部分與程式跟蹤有關的輔助函式
kernel/bpf/verifier.c:包含特定輔助函式使用的用於校驗eBPF map有效性的函式
kernel/bpf/:該目錄中的檔案包含了其他輔助函式(如cgroups,sockmaps等)
如何編寫eBPF程式
歷史上,需要使用核心的bpf_asm彙編器將eBPF程式轉換為BPF位元組碼。幸運的是,LLVM Clang編譯器支援將C語言編寫的eBPF後端編譯為位元組碼。bpf()
系統呼叫和BPF_PROG_LOAD
命令可以直接載入包含這些位元組碼的物件檔案。
可以使用C編寫eBPF程式,並使用Clang的 -march=bpf
引數進行編譯。在核心的samples/bpf/
目錄下有很多eBPF程式的例子。大多數檔名中都有一個_kern.c
字尾。Clang編譯出的目標檔案(eBPF位元組碼)需要由一個本機執行的程式進行載入(通常為使用_user.c
開頭的檔案)。為了簡化eBPF程式的編寫,核心提供了libbpf
庫,可以使用輔助函式來載入,建立和管理eBPF物件。例如,一個eBPF程式和使用libbpf
的使用者程式的大體流程為:
- 在使用者程式中讀取eBPF位元組流,並將其傳遞給
bpf_load_program()
。 - 當在核心中執行eBPF程式時,將會呼叫
bpf_map_lookup_elem()
在一個map中查詢元素,並儲存一個新的值。 - 使用者程式會呼叫
bpf_map_lookup_elem()
讀取由eBPF程式儲存的核心資料。
然而,大部分的例項程式碼都有一個主要的缺點:需要在核心原始碼樹中編譯自己的eBPF程式。幸運的是,BCC專案解決了這類問題。它包含了一個完整的工具鏈來編寫並載入eBPF程式,而不需要連結到核心原始碼樹。
seccomp 概述
下面內容來自Linux官方文件:
歷史
seccomp首個版本在2005年合入Linux 2.6.12版本。通過在 /proc/PID/seccomp
中寫入1
啟用該功能。一旦啟用,程式只能使用4個系統呼叫read()
, write()
, exit()
和sigreturn()
,如果程式呼叫其他系統呼叫將會導致SIGKILL
。該想法和補丁來自andreaarcangeli,作為一種安全執行他人程式碼的方法。然而,這個想法一直沒有實現。
在2007年,核心2.6.23中改變了啟用seccomp的方式。新增了 prctl()
操作方式(PR_SET_SECCOMP
和 SECCOMP_MODE_STRICT
引數),並移除了 /proc
介面。PR_GET_SECCOMP
操作的行為比較有趣:如果程式不處於seccomp模式,則會返回0,否則會發出SIGKILL
訊號(原因是prctl()
不是一個允許的系統呼叫)。Kerrisk說,這證明了核心開發人員確實有幽默感。
在接下來的五年左右,seccomp領域的情況一直很平靜,直到2012年linux3.5中加入了seccomp模式2
(或“seccomp過濾模式”)。為seccomp新增了第二個模式:SECCOMP_MODE_FILTER
。使用該模式,程式可以指定允許哪些系統呼叫。通過mini的BPF程式,程式可以限制整個系統呼叫或特定的引數值。現在已經有很多工具使用了seccomp過濾,包括 Chrome/Chromium瀏覽器, OpenSSH, vsftpd, 和Firefox OS。此外,容器中也大量使用了seccomp。
2013年的3.8核心版主中,在/proc/PID/status
中新增了一個“Seccomp”欄位。通過讀取該欄位,程式可以確定其seccomp模式(0為禁用,1為嚴格,2為過濾)。Kerrisk指出,程式可能需要從其他地方獲取一個檔案的檔案描述符,以確保不會收到SIGKILL。
2014 年3.17版本中加入了 seccomp()
系統呼叫(不會再使得prctl()
系統呼叫變得更加複雜)。 seccomp()
系統呼叫提供了現有功能的超集。它還增加了將一個程式的所有執行緒同步到同一組過濾器的能力,有助於確保即使是在安裝過濾器之前建立的執行緒也仍然受其影響。
BPF
seccomp的過濾模式允許開發者編寫BPF程式來根據傳入的引數數目和引數值來決定是否可以執行某個給定的系統呼叫。只有值傳遞有效(BPF虛擬機器不會取消對指標引數的引用)。
可以使用seccomp()
或prctl()
安裝過濾器。首先必須構造BPF程式,然後將其安裝到核心。之後每次執行系統呼叫時都會觸發過濾程式碼。也可以移除已經安裝的過濾器(因為安裝過濾器實際上是一種宣告,表明任何後續執行的程式碼都是不可信的)。
BPF語言幾乎早於Linux(Kerrisk)。首次出現在1992年,被用於tcpdump程式,用於監聽網路報文。但由於報文數目比較大,因此將所有的報文傳遞到用於空間再進行過濾的代價相當大。BPF提供了一種核心層面的過濾,這樣使用者空間只需要處理其感興趣的報文。
seccomp過濾器開發人員發現可以使用BPF實現其他型別的功能,後來BPF演化為允許過濾系統呼叫。核心中的小型核心內虛擬機器用於解釋一組簡單的BPF指令。
BPF允許分支,但僅允許向前的分支,因此不能出現迴圈,通過這種方式保證出現能夠結束。BPF程式的指令限制為4096個,且在載入期間完成有效性校驗。此外,校驗器可以保證程式能夠正常退出,並返回一條指令,告訴核心針對該系統呼叫應該採取何種動作。
BPF的推廣正在進行中,其中eBPF已經新增到了核心中,可以針對tracepoint(Linux 3.18)和raw socket(3.19)進行過濾,同時在4.1版本中合入了針對perf event的eBPF程式碼。
BPF有一個累加器暫存器,一個資料區(用於seccomp,包含系統呼叫的資訊),以及一個隱式程式計數器。所有的指令都是64位長度,其中16位元用於操作碼,兩個8bit欄位用於跳轉目的地,以及一個32位的欄位儲存依賴操作碼解析出的值。
BPF使用的基本的指令有:load,stora,jump,算術和邏輯運算,以及return。BPF支援條件和非條件跳轉指令,後者使用32位欄位作為其偏移量。條件跳轉會在指令中使用兩個跳轉目的欄位,每個欄位都包含一個跳轉偏移量(具體取決於跳轉為true還是false)。
由於具有兩個跳轉目的,BPF可以簡化條件跳轉指令(例如,可以使用"等於時跳轉",但不能使用"不等於時跳轉"),如果需要另一種意義上的比較,可以將這兩種偏移互換。目的地即是偏移量,0表示"不跳轉"(執行下一跳指令),由於它們是8位元的值,最大支援跳轉255條指令。正如前面所述,不允許負偏移量,避免迴圈。
給seccomp使用的BPF資料區(struct seccomp_data
)有幾個不同的欄位來描述正在進行的系統呼叫:系統呼叫號,架構,指令指標,以及系統呼叫引數。它是一個只讀buffer,程式無法修改。
編寫過濾器
可以使用常數和巨集編寫BPF程式,例如:
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, arch)))
上述命令將會建立一個載入(BPF_LD)字(BPF_W)的操作,使用指令中的值作為資料區的偏移量(BPF_ABS)。該值是architecture欄位與資料區域的偏移量,因此最終結果是一條指令,該指令會根據架構載入累加器(來自AUDIT.h
中的AUDIT_ARCH_*
值)。下一條指令為:
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K ,AUDIT_ARCH_X86_64 , 1, 0)
上述命令會建立一個jump-if-equal指令(BPF_JMP | BPF JEQ),將指令中的值(BPF_K)與累加器中的值進行比較。如果架構為x86-64,該跳轉會忽略嚇一跳指令(跳轉的指令數為"1"),否則會繼續執行(跳轉為false,"0")。
BPF程式應該首先對其架構進行校驗,確保系統呼叫與程式所期望的一致。BPF程式可能是在與它允許的架構不同的架構上建立的。
一旦建立了過濾器,在每次系統呼叫時都會允許該程式,同時也會對效能造成一定影響。每個程式在退出時必須返回一條指令,否則,校驗器會返回EINVAL
。返回的內容為一個32位的數值。高16位元指定了核心的動作,其他位元返回與動作相關的資料。
程式可以返回5個動作:SECCOMP_RET_ALLOW
表示允許執行系統呼叫;SECCOMP_RET_KILL
表示終止程式,就像該程式由於SIGSYS
(程式不會捕獲到該訊號)被殺死一樣;SECCOMP_RET_ERRNO
會告訴核心嘗試通知一個ptrace()
跟蹤器,使其有機會獲得控制權;SECCOMP_RET_TRAP
告訴核心立即傳送一個真實的SIGSYS
訊號,程式會在期望時捕獲到該訊號。
可以使用seccomp()
(since Linux 3.17) 或prctl()
安裝BPF程式,這兩種情況下都會傳遞一個 struct sock_fprog
指標,包含指令數目和一個指向程式的指標。為了成功執行指令,呼叫者要麼需要具有CAP_SYS_ADMIN
許可權,要麼給程式設定PR_SET_NO_NEW_PRIVS
屬性(使用execve()
執行新的程式時會忽略set-UID, set-GID, 和檔案capabilities)。
如果過濾器執行程式呼叫 prctl()
或seccomp()
,那麼就可以安裝更多的過濾器,它們將以與新增順序相反的順序執行,最終返回過濾器中具有最高優先順序的值(KILL
的優先順序最高,ALLOW
的優先順序最低)。如果篩選器允許呼叫fork()、clone()和execve(),則會在呼叫這些命令時保留篩選器。
seccomp過濾器的兩個主要用途是沙盒和故障模式測試。前者用於限制程式,特別是需要處理不可信輸入的系統呼叫,通常會用到白名單。對於故障模式測試,可以使用seccomp給程式注入各種不可預期的錯誤來幫助查詢bugs。
目前有很多工具和資源可以簡化seccomp過濾器和BPF的開發。Libseccomp提供了一組高階API來建立過濾器。libseccomp專案給出了很多幫助文件,如seccomp_init()
。
最後,核心有一個just-in-time (JIT)編譯器,用於將BPF位元組碼轉化為機器碼,通過這種方式可以提升2-3倍的效能。JIT編譯器預設是禁用的,可以通過在下面檔案中寫入1啟用。
/proc/sys/net/core/bpf_jit_enable
XDP
XDP是一個基於eBPF的高效能資料鏈路,在Linux 4.8核心版本合入。
XDP模式
模式介紹
XDP支援三種操作模式,預設會使用native
模式。
Native XDP(XDP_FLAGS_DRV_MODE)
:預設的工作模式,XDP BPF程式執行在網路驅動的早期接收路徑(RX佇列)上。大多數10G或更高階別的NIC都已經支援了native
XDP。Offloaded XDP(XDP_FLAGS_HW_MODE)
:offloaded
XDP模式中,XDP BPF程式直接在NIC中處理報文,而不會使用主機的CPU。因此,處理報文的成本非常低,效能要遠遠高於native
XDP。該模式通常由智慧網路卡實現,包含多執行緒,多核流量處理器(以及一個核心的JIT編譯器,將BPF轉變為該處理器可以執行的指令)。支援offloaded
XDP的驅動通常也支援native
XDP(某些BPF輔助函式通常僅支援native 模式)。Generic XDP(XDP_FLAGS_SKB_MODE)
:對於沒有實現native或offloaded模式的XDP,核心提供了一種處理XDP的通用方案。由於該模式執行在網路棧中,因此不需要對驅動進行修改。該模式主要用於給開發者測試使用XDP API編寫的程式,其效能要遠低於native或offloaded模式。在生產環境中,建議使用native或offloaded模式。
支援native
XDP的驅動如下:
-
Broadcom
- bnxt
-
Cavium
- thunderx
-
Intel
- ixgbe
- ixgbevf
- i40e
-
Mellanox
- mlx4
- mlx5
-
Netronome
- nfp
-
Others
- tun
- virtio_net
-
Qlogic
- qede
-
Solarflare
- sfc [1]
支援offloaded
XDP的驅動如下:
- Netronome
- nfp [2]
模式校驗
可以通過ip link
命令檢視已經安裝的XDP模式,generic/SKB (xdpgeneric
), native/driver (xdp
), hardware offload (xdpoffload
),如下xdpgeneric即generic模式。
# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:16:3e:00:2d:67 brd ff:ff:ff:ff:ff:ff
prog/xdp id 101 tag 3b185187f1855c4c jited
虛擬機器上的裝置可能無法支援native模式。在阿里雲ecs上執行下文的例子時出現了錯誤:
libbpf: Kernel error message: virtio_net: Too few free TX rings available
,且無許可權使用ethtool -G eth0 tx 4080
修改tx buffer的大小。建議使用物理機。可以使用ethtool檢視經XDP處理的報文統計:
# ethtool -S eth0 NIC statistics: rx_queue_0_packets: 547115 rx_queue_0_bytes: 719558449 rx_queue_0_drops: 0 rx_queue_0_xdp_packets: 0 rx_queue_0_xdp_tx: 0 rx_queue_0_xdp_redirects: 0 rx_queue_0_xdp_drops: 0 rx_queue_0_kicks: 20 tx_queue_0_packets: 134668 tx_queue_0_bytes: 30534028 tx_queue_0_xdp_tx: 0 tx_queue_0_xdp_tx_drops: 0 tx_queue_0_kicks: 127973
XDP Action
XDP用於報文的處理,支援如下action:
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP_PASS,
XDP_TX,
XDP_REDIRECT,
};
- XDP_DROP:在驅動層丟棄報文,通常用於實現DDos或防火牆
- XDP_PASS:允許報文上送到核心網路棧,同時處理該報文的CPU會分配並填充一個
skb
,將其傳遞到GRO引擎。之後的處理與沒有XDP程式的過程相同。 - XDP_TX:BPF程式通過該選項可以將網路報文從接收到該報文的NIC上傳送出去。例如當叢集中的部分機器實現了防火牆和負載均衡時,這些機器就可以作為hairpinned模式的負載均衡,在接收到報文,經過XDP BPF修改後將該報文原路傳送出去。
- XDP_REDIRECT:與XDP_TX類似,但是通過另一個網路卡將包發出去。另外,
XDP_REDIRECT
還可以將包重定向到一個 BPF cpumap,即,當前執行 XDP 程式的 CPU 可以將這個包交給某個遠端 CPU,由後者將這個包送到更上層的核心棧,當前 CPU 則繼續在這個網路卡執行接收和處理包的任務。這和XDP_PASS
類似,但當前 CPU 不用去做將包送到核心協議棧的準備工作(分配skb
,初始化等等),這部分開銷還是很大的。 - XDP_ABORTED:表示程式產生了異常,其行為和
XDP_DROP
相同,但XDP_ABORTED
會經過trace_xdp_exception
tracepoint,因此可以通過 tracing 工具來監控這種非正常行為。
AF_XDP
使用XDP_REDIRECT
action的XDP程式可以通過bpf_redirect_map()
函式將接收到的幀傳遞到其他啟用XDP的netdevs上,AF_XDP socket使得XDP程式可以將幀重定向到使用者空間的程式的記憶體buffer中。
可以通過socket()
系統呼叫建立AF_XDP socket (XSK)。每個XSK涉及兩個ring:RX ring和TX ring。一個socket可以從RX ring上接收報文,併傳送到TX ring。這兩個rings分別通過socket選項XDP_RX_RING
和XDP_TX_RING
進行註冊。每個socket必須至少具有其中一個ring。RX或TX ring描述符指向記憶體域中的data buffer,稱為UMEM。RX和TX可以共享相同的UMEM,這樣一個報文無需在RX和TX之間進行拷貝。此外,如果一個報文由於重傳需要保留一段時間,則指向該報文的描述符可以指向另外一個報文,這樣就避免了資料的拷貝。基本流程如下:
UMEM包含一系列大小相同的chunks,ring中的描述符通過引用幀的地址來引用該幀,該地址為整個UMEM域的偏移量。使用者空間會使用合適的方式(malloc,mmap,大頁記憶體等)為UMEM分配記憶體,然後使用使用新的socket選項XDP_UMEM_REG
將記憶體域註冊到核心中。UMEM也包含兩個ring:FILL ring和COMPLETION ring。應用會使用FILL ring下發addr,讓核心填寫RX包資料。一旦接收到報文,RX ring會引用這些幀。COMPLETION ring包含核心傳輸完的幀地址,且可以被使用者空間使用,用於TX或RX。因此COMPLETION ring中的幀地址為先前使用TX ring傳輸的地址。總之,RX和FILL ring用於RX路徑,TX和COMPLETION ring用於TX路徑。
最後會使用bind()呼叫將socket繫結到一個裝置以及該裝置指定的佇列id上,繫結沒有完成前無法傳輸流量。
可以在多個程式間共享UMEM 。如果一個程式需要更新UMEM,則會跳過註冊UMEM和其對應的兩個ring的過程。在bind呼叫中設定XDP_SHARED_UMEM
標誌,並提交該程式期望共享UMEM的XSK,以及新建立的XSK socket。新程式會在其共享UMEM的RX ring中接收到幀地址引用。注意,由於ring的結構是單生產者/單消費者的,新的程式的socket必須建立獨立的RX和TX ring。同樣的原因,每個UMEM也只能有一個FILL和COMPLETION ring。每個程式都需要正確地處理好UMEM。
那麼報文是怎麼從XDP程式分發到XSKs的呢?通過名為XSKMAP
(完整名為BPF_MAP_TYPE_XSKMAP`) BPF map。使用者空間的應用可以將一個XSK放到該map的任意位置,然後XDP程式就可以將一個報文重定向到該map中指定的索引中,此時XDP會校驗map中的XSK確實繫結到該裝置和ring號。如果沒有,則會丟棄該報文。如果map中的索引為空,也會丟棄該報文。因此,當前的實現中強制要求必須載入一個XDP程式(以及保證XSKMAP存在一個XSK),這樣才能通過XSK將流量傳送到使用者空間。
AF_XDP可以執行在兩種模式上:XDP_SKB
和XDP_DRV
。如果驅動不支援XDP,則在載入XDP程式是需要明確指定使用XDP_SKB,XDP_SKB
模式使用SKB和通用的XDP功能,並將資料複製到使用者空間,是一種適用於任何網路裝置的回退模式。 如果驅動支援XDP,將使用AF_XDP程式碼提供更好的效能,但仍然會將資料拷貝到使用者空間的操作。
術語
UMEM
UMEM是一個虛擬的連續記憶體域,分割為相同大小的幀。一個UMEM會關聯一個netdev以及該netdev的佇列id。通過XDP_UMEM_REG
socket選項進行建立和配置(chunk大小,headroom,開始地址和大小)。通過bind()
系統呼叫將一個UMEM繫結到一個netdev和佇列id。umem的基本結構如下:
一個AF_XDP為一個連結到一個獨立的UMEM的socket,但一個UMEM可以有多個AF_XDP socket。為了共享一個通過socket A建立的UMEM,socket B可以將結構體sockaddr_xdp
中的成員sxdp_flags設定為XDP_SHARED_UMEM
,並將A的檔案描述符傳遞給結構體sockaddr_xdp
的成員sxdp_shared_umem_fd
。
UMEM有兩個單生產者/單消費者ring,用於在核心和使用者空間應用程式之間轉移UMEM幀。
Rings
有4類不同型別的ring:FILL, COMPLETION, RX 和TX,所有的ring都是單生產者/單消費者,因此使用者空間的程式需要顯示地同步對這些rings進行讀/寫的多程式/執行緒。
UMEM使用2個ring:FILL和COMPLETION。每個關聯到UMEM的socket必須有1個RX佇列,1個TX佇列或同時擁有2個佇列。如果配置了4個socket(同時使用TX和RX),那麼此時會有1個FILL ring,1個COMPLETION ring,4個TX ring和4個RX ring。
ring是基於首(生產者)尾(消費者)的結構。一個生產者會在結構體xdp_ring的producer成員指出的ring索引處寫入資料,並增加生產者索引;一個消費者會結構體xdp_ring的consumer成員指出的ring索引處讀取資料,並增加消費者索引。
可以通過_RING setsockopt系統呼叫配置和建立ring,使用mmap(),並結合合適的偏移量,將其對映到使用者空間
ring的大小需要是2次冪。
UMEM Fill Ring
FILL ring用於將UMEM幀從使用者空間傳遞到核心空間,同時將UMEM地址傳遞給ring。例如,如果UMEM的大小為64k,且每個chunk的大小為4k,那麼UMEM包含16個chunk,可以傳遞的地址為0到64k。
傳遞給核心的幀用於ingress路徑(RX rings)。
使用者應用也會在該ring中生成UMEM地址。注意,如果以對齊的chunk模式執行應用,則核心會遮蔽傳入的地址。即,如果一個chunk大小為2k,則會遮蔽掉log2(2048) LSB的地址,意味著2048, 2050 和3000都將引用相同的chunk。如果使用者應用使用非對其的chunk模式執行,那麼傳入的地址將保持不變。
UMEM Completion Ring
COMPLETION Ring用於將UMEM幀從核心空間傳遞到使用者空間,與FILL ring相同,使用了UMEM索引。
已經傳送的從核心空間傳遞到使用者空間的幀還可以被使用者空間使用。
使用者應用會消費該ring種的UMEM地址。
RX Ring
RX ring位於socket的接收側,ring中的每個表項都是一個xdp_desc
結構的描述符。該描述符包含UMEM偏移量(地址)以及資料的長度。
如果沒有幀從FILL ring傳遞給核心,則RX ring中不會出現任何描述符。
使用者程式會消費該ring中的xdp_desc
描述符。
TX Ring
TX Ring用於傳送幀。在填充xdp_desc
(索引,長度和偏移量)描述符後傳遞給該ring。
如果要啟動資料傳輸,則必須呼叫sendmsg()
,未來可能會放寬這種限制。
使用者程式會給TX ring生成xdp_desc
描述符。
XSKMAP / BPF_MAP_TYPE_XSKMAP
在XDP側會用到型別為BPF_MAP_TYPE_XSKMAP
的BPF map,並結合bpf_redirect_map()
將ingress幀傳遞給socket。
使用者應用會通過bpf()
系統呼叫將socket插入該map。
注意,如果一個XDP程式嘗試將幀重定向到一個與佇列配置和netdev不匹配的socket時,會丟棄該幀。即,如果一個AF_XDP socket繫結到一個名為eth0,佇列為17的netdev上時,只有當XDP程式指定到eth0且佇列為17時,才會將資料傳遞給該socket。參見samples/bpf/
獲取例子
配置標誌位和socket選項
XDP_COPY 和XDP_ZERO_COPY bind標誌
當繫結到一個socket時,核心會首先嚐試使用零拷貝進行拷貝。如果不支援零拷貝,則會回退為使用拷貝模式。即,將所有的報文拷貝到使用者空間。但如果想強制指定一種特定的模式,則可以使用如下標誌:如果給bind呼叫傳遞了XDP_COPY
,則核心將強制進入拷貝模式;如果沒有使用拷貝模式,則bind呼叫會失敗,並返回錯誤。相反地,XDP_ZERO_COPY
將強制socket使用零拷貝或呼叫失敗。
XDP_SHARED_UMEM bind 標誌
該表示可以使多個socket繫結到系統的UMEM,但僅能使用系統的佇列id。這種模式下,每個socket都有其各自的RX和TX ring,但UMEM只能有一個FILL ring和一個COMPLETION ring。為了使用這種模式,需要建立第一個socket,並使用正常模式進行繫結。然後建立第二個socket,含一個RX和一個TX(或二者之一),但不會建立FILL 或COMPLETION ring(與第一個socket共享)。在bind呼叫中,設定XDP_SHARED_UMEM
選項,並在sxdp_shared_umem_fd中提供初始socket的fd。以此類推。
那麼當接收到一個報文後,應該上送到那個socket呢?答案是由XDP程式來決定。將所有的socket放到XDP_MAP中,然後將報文傳送給陣列中索引對應的socket。下面展示了一個簡單的以輪詢方式分發報文的例子:
#include <linux/bpf.h>
#include "bpf_helpers.h"
#define MAX_SOCKS 16
struct {
__uint(type, BPF_MAP_TYPE_XSKMAP);
__uint(max_entries, MAX_SOCKS);
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
} xsks_map SEC(".maps");
static unsigned int rr;
SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
{
rr = (rr + 1) & (MAX_SOCKS - 1);
return bpf_redirect_map(&xsks_map, rr, XDP_DROP);
}
注意,由於只有一個FILL和一個COMPLETION ring,且是單生產者單消費者的ring,需要確保多處理器或多執行緒不會同時使用這些ring。libbpf沒有提供原子同步功能。
當多個socket繫結到相同的umem時,libbpf會使用這種模式。然而,需要注意的是,需要在xsk_socket__create
呼叫中提供XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD
libbpf_flag,然後將其載入到自己的XDP程式中(因為libbpf沒有內建路由流量功能)。
XDP_USE_NEED_WAKEUP bind標誌
該選擇支援在FILL ring和TX ring中設定一個名為need_wakeup
的標誌,使用者空間作為這些ring的生產者。當在bind呼叫中設定了該選項,如果需要明確地通過系統呼叫喚醒核心來繼續處理報文時,會設定need_wakeup
標誌。
如果將該標誌設定給FILL ring,則應用需要呼叫poll()
,以便在RX ring上繼續接收報文。如,當核心檢測到FILL ring中沒有足夠的buff,且NIC的RX HW RING中也沒有足夠的buffer時會發生這種情況。此時會關中斷,這樣NIC就無法接收到任何報文(由於沒有足夠的buffer),由於設定了need_wakeup,這樣使用者空間就可以在FILL ring上增加buffer,然後呼叫poll()
,這樣核心驅動就可以將這些buffer新增到HW ring上繼續接收報文。
如果將該標誌設定給TX ring,意味著應用需要明確地通知核心傳送位於TX ring上的報文。可以通過呼叫poll()
,或呼叫sendto()
完成。
可以在samples/bpf/xdpsock_user.c中找到例子。在TX路徑上使用libbpf輔助函式的例子如下:
if (xsk_ring_prod__needs_wakeup(&my_tx_ring))
sendto(xsk_socket__fd(xsk_handle), NULL, 0, MSG_DONTWAIT, NULL, 0);
建議啟用該模式,由於減少了TX路徑上的系統呼叫的數目,因此可以在應用和驅動執行在同一個(或不同)core的情況下提升效能。
XDP_{RX|TX|UMEM_FILL|UMEM_COMPLETION}_RING setsockopts
這些socket選項分別設定RX, TX, FILL和COMPLETION ring的描述符數量(必須至少設定RX或TX ring的描述符大小)。如果同時設定了RX和TX,就可以同時接收和傳送來自應用的流量;如果僅設定了其中一個,就可以節省相應的資源。如果需要將一個UMEM繫結到socket,需要同時設定FILL ring和COMPLETION ring。如果使用了XDP_SHARED_UMEM
標誌,無需為除第一個socket之外的socket建立單獨的UMEM,所有的socket將使用共享的UMEM。注意ring為單生產者單消費者結構,因此多程式無法同時訪問同一個ring。參見XDP_SHARED_UMEM
章節。
使用libbpf時,可以通過給xsk_socket__create
函式的rx和tx引數設定NULL來建立Rx-only和Tx-only的socket。
如果建立了一個Tx-only的socket,建議不要在FILL ring中放入任何報文,否則,驅動可能會認為需要接收資料(但實際上並不是這樣的),進而影響效能。
XDP_UMEM_REG setsockopt
該socket選項會給一個socket註冊一個UMEM,其對應的區域包含了可以容納報文的buffer。該呼叫會使用一個指向該區域開始處的指標,以及該區域的大小。此外,還有一個UMEM可以切分的chunk大小引數(目前僅支援2K或4K)。如果一個UMEM區域的大小為128K,且chunk大小為2K,意味著該UMEM域最大可以有128K / 2K = 64個報文,且最大的報文大小為2K。
還有一個選項可以在UMEM中設定每個buffer的headroom。如果設定為N位元組,意味著報文會從buffer的第N個位元組開始,為應用保留前N個位元組。最後一個選項為標誌位欄位,會在每個UMEM標誌中單獨處理。
XDP_STATISTICS getsockopt
獲取一個socket丟棄資訊,用於除錯。支援的資訊為:
struct xdp_statistics {
__u64 rx_dropped; /* Dropped for reasons other than invalid desc */
__u64 rx_invalid_descs; /* Dropped due to invalid descriptor */
__u64 tx_invalid_descs; /* Dropped due to invalid descriptor */
};
XDP_OPTIONS getsockopt
獲取一個XDP socket的選項。目前僅支援XDP_OPTIONS_ZEROCOPY
,用於檢查是否使用了零拷貝。
從AF_XDP的特性上可以看到其侷限性:不能使用XDP將不同的流量重定向的多個AF_XDP socket上,原因是每個AF_XDP socket必須繫結到物理介面的TX佇列上。大多數的物理和模擬HW的每個介面僅支援一個RX/TX佇列,因此當該介面上繫結了一個AF_XDP後,後續的繫結操作都將失敗。僅有少數HW支援多RX/TX佇列,且通常僅有2/4/8個佇列,無法擴充套件給cloud中的上百個容器使用。
TC
除了XDP,BPF還可以在網路資料路徑的核心tc(traffic control)層之外使用。上文已經給出了XDP和TC的區別。
ingress
hook:__netif_receive_skb_core() -> sch_handle_ingress()
egress
hook:__dev_queue_xmit() -> sch_handle_egress()
執行在tc層的BPF程式使用的是 cls_bpf
(cls即Classifiers的簡稱)分類器。在tc中,將BPF的附著點描述為一個"分類器",這個詞有點誤導,因此它少描述了cls_bpf
的所支援的功能。即一個完整的可程式設計的報文處理器不僅可以讀取skb
的後設資料和報文資料,還可以對其進行任意修改,最後終止tc的處理,並返回裁定的action(見下)。cls_bpf
可以認為是一個自包含的,可以管理和執行tc BPF程式的實體。
cls_bpf
可以包含一個或多個tc BPF程式。通常,在傳統的tc方案中,分類器和action模組是分開的,每個分類器可以附加一個或多個action,一旦匹配到分類器時就會執行action。但在現代軟體資料路徑中使用這種模式的tc處理複雜的報文時會遇到擴充套件性問題。由於附加到cls_bpf的tc BPF程式是完全自包含的,因此可以有效地將解析和操作過程融合到一個單元中。幸好有了cls_bpf
的direct-action
模式,該模式下,僅需要返回tc action裁定結果並立即結束處理流即可,可以在網路資料流中實現可擴充套件的可程式設計報文處理流程,同時避免了action的線性迭代。cls_bpf
是tc層中唯一能夠實現這種快速路徑的“分類器”模組。
與XDP BPF程式類似,tc BPF程式可以在執行時通過cls_bpf自動更新,而不會中斷任何網路流或重啟服務。
cls_bpf
可以附加的tc ingress和egree鉤子都通過一個名為sch_clsact
的偽qdisc進行管理。由於該偽qdisc可以同時管理ingress和egress的tc鉤子,因此它是ingress qdisc的超集(也可直接替換)。對於__dev_queue_xmit()
中的tc的egress鉤子,需要注意的是,它不是在核心的qdisc root鎖下執行的。因此,tc ingress和egress鉤子都以無鎖的方式執行在快速路徑中,且這兩個鉤子都禁用了搶佔,並執行在RCU讀取側。
通常在egress上會存在附著到網路裝置上的qdisc,如sch_mq
,sch_fq
,sch_fq_codel
或sch_htb
,其中有些是可分類的qdisc(包含子類),因此會要求一個報文分類機制來決定在哪裡解複用資料包。該過程通過呼叫tcf_classify()
進行處理,進而呼叫tc分類器(如果存在)。cls_bpf
也可以附加並用於如下場景:一些在qdisc root鎖下的操作可能會收到鎖競爭的影響。sch_clsact
qdisc的egress鉤子出現在更早的時間點,但它不屬於這個鎖的範圍,因此作完全獨立於常規的egress qdiscs。因此,對於sch_htb
這樣的情況,sch_clsact
qdisc可以通過qdisc root鎖之外的tc BPF執行繁重的包分類工作,通過在這些 tc BPF 程式中設定 skb->mark
或 skb->priority
,這樣 sch_htb
只需要一個簡單的對映即可,不需要在root鎖下執行代價高昂的報文分類工作,通過這種方式可以減少鎖競爭。
在sch_clsact結合cls_bpf的場景下支援offloaded tc BPF程式,這種情況下,先前載入的BPF程式是從SmartNIC驅動程式jit生成的,以便在NIC上以本機方式執行。只有在direct-action
模式下執行的cls_bpf
程式才支援offloaded。cls_bpf
僅支援offload一個單獨的程式(無法offload多個程式),且只有ingress支援offload BPF程式。
一個cls_bpf
例項可以包含多個tc BPF程式,如果是這種情況,那麼TC_ACT_UNSPEC
程式返回碼可以繼續執行列表中的下一個tc BPF程式。然而,這樣做的缺點是,多個程式需要多次解析相同的報文,導致效能下降。
返回碼
tc的ingress和egress鉤子共享相同的action來返回tc BPF程式使用的裁定結果,定義在 linux/pkt_cls.h
系統標頭檔案中:
#define TC_ACT_UNSPEC (-1)
#define TC_ACT_OK 0
#define TC_ACT_SHOT 2
#define TC_ACT_STOLEN 4
#define TC_ACT_REDIRECT 7
系統標頭檔案中還有一些以TC_ACT_*
開頭的action變數,可以被兩個鉤子使用。但它們與上面的語義相同。即,從tc BPF的角度來看TC_ACT_OK
和TC_ACT_RECLASSIFY
的語義相同,三個TC_ACT_stelled
、TC_ACT_QUEUED
和TC_ACT_TRAP
操作碼的語義也是相同的。因此,對於這些情況,我們只描述 TC_ACT_OK
和 TC_ACT_STOLEN
操作碼。
從TC_ACT_UNSPEC
開始,表示"未指定的action",用於以下三種場景:i)當一個offloaded tc程式的tc ingress鉤子執行在cls_bpf
的位置,則該offloaded程式將返回TC_ACT_UNSPEC
;ii)為了在多程式場景下繼續執行cls_bpf
中的下一個BPF程式,後續的程式需要與步驟i中的offloaded tc BPF程式配合使用,但出現了一個非offloaded場景下執行的tc BPF程式;iii)TC_ACT_UNSPEC
還可以用於單個程式場景,用於告訴核心繼續使用skb,不會產生其他副作用。TC_ACT_UNSPEC
與TC_ACT_OK
類似,兩者都會將skb通過ingress向上傳遞到網路棧的上層,或者通過egress向下傳遞到網路裝置驅動程式,以便在egress進行傳輸。與TC_ACT_OK
的唯一不同之處是,TC_ACT_OK
基於tc BPF程式設定的classid來設定skb->tc_index
,而 TC_ACT_UNSPEC
是通過 tc BPF 程式之外的 BPF上下文中的 skb->tc_classid
進行設定。
TC_ACT_SHOT
通知核心丟棄報文,即網路棧上層將不會在ingress的skb中看到該報文,類似地,這類報文也不會在egress中傳送。TC_ACT_SHOT
和TC_ACT_STOLEN
本質上是相似的,僅存在部分差異:TC_ACT_SHOT
會通知核心已經通過kfree_skb()
釋放skb,且會立即給呼叫者返回NET_XMIT_DROP
;而TC_ACT_STOLEN會通過consume_skb()
釋放skb,並給上層返回NET_XMIT_SUCCESS
,假裝傳輸成功。perf的報文丟棄監控會記錄kfree_skb()
的操作,因此不會記錄任何因為TC_ACT_STOLEN
丟棄的報文,因為從語義上說,這些 skb
是被消費或排隊的而不是被丟棄的。
最後TC_ACT_REDIRECT
action允許tc BPF程式通過bpf_redirect()
輔助函式將skb重定向到相同或不同的裝置ingress或egress路徑上。通過將報文匯入其他裝置的ingress或egress方向,可以最大化地實現BPF的報文轉發功能。使用該方式不需要對目標網路裝置做任何更改,也不需要在目標裝置上執行另外一個cls_bpf
例項。
載入tc BPF程式
假設有一個名為prog.o
的tc BPF程式,可以通過tc命令將該程式載入到網路裝置山。與XDP不同,它不需要依賴驅動將BPF程式附加到裝置上,下面會用到一個名為em1
的網路裝置,並將程式附加到em1
的ingress
報文路徑上。
# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj prog.o
第一步首先配置一個clsact
qdisc。如上文所述,clsact是一個偽造的qdisc,與ingress
qdisc類似,僅包含分類器和action,但不會提供實際的佇列功能,它是附加bpf分類器所必需的。clsact
提供了兩個特殊的鉤子,稱為ingress
和egress
,分類器可以附加到這兩個鉤子上。ingress
和egress
鉤子都位於網路資料路徑的中央接收和傳送位置,每個經過裝置的報文都會經過此處。ingees
鉤子通過核心的__netif_receive_skb_core() -> sch_handle_ingress()
進行呼叫,egress
鉤子通過__dev_queue_xmit() -> sch_handle_egress()
進行呼叫。
將程式附加到egress
鉤子上的操作為:
# tc filter add dev em1 egress bpf da obj prog.o
clsact
qdisc以無鎖的方式處理來自ingress
和egress
方向的報文,且可以附加到一個無佇列虛擬裝置上,如連線到容器的veth
裝置。
在鉤子之後,tc filter
命令選擇使用bpf
的da
(direct-action)模式。推薦使用並指定da模式
,基本上意味著bpf分類器不再需要呼叫外部tc action模組,所有報文的修改,轉發或其他action都可以通過附加的BPF程式來實現,因此處理速度更快。
到此位置,已經附加bpf程式,一旦有報文傳輸到該裝置後就會執行該程式。與XDP相同,如果不使用預設的section名稱,則可以在載入期間進行指定,例如,下面指定的section名為foobar
:
# tc filter add dev em1 egress bpf da obj prog.o sec foobar
iptables2的BPF載入器允許跨程式型別使用相同的命令列語法。
附加的程式可以使用如下命令列出:
# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[ingress] direct-action id 1 tag c5f7825e5dac396f
# tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[egress] direct-action id 2 tag b2fd5adc0f262714
prog.o:[ingress]
的輸出說明程式段ingress
通過檔案prog.o
進行載入,且bpf
執行在direct-action
模式下。上面兩種情況附加了程式id
和tag
,其中後者表示對指令流的hash,該hash可以與目標檔案或帶有堆疊跟蹤的perf report等相關。最後,id
表示系統範圍內的BPF程式的唯一識別符號,可以使用bpftool
來檢視或dump附加的BPF程式。
tc可以附加多個BPF程式,它提供了其他可以連結在一起的分類器。但附加一個BPF程式已經可以完全滿足需求,因為通過da
(direct-action
)模式可以在一個程式中實現所有的報文操作,意味著BPF程式將返回tc action裁定結果,如TC_ACT_OK
, TC_ACT_SHOT
等。為了獲得最佳效能和靈活性,推薦使用這種方式。
在上述show
命令中,在BPF的相關輸出旁顯示了pref 49152
和handle 0x1
。如果沒有通過命令列顯式地提供,會自動生成的這兩個輸出。perf
表明了一個優先順序數字,即當附加了多個分類器時,將會按照優先順序上升的順序執行這些分類器。handle
表示一個識別符號,當一個perf
載入了系統分類器的多個例項時起作用。由於在BPF場景下,一個程式足矣,perf
和handle
通常可以忽略。
只有在需要自動替換附加的BPF程式的情況下,才會推薦在初始化載入前指定pref
和handle
,這樣在以後執行replace
操作時就不必在進行查詢。建立方式如下:
# tc filter add dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar
# tc filter show dev em1 ingress
filter protocol all pref 1 bpf
filter protocol all pref 1 bpf handle 0x1 prog.o:[foobar] direct-action id 1 tag c5f7825e5dac396f
對於原子替換,可以使用(來自檔案prog.o
中的foobar
section的BPF程式)如下命令來更新現有的ingress
鉤子上的程式
# tc filter replace dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar
最後,為了移除所有ingress和egress上附加的程式,可以使用如下命令:
# tc filter del dev em1 ingress
# tc filter del dev em1 egress
為了移除網路裝置上的整個clsact
qdisc,即移除掉ingress和egress鉤子上附加的所有程式,可以使用如下命令:
# tc qdisc del dev em1 clsact
如果NIC和驅動也像XDP BPF程式一樣支援offloaded,則tc BPF程式也可以是offloaded的。Netronome的nfp同時支援兩種型別的BPF offload。
# tc qdisc add dev em1 clsact
# tc filter replace dev em1 ingress pref 1 handle 1 bpf skip_sw da obj prog.o
Error: TC offload is disabled on net device.
We have an error talking to the kernel
如果出現瞭如上錯誤,則表示首先需要通過ethtool的hw-tc-offload
來啟動tc硬體offload:
# ethtool -K em1 hw-tc-offload on
# tc qdisc add dev em1 clsact
# tc filter replace dev em1 ingress pref 1 handle 1 bpf skip_sw da obj prog.o
# tc filter show dev em1 ingress
filter protocol all pref 1 bpf
filter protocol all pref 1 bpf handle 0x1 prog.o:[classifier] direct-action skip_sw in_hw id 19 tag 57cd311f2e27366b
in_hw
標誌表示程式已經offload到了NIC中。
注意不能同時offload tc和XDP BPF,必須且只能選擇其中之一。
下一篇將給出XDP和TC的使用例子。