跳转至

io_uring

是什么 / 解决什么问题

io_uring 是 Linux 5.1+ 引入的异步 I/O 框架。通过用户态与内核共享的环形缓冲区(ring buffer)提交和完成 I/O 请求,避免每次操作都陷入内核。它是第一个真正通用的异步 I/O 接口——支持磁盘、网络、定时器等几乎所有操作。

为什么需要 io_uring

已有方案 局限性
epoll + 非阻塞 对普通文件无效;每次 read/write 仍是 syscall
Linux AIO (io_submit) 只支持 O_DIRECT 文件;API 设计差
线程池模拟 线程开销大;调度不可控
POSIX AIO (aio_read) glibc 用线程池实现,性能差

io_uring 一次解决所有问题:文件和网络统一、零拷贝提交、批量操作、无需 syscall。

核心概念

用户态                              内核态
┌─────────────┐                ┌─────────────┐
│ Submission  │  ──提交请求──►  │             │
│ Queue (SQ)  │                │  io_uring   │
└─────────────┘                │  内核工作线程 │
┌─────────────┐                │             │
│ Completion  │  ◄──完成通知──  │             │
│ Queue (CQ)  │                └─────────────┘
└─────────────┘

SQE = Submission Queue Entry(提交项)
CQE = Completion Queue Entry(完成项)
  • SQ (Submission Queue):用户态写入 SQE,告诉内核"我要做什么操作"
  • CQ (Completion Queue):内核写入 CQE,告诉用户态"操作完成了,结果是什么"
  • 共享内存:SQ/CQ 通过 mmap 共享,读写无需 syscall

使用模式

基本流程(使用 liburing)

#include <liburing.h>

struct io_uring ring;

// 1. 初始化(队列深度 256)
io_uring_queue_init(256, &ring, 0);

// 2. 获取一个 SQE 槽位
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);

// 3. 填充操作(这里是 read)
io_uring_prep_read(sqe, fd, buf, buf_size, offset);
io_uring_sqe_set_data(sqe, my_context);  // 关联用户数据

// 4. 提交到内核
io_uring_submit(&ring);

// 5. 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);

// 6. 处理结果
int result = cqe->res;  // 等价于 read 的返回值
void *ctx = io_uring_cqe_get_data(cqe);

// 7. 标记 CQE 已消费
io_uring_cqe_seen(&ring, cqe);

// 清理
io_uring_queue_exit(&ring);

批量提交(高吞吐)

// 一次提交多个操作
for (int i = 0; i < batch_size; i++) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fds[i], bufs[i], sizes[i], 0);
    io_uring_sqe_set_data(sqe, &contexts[i]);
}
// 一次 syscall 提交所有
io_uring_submit(&ring);

// 批量收割完成事件
struct io_uring_cqe *cqes[batch_size];
int completed = io_uring_peek_batch_cqe(&ring, cqes, batch_size);
for (int i = 0; i < completed; i++) {
    process_result(cqes[i]);
}
io_uring_cq_advance(&ring, completed);

网络服务器(替代 epoll)

// io_uring 可以直接做 accept、recv、send,不需要 epoll
void setup_accept(struct io_uring *ring, int listen_fd) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_multishot_accept(sqe, listen_fd, NULL, NULL, 0);
    io_uring_sqe_set_data(sqe, ACCEPT_TAG);
    // multishot:一次注册,持续接受连接
}

void setup_recv(struct io_uring *ring, int fd, char *buf, size_t len) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_recv(sqe, fd, buf, len, 0);
    io_uring_sqe_set_data(sqe, conn_context);
}

void setup_send(struct io_uring *ring, int fd, char *buf, size_t len) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_send(sqe, fd, buf, len, 0);
    io_uring_sqe_set_data(sqe, conn_context);
}

// 事件循环
for (;;) {
    io_uring_submit_and_wait(&ring, 1);

    struct io_uring_cqe *cqe;
    unsigned head;
    io_uring_for_each_cqe(&ring, head, cqe) {
        void *tag = io_uring_cqe_get_data(cqe);
        if (tag == ACCEPT_TAG) {
            int conn_fd = cqe->res;  // 新连接 fd
            setup_recv(&ring, conn_fd, ...);
        } else {
            handle_io_completion(tag, cqe->res);
        }
    }
    io_uring_cq_advance(&ring, count);
}

文件 I/O(io_uring 的核心优势场景)

// 读取文件无需 O_DIRECT,page cache 也能异步
int fd = open("data.bin", O_RDONLY);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, 4096, offset);
io_uring_submit(&ring);
// 不阻塞!即使 page cache miss 也在后台完成

这解决了 epoll 对普通文件无效的问题。

行为与语义

操作类型支持(部分列表)

类别 操作
文件 read, write, readv, writev, fsync, fallocate
网络 accept, connect, recv, send, recvmsg, sendmsg
高级 splice, tee, shutdown, close, openat, statx
定时 timeout, link_timeout
其他 nop, cancel, poll_add (兼容 epoll 语义)

SQE 标志

