AF_XDP

概述

AF_XDP 是一个为高性能数据包处理而优化的地址族。

本文档假设读者熟悉 BPF 和 XDP。如果不熟悉,Cilium 项目有一个优秀的参考指南:http://cilium.readthedocs.io/en/latest/bpf/

通过 XDP 程序的 XDP_REDIRECT 动作,程序可以使用 bpf_redirect_map() 函数将入口帧重定向到其他启用 XDP 的 netdev。AF_XDP 套接字使 XDP 程序可以将帧重定向到用户空间应用程序中的内存缓冲区。

AF_XDP 套接字 (XSK) 使用普通的 socket() 系统调用创建。与每个 XSK 关联的是两个环:RX 环和 TX 环。套接字可以在 RX 环上接收数据包,并且可以在 TX 环上发送数据包。这些环使用 setsockopts XDP_RX_RING 和 XDP_TX_RING 分别注册和调整大小。每个套接字必须至少有一个环。RX 或 TX 描述符环指向 UMEM 内存区域中的数据缓冲区。RX 和 TX 可以共享同一个 UMEM,因此数据包不必在 RX 和 TX 之间复制。此外,如果一个数据包需要保留一段时间,因为可能需要重传,则指向该数据包的描述符可以更改为指向另一个描述符,并立即重用。这再次避免了复制数据。

UMEM 由许多大小相等的块组成。环中的描述符通过引用其 addr 来引用帧。addr 只是整个 UMEM 区域内的偏移量。用户空间使用其认为最合适的任何方法(malloc、mmap、huge pages 等)为此 UMEM 分配内存。然后,使用新的 setsockopt XDP_UMEM_REG 向内核注册此内存区域。UMEM 还有两个环:FILL 环和 COMPLETION 环。FILL 环由应用程序用于向下发送 addr,以便内核填充 RX 数据包数据。一旦收到每个数据包,对这些帧的引用将出现在 RX 环中。另一方面,COMPLETION 环包含内核已完全传输的帧 addr,现在可以被用户空间再次使用,用于 TX 或 RX。因此,出现在 COMPLETION 环中的帧 addr 是先前使用 TX 环传输的 addr。总而言之,RX 和 FILL 环用于 RX 路径,TX 和 COMPLETION 环用于 TX 路径。

最后,使用 bind() 调用将套接字绑定到设备以及该设备上的特定队列 ID,只有在绑定完成后,流量才会开始流动。

如果需要,UMEM 可以在进程之间共享。如果一个进程想要这样做,它只需跳过 UMEM 及其相应的两个环的注册,在 bind 调用中设置 XDP_SHARED_UMEM 标志,并提交它想要共享 UMEM 的进程的 XSK 以及它自己新创建的 XSK 套接字。然后,新进程将在其自己的 RX 环中接收指向此共享 UMEM 的帧 addr 引用。请注意,由于环结构是单消费者/单生产者(出于性能原因),因此新进程必须创建自己的带有相关 RX 和 TX 环的套接字,因为它无法与其他进程共享。这也是每个 UMEM 只有一个 FILL 和 COMPLETION 环的原因。由单个进程负责处理 UMEM。

那么数据包是如何从 XDP 程序分发到 XSK 的呢?有一个名为 XSKMAP 的 BPF 映射(或完整的 BPF_MAP_TYPE_XSKMAP)。用户空间应用程序可以将 XSK 放置在此映射中的任意位置。然后,XDP 程序可以将数据包重定向到此映射中的特定索引,此时 XDP 验证该映射中的 XSK 确实已绑定到该设备和环号。如果不是,则数据包将被丢弃。如果该索引处的映射为空,则数据包也会被丢弃。这也意味着目前必须加载 XDP 程序(并且 XSKMAP 中有一个 XSK)才能通过 XSK 将任何流量发送到用户空间。

AF_XDP 可以在两种不同的模式下运行:XDP_SKB 和 XDP_DRV。如果驱动程序不支持 XDP,或者在加载 XDP 程序时显式选择 XDP_SKB,则将采用 XDP_SKB 模式,该模式将 SKB 与通用 XDP 支持一起使用,并将数据复制到用户空间。这是一种适用于任何网络设备的后备模式。另一方面,如果驱动程序支持 XDP,则 AF_XDP 代码将使用它来提供更好的性能,但仍然需要将数据复制到用户空间。

