io_uring (Async I/O)

Linux io_uring 비동기 I/O 인터페이스: SQE/CQE 링 버퍼 아키텍처, 동작 모드(SQPOLL/IOPOLL), 커널 내부 구현, 고급 기능(제로카피, io_uring_cmd, multishot), liburing 프로그래밍, 보안 고려사항, 실전 패턴을 종합적으로 다룹니다.

관련 표준: NVMe Specification 2.0 (비동기 I/O 커맨드), POSIX.1-2017 (AIO 비교 기준) — io_uring은 POSIX AIO를 대체하는 고성능 비동기 I/O 인터페이스입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 시스템 콜(커널 진입 오버헤드)과 Block I/O(bio, 블록 계층)를 먼저 읽으세요.
일상 비유: io_uring은 셀프 서비스 주문 키오스크와 같습니다. 기존 I/O(시스템 콜)는 매번 카운터 직원(커널)을 호출해야 하지만, io_uring은 공유 주문 게시판(SQ/CQ 링 버퍼)에 주문을 올려두면 주방에서 알아서 처리하고 결과를 게시합니다. 카운터를 왔다 갔다 하지 않아도 되므로(시스템 콜 감소) 매우 빠릅니다.

핵심 요약

  • io_uring — Linux 5.1에서 도입된 고성능 비동기 I/O 인터페이스입니다.
  • SQ / CQ — Submission Queue(제출 큐)와 Completion Queue(완료 큐). 사용자-커널 간 공유 링 버퍼입니다.
  • SQPOLL — 커널 스레드가 SQ를 폴링하여 시스템 콜 없이 I/O를 처리하는 모드입니다.
  • liburing — io_uring을 쉽게 사용하기 위한 사용자 공간 라이브러리입니다.
  • 제로카피 — 데이터 복사 없이 네트워크 전송/수신을 수행하는 고급 기능입니다.

단계별 이해

  1. 왜 필요한가 — 기존 read()/write()는 매번 시스템 콜 전환이 필요하고, POSIX AIO는 제한적입니다.

    NVMe SSD처럼 수백만 IOPS 디바이스에서는 시스템 콜 오버헤드가 병목이 됩니다.

  2. 링 버퍼 이해 — 사용자가 SQE(Submission Queue Entry)를 SQ에 넣으면, 커널이 처리 후 CQE(Completion Queue Entry)를 CQ에 넣습니다.

    공유 메모리이므로 데이터 복사 없이 포인터만 이동합니다.

  3. liburing 체험io_uring_queue_init()으로 링을 초기화하고, io_uring_prep_readv()로 읽기를 준비합니다.

    io_uring_submit()으로 제출, io_uring_wait_cqe()로 완료를 기다립니다.

  4. 성능 확인fio --ioengine=io_uring --bs=4k --iodepth=64으로 io_uring 성능을 벤치마크합니다.

    기존 libaio 대비 latency와 IOPS에서 큰 향상을 확인할 수 있습니다.

개요

io_uring은 Linux 5.1(2019)에서 도입된 비동기 I/O 인터페이스입니다. 기존 AIO(io_submit/io_getevents)의 한계(버퍼드 I/O 미지원, 시스템 콜 오버헤드)를 해결하며, 사용자-커널 간 공유 링 버퍼를 통해 시스템 콜 없이 I/O를 제출하고 완료를 수확합니다.

io_uring 아키텍처 User Space Application liburing (helper library) SQ Ring head/tail + SQE[] CQ Ring head/tail + CQE[] mmap() 공유 메모리 Kernel Space io_uring core SQE 파싱 & 디스패치 io-wq worker thread pool VFS / Block read, write, fsync Net / Socket send, recv, accept 완료 → CQE 게시
io_uring 전체 아키텍처: 사용자-커널 공유 링 버퍼, io-wq 워커, 서브시스템 연동

io_uring 발전 역사

io_uring은 Jens Axboe가 설계하여 Linux 5.1에서 처음 도입되었으며, 이후 매 커널 릴리스마다 새로운 opcode와 기능이 추가되어 범용 비동기 인터페이스로 발전하고 있습니다.

커널 버전주요 추가 기능
5.1 (2019-05)io_uring 도입: READV/WRITEV, FSYNC, POLL_ADD, io_uring_setup/enter/register
5.2POLL_REMOVE, io-wq 워커 풀 도입
5.3TIMEOUT, SQE 링크(IOSQE_IO_LINK)
5.4TIMEOUT_REMOVE, ACCEPT, ASYNC_CANCEL, LINK_TIMEOUT
5.5CONNECT, FALLOCATE, OPENAT, CLOSE, STATX, PROVIDE_BUFFERS
5.6READ/WRITE (단순화), SPLICE, TEE, SQPOLL CPU affinity 개선
5.7EPOLL_CTL, MADVISE, OPENAT2
5.11SHUTDOWN, RENAMEAT, UNLINKAT, MKDIRAT
5.12SYMLINKAT, LINKAT, io_uring_disabled sysctl 보안 옵션
5.15MSG_RING (ring-to-ring 메시징)
5.18SOCKET (소켓 생성), 등록 파일 업데이트
5.19SEND_ZC (제로카피 전송), provided buf ring mmap API
6.0SEND_ZC 안정화, io_uring_cmd (NVMe passthrough)
6.1IORING_SETUP_SINGLE_ISSUER, IORING_SETUP_DEFER_TASKRUN
6.2RECV_ZC (제로카피 수신), 멀티 CQE32
6.3WAITID, IORING_REGISTER_RESTRICTIONS
6.7IORING_SETUP_NO_SQARRAY (SQ 배열 제거로 메모리 절약)
6.9+FUTEX_WAIT/WAKE, 네이티브 futex 지원, 추가 최적화

