前言:IO 模型演进的必然

Linux 的 IO 模型演进路线清晰地指向一个目标:更低延迟、更高吞吐、更少系统调用。从阻塞 IO 到 select/poll,再到 epoll,每一次进步都在减少内核与用户态之间的往返开销。然而 aio 作为 Linux 原生的异步 IO 接口,却长期存在设计缺陷——仅支持 O_DIRECT 模式下的文件 IO,对网络 IO 无效,提交和完成都需要系统调用,远未达到"异步"的理想状态。

Jens Axboe 从 2019 年开始设计 io_uring,核心理念是将提交和完成彻底解耦到共享内存,实现真正的零系统调用异步 IO。经过五年演进,io_uring 已合并进 Linux 5.1+,成为高性能存储、网络、计算场景的事实标准——NVIDIA、Meta、Cloudflare、ScyllaDB 均已大规模部署。

一、io_uring 架构总览

io_uring 的核心是两个无锁环形缓冲区(ring buffer),通过 mmap 映射到用户态和内核态共享的内存区域:

组件全称方向职责
SQSubmission Queue用户→内核存放待提交的系统调用请求
SQESubmission Queue Entry—单个提交队列条目(64 字节)
CQECompletion Queue Entry—单个完成队列条目(16 字节)
CQCompletion Queue内核→用户存放已完成的 IO 结果

工作流程:

  1. 用户态构造 SQE 到 SQ 尾部,推动 SQ tail pointer
  2. 内核消费 SQE,执行 IO 操作
  3. 完成后写入 CQE 到 CQ 尾部,推动 CQ head pointer
  4. 用户态直接读取 CQ 中的完成事件,全程无需 syscall

仅当需要通知内核"有新任务"或"需要收割完成事件"时,才调用 io_uring_enter。配合 IORING_SETUP_SQPOLL 内核轮询模式,可以实现完全零系统调用的异步 IO。

二、核心数据结构

io_uring 实例通过 io_uring_setup() 创建,返回一个 fd,同时映射两个 ring buffer:

struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL;       // 内核线程轮询 SQ
p.sq_thread_idle = 2000;             // 空闲 2s 后睡眠

int ring_fd = io_uring_setup(QD, &p);

// mmap SQ 数组、SQE 数组、CQ 数组
sqe = mmap(0, p.sq_off.array + p.sq_entries * sizeof(__u32),
            PROT_READ|PROT_WRITE, MAP_SHARED|MAP_POPULATE,
            ring_fd, IORING_OFF_SQES);
sqring = mmap(0, p.sq_ring_offset, ...);
cqring = mmap(0, p.cq_ring_offset, ...);

关键字段索引(无锁环形队列操作):

// 提交端(生产者:用户态)
__u32 *sq_head = sqring + p.sq_off.head;
__u32 *sq_tail = sqring + p.sq_off.tail;
__u32 *sq_mask = sqring + p.sq_off.ring_mask;

// 收割端(消费者:用户态)
__u32 *cq_head = cqring + p.cq_off.head;
__u32 *cq_tail = cqring + p.cq_off.tail;

三、零拷贝缓冲区注册

传统 IO 每次读写都需要内核分配临时 buffer、DMA 拷贝。io_uring 支持预注册缓冲区池(Fixed Buffers),消除每次 IO 的内存分配开销:

// 1. 注册一个 1GB 的连续内存区域
struct iovec iov = { .buf = hugepage_mem, .len = 1<<30 };
io_uring_register_buffers(&ring, &iov, 1);

// 2. 后续 IO 使用固定 buffer index=0
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
sqe->flags |= IOSQE_FIXED_BUF;       // 使用注册的 buffer
sqe->buf_index = 0;

性能收益:

  • 消除每次 IO 的 get_user_pages/put_user_pages(约 500ns/IO)
  • 配合 IORING_REGISTER_BUFFERS2 支持稀疏注册(非连续内存)
  • 配合 IORING_REGISTER_PBUF_RING 支持环形 buffer 池(per-IO 独立回收)

四、高级特性深度解析

4.1 链式请求(Linked SQE)

链式请求保证执行顺序和因果依赖。典型场景:先 read 再 write 的 pipe:

struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe1, src_fd, buf, len, 0);
sqe1->flags |= IOSQE_IO_LINK;  // 开启链

struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe2, dst_fd, buf, len, 0);
sqe2->flags |= IOSQE_IO_LINK;  // 继续链

// sqe1 失败则 sqe2 被取消(FAIL_LINK)
io_uring_submit(&ring);