概念

为了使用 AF_XDP 套接字,需要设置许多相关的对象。以下部分将解释这些对象及其选项。

有关 AF_XDP 如何工作的概述,您还可以查看 2018 年 Linux Plumbers 会议上关于此主题的论文:http://vger.kernel.org/lpc_net2018_talks/lpc18_paper_af_xdp_perf-v2.pdf。请勿查阅 2017 年关于“AF_PACKET v4”的论文,这是 AF_XDP 的首次尝试。自那时以来,几乎所有内容都发生了变化。Jonathan Corbet 还在 LWN 上撰写了一篇优秀的文章“使用 AF_XDP 加速网络”。可以在 https://lwn.net/Articles/750845/ 找到。

UMEM

UMEM 是虚拟连续内存区域,分为大小相等的帧。UMEM 与 netdev 以及该 netdev 的特定队列 ID 相关联。它通过使用 XDP_UMEM_REG setsockopt 系统调用创建和配置(块大小、headroom、起始地址和大小)。通过 bind() 系统调用将 UMEM 绑定到 netdev 和队列 ID。

AF_XDP 套接字链接到单个 UMEM,但一个 UMEM 可以有多个 AF_XDP 套接字。为了共享通过一个套接字 A 创建的 UMEM,下一个套接字 B 可以通过在 struct sockaddr_xdp 成员 sxdp_flags 中设置 XDP_SHARED_UMEM 标志,并将 A 的文件描述符传递给 struct sockaddr_xdp 成员 sxdp_shared_umem_fd 来实现。

UMEM 有两个单生产者/单消费者环,用于在内核和用户空间应用程序之间传输 UMEM 帧的所有权。

有四种不同类型的环:FILL、COMPLETION、RX 和 TX。所有环都是单生产者/单消费者,因此用户空间应用程序需要显式同步多个进程/线程才能读取/写入它们。

UMEM 使用两个环:FILL 和 COMPLETION。与 UMEM 关联的每个套接字必须有一个 RX 队列、TX 队列或两者都有。假设有一个包含四个套接字的设置(所有套接字都执行 TX 和 RX)。那么将有一个 FILL 环、一个 COMPLETION 环、四个 TX 环和四个 RX 环。

这些环是基于头(生产者)/尾(消费者)的环。生产者在 struct xdp_ring 生产者成员指向的索引处写入数据环,并增加生产者索引。消费者在 struct xdp_ring 消费者成员指向的索引处读取数据环,并增加消费者索引。

这些环通过 _RING setsockopt 系统调用配置和创建,并使用适当的 mmap() 偏移量映射到用户空间(XDP_PGOFF_RX_RING、XDP_PGOFF_TX_RING、XDP_UMEM_PGOFF_FILL_RING 和 XDP_UMEM_PGOFF_COMPLETION_RING)。

环的大小需要是 2 的幂。

UMEM 填充环

FILL 环用于将 UMEM 帧的所有权从用户空间传输到内核空间。UMEM addr 在环中传递。例如,如果 UMEM 为 64k,每个块为 4k,那么 UMEM 有 16 个块,可以传递 0 到 64k 之间的 addr。

传递给内核的帧用于入口路径(RX 环)。

用户应用程序将 UMEM addr 生成到此环。请注意,如果在对齐的块模式下运行应用程序,内核将屏蔽传入的 addr。例如,对于 2k 的块大小,addr 的 log2(2048) LSB 将被屏蔽,这意味着 2048、2050 和 3000 指的是同一个块。如果用户应用程序在未对齐的块模式下运行,则传入的 addr 将保持不变。

UMEM 完成环

COMPLETION 环用于将 UMEM 帧的所有权从内核空间传输到用户空间。就像 FILL 环一样,使用 UMEM 索引。

从内核传递到用户空间的帧是已发送(TX 环)的帧,可以再次被用户空间使用。

用户应用程序从此环中消耗 UMEM addr。

RX 环

RX 环是套接字的接收端。环中的每个条目都是一个 struct xdp_desc 描述符。该描述符包含 UMEM 偏移量 (addr) 和数据长度 (len)。