// 链式操作:上一个成功才执行下一个
sqe->flags |= IOSQE_IO_LINK;
// 例:read → process → write 形成链

// 固定文件:避免每次操作的 fd 查找开销
io_uring_register_files(&ring, fds, nr_fds);
sqe->flags |= IOSQE_FIXED_FILE;

// drain:等前面所有操作完成再执行
sqe->flags |= IOSQE_IO_DRAIN;

Setup 标志

// SQPOLL 模式:内核轮询 SQ,用户态提交无需 syscall
struct io_uring_params params = { .flags = IORING_SETUP_SQPOLL };
io_uring_queue_init_params(256, &ring, &params);
// 此模式下 io_uring_submit() 不进内核(如果内核线程还在轮询)

// 注意:SQPOLL 会占用一个 CPU 核心持续轮询

CQE 结果

cqe->res   // 操作结果,语义等同于对应 syscall 的返回值
            // read: 读取字节数 或 -errno
            // accept: 新 fd 或 -errno
cqe->flags  // 额外标志(如 IORING_CQE_F_MORE 表示 multishot 还有后续)

性能考量

io_uring vs epoll 性能对比

维度 epoll + 非阻塞 io_uring
syscall 次数 每次 read/write 都是 syscall 批量提交,可能 0 syscall (SQPOLL)
磁盘文件 不支持异步 完整异步支持
上下文切换 epoll_wait 必须进内核 SQPOLL 模式可避免
网络吞吐 更高(减少 syscall 开销)
延迟 更低(SQPOLL 消除了进入内核的延迟)
复杂度

什么时候 io_uring 更快

  1. 大量小 I/O 操作 — syscall 开销占比高时,批量提交优势明显
  2. 混合文件+网络 — 统一模型,无需线程池处理文件 I/O
  3. 超低延迟场景 — SQPOLL 避免用户/内核切换
  4. 高 IOPS 存储 — NVMe SSD 的 IOPS 瓶颈在软件而非硬件

什么时候 epoll 够用

  • 连接数中等(< 万级)且请求不密集
  • 纯网络服务(无磁盘 I/O)
  • 内核版本 < 5.1
  • 追求代码简洁性

调优参数

// 队列深度:根据并发量调整
io_uring_queue_init(4096, &ring, 0);  // 高并发场景

// 注册固定 buffer,避免每次 I/O 的 page pin/unpin
struct iovec iovs[N] = { ... };
io_uring_register_buffers(&ring, iovs, N);
io_uring_prep_read_fixed(sqe, fd, buf, len, offset, buf_index);

// 注册固定文件,减少 fd 引用计数操作
int fds[M] = { ... };
io_uring_register_files(&ring, fds, M);

常见陷阱与 FAQ

1. 忘记检查内核版本

不同 io_uring 功能需要不同最低版本:

功能 最低版本
基本 io_uring 5.1
SQPOLL 5.4
注册固定 buffer 5.1
multishot accept 5.19
send_zc (零拷贝发送) 6.0
// 运行时检测
struct io_uring_probe *probe = io_uring_get_probe_ring(&ring);
if (!io_uring_opcode_supported(probe, IORING_OP_SEND_ZC)) {
    // 回退到普通 send
}

2. SQ 满了导致 get_sqe 返回 NULL

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
    // SQ 满了,先提交再重试
    io_uring_submit(&ring);
    sqe = io_uring_get_sqe(&ring);
}

3. CQE 不及时消费导致溢出

CQ 大小默认是 SQ 的 2 倍。如果不及时消费 CQE,新完成的操作会被丢弃:

// 定期消费 CQE
io_uring_for_each_cqe(&ring, head, cqe) {
    // 处理
}
io_uring_cq_advance(&ring, count);

// 或设置更大的 CQ
params.flags |= IORING_SETUP_CQSIZE;
params.cq_entries = 8192;

4. SQPOLL 需要权限

// SQPOLL 需要 CAP_SYS_ADMIN 或设置了 sq_thread_idle
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 1000;  // 空闲 1s 后停止轮询,有新提交自动恢复

5. 链式操作中间失败

// IOSQE_IO_LINK 链中任一操作失败,后续操作会被取消(CQE.res = -ECANCELED)
// 需要处理 -ECANCELED 情况

观测与调试

# 查看进程是否使用 io_uring
ls /proc/<pid>/fdinfo/ | xargs grep -l io_uring

# strace 看 io_uring syscall
strace -e trace=io_uring_setup,io_uring_enter,io_uring_register -p <pid>

# 查看 io_uring 内核统计
cat /proc/<pid>/fdinfo/<uring_fd>
# 输出含 SqEntries, CqEntries, SqThreadCpu 等

# bpftrace 统计 io_uring 提交频率
bpftrace -e '
tracepoint:io_uring:io_uring_submit_sqe {
    @ops[args->opcode] = count();
}'

# 查看 io_uring 完成延迟
bpftrace -e '
tracepoint:io_uring:io_uring_complete {
    @latency = hist(nsecs - @start[args->ctx]);
}'

延伸阅读