AF_XDP

注意:这是一篇翻译的文章,翻译不准确的地方请参考原文

概述

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

本文假设读者熟悉 BPF 和 XDP。如果不了解,可以参考 Cilium 项目提供的优秀指南:http://cilium.readthedocs.io/en/latest/bpf/

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

一个 AF_XDP 套接字(XSK)通过普通的 socket() 系统调用创建。每个 XSK 关联两个环:RX 环和 TX 环。套接字可以在 RX 环上接收数据包,也可以在 TX 环上发送数据包。这些环通过 setsockopt 的 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,直到 bind 完成后数据流才开始流动。

如果需要,UMEM 可以在进程间共享。如果一个进程希望这样做,它只需跳过 UMEM 及其对应两个环的注册,在 bind 调用中设置 XDP_SHARED_UMEM 标志,并提交希望共享 UMEM 的进程的 XSK 以及自己新创建的 XSK 套接字。新进程将会在其自己的 RX 环中接收指向共享 UMEM 的帧地址。由于环结构是单消费者/单生产者(出于性能考虑),新进程必须创建自己的套接字及关联的 RX 和 TX 环,不能与其他进程共享。这也是每个 UMEM 只有一组 FILL 和 COMPLETION 环的原因。处理 UMEM 是单个进程的责任。

那么,如何将数据包从 XDP 程序分配到 XSK?有一个称为 XSKMAP(或全称 BPF_MAP_TYPE_XSKMAP)的 BPF 映射。用户空间应用程序可以将 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)及其特定的队列 ID。通过使用 XDP_UMEM_REG setsockopt 系统调用来创建和配置 UMEM(包括块大小、头部空间、起始地址和大小)。UMEM 通过 bind() 系统调用绑定到一个网络设备和队列 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)映射到用户空间。

环的大小必须是二的幂次方。

UMEM Fill 环

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

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

用户应用程序将 UMEM 地址生成到这个环中。需要注意的是,如果在对齐块模式(aligned chunk mode)下运行应用程序,内核将对传入地址进行掩码操作。例如,对于块大小为 2k 的情况,地址最低有效位(LSB)的 log ⁡ 2 ( 2048 ) \log_{2} {(2048)} log2(2048) 位将被屏蔽,这意味着 2048、2050 和 3000 指向同一个块。如果用户应用程序在非对齐块模式下运行,那么传入地址将保持不变。

UMEM Completion 环

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() 系统调用将套接字插入 Map 中。

请注意,如果 XDP 程序尝试重定向到与队列配置和网络设备不匹配的套接字,帧将被丢弃。例如,一个 AF_XDP 套接字绑定到 netdev 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 环对。你需要为每个唯一的 netdev 和队列 ID 元组创建一个这样的环对。

假设我们希望在绑定到相同 netdev 和队列 ID 的套接字之间共享 UMEM。由于我们只绑定到一个唯一的 netdev,queue_id 元组,UMEM(与第一个创建的套接字相关联)将只有一个 FILL 环和一个 COMPLETION 环。要使用这种模式,首先以正常方式创建第一个套接字并进行绑定。接下来创建第二个套接字,并创建一个 RX 和一个 TX 环,或至少其中一个,但不要创建 FILL 或 COMPLETION 环,因为将使用第一个套接字的环。在绑定调用中,设置 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_socket__create 调用中提供 XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD libbpf_flag,并加载你自己的 XDP 程序,因为 libbpf 中没有内置的程序来为你路由流量。

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

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

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

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

XDP_USE_NEED_WAKEUP 绑定标志

此选项增加了对 FILL 环和 TX 环中存在的 need_wakeup 新标志的支持,这些环是用户空间作为生产者的环。当在绑定调用中设置此选项时,如果内核需要通过系统调用显式唤醒以继续处理数据包,则 need_wakeup 标志将被设置。如果标志为零,则不需要系统调用。

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