如果没有通过 FILL 环将帧传递给内核,则 RX 环上不会(或不能)出现任何描述符。

用户应用程序从此环中消耗 struct xdp_desc 描述符。

TX 环

TX 环用于发送帧。struct xdp_desc 描述符被填充(索引、长度和偏移量)并传递到环中。

要启动传输,需要 sendmsg() 系统调用。将来可能会放宽此要求。

用户应用程序将 struct xdp_desc 描述符生成到此环。

Libbpf

Libbpf 是 eBPF 和 XDP 的辅助库,它使使用这些技术变得更加简单。它还在 tools/lib/bpf/xsk.h 中包含特定的辅助函数,用于促进 AF_XDP 的使用。它包含两种类型的函数:一种可以用于简化 AF_XDP 套接字的设置,另一种可以在数据平面中使用,以安全快速地访问环。要查看如何使用此 API 的示例,请查看 samples/bpf/xdpsock_usr.c 中的示例应用程序,该应用程序使用 libbpf 进行设置和数据平面操作。

我们建议您使用此库,除非您已成为高级用户。它将使您的程序简单得多。

XSKMAP / BPF_MAP_TYPE_XSKMAP

在 XDP 方面,有一个 BPF 映射类型 BPF_MAP_TYPE_XSKMAP (XSKMAP),它与 bpf_redirect_map() 结合使用,将入口帧传递给套接字。

用户应用程序通过 bpf() 系统调用将套接字插入到映射中。

请注意,如果 XDP 程序尝试重定向到与队列配置和 netdev 不匹配的套接字,则该帧将被丢弃。例如,AF_XDP 套接字绑定到 netdev eth0 和队列 17。只有为 eth0 和队列 17 执行的 XDP 程序才能成功地将数据传递到套接字。有关示例,请参阅示例应用程序 (samples/bpf/)。

配置标志和套接字选项

以下是可以用来控制和监视 AF_XDP 套接字行为的各种配置标志。

XDP_COPY 和 XDP_ZEROCOPY 绑定标志

当您绑定到套接字时,内核将首先尝试使用零拷贝。如果不支持零拷贝,它将回退到使用拷贝模式,即将所有数据包复制到用户空间。但是,如果您想强制使用某种模式,可以使用以下标志。如果您将 XDP_COPY 标志传递给 bind 调用,内核将强制套接字进入拷贝模式。如果它不能使用拷贝模式,则 bind 调用将失败并显示错误。相反,XDP_ZEROCOPY 标志将强制套接字进入零拷贝模式或失败。

XDP_SHARED_UMEM 绑定标志

此标志使您可以将多个套接字绑定到同一个 UMEM。它适用于同一队列 ID、不同队列 ID 之间以及 netdev/设备之间。在这种模式下,每个套接字都有自己的 RX 和 TX 环,但您将拥有一对或多对 FILL 和 COMPLETION 环。您必须为绑定到的每个唯一的 netdev 和队列 ID 元组创建一个这样的对。

首先从我们想要在绑定到同一 netdev 和队列 ID 的套接字之间共享 UMEM 的情况开始。UMEM(绑定到创建的第一个套接字)将只有一个 FILL 环和一个 COMPLETION 环,因为我们只绑定到一个唯一的 netdev,queue_id 元组。要使用此模式,请创建第一个套接字并以正常方式绑定它。创建第二个套接字,并创建一个 RX 和一个 TX 环,或者至少创建一个环,但不要创建 FILL 或 COMPLETION 环,因为将使用第一个套接字的环。在 bind 调用中,设置 XDP_SHARED_UMEM 选项,并在 sxdp_shared_umem_fd 字段中提供初始套接字的文件描述符。您可以通过这种方式附加任意数量的额外套接字。

那么数据包将到达哪个套接字呢?这由 XDP 程序决定。将所有套接字放入 XSK_MAP 中,并指示您想要将每个数据包发送到数组中的哪个索引。下面显示了一个简单的轮询数据包分发示例

#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 环,并且它们是单生产者、单消费者环,因此您需要确保多个进程或线程不同时使用这些环。在 libbpf 代码中没有任何同步原语可以在此时保护多个用户。

