Kernel Bypass and Zero-Copy — io_uring TODO
Concept
Traditional socket I/O crosses the user-kernel boundary on every system call and copies data between kernel and user buffers, so for workloads with very many small requests, that overhead dominates the total cost. Zero-copy is a family of techniques that eliminates or reduces that copying — sendfile or splice for file transfer, which keep data from passing through user space, are the classic examples. io_uring provides an asynchronous interface built on two ring buffers shared between kernel and user space (a submission queue and a completion queue): you write requests into the queue and read completions out of it, and you can submit many requests in a single system call, or in polling mode make progress with no system calls at all. Kernel bypass goes further still, mapping NIC queues directly into a user-space driver and skipping the kernel network stack entirely — latency drops sharply, but the application now has to take on the protocol handling and protection the kernel used to provide. What all these techniques share is reducing the number of boundary crossings and the number of copies, at the cost of complexity and portability.
In matching engines or ultra-high-frequency RPC gateways where microsecond-level latency matters, the bottleneck is often system-call and copy overhead rather than application logic, so you need to be able to tell how much of the cost is kernel cost.
Code & Formula
# 커널 바이패스와 zero-copy — io_uring 의 핵심 아이디어(제출 큐 SQ / 완료 큐 CQ 를 유저-커널이 공유)를 순수 파이썬으로 흉내낸다.
# 진짜 io_uring 은 시스템 콜 없이 링 버퍼만으로 요청/완료를 주고받지만, 여기서는 그 인터페이스 모양만 재현한다.
from collections import deque
class TinyIoUring:
def __init__(self):
self.submission_queue = deque() # SQ: 유저가 써넣고 커널이 소비
self.completion_queue = deque() # CQ: 커널이 써넣고 유저가 소비
self._next_id = 0
def submit(self, op, payload):
"""유저 공간: 요청을 SQ 에 밀어넣는다 — 요청마다 시스템 콜을 하지 않고 큐에만 쌓는다."""
req_id = self._next_id
self._next_id += 1
self.submission_queue.append((req_id, op, payload))
return req_id
def kernel_process_batch(self):
"""커널 쪽 처리를 흉내: SQ 에 쌓인 요청을 한 번에(배치로) 처리해 CQ 에 완료를 채운다.
여기서 핵심은 요청 N개를 시스템 콜 1번(=이 함수 호출 1번)으로 끝낸다는 것 — zero-copy 의 핵심도
'경계를 넘는 횟수'와 '복사 횟수'를 줄이는 데 있다."""
processed = 0
while self.submission_queue:
req_id, op, payload = self.submission_queue.popleft()
if op == "read":
result = f"data({payload})" # 실제로는 유저 버퍼로 직접 DMA 되어 복사가 생략됨
elif op == "write":
result = f"written:{len(payload)}bytes"
else:
result = None
self.completion_queue.append((req_id, result))
processed += 1
return processed
def reap_completions(self):
"""유저 공간: CQ 에서 완료된 결과를 꺼낸다 — 이것도 시스템 콜 없이 공유 메모리 읽기만으로 끝난다."""
out = []
while self.completion_queue:
out.append(self.completion_queue.popleft())
return out
ring = TinyIoUring()
# 전통적 블로킹 I/O 라면 read() 5번 = 시스템 콜 5번이지만, 여기서는 SQ 에 5개를 한꺼번에 밀어넣는다.
req_ids = [ring.submit("read", f"/file{i}") for i in range(5)]
req_ids.append(ring.submit("write", "payload-bytes"))
requests_processed = ring.kernel_process_batch() # 이 함수 호출 자체가 "시스템 콜 1회"에 대응한다
enter_syscalls = 1 # io_uring_enter() 를 딱 한 번만 부른 셈
completions = ring.reap_completions()
print("submitted requests:", len(req_ids))
print("requests completed in this batch:", requests_processed)
print("io_uring_enter() calls needed:", enter_syscalls,
f"-> 전통적 blocking read/write였다면 {len(req_ids)}번의 시스템 콜이 필요했다")
print("completions:", completions)
docs/code/algorithms/algorithms-41.py
Exercise
Implement the same echo server on epoll and on io_uring, and compare p99 latency and system calls per second (via strace -c or perf) under identical load.
Practical Connection
When a chain node or RPC proxy handling huge numbers of small JSON-RPC requests shows CPU time concentrated in kernel time, batched submission and reduced copying are worth checking before adding hardware.
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.