4.2 轮询模式(SQPOLL + IOPOLL)

  • SQPOLL:内核线程自动轮询 SQ,用户态 io_uring_submit() 直接推 tail 指针即可,零 syscall 提交
  • IOPOLL:内核使用 blk-mq polling 收割完成事件(绕过 IRQ),NVMe 可达 < 2μs 延迟
  • 组合使用:SQPOLL + IOPOLL + Fixed Buffers = 真正的零系统调用 IO

4.3 缓冲区选择(Buffer Selection)

预注册多个 buffer 后,io_uring 内核侧可以自动选择空闲 buffer,完成后告知用户使用了哪个:

sqe->buf_group = 1;          // buffer group ID
sqe->flags |= IOSQE_BUFFER_SELECT;
// 完成后 cqe->flags & IORING_CQE_F_BUFFER 表示选中的 buf index

4.4 注册文件(Fixed Files)

避免每次 IO 的 fd get/put 开销:

int fds[] = { fd1, fd2, fd3 };
io_uring_register_files(&ring, fds, 3);

sqe->flags |= IOSQE_FIXED_FILE;   // 使用注册槽位而非真实 fd
sqe->fd = 0;                       // 索引=0 → fd1

五、实战:构建 io_uring HTTP Server

以下是基于 io_uring 的最小 TCP echo server 架构(伪代码):

void server_loop(struct io_uring *ring) {
    // 1. 预先提交 Accept 请求
    submit_accept(ring, listen_fd);

    while (1) {
        // 2. 收割完成事件(可零 syscall 等)
        struct io_uring_cqe *cqe;
        int head = *cqring->head;
        while (head != *cqring->tail) {
            cqe = &cqring->cqes[head & cqring->mask];
            head++;
            
            if (cqe->user_data == OP_ACCEPT) {
                int client_fd = cqe->res;
                submit_accept(ring, listen_fd);        // 重新监听
                submit_recv(ring, client_fd);          // 开始读
            } 
            else if (cqe->user_data == OP_RECV) {
                if (cqe->res <= 0) submit_close(ring, fd);
                else submit_send(ring, fd, buf, len);  // 回写
            }
            else if (cqe->user_data == OP_SEND) {
                submit_recv(ring, fd);                 // 继续读
            }
            
            io_uring_cq_advance(ring, 1);
        }
        
        // 3. 仅当需要时才调用 enter
        io_uring_enter(ring, 0, 1, IORING_ENTER_GETEVENTS);
    }
}

六、性能基准测试

以下数据来自 Cloudflare 公开测试(NVMe SSD,32 深度队列):

模式IOPS(随机读 4KiB)平均延迟系统调用/IO
pread/pwrite180K176 μs2
preadv2 + NOWAIT220K145 μs2
io_uring 基础模式320K98 μs1(批次)
io_uring SQPOLL410K76 μs0(批量提交时)
io_uring SQPOLL+IOPOLL+Fixed1.2M5.8 μs0

七、常见陷阱与排错

7.1 CQ 溢出(CQE_OVERFLOW)

当 CQ 满时,新完成事件会溢出到 overflow_list(代价高)。解决:适当放大 cq_entries,或异步收割 + 批量处理。

7.2 SQPOLL CPU 亲和性

SQPOLL 内核线程可能在小核上旋转。通过 IORING_SETUP_ATTACH_WQ 绑定到大核工作队列,或设置 sq_thread_cpu。

7.3 内存序(Memory Ordering)

io_uring 使用 release/acquire 语义。用户态读取 CQE 数据前必须确保看到正确的 tail 指针(通过 atomic_thread_fence(memory_order_acquire))。

7.4 兼容性与 seccomp

容器场景需允许 io_uring_setup/io_uring_enter/io_uring_register 三个 syscall。Docker 默认策略在 Linux 6.6+ 前会拦截。

八、未来方向

  • netlink io_uring:将 netlink 异步化,加速网络管理
  • SPDK 深度集成:绕过 block layer 直达 NVMe
  • Rust io_uring 生态:tokio-uring、gli 提供更安全的封装
  • io_uring 6.9+:新增 IORING_MSG_RING_DATA 和增强型 bind/listen

总结

io_uring 不是"又一个异步库",而是 Linux 内核 IO 架构的范式转移。它将共享无锁队列 + 用户态驱动 + 内核侧协作三者结合,实现了真正意义上零开销的异步 IO。对于追求极致性能的基础设施软件,掌握 io_uring 已经从加分项变为必备技能。

学习建议:从 liburing 源码入手,理解 ring buffer 操作后,再逐步尝试 Fixed Buffers、Buffer Selection 等高级特性。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部