如果您创建多个绑定到同一 UMEM 的套接字,Libbpf 将使用此模式。但是,请注意,您需要使用 XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD libbpf_flag 和 xsk_socket__create 调用,并加载您自己的 XDP 程序,因为 libbpf 中没有内置的程序可以为您路由流量。

第二种情况是当您在绑定到不同队列 ID 和/或 netdev 的套接字之间共享 UMEM 时。在这种情况下,您必须为每个唯一的 netdev,queue_id 对创建一个 FILL 环和一个 COMPLETION 环。假设您想要在同一 netdev 上的两个不同的队列 ID 上创建两个套接字。以正常方式创建并绑定第一个套接字。创建第二个套接字,并为此套接字创建一个 RX 和一个 TX 环,或者至少创建一个环,然后创建一个 FILL 和 COMPLETION 环。然后在 bind 调用中,设置 XDP_SHARED_UMEM 选项,并在 sxdp_shared_umem_fd 字段中提供初始套接字的文件描述符,因为您已在该套接字上注册了 UMEM。这两个套接字现在将共享同一个 UMEM。

不需要像前一种情况那样提供 XDP 程序,即套接字绑定到同一队列 ID 和设备。相反,请使用 NIC 的数据包转向功能将数据包转向到正确的队列。在前面的示例中,套接字之间只共享一个队列,因此 NIC 无法执行此转向。它只能在队列之间转向。

在 libbpf 中,您需要使用 xsk_socket__create_shared() API,因为它需要引用将为您创建并绑定到共享 UMEM 的 FILL 环和 COMPLETION 环。您可以将此函数用于您创建的所有套接字,或者您可以将其用于第二个及其后面的套接字,并对第一个套接字使用 xsk_socket__create()。两种方法都会产生相同的结果。

请注意,UMEM 可以在同一队列 ID 和设备上的套接字之间共享,也可以在同一设备上的队列之间共享,也可以同时在设备之间共享。

XDP_USE_NEED_WAKEUP 绑定标志

此选项添加了对一个名为 need_wakeup 的新标志的支持,该标志存在于 FILL 环和 TX 环中,用户空间是这些环的生产者。如果在 bind 调用中设置了此选项,则如果内核需要通过系统调用显式唤醒才能继续处理数据包,则将设置 need_wakeup 标志。如果该标志为零,则不需要系统调用。

如果在 FILL 环上设置了该标志,则应用程序需要调用 poll() 才能继续在 RX 环上接收数据包。例如,当内核检测到 FILL 环上没有更多缓冲区,并且 NIC 的 RX HW 环上没有剩余缓冲区时,可能会发生这种情况。在这种情况下,中断被关闭,因为 NIC 无法接收任何数据包(因为没有缓冲区可以放置它们),并且设置了 need_wakeup 标志,以便用户空间可以将缓冲区放置在 FILL 环上,然后调用 poll(),以便内核驱动程序可以将这些缓冲区放置在 HW 环上并开始接收数据包。

如果为 TX 环设置了该标志,则意味着应用程序需要显式通知内核发送放置在 TX 环上的任何数据包。可以通过 poll() 调用(如在 RX 路径中)或通过调用 sendto() 来完成此操作。

在 samples/bpf/xdpsock_user.c 中可以找到如何使用此标志的示例。使用 libbpf 帮助程序的示例对于 TX 路径如下所示

if (xsk_ring_prod__needs_wakeup(&my_tx_ring))
    sendto(xsk_socket__fd(xsk_handle), NULL, 0, MSG_DONTWAIT, NULL, 0);

即,仅在设置了该标志时才使用系统调用。

我们建议您始终启用此模式,因为它通常会导致更好的性能,尤其是如果您在同一核心上运行应用程序和驱动程序,而且如果您为应用程序和内核驱动程序使用不同的核心,因为它减少了 TX 路径所需的系统调用数量。

XDP_{RX|TX|UMEM_FILL|UMEM_COMPLETION}_RING setsockopts

