全面介紹eBPF-概念
全面介紹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的使用例子。