前言: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 映射到用户态和内核态共享的内存区域:
| 组件 | 全称 | 方向 | 职责 |
|---|---|---|---|
| SQ | Submission Queue | 用户→内核 | 存放待提交的系统调用请求 |
| SQE | Submission Queue Entry | — | 单个提交队列条目(64 字节) |
| CQE | Completion Queue Entry | — | 单个完成队列条目(16 字节) |
| CQ | Completion Queue | 内核→用户 | 存放已完成的 IO 结果 |
工作流程:
- 用户态构造 SQE 到 SQ 尾部,推动 SQ tail pointer
- 内核消费 SQE,执行 IO 操作
- 完成后写入 CQE 到 CQ 尾部,推动 CQ head pointer
- 用户态直接读取 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/pwrite | 180K | 176 μs | 2 |
| preadv2 + NOWAIT | 220K | 145 μs | 2 |
| io_uring 基础模式 | 320K | 98 μs | 1(批次) |
| io_uring SQPOLL | 410K | 76 μs | 0(批量提交时) |
| io_uring SQPOLL+IOPOLL+Fixed | 1.2M | 5.8 μs | 0 |
七、常见陷阱与排错
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 等高级特性。

发表评论 取消回复