这些 setsockopts 分别设置 RX、TX、FILL 和 COMPLETION 环应具有的描述符数量。必须设置至少一个 RX 和 TX 环的大小。如果两者都设置了,您将能够从应用程序接收和发送流量,但如果您只想执行其中一项操作,则可以通过仅设置其中一个来节省资源。FILL 环和 COMPLETION 环都是必需的,因为您需要将 UMEM 绑定到您的套接字。但是,如果使用了 XDP_SHARED_UMEM 标志,则第一个套接字之后的任何套接字都没有 UMEM,因此在这种情况下不应创建任何 FILL 或 COMPLETION 环,因为将使用共享 UMEM 的环。请注意,环是单生产者单消费者,因此不要尝试同时从多个进程访问它们。请参阅 XDP_SHARED_UMEM 部分。

在 libbpf 中,您可以通过分别向 xsk_socket__create 函数的 rx 和 tx 参数提供 NULL 来创建仅 Rx 和仅 Tx 的套接字。

如果您创建了一个仅 Tx 的套接字,我们建议您不要将任何数据包放置在填充环上。如果您这样做,驱动程序可能会认为您将接收某些内容,但实际上不会,这可能会对性能产生负面影响。

XDP_UMEM_REG setsockopt

此 setsockopt 将 UMEM 注册到套接字。这是包含数据包可以驻留的所有缓冲区的区域。该调用需要一个指向此区域开头的指针以及其大小。此外,它还具有一个名为 chunk_size 的参数,该参数是 UMEM 分成的块的大小。目前它只能是 2K 或 4K。如果您有一个 128K 的 UMEM 区域和一个 2K 的块大小,这意味着您最多可以在 UMEM 区域中保存 128K / 2K = 64 个数据包,并且您的最大数据包大小可以是 2K。

还可以设置 UMEM 中每个缓冲区的 headroom。如果将其设置为 N 字节,则意味着数据包将在缓冲区的 N 字节处开始,从而为应用程序留下前 N 字节。最后一个选项是 flags 字段,但将在每个 UMEM 标志的单独部分中处理。

SO_BINDTODEVICE setsockopt

这是一个通用的 SOL_SOCKET 选项,可用于将 AF_XDP 套接字绑定到特定的网络接口。当套接字由特权进程创建并传递给非特权进程时,此选项非常有用。设置该选项后,内核将拒绝尝试将该套接字绑定到不同的接口。更新该值需要 CAP_NET_RAW。

XDP_STATISTICS getsockopt

获取套接字的丢包统计信息,这对于调试目的非常有用。支持的统计信息如下所示

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 套接字获取选项。目前唯一支持的选项是 XDP_OPTIONS_ZEROCOPY,它告诉您零拷贝是否已启用。

多缓冲区支持

通过多缓冲区支持,使用 AF_XDP 套接字的程序可以在拷贝和零拷贝模式下接收和发送由多个缓冲区组成的数据包。例如,一个数据包可以由两个帧/缓冲区组成,一个包含标头,另一个包含数据,或者可以通过将三个 4K 帧链接在一起来构造一个 9K 以太网巨型帧。

一些定义

  • 一个数据包由一个或多个帧组成

  • AF_XDP 环中的一个描述符始终引用一个帧。如果数据包由单个帧组成,则该描述符引用整个数据包。

要为 AF_XDP 套接字启用多缓冲区支持,请使用新的绑定标志 XDP_USE_SG。如果未提供此标志,则所有多缓冲区数据包将像以前一样被丢弃。请注意,加载的 XDP 程序也需要处于多缓冲区模式。这可以通过使用“xdp.frags”作为所使用的 XDP 程序的节名称来完成。

为了表示由多个帧组成的数据包,在 Rx 和 Tx 描述符的 options 字段中引入了一个名为 XDP_PKT_CONTD 的新标志。如果为真 (1),则数据包继续到下一个描述符;如果为假 (0),则意味着这是数据包的最后一个描述符。为什么与许多 NIC 中找到的数据包结束 (eop) 标志的逻辑相反?只是为了保持与非多缓冲区应用程序的兼容性,这些应用程序在 Rx 上将此位设置为 false,并且应用程序将 Tx 的 options 字段设置为零,因为任何其他值都将被视为无效描述符。

