← Documents Documentation/networking/iou-zcrx.rst GitHub 원문 ↗

Linux 6.18.37 · Networking

io_uring zero copy Rx

NIC header split과 전용 Rx queue를 io_uring 사용자 memory/refill ring에 연결해 TCP payload를 복사 없이 받는 API를 설명합니다.

Source pathDocumentation/networking/iou-zcrx.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약·해설

iou-zcrx.rst:1-202

io_uring ZC Rx는 TCP header 처리는 커널에 남기고 payload DMA 목적지만 등록한 사용자 memory로 바꿉니다. 사용자는 NIC queue 격리, memory area와 refill ring 등록, multishot receive, buffer 반환을 모두 관리합니다.

ZC Rx 데이터와 buffer 순환
NIC 전용 Rx queueHeader -> kernel TCPPayload -> registered areaCQE
사용자 처리RQE refillkernel/NIC 재사용

수신과 재활용 경로를 함께 표시합니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 .. SPDX-License-Identifier: GPL-2.0
2
3 =====================
4 io_uring zero copy Rx
5 =====================
6
7 Introduction
8 ============
9
10 io_uring zero copy Rx (ZC Rx) is a feature that removes kernel-to-user copy on
11 the network receive path, allowing packet data to be received directly into
12 userspace memory. This feature is different to TCP_ZEROCOPY_RECEIVE in that
13 there are no strict alignment requirements and no need to mmap()/munmap().
14 Compared to kernel bypass solutions such as e.g. DPDK, the packet headers are
15 processed by the kernel TCP stack as normal.
16
17 NIC HW Requirements
18 ===================
19
20 Several NIC HW features are required for io_uring ZC Rx to work. For now the
21 kernel API does not configure the NIC and it must be done by the user.
22
23 Header/data split
24 -----------------
25
26 Required to split packets at the L4 boundary into a header and a payload.
27 Headers are received into kernel memory as normal and processed by the TCP
28 stack as normal. Payloads are received into userspace memory directly.
29
30 Flow steering
31 -------------
32
33 Specific HW Rx queues are configured for this feature, but modern NICs
34 typically distribute flows across all HW Rx queues. Flow steering is required
35 to ensure that only desired flows are directed towards HW queues that are
36 configured for io_uring ZC Rx.
37
38 RSS
39 ---
40
41 In addition to flow steering above, RSS is required to steer all other non-zero
42 copy flows away from queues that are configured for io_uring ZC Rx.
43
44 Usage
45 =====
46
47 Setup NIC
48 ---------
49
50 Must be done out of band for now.
51
52 Ensure there are at least two queues::
53
54 ethtool -L eth0 combined 2
55
56 Enable header/data split::
57
58 ethtool -G eth0 tcp-data-split on
59
60 Carve out half of the HW Rx queues for zero copy using RSS::
61
62 ethtool -X eth0 equal 1
63
64 Set up flow steering, bearing in mind that queues are 0-indexed::
65
66 ethtool -N eth0 flow-type tcp6 ... action 1
67
68 Setup io_uring
69 --------------
70
71 This section describes the low level io_uring kernel API. Please refer to
72 liburing documentation for how to use the higher level API.
73
74 Create an io_uring instance with the following required setup flags::
75
76 IORING_SETUP_SINGLE_ISSUER
77 IORING_SETUP_DEFER_TASKRUN
78 IORING_SETUP_CQE32 or IORING_SETUP_CQE_MIXED
79
80 Create memory area
81 ------------------
82
83 Allocate userspace memory area for receiving zero copy data::
84
85 void *area_ptr = mmap(NULL, area_size,
86 PROT_READ | PROT_WRITE,
87 MAP_ANONYMOUS | MAP_PRIVATE,
88 0, 0);
89
90 Create refill ring
91 ------------------
92
93 Allocate memory for a shared ringbuf used for returning consumed buffers::
94
95 void *ring_ptr = mmap(NULL, ring_size,
96 PROT_READ | PROT_WRITE,
97 MAP_ANONYMOUS | MAP_PRIVATE,
98 0, 0);
99
100 This refill ring consists of some space for the header, followed by an array of
101 ``struct io_uring_zcrx_rqe``::
102
103 size_t rq_entries = 4096;
104 size_t ring_size = rq_entries * sizeof(struct io_uring_zcrx_rqe) + PAGE_SIZE;
105 /* align to page size */
106 ring_size = (ring_size + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
107
108 Register ZC Rx
109 --------------
110
111 Fill in registration structs::
112
113 struct io_uring_zcrx_area_reg area_reg = {
114 .addr = (__u64)(unsigned long)area_ptr,
115 .len = area_size,
116 .flags = 0,
117 };
118
119 struct io_uring_region_desc region_reg = {
120 .user_addr = (__u64)(unsigned long)ring_ptr,
121 .size = ring_size,
122 .flags = IORING_MEM_REGION_TYPE_USER,
123 };
124
125 struct io_uring_zcrx_ifq_reg reg = {
126 .if_idx = if_nametoindex("eth0"),
127 /* this is the HW queue with desired flow steered into it */
128 .if_rxq = 1,
129 .rq_entries = rq_entries,
130 .area_ptr = (__u64)(unsigned long)&area_reg,
131 .region_ptr = (__u64)(unsigned long)&region_reg,
132 };
133
134 Register with kernel::
135
136 io_uring_register_ifq(ring, &reg);
137
138 Map refill ring
139 ---------------
140
141 The kernel fills in fields for the refill ring in the registration ``struct
142 io_uring_zcrx_ifq_reg``. Map it into userspace::
143
144 struct io_uring_zcrx_rq refill_ring;
145
146 refill_ring.khead = (unsigned *)((char *)ring_ptr + reg.offsets.head);
147 refill_ring.khead = (unsigned *)((char *)ring_ptr + reg.offsets.tail);
148 refill_ring.rqes =
149 (struct io_uring_zcrx_rqe *)((char *)ring_ptr + reg.offsets.rqes);
150 refill_ring.rq_tail = 0;
151 refill_ring.ring_ptr = ring_ptr;
152
153 Receiving data
154 --------------
155
156 Prepare a zero copy recv request::
157
158 struct io_uring_sqe *sqe;
159
160 sqe = io_uring_get_sqe(ring);
161 io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, fd, NULL, 0, 0);
162 sqe->ioprio |= IORING_RECV_MULTISHOT;
163
164 Now, submit and wait::
165
166 io_uring_submit_and_wait(ring, 1);
167
168 Finally, process completions::
169
170 struct io_uring_cqe *cqe;
171 unsigned int count = 0;
172 unsigned int head;
173
174 io_uring_for_each_cqe(ring, head, cqe) {
175 struct io_uring_zcrx_cqe *rcqe = (struct io_uring_zcrx_cqe *)(cqe + 1);
176
177 unsigned long mask = (1ULL << IORING_ZCRX_AREA_SHIFT) - 1;
178 unsigned char *data = area_ptr + (rcqe->off & mask);
179 /* do something with the data */
180
181 count++;
182 }
183 io_uring_cq_advance(ring, count);
184
185 Recycling buffers
186 -----------------
187
188 Return buffers back to the kernel to be used again::
189
190 struct io_uring_zcrx_rqe *rqe;
191 unsigned mask = refill_ring.ring_entries - 1;
192 rqe = &refill_ring.rqes[refill_ring.rq_tail & mask];
193
194 unsigned long area_offset = rcqe->off & ~IORING_ZCRX_AREA_MASK;
195 rqe->off = area_offset | area_reg.rq_area_token;
196 rqe->len = cqe->res;
197 IO_URING_WRITE_ONCE(*refill_ring.ktail, ++refill_ring.rq_tail);
198
199 Testing
200 =======
201
202 See ``tools/testing/selftests/drivers/net/hw/iou-zcrx.c``
203

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

