QUESTION
io_uring_enter가 반환되면 제출한 buffer를 바로 재사용해도 되는가?
io_uring setup은 kernel과 userspace가 공유하는 submission/completion ring memory를 만든다. userspace가 SQE를 채워 tail을 publish하고 enter로 kernel에 알리면 kernel request가 실행된 뒤 CQE를 쓴다.
submit syscall 반환은 작업 완료가 아니다. CQE를 소비할 때까지 operation이 user buffer와 file을 참조할 수 있으므로 request별 ownership과 cancellation 결과를 추적해야 한다.
STRUCTURE
구조 그림
Submission Queue
Kernel in-flight
Completion Queue
SQE slot은 kernel이 가져간 뒤 재사용할 수 있지만 user buffer는 CQE 완료 전까지 유지해야 한다. 두 수명이 같지 않다.
CALL PATH
호출 흐름
SQE slot 수명, request 수명, user buffer 수명, CQE 수명을 한 줄로 겹쳐 그린다. slot 재사용과 buffer 재사용은 서로 다른 완료 조건을 가진다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| io_uring/io_uring.c | io_uring_setup(), io_uring_enter() | ring context 생성과 submission/completion 진입 |
| io_uring/io_uring.c | io_submit_sqes(), io_init_req() | SQE를 kernel request로 변환 |
| io_uring/io_uring.c | io_req_complete_post(), io_cqring_event_overflow() | CQE publish와 overflow 처리 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 uring_read.c -luring -o uring_read01#include <fcntl.h>
02#include <liburing.h>
03#include <stdio.h>
04#include <string.h>
05#include <unistd.h>
06
07int main(int argc, char **argv)
08{
09 if (argc != 2)
10 return 2;
11 int fd = open(argv[1], O_RDONLY | O_CLOEXEC);
12 struct io_uring ring;
13 if (fd < 0 || io_uring_queue_init(8, &ring, 0) < 0)
14 return 1;
15
16 char buffer[256];
17 struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
18 io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
19 io_uring_sqe_set_data64(sqe, 0x1001);
20 if (io_uring_submit(&ring) < 0)
21 return 1;
22
23 struct io_uring_cqe *cqe;
24 if (io_uring_wait_cqe(&ring, &cqe) < 0)
25 return 1;
26 int result = cqe->res;
27 if (result > 0)
28 write(STDOUT_FILENO, buffer, (size_t)result);
29 else if (result < 0)
30 fprintf(stderr, "read: %s\n", strerror(-result));
31 io_uring_cqe_seen(&ring, cqe);
32 io_uring_queue_exit(&ring);
33 close(fd);
34 return result < 0;
35}
CODE NOTES
코드 조각별 설명
io_uring_queue_init(88-entry SQ/CQ ring과 ring fd, shared mappings를 준비한다. 실제 CQ 크기와 feature는 setup 결과를 확인한다.
char buffer[256]read completion 전까지 stack frame과 buffer가 유효해야 한다. function을 먼저 반환하거나 다른 request에 재사용하면 data race가 생긴다.
io_uring_sqe_set_data64completion을 application request object와 연결할 opaque key를 넣는다. fd 번호만 넣으면 재사용 문제를 해결하지 못한다.
int result = cqe->resCQE 결과는 libc처럼 -1/errno가 아니라 성공 byte 또는 음수 errno다. -result를 strerror에 넘긴다.
io_uring_cqe_seenCQ head를 진행시켜 해당 completion slot을 ring에 반환한다. 결과와 user_data를 먼저 복사한 뒤 호출한다.
DETAILS
세부 동작
비동기 여부는 opcode와 file type에 따라 달라진다
모든 operation이 항상 별도 worker나 hardware async로 실행되는 것은 아니다. inline completion, task work, io-wq offload가 섞일 수 있다.
syscall 감소만 보지 말고 completion latency, worker saturation, context switch를 측정한다.
registered resource는 pinning 비용을 바꾼다
fixed file과 registered buffer는 매 request fd lookup/pinning을 줄일 수 있지만 registration table과 page pin 수명을 길게 만든다. update/unregister 동안 in-flight request를 고려한다.
장기 pinned page는 reclaim/migration에 영향을 줄 수 있다.
cancel도 completion으로 확인한다
cancel SQE 성공은 target operation을 찾고 취소 요청을 적용했다는 결과이며 target CQE와 cancel CQE의 순서를 모두 처리해야 한다. 이미 완료된 request에는 ENOENT 같은 결과가 올 수 있다.
timeout link와 multishot operation은 한 user_data가 여러 CQE를 만들 수 있는 flag를 확인한다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
io_ring_ctx | queue_init/setup에서 생기고 queue_exit/ring fd close에서 해제된다 | SQ/CQ, task refs, worker |
SQE/io_kiocb | SQ slot 작성 후 kernel request로 바뀌고 completion까지 in-flight다 | opcode, user_data, buffer/fd refs |
CQE | completion publish에서 생기고 cqe_seen으로 slot이 반환된다 | res, flags, user_data |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| buffer 내용 corruption | completion 전 재사용/stack 반환 | request owner와 CQE timeline |
| CQ overflow/stall | completion을 충분히 소비하지 않음 | CQ depth, overflow flag, seen 호출 |
| 오류 해석 이상 | cqe->res를 errno 방식으로 처리 | 음수 errno 규칙 |
LAB
직접 확인
- 여러 offset read를 제출하고 user_data로 완료 순서가 제출 순서와 다를 수 있음을 기록한다.
- registered file/buffer 전후 syscall, throughput, tail latency, pinned memory를 비교한다.
- linked timeout과 async cancel을 추가해 target CQE와 cancel/timeout CQE 조합을 표로 만든다.
./uring_read /etc/hostnamestrace -e trace=io_uring_setup,io_uring_enter,io_uring_register,mmap,munmap ./uring_read /etc/hostnamePRIMARY REFERENCES