使用 BPF 的内核子系统也是 BPF 基础设施的一部分,像网络子系统、追踪子系统都可以 attch BPF 程序,这篇博客 中讲述了 tc 和 XDP 在 Cilium 整个数据链路的位置。在 eBPF XDP 子系统 中介绍了 XDP 子系统,网络包在还未进入网络协议栈之前就处理,带来巨大的性能提升。也正是因为协议栈此时还没有从包中提取出元数据,因此 XDP BPF 程序无法利用这些元数据。与 XDP 相比,tc BPF 程序在内核栈中稍后面的一些地方执行,因此它们可以访问更多的元数据和一 些核心的内核功能。
TC 概念
TC全称「Traffic Control」,直译过来是「流量控制」,在这个领域,你可能更熟悉的是 Linux iptables 或者 netfilter,它们都能做 packet mangling,而TC更专注于packet scheduler,所谓的网络包调度器,调度网络包的延迟、丢失、传输顺序和速度控制。
TC优势
使用并配置TC,为用户带来了对于网络包的可预测性,减少对于网络资源的争夺,实现对不同优先等级的网络服务分配网络资源(如带宽),达到互不干扰的目的,因此服务质量(QoS)一词经常被用作 TC 的代名词。
TC劣势
配置复杂性成为使用TC最显著的缺点,如果配置TC得当,可以使网络资源分配更加公平。但一旦它以不恰当的方式配置使用,可能会导致资源的进一步争夺。因此相比学习如何正确配置TC,很多IT企业可能会倾向购买更高的带宽资源。
从内核 4.1 版本起,引入了一个特殊的 qdisc,叫做 clsact,它为TC提供了一个可以加载BPF程序的入口,使TC和XDP一样,成为一个可以加载BPF程序的网络钩子。
TC vs XDP
从高层看,tc BPF 程序和 XDP BPF 程序有三点主要不同:
输入上下文
BPF 的输入上下文(input context)是一个 sk_buff
而不是 xdp_buff
。当内核协议栈收到一个包时(说明包通过了 XDP 层),它会分配一个缓冲区,解析包,并存储包的元数据。表示这个包的结构体就是 sk_buff
。这个结构体会暴露给 BPF 输入上下文, 因此 tc ingress 层的 BPF 程序就可以利用这些(由协议栈提取的)包的元数据。这些元数据很有用,但在包达到 tc 的 hook 点之前,协议栈执行的缓冲区分配、元数据提取和其他处理等过程也是有开销的。从定义来看,xdp_buff
不需要访问这些元数据,因为 XDP hook 在协议栈之前就会被调用。这是 XDP 和 tc hook 性能差距的重要原因之一 。
因此,attach 到 tc BPF hook 的 BPF 程序可以读取 skb 的 mark
、pkt_type
、 protocol
、priority
、queue_mapping
、napi_id
、cb[]
、hash
、tc_classid
、tc_index
、vlan 元数据、XDP 层传过来的自定义元数据以及其他信息。 tc BPF 的 BPF 上下文中使用了 struct __sk_buff
,这个结构体中的所有成员字段都定 义在 linux/bpf.h
系统头文件。
1 | * user accessible mirror of in-kernel sk_buff. |
通常来说,sk_buff
和 xdp_buff
完全不同,二者各有优劣。
sk_buff
修改与其关联的元数据非常方便,但它包含了大量协议相关的信息(例如 GSO 相关的状态),这使得无法仅仅通过重写包数据来切换协议。- 这是因为协议栈是基于元数据处理包的,而不是每次都去读包的内容。因此,BPF 辅助函数需要额外的转换,并且还要正确处理
sk_buff
内部信息。 xdp_buff
没有这些问题,因为它所处的阶段非常早,此时内核还没有分配sk_buff
,因此很容易实现各种类型的数据包重写。- 但是,
xdp_buff
的缺点是在它这个阶段进行 mangling 的时候,无法利用到sk_buff
元数据。 - 解决这个问题的方式是从 XDP BPF 传递自定义的元数据到 tc BPF。这样,根据使用场景的不同,可以同时利用这两者 BPF 程序,以达到互补的效果。
hook 触发点
tc BPF 程序在数据路径上的 ingress 和 egress 点都可以触发;而 XDP BPF 程序只能在 ingress 点触发。内核两个 hook 点:
- ingress hook
sch_handle_ingress()
:由__netif_receive_skb_core()
触发 - egress hook
sch_handle_egress()
:由__dev_queue_xmit()
触发
__netif_receive_skb_core()
和 __dev_queue_xmit()
是 data path 的主要接收和发送函数,不考虑 XDP 的话(XDP 可能会拦截或修改,导致不经过这两个 hook 点), 每个网络进入或离开系统的网络包都会经过这两个点,从而使得 tc BPF 程序具备完全可观测性。
是否依赖驱动支持
tc BPF 程序不需要驱动做任何改动,因为它们运行在网络栈通用层中的 hook 点。因此,它们可以 attach 到任何类型的网络设备上。
Ingress
这提供了很好的灵活性,但跟运行在原生 XDP 层的程序相比,性能要差一些。然而,tc BPF 程序仍然是内核的通用 data path 做完 GRO 之后、且处理任何协议之前 最早的处理点。传统的 iptables 防火墙也是在这里处理的,例如 iptables PREROUTING 或 nftables ingress hook 或其他数据包包处理过程。
Egress
类似的,对于 egress,tc BPF 程序在将包交给驱动之前的最晚的地方执行,这个地方在传统 iptables 防火墙 hook 之后(例如 iptables POSTROUTING), 但在内核 GSO 引擎之前。
唯一需要驱动做改动的场景是:将 tc BPF 程序 offload 到网卡。形式通常和 XDP offload 类似,只是特性列表不同,因为二者的 BPF 输入上下文、辅助函数和返回码( verdict)不同。支持 offload tc BPF 程序的驱动 Netronome/nfp。
TC BPF
cls_bpf 分类器
运行在 tc 层的 BPF 程序使用的是 cls_bpf
分类器。在 tc 术语中 “BPF 附着点”被称为“分类器”,但这个词其实有点误导,因为它少描述了前者可以做的事情。attachment point 是一个完全可编程的包处理器,不仅能够读取 skb
元数据 和包数据,还可以任意 mangle 这两者,最后结束 tc 处理过程,返回一个裁定结果( verdict)。因此,cls_bpf
可以认为是一个管理和执行 tc BPF 程序的自包含实体( self-contained entity)。
cls_bpf
可以持有(hold)一个或多个 tc BPF 程序。Cilium 在部署 cls_bpf
程序时 ,对于一个给定的 hook 点只会附着一个程序,并且用的是 direct-action
模式。 典型情况下,在传统 tc 方案中,分类器(classifier )和动作模块(action modules) 之间是分开的,每个分类器可以 attach 多个 action,当匹配到这个分类器时这些 action 就会执行。在现代世界,在软件 data path 中使用 tc 做复杂包处理时这种模型扩展性不好。 考虑到附着到 cls_bpf
的 tc BPF 程序 是完全自包含的,因此它们有效地将解析和 action 过程融合到了单个单元(unit)中。得益于 cls_bpf
的 direct-action
模式,它只需要返回 tc action 判决结果,然后立即终止处理流水线。这使得能够在网络 data path 中实现可扩展可编程的包处理,避免动作的线性迭代。cls_bpf
是 tc 层中唯一支持这种快速路径(fast-path)的一个分类器模块。
和 XDP BPF 程序类似,tc BPF 程序能在运行时通过 cls_bpf
原子地更新, 而不会导致任何网络流量中断,也不用重启服务。
cls_bpf
可以附着的 tc ingress 和 egress hook 点都是由一个名为 sch_clsact
的 伪 qdisc 管理的,它是 ingress qdisc 的一个超集,可以无缝替换后者,因为它既可以管理 ingress tc hook 又可以管理 egress tc hook。对于 __dev_queue_xmit()
内的 tc egress hook,需要注意的是这个 hook 并不是在内核的 qdisc root lock 下执行的。因此,ingress 和 egress hook 都是在快速路径中以无锁( lockless)方式执行的。不管是 ingress 还是 egress,抢占(preemption )都被关闭, 执行发生在 RCU 读侧(execution happens under RCU read side)。
通常在 egress 的场景下,有很多类型的 qdisc 会 attach 到 netdevice,例如 sch_mq
, sch_fq
, sch_fq_codel
or sch_htb
,其中某些是 classful qdiscs,这些 qdisc 包 含 subclasses 因此需要一个对包进行分类的机制,决定将包 demux 到哪里。这个机制是 由调用 tcf_classify()
实现的,这个函数会进一步调用 tc 分类器(如果提供了)。在 这种场景下, cls_bpf
也可以被 attach 和使用。这种操作通常发生在 qdisc root lock 下面,因此会面临锁竞争的问题。sch_clsact
qdisc 的 egress hook 点位于更前 面,没有落入这个锁的范围内,因此完全独立于常规 egress qdisc 而执行。 因此对于 sch_htb
这种场景,sch_clsact
qdisc 可以将繁重的包分类工作放到 tc BPF 程序,在 qdisc root lock 之外执行,在这些 tc BPF 程序中设置 skb->mark
或 skb->priority
,因此随后 sch_htb
只需要一个简单的映射,没有原来在 root lock 下面昂贵的包分类开销,还减少了锁竞争。
在 sch_clsact
in combination with cls_bpf
场景下支持 Offloaded tc BPF 程序, 在这种场景下,原来加载到智能网卡驱动的 BPF 程序被 JIT,在网卡原生执行。 只有工作在 direct-action
模式的 cls_bpf
程序支持 offload。 cls_bpf
只支持 offload 单个程序,不支持同时 offload 多个程序。另外,只有 ingress hook 支持 offloading BPF 程序。
一个 cls_bpf
实例内部可以 hold 多个 tc BPF 程序。如果由多个程序, TC_ACT_UNSPEC
程序返回码就是让继续执行列表中的下一个程序。但这种方式的缺点是: 每个程序都需要解析一遍数据包,性能会下降。
tc BPF 程序返回码
tc ingress 和 egress hook 共享相同的返回码(动作判决),定义在 linux/pkt_cls.h
系统头文件:
1 |
系统头文件中还有一些 TC_ACT_*
动作判决,也用在了这两个 hook 中。但是,这些判决和上面列出的那几个共享相同的语义。这意味着,从 tc BPF 的角度看, TC_ACT_OK
和 TC_ACT_RECLASSIFY
有相同的语义, TC_ACT_STOLEN
, TC_ACT_QUEUED
and TC_ACT_TRAP
返回码也是类似的情况。因此, 对于这些情况,我们只描述 TC_ACT_OK
和 TC_ACT_STOLEN
操作码。
TC_ACT_UNSPEC
和 TC_ACT_OK
TC_ACT_UNSPEC
表示“未指定的动作”(unspecified action),在三种情况下会用到:
- attach 了一个 offloaded tc BPF 程序,tc ingress hook 正在运行,被 offload 的 程序的
cls_bpf
表示会返回TC_ACT_UNSPEC
- 为了在
cls_bpf
多程序的情况下,继续下一个 tc BPF 程序。这种情况可以和 第一种情况中提到的 offloaded tc BPF 程序一起使用,此时第一种情况返回的TC_ACT_UNSPEC
继续执行下一个没有被 offloaded BPF 程序? TC_ACT_UNSPEC
还用于单个程序从场景,只是通知内核继续执行 skb 处理,但不要带来任何副作用。
TC_ACT_UNSPEC
在某些方面和 TC_ACT_OK
非常类似,因为二者都是将 skb
向下一个 处理阶段传递,在 ingress 的情况下是传递给内核协议栈的更上层,在 egress 的情况下 是传递给网络设备驱动。唯一的不同是 TC_ACT_OK
基于 tc BPF 程序设置的 classid 来 设置 skb->tc_index
,而 TC_ACT_UNSPEC
是通过 tc BPF 程序之外的 BPF 上下文中的 skb->tc_classid
设置。
TC_ACT_SHOT
和 TC_ACT_STOLEN
这两个返回码指示内核将包丢弃。这两个返回码很相似,只有少数几个区别:
TC_ACT_SHOT
提示内核skb
是通过kfree_skb()
释放的,并返回NET_XMIT_DROP
给调用方,作为立即反馈TC_ACT_STOLEN
通过consume_skb()
释放skb
,返回NET_XMIT_SUCCESS
给上 层假装这个包已经被正确发送了
perf 的丢包监控是跟踪的 kfree_skb()
,因此在 TC_ACT_STOLEN
的场景下它无法看到任何丢包统计,因为从语义上说,此时这些 skb
是被”consumed” 或 queued 而不是被 dropped。
TC_ACT_REDIRECT
这个返回码加上 bpf_redirect()
辅助函数,允许重定向一个 skb
到同一个或另一个 设备的 ingress 或 egress 路径。能够将包注入另一个设备的 ingress 或 egress 路径使 得基于 BPF 的包转发具备了完全的灵活性。对目标网络设备没有额外的要求,只要本身是 一个网络设备就行了,在目标设备上不需要运行 cls_bpf
实例或其他限制。
tc BPF FAQ
本节列出一些经常被问的、与 tc BPF 程序有关的问题。
用
act_bpf
作为 tc action module 怎么样,现在用的还多吗?不多。虽然对于 tc BPF 程序来说
cls_bpf
和act_bpf
有相同的功能 ,但前者更加灵活,因为它是后者的一个超集(superset)。tc 的工作原理是将 tc actions attach 到 tc 分类器。要想实现与cls_bpf
一样的灵活性,act_bpf
需要 被 attach 到cls_matchall
分类器。如名字所示,为了将包传递给 attached tc action 去处理,这个分类器会匹配每一个包。相比于工作在direct-action
模式的cls_bpf
,act_bpf
这种方式会导致较低的包处理性能。如果act_bpf
用在cls_bpf
orcls_matchall
之外的其他分类器,那性能会更差,这是由 tc 分类器的 操作特性(nature of operation of tc classifiers)决定的。同时,如果分类器 A 未 匹配,那包会传给分类器 B,B 会重新解析这个包以及重复后面的流量,因此这是一个线 性过程,在最坏的情况下需要遍历 N 个分类器才能匹配和(在匹配的分类器上)执行act_bpf
。因此,act_bpf
从未大规模使用过。另外,和cls_bpf
相比,act_bpf
也没有提供 tc offload 接口。是否推荐在使用
cls_bpf
时选择direct-action
之外的其他模式?不推荐。原因和上面的问题类似,选择其他模式无法应对更加复杂的处理情况。tc BPF 程序本身已经能以一种高效的方式做任何处理,因此除了
direct-action
这个模式 之外,不需要其他的任何东西了。offloaded
cls_bpf
和 offloaded XDP 有性能差异吗?没有。二者都是由内核内的同一个编译器 JIT 的,这个编译器负责 offload 到智能网卡,并且对二者的加载机制是非常相似的。因此,要在 NIC 上原生执行,BPF 程序会被翻译成相同的目标指令。
tc BPF 和 XDP BPF 这两种程序类型有不同的特性集合,因此根据使用场景的不同,你可以选择 tc BPF 或者是 XDP BPF,例如,二者的在 offload 场景下的辅助函数可能会有差异。
tc BPF 使用案例
本节列出了 tc BPF 程序的主要使用案例。但要注意,这里列出的并不是全部案例,而且考虑到 tc BPF 的可编程性和效率,人们很容易对它进行定制化然后集成到编排系统,用来解决特定的问题。XDP 的一些案例可能有重叠,但 tc BPF 和 XDP BPF 大部分情况下都是互补的,可以单独使用,也可以同时使用,就看哪种情况更适合解决给定的问题了 。
为容器落实策略(Policy enforcement)
tc BPF 程序适合用来给容器实现安全策略、自定义防火墙或类似的安全工具。在传统方式中,容器隔离是通过网络命名空间时实现的,veth pair 的一端连接到宿主机的初始命名空间,另一端连接到容器的命名空间。因为 veth pair 的 一端移动到了容器的命名空间,而另一端还留在宿主机上(默认命名空间),容器所有的网络流量都需要经过主机端的 veth 设备,因此可以在这个 veth 设备的 tc ingress 和 egress hook 点 attach tc BPF 程序。目标地址是容器的网络流量会经过主机端的 veth 的 tc egress hook,而从容器出来的网络流量会经过主机端的 veth 的 tc ingress hook。
对于像 veth 这样的虚拟设备,XDP 在这种场景下是不合适的,因为内核在这里只操作
skb
,而通用 XDP 有几个限制,导致无法操作克隆的skb
。而克隆skb
在 TCP/IP 协议栈中用的非常多,目的是持有(hold)准备重传的数据片(data segments),而通 用 XDP hook 在这种情况下回被直接绕过。另外,generic XDP 需要顺序化(linearize )整个skb
导致严重的性能下降。相比之下, tc BPF 非常灵活,因为设计中它就是工作在接 收skb
格式的输入上下文中,因此没有 generic XDP 遇到的那些问题。转发和负载均衡
转发和负载均衡的使用场景和 XDP 很类似,只是目标更多的是在东西向容器流量而不是南北向(虽然两者都可以用于东西向或南北向场景)。XDP 只能在 ingress 方向使用, tc BPF 程序还可以在 egress 方向使用,例如,可以在初始命名空间内(宿主机上的 veth 设备上),通过 BPF 对容器的 egress 流量同时做地址转化(NAT)和负载均衡, 整个过程对容器是透明的。由于在内核网络栈的实现中,egress 流量已经是
sk_buff
形式的了,因此很适合 tc BPF 对其进行重写(rewrite)和重定向(redirect)。 使用bpf_redirect()
辅助函数,BPF 就可以接管转发逻辑,将包推送到另一个网络设备的 ingress 或 egress 路径上。因此,有了 tc BPF 程序实现的转发网格( forwarding fabric),网桥设备都可以不用了。流抽样与监控
和 XDP 类似,可以通过高性能无锁 per-CPU 内存映射 perf 环形缓冲区(ring buffer )实现流抽样(flow sampling)和监控,在这种场景下,BPF 程序能够将自定义数据、 全部或截断的包内容或者二者同时推送到一个用户空间应用。在 tc BPF 程序中这是通过
bpf_skb_event_output()
BPF 辅助函数实现的,它和bpf_xdp_event_output()
有相 同的函数签名和语义。考虑到 tc BPF 程序可以同时 attach 到 ingress 和 egress,而 XDP 只能 attach 到 ingress,另外,这两个 hook 都在(通用)网络栈的更低层,这使得可以监控每台节点 的所有双向网络流量。这和 tcpdump 和 Wireshark 使用的 cBPF 比较相关,但是,不需要克隆
skb
,而且因为其可编程性而更加灵活,例如。BPF 能够在内核中完成聚合 ,而不用将所有数据推送到用户空间;也可以对每个放到 ring buffer 的包添加自定义 的 annotations。Cilium 大量使用了后者,对被 drop 的包进一步 annotate,关联到 容器标签以及 drop 的原因(例如因为违反了安全策略),提供了更丰富的信息。包调度器预处理(Packet scheduler pre-processing)
sch_clsact
’s egress hook 被sch_handle_egress()
调用,在获得内核的 qdisc root lock 之前执行,因此 tc BPF 程序可以在包被发送到一个真实的 full blown qdis (例如sch_htb
)之前,用来执行包分类和 mangling 等所有这些高开销工作。 这种sch_clsact
和后面的发送阶段的真实 qdisc(例如sch_htb
) 之间的交互, 能够减少发送时的锁竞争,因为sch_clsact
的 egress hook 是在无锁的上下文中执行的。
同时使用 tc BPF 和 XDP BPF 程序的一个具体例子是 Cilium。Cilium 是一个开源软件, 透明地对(K8S 这样的容器编排平台中的)容器之间的网络连接进行安全保护,工作在 L3/L4/L7。Cilium 的核心基于 BPF,用来实现安全策略、负载均衡和监控。
加载 tc BPF 对象文件
用 tc 加载 BPF 程序
给定一个为 tc 编译的 BPF 对象文件 prog.o
, 可以通过 tc
命令将其加载到一个网络设备。但与 XDP 不同,设备是否支持 attach BPF 程序并不依赖驱动 (即任何网络设备都支持 tc BPF)。下面的命令可以将程序 attach 到 em1
的 ingress
网络:
1 | $ tc qdisc add dev em1 clsact |
第一步创建了一个 clsact
qdisc (Linux 排队规则,Linux queueing discipline)。
clsact
是一个 dummy qdisc,和 ingress
qdisc 类似,可以持有(hold)分类器和动作(classifier and actions),但不执行真正的排队(queueing)。后面 attach bpf
分类器需要用到它。clsact
qdisc 提供了两个特殊的 hook:ingress
and egress
,分类器可以 attach 到这两个 hook 点。这两个 hook 都位于 datapath 的 关键收发路径上,设备 em1
的每个包都会经过这两个点。这两个 hook 分别会被下面的内 核函数调用:
ingress
hook:__netif_receive_skb_core() -> sch_handle_ingress()
egress
hook:__dev_queue_xmit() -> sch_handle_egress()
类似地,将程序 attach 到 egress
hook:
1 | $ tc filter add dev em1 egress bpf da obj prog.o |
clsact
qdisc 在 ingress
和 egress
方向以无锁(lockless)方式执行,而且 可以 attach 到虚拟的、无队列的设备(virtual, queue-less devices),例如连接容器和 宿主机的 veth
设备。
第二条命令,tc filter
选择了在 da
(direct-action)模式中使用 bpf
。da
是 推荐的模式,并且应该永远指定这个参数。粗略地说,da
模式表示 bpf
分类器不需 要调用外部的 tc
action 模块。事实上 bpf
分类器也完全不需要调用外部模块,因 为所有的 packet mangling、转发或其他类型的 action 都可以在这单个 BPF 程序内完成 ,因此执行会明显更快。
配置了这两条命令之后,程序就 attach 完成了,接下来只要有包经过这个设备,就会触发 这个程序执行。和 XDP 类似,如果没有使用默认 section 名字,那可以在加载时指定,例 如指定 section 为 foobar
:
1 | $ tc filter add dev em1 egress bpf da obj prog.o sec foobar |
iproute2 BPF 加载器的命令行语法对不同的程序类型都是一样的,因此 obj prog.o sec foobar
命令行格式和前面看到的 XDP 的加载是类似的。
查看已经 attach 的程序:
1 | $ tc filter show dev em1 ingress |
输出中的 prog.o:[ingress]
表示 section ingress
中的程序是从 文件 prog.o
加 载的,而且 bpf
工作在 direct-action
模式。上面还打印了程序的 id
和 tag
, 其中 tag
是指令流(instruction stream)的哈希,可以关联到对应的对象文件或用 perf
查看调用栈信息。id
是一个操作系统层唯一的 BPF 程序标识符,可以用 bpftool
进一步查看或 dump 相关的程序信息。
tc 可以 attach 多个 BPF 程序,并提供了其他的一些分类器,这些分类器可以 chain 到 一起使用。但是,attach 单个 BPF 程序已经完全足够了,因为有了 da
模式,所有的包 操作都可以放到同一个程序中,这意味着 BPF 程序自身将会返回 tc action verdict,例 如 TC_ACT_OK
、TC_ACT_SHOT
等等。出于最佳性能和灵活性考虑,这(da
模式)是推 荐的使用方式。
程序优先级和句柄
在上面的 show
命令中,tc 还打印出了 pref 49152
和 handle 0x1
。如果之前没有 通过命令行显式指定,这两个数据就会自动生成。pref
表示优先级,如果指定了多个分 类器,它们会按照优先级从高到低依次执行;handle
是一个标识符,在加载了同一分类器的多 个实例并且它们的优先级(pref
)都一样的情况下会用到这个标识符。因为 在 BPF 的场景下,单个程序就足够了,因此 pref
和 handle
通常情况下都可以忽略。
除非打算后面原子地替换 attached BPF 程序,否则不建议在加载时显式指定 pref
和 handle
。显式指定这两个参数的好处是,后面执行 replace
操作时,就不需要再去动 态地查询这两个值。显式指定 pref
和 handle
时的加载命令:
1 | $ tc filter add dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar |
对应的原子 replace
命令:将 ingress
hook 处的已有程序替换为 prog.o
文件中 foobar
section 中的新 BPF 程序,
1 | $ tc filter replace dev em1 ingress pref 1 handle 1 bpf da obj prog.o sec foobar |
用 tc 删除 BPF 程序
最后,要分别从 ingress
和 egress
删除所有 attach 的程序,执行:
1 | $ tc filter del dev em1 ingress |
要从 netdevice 删除整个 clsact
qdisc(会隐式地删除 attach 到 ingress
和 egress
hook 上面的所有程序),执行:
1 | $ tc qdisc del dev em1 clsact |
offload 到网卡
和 XDP BPF 程序类似,如果网卡驱动支持 tc BPF 程序,那也可以将它们 offload 到网卡 。Netronome 的 nfp 网卡对 XDP 和 tc BPF 程序都支持 offload。
1 | $ tc qdisc add dev em1 clsact |
如果显式以上错误,那需要先启用网卡的 hw-tc-offload
功能:
1 | $ ethtool -K em1 hw-tc-offload on |
其中的 in_hw
标志表示这个程序已经被 offload 到网卡了。
注意,tc 和 XDP offload 无法同时加载,因此必须要指明是 tc 还是 XDP offload 选项 。
通过 netdevsim 驱动测试 BPF offload
netdevsim 驱动是 Linux 内核的一部分,它是一个 dummy driver,实现了 XDP BPF 和 tc BPF 程序的 offload 接口,以及其他一些设施,这些设施可以用来测试内核的改动,或者 某些利用内核的 UAPI 实现了一个控制平面功能的底层用户空间程序。
可以用如下命令创建一个 netdevsim 设备:
1 | $ modprobe netdevsim |
然后就可以加载 XDP 或 tc BPF 程序,命令和前面的一些例子一样:
1 | $ ip -force link set dev eth0 xdpoffload obj prog.o |
这是用 iproute2 加载 XDP/tc BPF 程序的两个标准步骤。
还有很多对 XDP 和 tc
都适用的 BPF 加载器高级选项,下面列出其中一些。为简单 起见,这里只列出了 XDP 的例子。
打印更多 log(Verbose),即使命令执行成功
在命令最后加上
verb
选项可以打印校验器的日志:1
2
3
4
5
6
7
8
9
10
11
12$ ip link set dev em1 xdp obj xdp-example.o verb
Prog section 'prog' loaded (5)!
- Type: 6
- Instructions: 2 (0 over limit)
- License: GPL
Verifier analysis:
0: (b7) r0 = 1
1: (95) exit
processed 2 insns加载已经 pin 在 BPF 文件系统中的程序
除了从对象文件加载程序之外,iproute2 还可以从 BPF 文件系统加载程序。在某些场 景下,一些外部实体会将 BPF 程序 pin 在 BPF 文件系统并 attach 到设备。加载命 令:
1
$ ip link set dev em1 xdp pinned /sys/fs/bpf/prog
iproute2 还可以使用更简短的相对路径方式(相对于 BPF 文件系统的挂载点):
1
$ ip link set dev em1 xdp pinned m:prog
在加载 BPF 程序时,iproute2 会自动检测挂载的文件系统实例。如果发现还没有挂载,tc 就会自动将其挂载到默认位置 /sys/fs/bpf/
。
如果发现已经挂载了一个 BPF 文件系统实例,接下来就会使用这个实例,不会再挂载新的 了:
1 | $ mkdir /var/run/bpf |
默认情况下,tc
会创建一个如上面所示的初始目录,所有子系统的用户都会通过符号 链接(symbolic links)指向相同的位置,也是就是 globals
命名空间,因此 pinned BPF maps 可以被 iproute2
中不同类型的 BPF 程序使用。如果文件系统实例已经挂载、 目录已经存在,那 tc 是不会覆盖这个目录的。因此对于 lwt
, tc
和 xdp
这几种类 型的 BPF maps,可以从 globals
中分离出来,放到各自的目录存放。
在前面的 LLVM 小节中简要介绍过,安装 iproute2 时会向系统中安装一个头文件,BPF 程 序可以直接以标准路(standard include path)径来 include 这个头文件:
1 |
这个头文件中提供的 API 可以让程序使用 maps 和默认 section 名字。它是 iproute2
和 BPF 程序之间的一份稳定契约(contract )。
iproute2 中 map 的定义是 struct bpf_elf_map
。这个结构体内的成员变量已经在 LLVM 小节中介绍过了。
When parsing the BPF object file, the iproute2 loader will walk through all ELF sections. It initially fetches ancillary sections like
maps
andlicense
. Formaps
, thestruct bpf_elf_map
array will be checked for validity and whenever needed, compatibility workarounds are performed. Subsequently all maps are created with the user provided information, either retrieved as a pinned object, or newly created and then pinned into the BPF file system. Next the loader will handle all program sections that contain ELF relocation entries for maps, meaning that BPF instructions loading map file descriptors into registers are rewritten so that the corresponding map file descriptors are encoded into the instructions immediate value, in order for the kernel to be able to convert them later on into map kernel pointers. After that all the programs themselves are created through the BPF system call, and tail called maps, if present, updated with the program’s file descriptors.