소개

1-16

io_uring zero-copy Rx(ZC Rx)는 네트워크 수신 경로의 커널-사용자 복사를 없애 payload를 사용자 공간 memory로 직접 받습니다. `TCP_ZEROCOPY_RECEIVE`와 달리 엄격한 정렬 조건이 없고 `mmap()`/`munmap()`을 반복할 필요가 없습니다. DPDK 같은 kernel bypass와 달리 packet header는 정상적으로 커널 TCP stack에서 처리됩니다.

.. SPDX-License-Identifier: GPL-2.0

=====================
io_uring zero copy Rx
=====================

Introduction
============

io_uring zero copy Rx (ZC Rx) is a feature that removes kernel-to-user copy on
the network receive path, allowing packet data to be received directly into
userspace memory. This feature is different to TCP_ZEROCOPY_RECEIVE in that
there are no strict alignment requirements and no need to mmap()/munmap().
Compared to kernel bypass solutions such as e.g. DPDK, the packet headers are
processed by the kernel TCP stack as normal.

NIC 하드웨어 요구 사항

17-43

현재 커널 API는 NIC를 자동 설정하지 않으므로 사용자가 필요한 기능을 직접 켜야 합니다. Header/data split은 L4 경계에서 packet을 header와 payload로 나눕니다. header는 커널 memory로 들어가 TCP stack이 처리하고 payload만 사용자 memory로 직접 들어갑니다.

