全面介绍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的使用例子。

相关文章
相关标签/搜索