io_uring 零拷贝 Rx

简介

io_uring 零拷贝 Rx (ZC Rx) 是一种功能,它消除了网络接收路径上的内核到用户的复制,允许将数据包数据直接接收到用户空间内存中。此功能与 TCP_ZEROCOPY_RECEIVE 的不同之处在于,它没有严格的对齐要求,也不需要 mmap()/munmap()。 与内核旁路解决方案(例如 DPDK)相比,数据包标头由内核 TCP 协议栈像往常一样处理。

网卡硬件要求

io_uring ZC Rx 要工作,需要几个网卡硬件功能。目前,内核 API 不配置网卡,必须由用户完成。

报头/数据分离

需要将数据包在 L4 边界处拆分为报头和有效负载。报头像往常一样接收到内核内存中,并由 TCP 协议栈像往常一样处理。有效负载直接接收到用户空间内存中。

流控制

为该功能配置了特定的硬件 Rx 队列,但现代网卡通常将流分布在所有硬件 Rx 队列中。流控制是必需的,以确保只有所需的流被定向到为 io_uring ZC Rx 配置的硬件队列。

RSS

除了上面的流控制之外,还需要 RSS 将所有其他非零拷贝流从配置为 io_uring ZC Rx 的队列中移开。

用法

设置网卡

目前必须带外完成。

确保至少有两个队列

ethtool -L eth0 combined 2

启用报头/数据分离

ethtool -G eth0 tcp-data-split on

使用 RSS 划分一半的硬件 Rx 队列用于零拷贝

ethtool -X eth0 equal 1

设置流控制,请记住队列从 0 开始索引

ethtool -N eth0 flow-type tcp6 ... action 1

设置 io_uring

本节介绍底层 io_uring 内核 API。 有关如何使用更高级别 API 的信息,请参阅 liburing 文档。

使用以下必需的设置标志创建一个 io_uring 实例

IORING_SETUP_SINGLE_ISSUER
IORING_SETUP_DEFER_TASKRUN
IORING_SETUP_CQE32

创建内存区域

分配用户空间内存区域以接收零拷贝数据

void *area_ptr = mmap(NULL, area_size,
                      PROT_READ | PROT_WRITE,
                      MAP_ANONYMOUS | MAP_PRIVATE,
                      0, 0);

创建填充环

为用于返回已用缓冲区的共享环形缓冲区分配内存

void *ring_ptr = mmap(NULL, ring_size,
                      PROT_READ | PROT_WRITE,
                      MAP_ANONYMOUS | MAP_PRIVATE,
                      0, 0);

此填充环包含用于报头的一些空间,后跟一个 struct io_uring_zcrx_rqe 数组

size_t rq_entries = 4096;
size_t ring_size = rq_entries * sizeof(struct io_uring_zcrx_rqe) + PAGE_SIZE;
/* align to page size */
ring_size = (ring_size + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);

注册 ZC Rx

填写注册结构体

struct io_uring_zcrx_area_reg area_reg = {
  .addr = (__u64)(unsigned long)area_ptr,
  .len = area_size,
  .flags = 0,
};

struct io_uring_region_desc region_reg = {
  .user_addr = (__u64)(unsigned long)ring_ptr,
  .size = ring_size,
  .flags = IORING_MEM_REGION_TYPE_USER,
};

struct io_uring_zcrx_ifq_reg reg = {
  .if_idx = if_nametoindex("eth0"),
  /* this is the HW queue with desired flow steered into it */
  .if_rxq = 1,
  .rq_entries = rq_entries,
  .area_ptr = (__u64)(unsigned long)&area_reg,
  .region_ptr = (__u64)(unsigned long)&region_reg,
};

向内核注册

io_uring_register_ifq(ring, &reg);

映射填充环

内核在注册 struct io_uring_zcrx_ifq_reg 中填写填充环的字段。将其映射到用户空间

struct io_uring_zcrx_rq refill_ring;

refill_ring.khead = (unsigned *)((char *)ring_ptr + reg.offsets.head);
refill_ring.khead = (unsigned *)((char *)ring_ptr + reg.offsets.tail);
refill_ring.rqes =
  (struct io_uring_zcrx_rqe *)((char *)ring_ptr + reg.offsets.rqes);
refill_ring.rq_tail = 0;
refill_ring.ring_ptr = ring_ptr;

接收数据

准备一个零拷贝接收请求

struct io_uring_sqe *sqe;

sqe = io_uring_get_sqe(ring);
io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, fd, NULL, 0, 0);
sqe->ioprio |= IORING_RECV_MULTISHOT;

现在,提交并等待

io_uring_submit_and_wait(ring, 1);

最后,处理完成

struct io_uring_cqe *cqe;
unsigned int count = 0;
unsigned int head;

io_uring_for_each_cqe(ring, head, cqe) {
  struct io_uring_zcrx_cqe *rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);

  unsigned long mask = (1ULL << IORING_ZCRX_AREA_SHIFT) - 1;
  unsigned char *data = area_ptr + (rcqe->off & mask);
  /* do something with the data */

  count++;
}
io_uring_cq_advance(ring, count);

回收缓冲区

将缓冲区返回给内核以再次使用

struct io_uring_zcrx_rqe *rqe;
unsigned mask = refill_ring.ring_entries - 1;
rqe = &refill_ring.rqes[refill_ring.rq_tail & mask];

unsigned long area_offset = rcqe->off & ~IORING_ZCRX_AREA_MASK;
rqe->off = area_offset | area_reg.rq_area_token;
rqe->len = cqe->res;
IO_URING_WRITE_ONCE(*refill_ring.ktail, ++refill_ring.rq_tail);

测试

参见 tools/testing/selftests/drivers/net/hw/iou-zcrx.c