全面介紹eBPF-概念

全面介紹eBPF-概念

前面介紹了BCC可觀測性BCC網絡,但對底層使用的eBPF的介紹相對較少,且官方欠缺對網絡方面的介紹。下面對eBPF進行全面介紹。html

BPF概述

下面內容來自Linux官方文檔linux

eBPF的演進

最初的[Berkeley Packet Filter (BPF) PDF]是爲捕捉和過濾符合特定規則的網絡包而設計的,過濾器爲運行在基於寄存器的虛擬機上的程序。git

在內核中運行用戶指定的程序被證實是一種有用的設計,但最初BPF設計中的一些特性卻並無獲得很好的支持。例如,虛擬機的指令集架構(ISA)相對落後,如今處理器已經使用64位的寄存器,併爲多核系統引入了新的指令,如原子指令XADD。BPF提供的一小部分RISC指令已經沒法在現有的處理器上使用。github

所以Alexei Starovoitov在eBPF的設計中介紹瞭如何利用現代硬件,使eBPF虛擬機更接近當代處理器,eBPF指令更接近硬件的ISA,便於提高性能。其中最大的變更之一是使用了64位的寄存器,並將寄存器的數量從2提高到了10個。因爲現代架構使用的寄存器遠遠大於10個,這樣就能夠像本機硬件同樣將參數經過eBPF虛擬機寄存器傳遞給對應的函數。另外,新增的BPF_CALL指令使得調用內核函數更加便利。sql

將eBPF映射到本機指令有助於實時編譯,提高性能。3.15內核中新增的eBPF補丁使得x86-64上運行的eBPF相比老的BPF(cBPF)在網絡過濾上的性能提高了4倍,大部分狀況下會保持1.5倍的性能提高。不少架構 (x86-64, SPARC, PowerPC, ARM, arm64, MIPS, and s390)已經支持即時(JIT)編譯。shell

使用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_startapi

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_SKBBPF_PROG_TYPE_SOCKET_FILTER)。

當前內核支持的eBPF程序類型爲:

  • BPF_PROG_TYPE_SOCKET_FILTER: a network packet filter
  • BPF_PROG_TYPE_KPROBE: determine whether a kprobe should fire or not
  • BPF_PROG_TYPE_SCHED_CLS: a network traffic-control classifier
  • BPF_PROG_TYPE_SCHED_ACT: a network traffic-control action
  • BPF_PROG_TYPE_TRACEPOINT: determine whether a tracepoint should fire or not
  • BPF_PROG_TYPE_XDP: a network packet filter run from the device-driver receive path
  • BPF_PROG_TYPE_PERF_EVENT: determine whether a perf event handler should fire or not
  • BPF_PROG_TYPE_CGROUP_SKB: a network packet filter for control groups
  • BPF_PROG_TYPE_CGROUP_SOCK: a network packet filter for control groups that is allowed to modify socket options
  • BPF_PROG_TYPE_LWT_*: a network packet filter for lightweight tunnels
  • BPF_PROG_TYPE_SOCK_OPS: a program for setting socket parameters
  • BPF_PROG_TYPE_SK_SKB: a network packet filter for forwarding packets between sockets
  • BPF_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 table
  • BPF_MAP_TYPE_ARRAY: an array map, optimized for fast lookup speeds, often used for counters
  • BPF_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 protocols
  • BPF_MAP_TYPE_PERCPU_ARRAY: a per-CPU array, used to implement histograms of latency
  • BPF_MAP_TYPE_PERF_EVENT_ARRAY: stores pointers to struct perf_event, used to read and store perf event counters
  • BPF_MAP_TYPE_CGROUP_ARRAY: stores pointers to control groups
  • BPF_MAP_TYPE_PERCPU_HASH: a per-CPU hash table
  • BPF_MAP_TYPE_LRU_HASH: a hash table that only retains the most recently used items
  • BPF_MAP_TYPE_LRU_PERCPU_HASH: a per-CPU hash table that only retains the most recently used items
  • BPF_MAP_TYPE_LPM_TRIE: a longest-prefix match trie, good for matching IP addresses to a range
  • BPF_MAP_TYPE_STACK_TRACE: stores stack traces
  • BPF_MAP_TYPE_ARRAY_OF_MAPS: a map-in-map data structure
  • BPF_MAP_TYPE_HASH_OF_MAPS: a map-in-map data structure
  • BPF_MAP_TYPE_DEVICE_MAP: for storing and looking up network device references
  • BPF_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_SECCOMPSECCOMP_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

支持offloaded XDP的驅動以下:

  • Netronome

模式校驗

能夠經過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_RINGXDP_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_SKBXDP_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中的上百個容器使用。

更多細節參見AF_XDP官方文檔以及這篇論文

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_bpfdirect-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_mqsch_fqsch_fq_codelsch_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->markskb->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_OKTC_ACT_RECLASSIFY的語義相同,三個TC_ACT_stelledTC_ACT_QUEUEDTC_ACT_TRAP操做碼的語義也是相同的。所以,對於這些狀況,咱們只描述 TC_ACT_OKTC_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_UNSPECTC_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_SHOTTC_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的網絡設備,並將程序附加到em1ingress報文路徑上。

# 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 提供了兩個特殊的鉤子,稱爲ingressegress,分類器能夠附加到這兩個鉤子上。ingressegress鉤子都位於網絡數據路徑的中央接收和發送位置,每一個通過設備的報文都會通過此處。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以無鎖的方式處理來自ingressegress方向的報文,且能夠附加到一個無隊列虛擬設備上,如鏈接到容器的veth設備。

在鉤子以後,tc filter命令選擇使用bpfda(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模式下。上面兩種狀況附加了程序idtag,其中後者表示對指令流的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 49152handle 0x1。若是沒有經過命令行顯式地提供,會自動生成的這兩個輸出。perf代表了一個優先級數字,即當附加了多個分類器時,將會按照優先級上升的順序執行這些分類器。handle表示一個標識符,當一個perf加載了系統分類器的多個實例時起做用。因爲在BPF場景下,一個程序足矣,perfhandle一般能夠忽略。

只有在須要自動替換附加的BPF程序的狀況下,纔會推薦在初始化加載前指定prefhandle,這樣在之後執行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的使用例子。

相關文章
相關標籤/搜索