시스템 콜 인터페이스

io_uring은 3개의 시스템 콜로 동작합니다. 초기 설정 이후에는 io_uring_enter()조차 호출하지 않는 완전한 커널 폴링 모드도 가능합니다.

/* 1. io_uring 인스턴스 생성 */
int io_uring_setup(u32 entries, struct io_uring_params *params);
/* entries: SQ 크기 (2의 거듭제곱으로 올림)
 * params: 설정 플래그 + 커널이 채워주는 SQ/CQ 오프셋 정보
 * 반환: io_uring fd → mmap()으로 SQ/CQ 매핑 */

/* 2. I/O 제출 및 완료 대기 */
int io_uring_enter(int fd, u32 to_submit, u32 min_complete,
                    u32 flags, sigset_t *sig);
/* to_submit: 제출할 SQE 수
 * min_complete: 최소 완료 대기 수 (0이면 논블로킹)
 * flags: IORING_ENTER_GETEVENTS, IORING_ENTER_SQ_WAKEUP 등 */

/* 3. 리소스 사전 등록 (선택) */
int io_uring_register(int fd, u32 opcode, void *arg, u32 nr_args);
/* fd/버퍼를 커널에 사전 등록 → 매 I/O마다 fget/fput, 페이지 핀 비용 제거
 * IORING_REGISTER_BUFFERS: 고정 버퍼 등록
 * IORING_REGISTER_FILES: 고정 파일 디스크립터 등록 */

SQE / CQE 자료구조

SQE(Submission Queue Entry)는 I/O 요청을, CQE(Completion Queue Entry)는 완료 결과를 나타냅니다. 두 구조체 모두 고정 크기로 캐시 친화적입니다.

/* include/uapi/linux/io_uring.h */
struct io_uring_sqe {
    __u8    opcode;     /* IORING_OP_READ, IORING_OP_WRITE, ... */
    __u8    flags;      /* IOSQE_FIXED_FILE, IOSQE_IO_LINK, ... */
    __u16   ioprio;     /* I/O 우선순위 */
    __s32   fd;         /* 대상 파일 디스크립터 */
    union {
        __u64 off;      /* 파일 오프셋 */
        __u64 addr2;    /* 두 번째 주소 (opcode에 따라) */
    };
    union {
        __u64 addr;     /* 버퍼 주소 또는 iovec 포인터 */
        __u64 splice_off_in;
    };
    __u32   len;        /* 버퍼 크기 또는 iovec 수 */
    union {
        __kernel_rwf_t rw_flags;
        __u32          fsync_flags;
        __u32          poll_events;
        __u32          msg_flags;
        __u32          accept_flags;
    };
    __u64   user_data;  /* CQE에 그대로 복사 → 요청 식별자 */
    union {
        __u16 buf_index; /* 고정 버퍼 인덱스 */
        __u16 buf_group; /* 버퍼 그룹 ID (provided buffers) */
    };
    __u16   personality;
    union {
        __s32 splice_fd_in;
        __u32 file_index;
    };
    __u64   __pad2[2];
};  /* sizeof = 64 bytes (1 캐시라인) */

struct io_uring_cqe {
    __u64   user_data;  /* SQE에서 복사된 사용자 데이터 */
    __s32   res;        /* 결과값 (바이트 수 또는 -errno) */
    __u32   flags;      /* IORING_CQE_F_BUFFER, IORING_CQE_F_MORE */
};  /* sizeof = 16 bytes */

링 버퍼 동작 원리

SQ/CQ 링은 mmap()으로 사용자 공간에 매핑된 lock-free SPSC(Single-Producer Single-Consumer) 링 버퍼입니다. 메모리 배리어만으로 동기화합니다.

/* SQE 제출 과정 (사용자 공간) */
unsigned idx = sq->tail & sq->ring_mask;
struct io_uring_sqe *sqe = &sq->sqes[idx];

sqe->opcode  = IORING_OP_READ;
sqe->fd      = file_fd;
sqe->addr    = (unsigned long)buf;
sqe->len     = buf_size;
sqe->off     = offset;
sqe->user_data = my_request_id;

/* SQ tail 포인터 갱신 (write barrier 필수) */
io_uring_smp_store_release(&sq->tail, sq->tail + 1);
io_uring_enter(ring_fd, 1, 0, 0, NULL);

/* CQE 수확 과정 (사용자 공간) */
unsigned head = io_uring_smp_load_acquire(&cq->head);
while (head != cq->tail) {
    struct io_uring_cqe *cqe = &cq->cqes[head & cq->ring_mask];
    handle_completion(cqe->user_data, cqe->res);
    head++;
}
io_uring_smp_store_release(&cq->head, head);

