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.
Where it lands in Jayverse
- Devnet: profile syscalls before scaling Anvil's RPC layer. If the hosted devnet's RPC node ever shows CPU time concentrated in kernel time under many small JSON-RPC requests, run the strace -c comparison this card describes before adding instances — batched submission or reduced copying may be the actual fix.
- Bridge: the relayer processing many small lock/mint/burn events across Anvil and Sepolia is the same workload shape. If relayer throughput or latency ever becomes the bottleneck, the epoll-vs-io_uring comparison applies there too, not just to a generic RPC gateway.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| cross the boundary | 경계를 넘나들다 · 매 시스템콜마다 유저-커널 경계를 넘는 오버헤드를 설명 · "crosses the user-kernel boundary on every system call" |
| dominate | (비용·시간에서) 압도적 비중을 차지하다 · 오버헤드가 전체 비용의 대부분을 차지함 · "that overhead dominates the total cost" |
| keep ~ from ~ing | ~가 ~하지 못하게 막다 · 데이터가 유저 공간을 거치지 않도록 막는 기법 · "keep data from passing through user space" |
| take on | (책임·역할을) 떠맡다 · 커널이 하던 프로토콜 처리를 애플리케이션이 대신 떠맡음 · "the application now has to take on the protocol handling" |
| at the cost of | ~을 대가로, ~을 희생하고 · 복잡도와 이식성을 대가로 경계 넘나듦을 줄임 · "at the cost of complexity and portability" |
| make progress | 진전을 이루다, 작업을 처리하다 · 시스템콜 없이도 처리를 이어갈 수 있음 · "make progress with no system calls at all" |
| concentrated in | ~에 집중되어 있다 · CPU 시간이 커널 시간에 몰려 있는 현상 · "shows CPU time concentrated in kernel time" |
| io_uring | 아이오유링(io_uring) · 커널·유저 공간이 링 버퍼를 공유해 시스템콜 없이도 비동기 I/O를 처리하는 리눅스 인터페이스. "io_uring provides an asynchronous interface built on two ring buffers" |
| NIC | 네트워크 인터페이스 카드(Network Interface Card, NIC) · 커널 바이패스가 큐를 유저공간 드라이버로 직접 매핑하는 하드웨어. "mapping NIC queues directly into a user-space driver" |
| epoll | 이폴(epoll) · 리눅스의 전통적 이벤트 기반 I/O 다중화 API, io_uring과의 비교 대상. "Implement the same echo server on epoll and on io_uring" |
| p99 | p99 레이턴시(99번째 백분위수 지연) · 전체 요청 중 99%가 이 값 이하로 끝났음을 나타내는 성능 지표. "compare p99 latency and system calls per second" |
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/.