以下是用于将由多个帧组成的数据包生成到 AF_XDP Tx 环上的语义

  • 当找到无效描述符时,此数据包的所有其他描述符/帧都将被标记为无效且未完成。下一个描述符被视为新数据包的开始,即使这并不是本意(因为我们无法猜测意图)。与以前一样,如果您的程序正在生成无效描述符,则说明您有一个必须修复的错误。

  • 零长度描述符被视为无效描述符。

  • 对于拷贝模式,数据包中支持的最大帧数等于 CONFIG_MAX_SKB_FRAGS + 1。如果超过此限制,则到目前为止累积的所有描述符都将被丢弃并被视为无效。要生成一个可以在任何系统上工作的应用程序,无论此配置设置如何,请将 frags 的数量限制为 18,因为该配置的最小值是 17。

  • 对于零拷贝模式,限制取决于 NIC HW 支持的限制。通常至少是我们检查过的 NIC 上的五个。我们有意选择不对零拷贝模式强制执行严格的限制(例如 CONFIG_MAX_SKB_FRAGS + 1),因为这会导致在底层执行拷贝操作以适应 NIC 支持的限制。这有点违背了零拷贝模式的目的。如何在“探测多缓冲区支持”部分中解释了如何探测此限制。

在拷贝模式的 Rx 路径中,xsk 核心会根据需要将 XDP 数据复制到多个描述符中,并如前所述设置 XDP_PKT_CONTD 标志。零拷贝模式的工作方式相同,只是数据未被复制。当应用程序获取一个 XDP_PKT_CONTD 标志设置为 1 的描述符时,它意味着数据包由多个缓冲区组成,并且它在下一个描述符中继续到下一个缓冲区。当收到一个 XDP_PKT_CONTD == 0 的描述符时,它意味着这是数据包的最后一个缓冲区。AF_XDP 保证只有完整的数据包(数据包中的所有帧)才会被发送到应用程序。如果 AF_XDP Rx 环中没有足够的空间,则数据包的所有帧都将被丢弃。

如果应用程序读取一批描述符,例如使用 libxdp 接口,则不能保证该批次将以完整的数据包结束。它可能会在一个数据包的中间结束,并且该数据包的其余缓冲区将在下一批次的开头到达,因为 libxdp 接口不会读取整个环(除非您有一个巨大的批次大小或一个非常小的环大小)。

可以在本文档后面找到 Rx 和 Tx 多缓冲区支持的示例程序。

用法

为了使用 AF_XDP 套接字,需要两个部分。用户空间应用程序和 XDP 程序。有关完整的设置和用法示例,请参阅示例应用程序。用户空间端是 xdpsock_user.c,XDP 端是 libbpf 的一部分。

tools/lib/bpf/xsk.c 中包含的 XDP 代码示例如下

SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
{
    int index = ctx->rx_queue_index;

    // A set entry here means that the corresponding queue_id
    // has an active AF_XDP socket bound to it.
    if (bpf_map_lookup_elem(&xsks_map, &index))
        return bpf_redirect_map(&xsks_map, index, 0);

    return XDP_PASS;
}

一个简单但性能不佳的环出队和入队可能如下所示

// struct xdp_rxtx_ring {
//     __u32 *producer;
//     __u32 *consumer;
//     struct xdp_desc *desc;
// };

// struct xdp_umem_ring {
//     __u32 *producer;
//     __u32 *consumer;
//     __u64 *desc;
// };

// typedef struct xdp_rxtx_ring RING;
// typedef struct xdp_umem_ring RING;

// typedef struct xdp_desc RING_TYPE;
// typedef __u64 RING_TYPE;

int dequeue_one(RING *ring, RING_TYPE *item)
{
    __u32 entries = *ring->producer - *ring->consumer;

    if (entries == 0)
        return -1;

    // read-barrier!

    *item = ring->desc[*ring->consumer & (RING_SIZE - 1)];
    (*ring->consumer)++;
    return 0;
}

int enqueue_one(RING *ring, const RING_TYPE *item)
{
    u32 free_entries = RING_SIZE - (*ring->producer - *ring->consumer);

    if (free_entries == 0)
        return -1;

    ring->desc[*ring->producer & (RING_SIZE - 1)] = *item;

    // write-barrier!

    (*ring->producer)++;
    return 0;
}

但是请使用 libbpf 函数,因为它们经过优化且可以使用。这将使您的生活更轻松。

用法多缓冲区 Rx