SQ는 간접 인덱싱을 사용합니다: sq->array[idx]가 실제 sqes[] 인덱스를 가리킵니다. 이를 통해 SQE를 순서 무관하게 재사용할 수 있습니다. CQ는 직접 인덱싱으로 더 단순합니다.

주요 연산 (opcodes)

카테고리Opcode설명도입
파일 I/OIORING_OP_READ파일 읽기 (고정 버퍼 지원)5.6
IORING_OP_WRITE파일 쓰기 (고정 버퍼 지원)5.6
IORING_OP_READV / WRITEVScatter-gather I/O (iovec)5.1
IORING_OP_FSYNC파일 동기화 (fdatasync 포함)5.1
네트워크IORING_OP_ACCEPT소켓 연결 수락 (multishot 지원)5.5
IORING_OP_CONNECT소켓 연결5.5
IORING_OP_SEND / RECV소켓 송수신5.6
IORING_OP_SEND_ZC제로카피 송신6.0
고급IORING_OP_POLL_ADD이벤트 폴링 (epoll 대체)5.2
IORING_OP_TIMEOUT타임아웃 설정5.4
IORING_OP_LINK_TIMEOUT링크된 SQE에 타임아웃 부여5.5
IORING_OP_ASYNC_CANCEL진행 중인 요청 취소5.5

io_uring_params 플래그 상세

io_uring_setup() 호출 시 io_uring_params.flags에 설정하는 플래그들은 링의 동작 방식을 결정합니다.

플래그도입설명
IORING_SETUP_IOPOLL5.1완료를 인터럽트 대신 폴링으로 확인. O_DIRECT 전용
IORING_SETUP_SQPOLL5.1커널 스레드가 SQ를 폴링. 시스템 콜 없이 I/O 제출
IORING_SETUP_SQ_AFF5.1SQPOLL 스레드를 sq_thread_cpu에 바인딩
IORING_SETUP_CQSIZE5.5CQ 크기를 cq_entries로 별도 지정
IORING_SETUP_ATTACH_WQ5.6기존 ring의 io-wq 워커 풀을 공유
IORING_SETUP_R_DISABLED5.10ring을 비활성 상태로 생성. ENABLE_RINGS로 활성화
IORING_SETUP_COOP_TASKRUN5.19task_work를 협력적으로 처리. io_uring_enter() 진입 시에만 완료
IORING_SETUP_SQE1285.19SQE를 128바이트로 확장 (NVMe passthrough 등)
IORING_SETUP_CQE325.19CQE를 32바이트로 확장
IORING_SETUP_SINGLE_ISSUER6.0단일 태스크만 제출 보장. 내부 잠금 최적화
IORING_SETUP_DEFER_TASKRUN6.1SINGLE_ISSUER 필요. task_work를 io_uring_enter() 시 일괄 처리
IORING_SETUP_NO_SQARRAY6.7SQ 간접 인덱스 배열 생략. 메모리 절약
💡

최고 성능 조합: IORING_SETUP_SQPOLL | IORING_SETUP_IOPOLL | IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN. NVMe O_DIRECT 워크로드에서 시스템 콜과 인터럽트 없이 극한의 IOPS를 달성합니다.

동작 모드

기본 모드 (Interrupt Driven)

struct io_uring_params params = {};
int ring_fd = io_uring_setup(256, &params);
io_uring_enter(ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL);

SQPOLL 모드 (커널 폴링)

커널 스레드(io_uring-sq)가 SQ를 지속적으로 폴링합니다. 시스템 콜 없이 SQ tail만 갱신하면 커널이 자동으로 처리합니다.

struct io_uring_params params = {
    .flags = IORING_SETUP_SQPOLL,
    .sq_thread_idle = 2000,  /* 2초 유휴 시 스레드 슬립 */
};
int ring_fd = io_uring_setup(256, &params);

/* 커널 스레드가 슬립했다면 깨워야 함 */
if (*sq->flags & IORING_SQ_NEED_WAKEUP)
    io_uring_enter(ring_fd, 0, 0, IORING_ENTER_SQ_WAKEUP, NULL);

IOPOLL 모드 (하드웨어 폴링)

커널이 블록 디바이스 완료를 인터럽트 대신 폴링으로 확인합니다. NVMe 등 고성능 스토리지에서 인터럽트 지연을 제거합니다. O_DIRECT 전용입니다.

struct io_uring_params params = {
    .flags = IORING_SETUP_IOPOLL,
};
/* SQPOLL + IOPOLL = 완전한 폴링 기반 I/O (시스템 콜 0, 인터럽트 0) */

고급 기능

/* write → fsync 순차 실행 보장 */
sqe1->opcode = IORING_OP_WRITE;
sqe1->flags  = IOSQE_IO_LINK;
sqe2->opcode = IORING_OP_FSYNC;
sqe2->flags  = 0;
/* IOSQE_IO_HARDLINK: 앞 SQE 실패해도 계속 실행 */

고정 파일/버퍼 (Registered Resources)

매 I/O마다 발생하는 fget()/fput()와 페이지 핀(GUP) 비용을 제거합니다.