전용 hardware Rx queue에는 원하는 flow만 들어와야 하므로 flow steering이 필요합니다. 현대 NIC가 기본적으로 flow를 모든 Rx queue에 분산하므로, RSS로 일반 non-zero-copy flow를 ZC Rx queue 밖으로 보내야 합니다.

ZC Rx NIC 기능
기능역할
Header/data splitL4 header는 커널, payload는 사용자 memory로 분리
Flow steering대상 flow를 ZC Rx hardware queue로 보냄
RSS나머지 flow를 ZC Rx queue 밖으로 분산

각 기능이 전용 queue를 만드는 데 맡는 역할입니다.

NIC HW Requirements
===================

Several NIC HW features are required for io_uring ZC Rx to work. For now the
kernel API does not configure the NIC and it must be done by the user.

Header/data split
-----------------

Required to split packets at the L4 boundary into a header and a payload.
Headers are received into kernel memory as normal and processed by the TCP
stack as normal. Payloads are received into userspace memory directly.

Flow steering
-------------

Specific HW Rx queues are configured for this feature, but modern NICs
typically distribute flows across all HW Rx queues. Flow steering is required
to ensure that only desired flows are directed towards HW queues that are
configured for io_uring ZC Rx.

RSS
---

In addition to flow steering above, RSS is required to steer all other non-zero
copy flows away from queues that are configured for io_uring ZC Rx.

NIC 설정

44-67

NIC 설정은 현재 out-of-band로 수행합니다. `ethtool -L eth0 combined 2`로 queue를 최소 2개 확보하고, `ethtool -G eth0 tcp-data-split on`으로 header/data split을 켭니다.

`ethtool -X eth0 equal 1`은 hardware Rx queue 절반을 RSS에서 zero-copy용으로 분리합니다. queue 번호는 0부터 시작하므로 `ethtool -N eth0 flow-type tcp6 ... action 1`처럼 flow steering rule의 action을 대상 queue 1로 지정합니다.

Usage
=====

Setup NIC
---------

Must be done out of band for now.

Ensure there are at least two queues::

  ethtool -L eth0 combined 2

Enable header/data split::

  ethtool -G eth0 tcp-data-split on

Carve out half of the HW Rx queues for zero copy using RSS::

  ethtool -X eth0 equal 1

Set up flow steering, bearing in mind that queues are 0-indexed::

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

io_uring 설정

68-79

이 절은 저수준 io_uring 커널 API를 설명하며 고수준 API는 liburing 문서를 참고합니다. ring은 단일 제출자를 요구하는 `IORING_SETUP_SINGLE_ISSUER`, task 실행을 미루는 `IORING_SETUP_DEFER_TASKRUN`, 확장 completion entry를 위한 `IORING_SETUP_CQE32` 또는 `IORING_SETUP_CQE_MIXED` flag로 생성해야 합니다.

Setup io_uring
--------------

This section describes the low level io_uring kernel API. Please refer to
liburing documentation for how to use the higher level API.

Create an io_uring instance with the following required setup flags::

  IORING_SETUP_SINGLE_ISSUER
  IORING_SETUP_DEFER_TASKRUN
  IORING_SETUP_CQE32 or IORING_SETUP_CQE_MIXED

수신 memory 영역

80-89

Zero-copy payload를 받을 사용자 공간 memory는 `mmap()`으로 익명 private read/write 영역을 할당합니다. 시작 주소 `area_ptr`와 길이 `area_size`는 이후 등록 구조체에 전달합니다.

Create memory area
------------------

Allocate userspace memory area for receiving zero copy data::

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

Refill ring 생성

90-107

소비한 buffer를 커널에 돌려줄 shared ring buffer도 별도 `mmap()`으로 할당합니다. Ring은 한 page 크기의 header 공간 뒤에 `struct io_uring_zcrx_rqe` 배열이 이어지는 구조입니다.

예제는 entry 4096개에 구조체 크기를 곱하고 `PAGE_SIZE` header를 더한 뒤 전체 크기를 page 경계로 올림 정렬합니다. 이 정렬된 `ring_size`만큼 익명 private read/write memory를 확보합니다.