这是一个简单的 Rx 路径伪代码示例(为简单起见,使用 libxdp 接口)。为了保持简短,已排除错误路径

void rx_packets(struct xsk_socket_info *xsk)
{
    static bool new_packet = true;
    u32 idx_rx = 0, idx_fq = 0;
    static char *pkt;

    int rcvd = xsk_ring_cons__peek(&xsk->rx, opt_batch_size, &idx_rx);

    xsk_ring_prod__reserve(&xsk->umem->fq, rcvd, &idx_fq);

    for (int i = 0; i < rcvd; i++) {
        struct xdp_desc *desc = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx++);
        char *frag = xsk_umem__get_data(xsk->umem->buffer, desc->addr);
        bool eop = !(desc->options & XDP_PKT_CONTD);

        if (new_packet)
            pkt = frag;
        else
            add_frag_to_pkt(pkt, frag);

        if (eop)
            process_pkt(pkt);

        new_packet = eop;

        *xsk_ring_prod__fill_addr(&xsk->umem->fq, idx_fq++) = desc->addr;
    }

    xsk_ring_prod__submit(&xsk->umem->fq, rcvd);
    xsk_ring_cons__release(&xsk->rx, rcvd);
}

用法多缓冲区 Tx

这是一个 Tx 路径伪代码示例(为简单起见,使用 libxdp 接口),忽略了 umem 的大小是有限的,并且我们最终会耗尽要发送的数据包。还假设 pkts.addr 指向 umem 中的有效位置。

void tx_packets(struct xsk_socket_info *xsk, struct pkt *pkts,
                int batch_size)
{
    u32 idx, i, pkt_nb = 0;

    xsk_ring_prod__reserve(&xsk->tx, batch_size, &idx);

    for (i = 0; i < batch_size;) {
        u64 addr = pkts[pkt_nb].addr;
        u32 len = pkts[pkt_nb].size;

        do {
            struct xdp_desc *tx_desc;

            tx_desc = xsk_ring_prod__tx_desc(&xsk->tx, idx + i++);
            tx_desc->addr = addr;

            if (len > xsk_frame_size) {
                tx_desc->len = xsk_frame_size;
                tx_desc->options = XDP_PKT_CONTD;
            } else {
                tx_desc->len = len;
                tx_desc->options = 0;
                pkt_nb++;
            }
            len -= tx_desc->len;
            addr += xsk_frame_size;

            if (i == batch_size) {
                /* Remember len, addr, pkt_nb for next iteration.
                 * Skipped for simplicity.
                 */
                break;
            }
        } while (len);
    }

    xsk_ring_prod__submit(&xsk->tx, i);
}

探测多缓冲区支持

要发现驱动程序是否在 SKB 或 DRV 模式下支持多缓冲区 AF_XDP,请使用 linux/netdev.h 中 netlink 的 XDP_FEATURES 功能来查询 NETDEV_XDP_ACT_RX_SG 支持。这与查询 XDP 多缓冲区支持的标志相同。如果 XDP 在驱动程序中支持多缓冲区,则 AF_XDP 也将在 SKB 和 DRV 模式下支持该多缓冲区。

要发现驱动程序是否在零拷贝模式下支持多缓冲区 AF_XDP,请使用 XDP_FEATURES 并首先检查 NETDEV_XDP_ACT_XSK_ZEROCOPY 标志。如果设置了该标志,则意味着至少支持零拷贝,您应该检查 linux/netdev.h 中的 netlink 属性 NETDEV_A_DEV_XDP_ZC_MAX_SEGS。将返回一个无符号整数值,说明此设备在零拷贝模式下支持的最大 frags 数量。以下是可能的返回值

1:此设备不支持零拷贝的多缓冲区,因为 max

支持一个片段意味着不可能进行多缓冲区。

>=2:此设备支持零拷贝模式下的多缓冲区。该

返回的数字表示支持的最大 frags 数量。

有关如何通过 libbpf 使用这些信息的示例,请查看 tools/testing/selftests/bpf/xskxceiver.c。

零拷贝驱动程序的多缓冲区支持

零拷贝驱动程序通常使用批处理 API 进行 Rx 和 Tx 处理。请注意,Tx 批处理 API 保证它将提供一批 Tx 描述符,这些描述符以末尾的完整数据包结束。这是为了方便使用多缓冲区支持扩展零拷贝驱动程序。