int fds[] = {fd1, fd2, fd3};
io_uring_register(ring_fd, IORING_REGISTER_FILES, fds, 3);
sqe->flags |= IOSQE_FIXED_FILE;
sqe->fd = 0;  /* fds[0] = fd1 사용 */

struct iovec iovs[] = { { buf1, 4096 }, { buf2, 4096 } };
io_uring_register(ring_fd, IORING_REGISTER_BUFFERS, iovs, 2);
sqe->opcode = IORING_OP_READ_FIXED;
sqe->buf_index = 0;

Provided Buffers (커널 버퍼 선택)

버퍼 풀을 커널에 제공하고, 커널이 완료 시 적절한 버퍼를 자동 선택합니다.

struct io_uring_buf_ring *br;
br = mmap(..., ring_fd, IORING_OFF_PBUF_RING);
for (int i = 0; i < nr_bufs; i++)
    io_uring_buf_ring_add(br, bufs[i], buf_size, i, mask, i);
io_uring_buf_ring_advance(br, nr_bufs);

sqe->opcode = IORING_OP_RECV;
sqe->flags  = IOSQE_BUFFER_SELECT;
sqe->buf_group = group_id;

Multishot 연산

하나의 SQE로 여러 번의 CQE를 생성합니다. accept, recv, poll 등에서 반복적인 SQE 재제출 오버헤드를 제거합니다.

/* Multishot accept */
sqe->opcode = IORING_OP_ACCEPT;
sqe->fd     = listen_fd;
sqe->ioprio = IORING_ACCEPT_MULTISHOT;
/* 새 연결마다 CQE 생성, CQE.flags에 IORING_CQE_F_MORE 설정 */

/* Multishot recv */
sqe->opcode    = IORING_OP_RECV;
sqe->ioprio    = IORING_RECV_MULTISHOT;
sqe->flags     = IOSQE_BUFFER_SELECT;
sqe->buf_group = group_id;

Cancel / Timeout 연산 심화

IORING_OP_ASYNC_CANCEL

sqe->opcode = IORING_OP_ASYNC_CANCEL;
sqe->addr   = target_user_data;
/* 결과: 0=취소됨, -ENOENT=없음, -EALREADY=이미 완료 중 */

/* fd 기반 취소 (6.0+) */
sqe->fd    = target_fd;
sqe->flags = IORING_ASYNC_CANCEL_FD;

/* 모든 요청 취소 (6.1+) */
sqe->cancel_flags = IORING_ASYNC_CANCEL_ANY;

IORING_OP_TIMEOUT

struct __kernel_timespec ts = { .tv_sec = 2 };
sqe->opcode = IORING_OP_TIMEOUT;
sqe->addr   = (unsigned long)&ts;
sqe->len    = 1;
sqe->off    = 5;  /* 5개 CQE 완료되면 조기 해제 */
/* -ETIME=만료, 0=조기 해제, -ECANCELED=취소됨 */
/* read가 3초 내 완료되지 않으면 취소 */
sqe1->opcode = IORING_OP_READ;
sqe1->flags  = IOSQE_IO_LINK;

struct __kernel_timespec ts = { .tv_sec = 3 };
sqe2->opcode = IORING_OP_LINK_TIMEOUT;
sqe2->addr   = (unsigned long)&ts;
sqe2->len    = 1;

Zero-copy 네트워킹

IORING_OP_SEND_ZC는 사용자 공간 버퍼를 복사 없이 커널 네트워크 스택에 직접 전달합니다.

sqe->opcode = IORING_OP_SEND_ZC;
sqe->fd     = sock_fd;
sqe->addr   = (unsigned long)send_buf;
sqe->len    = send_len;

/* 주의: 2개의 CQE가 생성됨
 * 1) 전송 완료 (IORING_CQE_F_MORE)
 * 2) notification: 버퍼 해제 가능 (IORING_CQE_F_NOTIF) */
if (cqe->flags & IORING_CQE_F_NOTIF)
    recycle_buffer(cqe->user_data);

제로카피 전송은 64KB 이상의 대용량 전송에서 효과적이며, 10GbE 이상의 고속 네트워크에서 CPU 사용량을 30-50% 절감할 수 있습니다.

io_uring_cmd (Passthrough)

IORING_OP_URING_CMD는 디바이스 드라이버에 io_uring을 통해 직접 커스텀 명령을 전달합니다. NVMe passthrough가 대표적입니다.

/* NVMe passthrough (IORING_SETUP_SQE128 필요) */
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd     = nvme_ns_fd;       /* /dev/ng0n1 */
sqe->cmd_op = NVME_URING_CMD_IO;

struct nvme_uring_cmd *cmd = (struct nvme_uring_cmd *)sqe->cmd;
cmd->opcode  = nvme_cmd_read;
cmd->addr    = (__u64)buffer;
cmd->data_len = 4096;

/* 커널 드라이버 측 */
static const struct file_operations my_fops = {
    .uring_cmd = my_uring_cmd_handler,
};

CQE Overflow 처리

CQ 링이 가득 찬 상태에서 새 CQE가 생성되면 오버플로가 발생합니다. 커널은 내부 오버플로 리스트에 CQE를 보관하고, 사용자가 CQ에서 CQE를 소비하면 자동으로 옮겨줍니다.

