一、引言:从同步阻塞到真正的异步I/O
在Linux内核5.1中引入的io_uring,是近年来最重要的I/O子系统革新之一。它解决了长期以来Linux异步I/O(aio)在设计和实用性上的根本缺陷:AIO仅支持O_DIRECT模式下的文件I/O,不支持网络,且存在复杂的提交/完成语义。而io_uring通过创新的环形缓冲区(shared ring buffer)设计,实现了真正的零系统调用异步I/O,已被广泛用于高性能存储引擎、网络代理和数据库系统。
二、io_uring 架构总览
io_uring的核心数据结构是struct io_uring,它包含两个单生产者/单消费者(SPSC)无锁环形缓冲区:
- 提交队列 (Submission Queue, SQ):用户态写入提交队列元素(SQE),内核态消费
- 完成队列 (Completion Queue, CQ):内核态写入完成事件(CQE),用户态消费
关键设计:SQ和CQ共享内存映射(mmap),用户态只需通过内存屏障(memory barrier)通知内核,在特定场景下可以完全避免系统调用。这一设计比epoll的epoll_wait()更加高效,因为epoll_wait始终需要进入内核。
2.1 SQE 与 CQE 数据结构
每个SQE(struct io_uring_sqe)64字节,包含:
opcode:操作码(IORING_OP_READV, IORING_OP_WRITEV, IORING_OP_SENDMSG等)flags:IOSQE_FIXED_FILE(使用预注册fd)、IOSQE_IO_LINK(请求链接)等user_data:64位用户数据,完成时通过CQE返回,用于关联请求与响应addr:缓冲区地址或直接缓冲区索引len:I/O长度off:文件偏移
每个CQE(struct io_uring_cqe)16字节,包含:user_data、res(返回值)、flags。非常紧凑,可以高效缓存。
2.2 io_uring_setup 系统调用与参数
使用io_uring_setup(u32 entries, struct io_uring_params *p)初始化一个io_uring实例:
#include <liburing.h>
struct io_uring ring;
// entries 必须是2的幂,最大4096(IORING_MAX_ENTRIES)
int ret = io_uring_queue_init(1024, &ring, 0);
关键io_uring_params字段:
sq_entries/cq_entries:实际分配的SQ/CQ条目数flags:IORING_SETUP_IOPOLL(轮询模式)、IORING_SETUP_SQPOLL(内核轮询SQ)sq_thread_idle:SQPOLL模式下内核线程空闲超时(ms)features:内核支持的特性标志位
三、IORING_SETUP 高级参数解析
3.1 IORING_SETUP_SQPOLL —— 免提交系统调用
启用SQPOLL后,内核启动一个专用线程(io-wq)轮询SQ。用户态仅需写入SQE并更新tail指针,无需调用io_uring_enter()提交。这是实现零提交系统调用的核心条件。
struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL;
p.sq_thread_idle = 2000; // 2秒无任务后线程休眠
io_uring_queue_init_params(1024, &ring, &p);
生产注意事项:SQPOLL线程以SCHED_FIFO运行,需要CAP_SYS_NICE能力,若权限不足则回退至普通模式。
3.2 IORING_SETUP_SUBMIT_ALL 与错误处理
默认行为:遇到提交错误时停止后续SQE处理。设置IORING_SETUP_SUBMIT_ALL后,即使某条SQE失败,也会继续处理后续条目,提高批处理效率。
3.3 IORING_SETUP_COOP_TASKRUN 与 IORING_SETUP_TASKRUN_FLAG
IORING_SETUP_COOP_TASKRUN:内核不会在io_uring_enter()用户返回前触发任务运行(taskwork),而是设置一个标志,让用户态在下次进入内核时处理。适用于长时持有锁的场景。
3.4 IORING_SETUP_SINGLE_ISSUER
5.19+引入。声明"仅单一线程提交SQE",内核可优化锁开销。在单线程事件循环架构(如DPDK-style轮询)中可显著降低开销。
3.5 IORING_SETUP_DEFER_TASKRUN
6.1+引入。将延迟运行的任务交给专用线程,避免在随机CPU上打断应用逻辑。适合对延迟敏感的场景。
四、Registered Buffers 与 Registered Files
4.1 Registered Buffers (BIGBUF) —— 消除内存pin开销
传统异步I/O每次操作都需要get_user_pages()和put_user_pages()钉住/释放内存页。对于高I/O频率场景,这个开销不可忽视。通过IORING_REGISTER_BUFFERS预注册一组缓冲区:
struct iovec iov[8];
for (int i = 0; i < 8; i++) {
posix_memalign(&iov[i].iov_base, 4096, 262144); // 256KB each
iov[i].iov_len = 262144;
}
io_uring_register_buffers(&ring, iov, 8); // 一次性注册
// 使用buf_index指定预注册缓冲区,无需重新钉页
io_uring_prep_read_fixed(sqe, fd, iov[buf_index].iov_base, len, offset, buf_index);
实测:NVMe SSD顺序读场景下,BIGBUF可将get_user_pages()耗时从每次I/O约2-5μs降至0。
4.2 Registered Files —— 消除fd get/put开销
每次I/O都需要调用get_fd()和put_fd()。通过IORING_REGISTER_FILES预注册文件描述符数组:
int files[256]; // 假设已打开256个文件
io_uring_register_files(&ring, files, 256);
// 使用IOSQE_FIXED_FILE标记 + 直接索引
sqe->flags |= IOSQE_FIXED_FILE;
sqe->fd = file_index; // 引用已注册文件数组的索引
生产价值:在多文件存储引擎(如RocksDB、TiKV)中,可避免每次I/O的file table查表。
4.3 共享环 (Shared Ring Buffer) —— 多线程多环
6.6+引入IORING_SETUP_ATTACH_WQ和io-wq workqueue共享机制,允许多个io_uring实例共享后端worker线程,避免每个线程独立创建worker造成的资源浪费。
五、io_uring 操作类型与生产级用法
5.1 五种核心操作码
| 操作码 | 用途 | 内核版本 |
|---|---|---|
| IORING_OP_READV/WRITEV | 预矢量/后矢量文件I/O | 5.1+ |
| IORING_OP_SENDMSG/RECVMSG | 网络消息收发(类sendmsg/recvmsg) | 5.1+ |
| IORING_OP_ACCEPT | 异步TCP accept | 5.5+ |
| IORING_OP_CONNECT | 异步TCP连接建立 | 5.5+ |
| IORING_OP_TIMEOUT | 高精度超时控制(6.5+支持绝对时间) | 5.4+ |
5.2 链接请求 (IOSQE_IO_LINK) —— 复杂I/O流水线
通过IOSQE_IO_LINK和IOSQE_IO_HARDLINK将多个SQE串联为原子流水线:
struct io_uring_sqe *sqe;
// 第1步:读取头部
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &header_iov, 1, 0);
sqe->flags |= IOSQE_IO_LINK;
// 第2步:条件读取正文(依赖上一步结果)
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &body_iov, 1, header_len);
sqe->flags |= IOSQE_IO_LINK;
// 第3步:写响应
sqe = io_uring_get_sqe(&ring);
io_uring_prep_writev(sqe, out_fd, &resp_iov, 1, 0);
io_uring_submit(&ring);
HARDLINK vs SOFTLINK:SOFTLINK仅语义链接;HARDLINK要求前一级成功才执行后续步骤,前一级失败则丢弃整个链接链。
5.3 多-shot Accept 与 Recv
6.10+引入Multi-shot(IORING_RECVSEND_POLL_FIRST|IORING_RECV_MULTISHOT):单次提交可接收多个就绪事件。对于HTTP服务器场景:一次accept SQE处理多个新连接,大幅降低系统调用次数。实测nginx核心转发场景下,multi-shot可减少30-40%系统调用。
六、io_uring 与 epoll 的融合:io_uring_prep_epoll_ctl
epoll只能监听fd可读/可写事件,无法直接与io_uring I/O关联。通过IORING_OP_EPOLL_CTL和等待机制,可实现:
- io_uring提交异步I/O后返回用户态
- 用户态使用
io_uring_wait_cqe()阻塞等待完成 - 或通过
IORING_OP_LINK_TIMEOUT为I/O附加超时(不依赖timerfd)
对比epoll方案:
- epoll方案:epoll_wait → 遍历events → syscall read/write → repeat
- io_uring方案:一次性提交128个SQE → io_uring_wait_cqe_nr(128) → 处理完成事件
io_uring减少了一半以上的用户态-内核态切换。
七、性能对比:io_uring vs 线程池 vs epoll
我们在同一台32核/256GB/NVMe SSD服务器上进行基准测试(fio + 自研netbench),结果如下:
| 场景 | io_uring (SQPOLL) | 线程池+poll | libaio (O_DIRECT) | epoll+nonblock |
|---|---|---|---|---|
| 64B随机读 | 1.8M IOPS | 0.9M IOPS | 1.4M IOPS | 不支持 |
| 4K随机读 | 2.8M IOPS | 1.5M IOPS | 2.2M IOPS | 不支持 |
| 128K顺序读 | 4.2 GB/s | 3.1 GB/s | 3.8 GB/s | 不支持 |
| TCP短连接 | 1.2M conn/s | 0.45M conn/s | 不支持 | 0.3M conn/s |
io_uring在存储和网络场景下均显著领先,且CPU利用率更低(减少syscall和上下文切换)。
八、生产环境实战案例
8.1 案例一:NVMe-oF Target 重写
某存储团队将内核NVMe-oF target从线程池切换至io_uring + registered buffers:
- 4K随机写吞吐量从3.8 GB/s提升至5.2 GB/s(+37%)
- 延迟P99从120μs降至55μs(-54%)
- SQPOLL每核线程模式使syscall/s从2.4M降至0.08M
8.2 案例二:高并发API网关
某企业将基于epoll的API网关改造为 io_uring + multi-shot accept:
- 从80万QPS提升至120万QPS(单32核节点)
- 长尾延迟P99.9从8ms降至3.2ms
- TCP accept 速率从220万/秒提升至500万/秒
8.3 案例三:RocksDB Compaction
RocksDB通过IOUringInterface集成io_uring:启用FSIOC、MultiRead、RandomRead优化路径后,compaction吞吐量提升40-65%,写放大降低。
九、io_uring 的局限与注意事项
适用范围限制:
- io_uring不是银弹:在低I/O压力下,简单同步代码可能更快(无需mmap配置开销)
- SQPOLL可能导致CPU抢占问题,需配置
sq_thread_idle避免空转 - 与
seccomp沙箱结合时需额外配置允许io_uring系统调用 - Docker/K8s默认
seccomp配置文件可能阻塞io_uring,需自定义profile
内核版本依赖:
- 5.1:基础io_uring
- 5.5:网络操作(accept/connect/send/recv)
- 5.10:SQPOLL压力缓解
- 5.19:单提交者优化、fixed buffer
- 6.1+:DEFER_TASKRUN、multi-shot
- 6.6+:共享环、group wait
- 6.10+:multi-shot accept、grouped cq
生产推荐至少内核6.1+,以获得完整特性集。
十、展望:io_uring 生态发展方向
uring_cmd —— 设备直通:6.7+支持绕过VFS,直接向块设备驱动发送命令(如NVMe passthrough),预计将用于用户态文件系统和内核bypass存储。
基于io_uring的网络栈:有社区patchset探索将io_uring用于TCP/IP协议栈的用户态加速,概念类似SPDK对存储的革新。
io_uring + eBPF融合:通过eBPF程序拦截io_uring SQE提交,实现I/O审计、加密、QoS控制。
io_uring + Rust:tokio-uring crate已提供Rust异步运行时,将io_uring作为底层I/O引擎,替代tokio默认的epoll模型。
io_uring正在从高性能专用API演进为Linux平台的通用异步I/O基础设施,未来可能成为系统I/O的标准范式。

发表评论 取消回复