요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. 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 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.
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
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
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);
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);
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)®ion_reg,
};
Register with kernel::
io_uring_register_ifq(ring, ®);
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;
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);
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);
Testing
=======
See ``tools/testing/selftests/drivers/net/hw/iou-zcrx.c``
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
소개
1-16io_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 밖으로 보내야 합니다.
각 기능이 전용 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-67NIC 설정은 현재 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-89Zero-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, ®)`로 커널에 등록합니다.
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)®ion_reg,
};
Register with kernel::
io_uring_register_ifq(ring, ®);
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를 전진시킵니다.
요청에서 사용자 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``
요약·해설
iou-zcrx.rst:1-202io_uring ZC Rx는 TCP header 처리는 커널에 남기고 payload DMA 목적지만 등록한 사용자 memory로 바꿉니다. 사용자는 NIC queue 격리, memory area와 refill ring 등록, multishot receive, buffer 반환을 모두 관리합니다.
수신과 재활용 경로를 함께 표시합니다.