/* 오버플로 감지 */
if (*sq->flags & IORING_SQ_CQ_OVERFLOW)
    io_uring_enter(ring_fd, 0, 0, IORING_ENTER_GETEVENTS, NULL);

/* 오버플로 방지: CQ 크기를 충분히 크게 */
params.flags |= IORING_SETUP_CQSIZE;
params.cq_entries = 4096;  /* SQ의 4배 이상 권장 */

CQE 오버플로는 성능 저하의 원인이 됩니다. 오버플로 리스트는 GFP_ATOMIC 할당을 사용하며, 지속되면 메모리 부족으로 CQE가 손실될 수 있습니다. CQ 크기를 충분히 설정하고 CQE를 적시에 소비하세요.

커널 내부 구현

/* io_uring/io_uring.c - 핵심 커널 자료구조 */
struct io_ring_ctx {
    struct {
        unsigned int        flags;
        unsigned int        sq_entries;
        unsigned int        cq_entries;
        struct io_rings     *rings;
        struct io_uring_sqe *sq_sqes;
    } ____cacheline_aligned_in_smp;

    struct io_sq_data    *sq_data;   /* SQPOLL 스레드 */
    struct io_wq        *io_wq;     /* 비동기 워커 풀 */
    struct io_rsrc_data *file_data;  /* 고정 파일 */
    struct io_rsrc_data *buf_data;   /* 고정 버퍼 */
};

/* SQE 처리 흐름 */
io_uring_enter()
  → io_submit_sqes()
      → io_get_sqe()         /* SQ에서 SQE 가져오기 */io_init_req()        /* SQE → io_kiocb 변환 */io_issue_sqe()       /* opcode별 핸들러 디스패치 */io_read() → vfs_read()
          → io_req_complete()  /* 즉시 완료: CQE 게시 */io_queue_async()   /* 블로킹: io-wq에 위임 */

io_kiocb 생명주기

io_kiocb는 io_uring 내부에서 각 I/O 요청을 추적하는 핵심 구조체입니다.

struct io_kiocb {
    struct file        *file;
    u8                  opcode;
    u64                 user_data;
    s32                 result;
    struct io_ring_ctx  *ctx;
    struct task_struct  *task;
    struct io_kiocb     *link;
    struct io_wq_work   work;
};
io_kiocb 생명주기:

  io_alloc_req()         SQE 파싱 시 할당 (slab 캐시)
       │
  io_init_req()          SQE → io_kiocb 필드 복사
       │
  io_issue_sqe()         opcode별 핸들러 호출
       │
  ┌────┴────┐
 동기      비동기 → io_queue_async() → io-wq 워커
  └────┬────┘
       │
  io_req_complete()      CQE에 결과 기록
       │
  io_req_task_complete() task_work로 완료 처리
       │
  io_free_req()          io_kiocb 해제 (캐시 풀 반환)

task_work 메커니즘: io_uring은 완료 처리를 제출자 태스크의 컨텍스트에서 수행하기 위해 task_work_add()를 사용합니다. DEFER_TASKRUN 플래그를 사용하면 task_work가 io_uring_enter() 호출 시에만 일괄 실행되어 효율이 높아집니다.

io-wq 워커 스레드 풀

즉시 완료되지 않는 (블로킹) 요청은 io-wq 커널 워커 스레드 풀로 넘겨집니다.

워커 유형용도최대 수
Bounded블로킹 파일 I/O (buffered read/write)RLIMIT_NPROC 기반
Unbounded네트워크 I/O, 긴 대기 작업별도 제한
# 실행 중인 io-wq 워커 확인
ps -eo pid,comm | grep io_uring
ls /proc/<pid>/task/ | wc -l

liburing 사용 예제

liburing은 io_uring 시스템 콜의 저수준 복잡성을 추상화하는 헬퍼 라이브러리입니다.

#include <liburing.h>

int main(void) {
    struct io_uring ring;
    struct io_uring_sqe *sqe;
    struct io_uring_cqe *cqe;
    char buf[4096];

    io_uring_queue_init(256, &ring, 0);

    sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
    io_uring_sqe_set_data(sqe, my_context);

    io_uring_submit(&ring);

    io_uring_wait_cqe(&ring, &cqe);
    if (cqe->res < 0)
        fprintf(stderr, "I/O error: %s\n", strerror(-cqe->res));
    else
        printf("Read %d bytes\n", cqe->res);

    io_uring_cqe_seen(&ring, cqe);
    io_uring_queue_exit(&ring);
    return 0;
}

성능 비교

I/O 방식시스템 콜/요청컨텍스트 스위치특징
동기 read/write1블로킹 시 발생단순, 저처리량
epoll + 논블로킹2+ (epoll_wait + read)이벤트 기반네트워크에 적합, 파일 I/O 제한
Linux AIO (io_submit)2 (submit + getevents)최소O_DIRECT 전용, 제한적
io_uring (기본)1 (io_uring_enter)최소범용, 배치 제출
io_uring (SQPOLL)0없음최고 성능, CPU 사용
io_uring (SQPOLL+IOPOLL)0없음극한 저지연 (NVMe)
💡