如果 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 的套接字,我们建议你不要在 fill 环上放置任何数据包。如果你这样做,驱动程序可能会认为你要接收某些东西,而实际上你并不会,这可能会对性能产生负面影响。

XDP_UMEM_REG setsockopt

此 setsockopt 将一个 UMEM 注册到套接字。这是包含所有数据包缓冲区的区域。调用时需要提供该区域的起始指针及其大小。此外,还需要提供一个名为 chunk_size 的参数,这是 UMEM 分割的大小。目前,它只能是 2K 或 4K。如果你有一个 128K 的 UMEM 区域和一个 2K 的块大小,这意味着你可以在 UMEM 区域中容纳最多 128K / 2K = 64 个数据包,并且你的最大数据包大小可以是 2K。

还可以选择设置每个 UMEM 缓冲区的头部空间。如果你将其设置为 N 字节,这意味着数据包将从缓冲区的 N 字节处开始,前 N 字节留给应用程序使用。最后一个选项是标志字段,但它将在每个 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 描述符的选项字段中引入了一个新标志 XDP_PKT_CONTD。如果为真(1),则数据包在下一个描述符中继续;如果为假(0),则表示这是数据包的最后一个描述符。为什么采用与许多 NIC 中的结束数据包(eop)标志相反的逻辑?仅为了与非多缓冲应用程序兼容,这些应用程序在 Rx 上将此位设置为假,对于 Tx,应用程序将选项字段设置为零,否则会被视为无效描述符。

以下是将多个帧组成的数据包放入 AF_XDP Tx 环的语义:

  • 当发现无效描述符时,该数据包的所有其他描述符/帧都将标记为无效且未完成。即使这不是预期,下一描述符也被视为新数据包的开始(因为我们无法猜测意图)。与之前一样,如果程序生成无效描述符,则必须修复错误。
  • 零长度描述符被视为无效描述符。
  • 对于拷贝模式,数据包中支持的最大帧数等于 CONFIG_MAX_SKB_FRAGS + 1。如果超过此限制,累积到目前为止的所有描述符将被丢弃并视为无效。为了生成在任何系统上都能工作的应用程序,无论配置设置如何,请将碎片数限制为 18,因为配置的最小值为 17。
  • 对于零拷贝模式,限制取决于 NIC 硬件的支持。在我们检查的 NIC 上,通常至少支持五个帧。我们有意选择不对零拷贝模式强制执行严格限制(如 CONFIG_MAX_SKB_FRAGS + 1),因为这会导致在幕后进行拷贝操作,以符合 NIC 支持的限制。这有违零拷贝模式的目的。如何探测此限制将在“探测多缓冲支持”部分中解释。

在拷贝模式的 Rx 路径中,xsk 核心将 XDP 数据复制到多个描述符中(如果需要),并如前所述设置 XDP_PKT_CONTD 标志。零拷贝模式的工作方式相同,尽管数据没有被复制。当应用程序获取到带有 XDP_PKT_CONTD 标志的描述符时,意味着数据包由多个缓冲区组成,并在下一个描述符中的下一个缓冲区继续。当接收到 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,请使用 netlink 中的 XDP_FEATURES 功能(在 linux/netdev.h 中)查询 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。返回的无符号整数值将指示该设备在零拷贝模式下支持的最大碎片数。这些是可能的返回值:

1:该设备不支持零拷贝的多缓冲,因为最大支持一个碎片意味着多缓冲不可能。
≥2:该设备在零拷贝模式下支持多缓冲。返回的数字表示支持的最大碎片数。
有关如何通过 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 只有一个队列,即队列 ID 0,然后绑定到队列 0。你可以使用 ethtool 来实现:

sudo ethtool -L <interface> combined 1

如果你只想看到部分流量,可以通过 ethtool 编程 NIC,将你的流量过滤到你可以绑定 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 在拷贝模式下实现不同 umems 之间的切换吗?

答:简短的回答是否定的,目前不支持。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 环。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值