示例应用程序

包含一个xdpsock 基准测试/测试应用程序,演示了如何将 AF_XDP 套接字与私有 UMEM 结合使用。假设您希望来自端口 4242 的 UDP 流量最终进入队列 16,我们将在该队列上启用 AF_XDP。在这里,我们使用 ethtool 来实现这一点

ethtool -N p3p2 rx-flow-hash udp4 fn
ethtool -N p3p2 flow-type udp4 src-port 4242 dst-port 4242 \
    action 16

然后可以使用以下命令在 XDP_DRV 模式下运行 rxdrop 基准测试

samples/bpf/xdpsock -i p3p2 -q 16 -r -N

对于 XDP_SKB 模式,请使用开关“-S”代替“-N”,并且所有选项都可以像往常一样使用“-h”显示。

此示例应用程序使用 libbpf 来简化 AF_XDP 的设置和使用。如果您想了解 AF_XDP 的原始 uapi 实际上是如何用于实现更高级的功能的,请查看 tools/lib/bpf/xsk.[ch] 中的 libbpf 代码。

常见问题解答

问:我在套接字上看不到任何流量。我做错了什么?

答:当物理网卡的 netdev 初始化时,Linux 通常

每个核心分配一个 RX 和 TX 队列对。因此,在 8 核系统上,将分配队列 ID 0 到 7,每个核心一个。在 AF_XDP bind 调用或 xsk_socket__create libbpf 函数调用中,您指定要绑定的特定队列 ID,并且您将在套接字上获得的只是指向该队列的流量。因此,在上面的示例中,如果您绑定到队列 0,您将不会获得任何分配到队列 1 到 7 的流量。如果您幸运的话,您会看到流量,但通常它会最终出现在您未绑定的队列之一上。

有很多方法可以解决将您想要的流量发送到您绑定的队列 ID 的问题。如果您想查看所有流量,您可以强制 netdev 仅具有 1 个队列,队列 ID 0,然后绑定到队列 0。您可以使用 ethtool 来执行此操作

sudo ethtool -L <interface> combined 1

如果您只想查看部分流量,您可以编程 NIC 通过 ethtool 过滤流量到您可以绑定 XDP 套接字的单个队列 ID。这是一个示例,其中往返端口 4242 的 UDP 流量被发送到队列 2

sudo ethtool -N <interface> rx-flow-hash udp4 fn
sudo ethtool -N <interface> flow-type udp4 src-port 4242 dst-port \
4242 action 2

其他许多方法都是可能的,所有这些都取决于您拥有的 NIC 的功能。

问:我可以使用 XSKMAP 在不同的 umem 之间实现切换

在复制模式下吗?

答:简短的回答是不,目前不支持。该

XSKMAP 只能用于将队列 ID X 上传入的流量切换到绑定到相同队列 ID X 的套接字。XSKMAP 可以包含绑定到不同队列 ID 的套接字,例如 X 和 Y,但只有从队列 ID Y 传入的流量才能被定向到绑定到相同队列 ID Y 的套接字。在零拷贝模式下,您应该使用 NIC 中的交换机或其他分配机制来将流量定向到正确的队列 ID 和套接字。

问:我的数据包有时会损坏。怎么回事?

答:必须注意不要将 UMEM 中的同一缓冲区馈送到

同时进入多个环。例如,如果您同时将同一个缓冲区馈送到 FILL 环和 TX 环,则 NIC 可能会在发送数据的同时接收数据到缓冲区中。这将导致某些数据包损坏。同样的事情也适用于将同一个缓冲区馈送到属于不同队列 ID 或使用 XDP_SHARED_UMEM 标志绑定的 netdev 的 FILL 环中。

鸣谢

  • Björn Töpel (AF_XDP 核心)

  • Magnus Karlsson (AF_XDP 核心)

  • Alexander Duyck

  • Alexei Starovoitov

  • Daniel Borkmann

  • Jesper Dangaard Brouer

  • John Fastabend

  • Jonathan Corbet (LWN 报道)

  • Michael S. Tsirkin

  • Qi Z Zhang

  • Willem de Bruijn