NVMe SSD에서 SQPOLL+IOPOLL 모드는 동기 I/O 대비 IOPS 2~5배, 지연 시간 50% 이상 감소를 달성할 수 있습니다. 단, CAP_SYS_NICE 권한이 필요하며 유휴 시에도 CPU를 소비합니다.

io_uring vs epoll 상세 비교

epoll 모델 io_uring 모델 epoll_wait() fd ready 확인 read()/write() 실제 I/O 수행 이벤트당 최소 2회 시스템 콜 User↔Kernel 전환 반복 파일 I/O는 논블로킹 불가 (스레드 풀 필요) SQE 작성 CQE 수확 공유 메모리로 직접 교환 io_uring_enter() (배치/선택) SQPOLL: 시스템 콜 0회 파일 I/O + 네트워크 I/O 통합 배치 제출 + 배치 완료 SQE 링크로 순서 보장
epoll vs io_uring: 시스템 콜 흐름과 아키텍처 비교
비교 항목epollio_uring
시스템 콜이벤트당 2회+0~1회 (SQPOLL이면 0)
I/O 유형네트워크(소켓) 중심파일 + 네트워크 + 기타 모두 통합
파일 I/O논블로킹 불가 → 스레드 풀 필요네이티브 비동기 (io-wq 자동)
배치 처리이벤트 수집만 배치제출 + 완료 모두 배치
메모리 복사커널-사용자 간 이벤트 복사공유 메모리로 제로카피
연산 체이닝불가SQE 링크로 순서 보장
학습 곡선낮음높음 (liburing 사용 시 완화)
적합 시나리오소켓 이벤트 다중화고성능 스토리지/네트워크, 통합 이벤트 루프

실전 패턴: 고성능 Echo 서버

liburing 기반 multishot accept + provided buffers를 활용한 이벤트 루프 구현입니다.

#include <liburing.h>
#include <netinet/in.h>

#define ENTRIES   256
#define BUF_COUNT 64
#define BUF_SIZE  4096
#define BUF_BGID  0

enum { EV_ACCEPT, EV_RECV, EV_SEND };
struct conn_info { int fd; int type; };

static char bufs[BUF_COUNT][BUF_SIZE];
static struct io_uring_buf_ring *buf_ring;

static void setup_buf_ring(struct io_uring *ring) {
    struct io_uring_buf_reg reg = {
        .ring_entries = BUF_COUNT, .bgid = BUF_BGID,
    };
    buf_ring = io_uring_setup_buf_ring(ring, &reg, 0);
    for (int i = 0; i < BUF_COUNT; i++)
        io_uring_buf_ring_add(buf_ring, bufs[i], BUF_SIZE,
                              i, BUF_COUNT - 1, i);
    io_uring_buf_ring_advance(buf_ring, BUF_COUNT);
}

int main(void) {
    struct io_uring ring;
    io_uring_queue_init(ENTRIES, &ring, 0);
    setup_buf_ring(&ring);

    int listen_fd = /* socket + bind + listen */;

    /* Multishot accept 등록 */
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_multishot_accept(sqe, listen_fd, NULL, NULL, 0);
    struct conn_info ci = { listen_fd, EV_ACCEPT };
    memcpy(&sqe->user_data, &ci, sizeof(ci));

    while (1) {
        io_uring_submit_and_wait(&ring, 1);

        struct io_uring_cqe *cqe;
        unsigned head, count = 0;
        io_uring_for_each_cqe(&ring, head, cqe) {
            struct conn_info ci;
            memcpy(&ci, &cqe->user_data, sizeof(ci));

            if (ci.type == EV_ACCEPT && cqe->res >= 0) {
                /* 새 연결: multishot recv 등록 */
                struct io_uring_sqe *s = io_uring_get_sqe(&ring);
                io_uring_prep_recv_multishot(s, cqe->res, NULL, 0, 0);
                s->flags |= IOSQE_BUFFER_SELECT;
                s->buf_group = BUF_BGID;
                struct conn_info ri = { cqe->res, EV_RECV };
                memcpy(&s->user_data, &ri, sizeof(ri));
            } else if (ci.type == EV_RECV && cqe->res > 0) {
                int bid = cqe->flags >> IORING_CQE_BUFFER_SHIFT;
                /* echo: 받은 데이터 그대로 전송 */
                struct io_uring_sqe *s = io_uring_get_sqe(&ring);
                io_uring_prep_send(s, ci.fd, bufs[bid], cqe->res, 0);
                struct conn_info si = { ci.fd, EV_SEND };
                memcpy(&s->user_data, &si, sizeof(si));
                /* 버퍼 반환 */
                io_uring_buf_ring_add(buf_ring, bufs[bid], BUF_SIZE,
                                      bid, BUF_COUNT - 1, 0);
                io_uring_buf_ring_advance(buf_ring, 1);
            } else if (ci.type == EV_RECV && cqe->res <= 0) {
                close(ci.fd);
            }
            count++;
        }
        io_uring_cq_advance(&ring, count);
    }
}

실전 패턴: 비동기 파일 복사

io_uring의 SQE 링크를 활용하여 read→write 체인으로 비동기 파일 복사를 구현합니다.

