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