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

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

如果需要,可以在进程之间共享 UMEM。如果进程想要执行此操作,它只需跳过 UMEM 及其对应的两个环的注册,在 bind 调用中设置 XDP_SHARED_UMEM 标志,并提交它想要与之共享 UMEM 的进程的 XSK 以及它自己新创建的 XSK 套接字。然后,新进程将在其自己的 RX 环中接收指向此共享 UMEM 的帧地址引用。请注意,由于环结构是单消费者/单生产者(出于性能原因),因此新进程必须创建自己的带有相关 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 系统调用创建和配置的(块大小、头部空间、起始地址和大小)。UMEM 通过 bind() 系统调用绑定到 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 地址在环中传递。例如,如果 UMEM 为 64k,每个块为 4k,则 UMEM 有 16 个块,可以传递 0 到 64k 之间的地址。

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

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

UMEM 完成环

COMPLETION 环用于将 UMEM 帧的所有权从内核空间转移到用户空间。与 FILL 环一样,使用 UMEM 索引。

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

用户应用程序从此环消耗 UMEM 地址。

RX 环

RX 环是套接字的接收端。环中的每个条目都是一个 struct xdp_desc 描述符。描述符包含 UMEM 偏移量(地址)和数据长度 (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 程序尝试重定向到与队列配置和网络设备不匹配的套接字,则该帧将被丢弃。例如,AF_XDP 套接字绑定到网络设备 eth0 和队列 17。只有为 eth0 和队列 17 执行的 XDP 程序才能成功将数据传递到套接字。请参考示例应用程序 (samples/bpf/) 中的示例。

配置标志和套接字选项

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

XDP_COPY 和 XDP_ZEROCOPY 绑定标志

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

XDP_SHARED_UMEM 绑定标志

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

从我们希望在绑定到同一网络设备和队列 ID 的套接字之间共享 UMEM 的情况开始。UMEM(与创建的第一个套接字绑定)将只有一个 FILL 环和一个 COMPLETION 环,因为我们只绑定了一个唯一的 netdev,queue_id 元组。要使用此模式,请创建第一个套接字并以正常方式绑定它。创建第二个套接字并创建一个 RX 和一个 TX 环,或者至少其中一个,但不要创建 FILL 或 COMPLETION 环,因为将使用第一个套接字的环。在绑定调用中,设置 XDP_SHARED_UMEM 选项并在 sxdp_shared_umem_fd 字段中提供初始套接字的 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_socket__create 调用提供 XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD libbpf_flag 并加载您自己的 XDP 程序,因为 libbpf 中没有内置的程序来为您路由流量。

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

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

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

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

XDP_USE_NEED_WAKEUP 绑定标志

此选项为 FILL 环和 TX 环(用户空间是生产者的环)中存在的一个名为 need_wakeup 的新标志添加了支持。如果在绑定调用中设置此选项,如果内核需要通过系统调用显式唤醒以继续处理数据包,则将设置 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 注册到一个套接字。UMEM 是包含所有数据包可以驻留的缓冲区的区域。该调用接受指向此区域起始位置的指针和它的大小。此外,它还有一个名为 chunk_size 的参数,表示 UMEM 被划分成的块大小。目前,它只能是 2K 或 4K。如果你有一个 128K 的 UMEM 区域,并且块大小为 2K,这意味着你最多可以在 UMEM 区域中容纳 128K / 2K = 64 个数据包,并且你的最大数据包大小可以是 2K。

还有一个选项可以设置 UMEM 中每个单独缓冲区的头部空间。如果将其设置为 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 的新标志。如果为 true (1),则数据包将继续使用下一个描述符;如果为 false (0),则表示这是数据包的最后一个描述符。为什么采用与许多 NIC 中发现的数据包结束 (eop) 标志相反的逻辑?只是为了保持与非多缓冲区应用程序的兼容性,这些应用程序在 Rx 上为所有数据包将此位设置为 false,并且应用程序在 Tx 上将 options 字段设置为零,因为任何其他值都将被视为无效的描述符。

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

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

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

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

  • 对于零拷贝模式,限制取决于 NIC HW 支持的上限。通常,在我们检查过的 NIC 上至少为 5。我们有意选择不强制执行严格的限制(例如 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:此设备不支持零拷贝的多缓冲区,因为最大

支持一个分段意味着无法进行多缓冲区。

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

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

有关如何通过 libbpf 使用它们的示例,请参阅 tools/testing/selftests/bpf/xskxceiver.c。

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

零拷贝驱动程序通常使用批量 API 进行 Rx 和 Tx 处理。请注意,Tx 批量 API 保证它将提供一批 Tx 描述符,这些描述符在最后以完整的数据包结束。这有助于通过多缓冲区支持扩展零拷贝驱动程序。

示例应用程序

包含一个 xdpsock 基准测试/测试应用程序,演示了如何使用带有私有 UMEM 的 AF_XDP 套接字。假设你希望将来自端口 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 代码。

常见问题解答

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

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

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

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

sudo ethtool -L <interface> combined 1

如果您只想查看部分流量,您可以通过 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

根据您所拥有的网卡的功能,还有许多其他方法是可行的。

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

在复制模式下?

答:简短的答案是否定的,目前不支持这样做。

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

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

答:必须小心,不要同时将 UMEM 中的同一个缓冲区馈送到

多个环路中。例如,如果您同时将同一个缓冲区馈送到 FILL 环路和 TX 环路,则网卡可能会在发送数据的同时接收数据到该缓冲区。这将导致一些数据包损坏。同样的情况也适用于将同一个缓冲区馈送到属于不同队列 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