#include <liburing.h>
#include <fcntl.h>

#define BLOCK_SIZE  (128 * 1024)
#define QUEUE_DEPTH 32

static int copy_file(const char *src, const char *dst) {
    struct io_uring ring;
    io_uring_queue_init(QUEUE_DEPTH, &ring, 0);

    int in_fd  = open(src, O_RDONLY);
    int out_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);

    off_t offset = 0;
    int inflight = 0, done = 0;
    char *bufs[QUEUE_DEPTH];
    for (int i = 0; i < QUEUE_DEPTH; i++)
        bufs[i] = malloc(BLOCK_SIZE);

    while (!done || inflight) {
        while (!done && inflight < QUEUE_DEPTH) {
            /* read → write 링크 */
            struct io_uring_sqe *sqe_r = io_uring_get_sqe(&ring);
            io_uring_prep_read(sqe_r, in_fd, bufs[inflight],
                               BLOCK_SIZE, offset);
            sqe_r->flags |= IOSQE_IO_LINK;
            sqe_r->user_data = offset;

            struct io_uring_sqe *sqe_w = io_uring_get_sqe(&ring);
            io_uring_prep_write(sqe_w, out_fd, bufs[inflight],
                                BLOCK_SIZE, offset);
            sqe_w->user_data = offset | (1ULL << 63);

            offset += BLOCK_SIZE;
            inflight++;
        }
        io_uring_submit(&ring);

        struct io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);
        if (cqe->res == 0 && !(cqe->user_data & (1ULL << 63)))
            done = 1;
        if (cqe->user_data & (1ULL << 63))
            inflight--;
        io_uring_cqe_seen(&ring, cqe);
    }

    for (int i = 0; i < QUEUE_DEPTH; i++) free(bufs[i]);
    close(in_fd); close(out_fd);
    io_uring_queue_exit(&ring);
    return 0;
}

io_uring Restrictions (샌드박싱)

IORING_REGISTER_RESTRICTIONS를 사용하면 허용되는 opcode, 플래그, 등록 연산을 제한할 수 있습니다.

struct io_uring_params params = {
    .flags = IORING_SETUP_R_DISABLED,
};
int ring_fd = io_uring_setup(256, &params);

struct io_uring_restriction res[] = {
    { .opcode = IORING_RESTRICTION_SQE_OP,
      .sqe_op = IORING_OP_READ },
    { .opcode = IORING_RESTRICTION_SQE_OP,
      .sqe_op = IORING_OP_WRITE },
    { .opcode = IORING_RESTRICTION_SQE_FLAGS_ALLOWED,
      .sqe_flags = IOSQE_FIXED_FILE },
    { .opcode = IORING_RESTRICTION_REGISTER_OP,
      .register_op = IORING_REGISTER_FILES },
};
io_uring_register(ring_fd, IORING_REGISTER_RESTRICTIONS,
                  res, sizeof(res) / sizeof(res[0]));

/* ring 활성화 (이후 restriction 변경 불가) */
io_uring_register(ring_fd, IORING_REGISTER_ENABLE_RINGS,
                  NULL, 0);

io_uring 디버깅

/proc/PID/fdinfo

# io_uring fd의 상세 정보 확인
cat /proc/<pid>/fdinfo/<uring_fd>
# SqSize, CqSize, SqThreadCpu, UserFiles, UserBufs 등 출력

io_uring tracepoints

# 사용 가능한 io_uring tracepoint 목록
ls /sys/kernel/tracing/events/io_uring/
# io_uring_create, io_uring_submit_sqe, io_uring_complete,
# io_uring_queue_async_work, io_uring_poll_arm, io_uring_task_add

# ftrace로 io_uring 이벤트 추적
echo 1 > /sys/kernel/tracing/events/io_uring/enable
cat /sys/kernel/tracing/trace_pipe

bpftrace 원라이너

# io_uring SQE 제출 추적 (opcode별 카운트)
bpftrace -e 'tracepoint:io_uring:io_uring_submit_sqe {
    @ops[args->opcode] = count();
}'

# io_uring 완료 지연 시간 히스토그램
bpftrace -e '
tracepoint:io_uring:io_uring_submit_sqe {
    @start[args->req] = nsecs;
}
tracepoint:io_uring:io_uring_complete /@start[args->req]/ {
    @latency_us = hist((nsecs - @start[args->req]) / 1000);
    delete(@start[args->req]);
}'

# CQE 오버플로 감지
bpftrace -e 'kprobe:io_cqring_overflow_flush {
    @overflow = count();
}'

보안 고려사항

io_uring의 강력한 기능은 보안 관점에서 주의가 필요합니다. 공유 메모리를 통한 시스템 콜 우회로 seccomp 필터링이 어렵고, 커널 공격 표면이 넓습니다.

이슈설명대응
seccomp 우회SQPOLL에서 커널 스레드가 I/O 수행 → seccomp 미적용Linux 6.0+: SQPOLL 시 seccomp 적용
권한 상승복잡한 커널 코드 → CVE 다수 발생io_uring_disabled sysctl로 비활성화
리소스 소진대량 SQE 제출 → 메모리/CPU 소비RLIMIT_MEMLOCK으로 mmap 크기 제한
# io_uring 사용 제한 (Linux 5.12.4+)
# 0: 모든 사용자 허용 (기본)
# 1: 권한 없는 사용자 비활성화
# 2: 모든 사용자 비활성화
sysctl -w kernel.io_uring_disabled=1

