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)®ion_reg,
};
向内核注册
io_uring_register_ifq(ring, ®);
映射填充环¶
内核在注册 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