Create refill ring
------------------

Allocate memory for a shared ringbuf used for returning consumed buffers::

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

This refill ring consists of some space for the header, followed by an array of
``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 등록

108-137

`io_uring_zcrx_area_reg`에는 수신 영역 주소와 크기를 넣습니다. `io_uring_region_desc`에는 refill ring 주소와 크기, `IORING_MEM_REGION_TYPE_USER` flag를 설정합니다.

`io_uring_zcrx_ifq_reg`에는 `if_nametoindex("eth0")`로 얻은 인터페이스 index, flow를 steering한 hardware queue 1, refill entry 수, 두 등록 구조체의 주소를 넣습니다. 완성한 구조체는 `io_uring_register_ifq(ring, &reg)`로 커널에 등록합니다.

Register ZC Rx
--------------

Fill in registration structs::

  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,
  };

Register with kernel::

  io_uring_register_ifq(ring, &reg);

Refill ring 매핑

138-152

등록이 성공하면 커널이 `io_uring_zcrx_ifq_reg.offsets`에 refill ring 내부 필드 오프셋을 채웁니다. 사용자 공간은 `ring_ptr + offsets.head`, `tail`, `rqes`로 kernel head, kernel tail, request entry 배열 포인터를 계산하고 로컬 `rq_tail`을 0으로 초기화합니다.

원문 예제 146~147행은 `head`와 `tail` 계산 결과를 모두 `refill_ring.khead`에 대입합니다. 아래 원문은 수정하지 않고 보존했으며 실제 구현에서는 구조체 정의와 최신 selftest를 기준으로 head/tail 포인터 필드를 확인해야 합니다.

Map refill ring
---------------

The kernel fills in fields for the refill ring in the registration ``struct
io_uring_zcrx_ifq_reg``. Map it into userspace::

  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;

데이터 수신

153-184

`io_uring_get_sqe()`로 SQE를 얻고 `io_uring_prep_rw(IORING_OP_RECV_ZC, sqe, fd, NULL, 0, 0)`로 zero-copy 수신을 준비합니다. `sqe->ioprio`에 `IORING_RECV_MULTISHOT`을 설정하면 하나의 요청에서 여러 completion을 받을 수 있습니다. 이후 `io_uring_submit_and_wait(ring, 1)`로 제출하고 최소 한 completion을 기다립니다.

Completion 순회에서는 일반 `cqe` 바로 뒤의 `struct io_uring_zcrx_cqe`를 읽습니다. `IORING_ZCRX_AREA_SHIFT`로 만든 mask를 `rcqe->off`에 적용해 `area_ptr` 안의 실제 payload 주소를 계산하고 데이터를 처리합니다. 처리한 CQE 수를 센 뒤 `io_uring_cq_advance()`로 completion queue를 전진시킵니다.

Zero-copy 수신
IORING_OP_RECV_ZC SQEMULTISHOT 설정submit_and_waitCQE + zcrx_cqeoff를 area_ptr에 적용payload 직접 처리

요청에서 사용자 memory payload까지의 흐름입니다.

Receiving data
--------------

Prepare a zero copy recv request::

  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;

Now, submit and wait::

  io_uring_submit_and_wait(ring, 1);

Finally, process completions::

  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);

Buffer 재활용

185-198

소비한 buffer는 refill ring의 현재 tail entry에 기록해 커널로 돌려줍니다. `ring_entries - 1` mask로 순환 index를 구하고 `rcqe->off`에서 `IORING_ZCRX_AREA_MASK` 바깥의 area offset을 추출합니다.

`rqe->off`에는 area offset과 등록 때 받은 `rq_area_token`을 결합하고, `rqe->len`에는 `cqe->res` 길이를 넣습니다. 마지막으로 `IO_URING_WRITE_ONCE`로 shared kernel tail을 증가시켜 새 refill entry를 공개합니다.

Recycling buffers
-----------------

Return buffers back to the kernel to be used again::

  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);

시험

199-202

완전한 시험 예제는 `tools/testing/selftests/drivers/net/hw/iou-zcrx.c`에 있습니다. 저수준 API 사용 시 이 selftest를 구조체 필드와 ring 처리의 기준 구현으로 함께 확인하는 것이 좋습니다.

Testing
=======

See ``tools/testing/selftests/drivers/net/hw/iou-zcrx.c``