# Docker/Kubernetes: seccomp 프로파일에서 io_uring_* 차단

Google, Chromium, Docker 등에서 기본 seccomp 프로파일에 io_uring 시스템 콜을 차단하고 있습니다. 컨테이너 환경에서는 io_uring_disabled=1 설정을 권장하며, 필요한 경우 IORING_REGISTER_RESTRICTIONS로 최소 권한만 부여하세요.

io_uring 주요 보안 취약점 사례

io_uring은 Linux 5.1에서 도입된 이후 빠른 기능 확장과 함께 다수의 심각한 보안 취약점이 발견되었습니다. 복잡한 비동기 상태 관리, 커널 스레드 기반의 SQPOLL, 다양한 opcode의 조합 등이 공격 면적을 크게 확장시킵니다. 2021~2023년에 CVE가 집중 발생하여, Google이 Android/ChromeOS에서 io_uring을 완전 비활성화하는 결정을 내리기도 했습니다.

CVE-2021-41073 — io_uring 타입 혼동으로 권한 상승 (CVSS 7.8):

io_uring의 파일 등록 메커니즘에서 IORING_REGISTER_FILESIORING_REGISTER_FILES_UPDATE의 처리 과정에서 파일 디스크립터 타입 검증이 누락되어, 일반 파일 디스크립터를 특수 파일(예: 커널 내부 파일)로 교체할 수 있었습니다. 이를 통해 권한 검사를 우회하고 임의 코드를 실행할 수 있습니다.

CVE-2022-29582 — io_uring timeout UAF (CVSS 7.0):

io_uringIORING_OP_LINK_TIMEOUT 처리에서, 링크된 요청이 완료된 후에도 timeout 요청의 io_kiocb가 해제되지 않은 채 타이머 콜백에서 참조되어 Use-After-Free가 발생합니다. 타이머 만료와 요청 완료 사이의 경쟁 조건이 근본 원인입니다.

CVE-2023-2598 — io_uring 고정 버퍼 범위 초과 (CVSS 7.8):

IORING_REGISTER_BUFFERS로 등록된 고정 버퍼(fixed buffer)의 경계 검사가 불충분하여, coalesced 버퍼에서 범위 밖 읽기/쓰기가 가능합니다. 물리적으로 연속된 페이지를 병합하는 과정에서 길이 계산 오류가 발생합니다.

/* io_uring 취약점 타임라인 (주요 항목) */

/*
 * 2021:
 * CVE-2021-20226  — io_uring close 연산에서 UAF
 * CVE-2021-41073  — 파일 등록 타입 혼동 → 권한 상승
 * CVE-2021-3491   — io_uring PROVIDE_BUFFERS OOB 쓰기
 *
 * 2022:
 * CVE-2022-29582  — LINK_TIMEOUT UAF (타이머 경쟁 조건)
 * CVE-2022-1043   — io_uring sendmsg/recvmsg UAF
 * CVE-2022-2602   — io_uring + Unix socket GC UAF
 *
 * 2023:
 * CVE-2023-2598   — 고정 버퍼 경계 초과
 * CVE-2023-2235   — io_uring timer의 이중 해제
 * CVE-2023-21400  — io_uring 파일 테이블 오프셋 UAF
 *
 * 근본 원인 분류:
 * - Use-After-Free: ~60% (비동기 생명주기 관리 실패)
 * - 경쟁 조건: ~20% (완료/취소/타임아웃 간 race)
 * - 범위 초과: ~15% (버퍼/인덱스 경계 검사 누락)
 * - 타입 혼동: ~5% (파일/소켓 타입 검증 누락)
 */

/* io_uring 비동기 생명주기 관리의 복잡성 */
struct io_kiocb {
    /* 하나의 요청(SQE)에 대한 커널 제어 블록
     * 생명주기:
     *   submit → queued → in_flight → completed → freed
     *
     * 위험 지점:
     * 1. cancel과 complete가 동시에 발생 (race)
     * 2. linked request 체인에서 중간 요청 실패 시 후속 정리
     * 3. SQPOLL 커널 스레드와 사용자 스레드의 동시 접근
     * 4. timeout과 target 요청의 상호 참조 해제
     */
    struct io_ring_ctx *ctx;     /* io_uring 인스턴스 */
    u8 opcode;                    /* 연산 종류 */
    struct io_kiocb *link;       /* 다음 연결된 요청 */
    struct io_tw_state tw;       /* task work 상태 */
    atomic_t refs;                /* 참조 카운트 */
};

/* io_uring 보안 설정 권장 */
# 시스템 전체에서 io_uring 비활성화 (보안 우선 환경)
sysctl -w kernel.io_uring_disabled=2  # 0=허용, 1=비특권 차단, 2=완전 차단

# 비특권 사용자만 차단 (일반 서버)
sysctl -w kernel.io_uring_disabled=1