一、io_uring 概述
io_uring 是 Linux 内核 5.5 引入的全新异步 I/O 框架,由 Jens Axboe 开发(同时也是 block 层和 io_uring 的维护者)。它通过共享 ring buffer 的方式实现用户态与内核态之间的零拷贝通信,将传统 read/write 系统调用的异步延迟从每次约 3μs 降至约 700ns。相比 libaio 仅支持直接 I/O(O_DIRECT)且功能受限的情况,io_uring 支持缓冲 I/O、格式化 I/O、几乎涵盖了所有文件操作。
二、核心架构:双 ring buffer
io_uring 的核心设计是两块共享内存区域:SQ(Submission Queue)负责提交 I/O 请求,CQ(Completion Queue)负责收集完成通知。SQ 由用户态写入 SQE(Submission Queue Element),CQ 由内核态写入 CQE(Completion Queue Element)。两者共享一块内核分配的内存(通过 mmap),无需 ioctl 系统调用即可完成请求提交与完成接收。
三、初始化与固定缓冲区
3.1 队列初始化
struct io_uring_params p = { };
io_uring_queue_init(QUEUE_DEPTH, &ring, &p);
3.2 缓冲区注册
对于大块且长期存活的 I/O 缓冲区,必须通过 io_uring_register(IORING_REGISTER_BUFFERS) 预注册一组缓冲区,后续 read/write 请求采用 fixed buffer 模式,避免每次 IO 都要 get_user_pages + pin 页的开销。
struct iovec iovecs[] = { { .iov_base = buf, .iov_len = 4096 } };
io_uring_register(&ring, IORING_REGISTER_BUFFERS, iovecs, 1);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, buf, 4096, 0, buf_index, 0);
四、批量提交与链接请求
4.1 单次批量提交
// 写入多个 SQE
for (int i = 0; i < num; i++) {
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fds[i], bufs[i], 4096, 0);
sqe->user_data = (uint64_t)i;
}
// 提交 num 个请求给内核
io_uring_submit(&ring);
// 收集完成
io_uring_wait_cqe(&ring, &cqe);
当 SQ 的容量耗尽(即环形缓冲区满)后,可连续处理的话,io_uring_submit 会隐式驱动提交。无需单独写入后再 io_uring_submit 一次。
4.2 IOSQE_IO_LINK 链接请求
利用 IOSQE_IO_LINK flag 可以将多个 SQE 串联成链,例如先 write 后 fsync,内核保证前一个完成后才执行下一个,避免显式同步。
sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, fd, buf, len, 0);
sqe->flags |= IOSQE_IO_LINK;
sqe = io_uring_get_sqe(&ring);
io_uring_prep_fsync(sqe, fd, 0);
五、liburing 封装与 io_uring_cmd
5.1 liburing 核心接口
liburing 封装了原始 syscall,提供了简洁 API,包括:io_uring_queue_init、io_uring_get_sqe、io_uring_submit、io_uring_wait_cqe、io_uring_peek_cqe 等。这些接口大幅降低了使用门槛。
5.2 io_uring_cmd 自定义命令
Linux 6.6 引入 io_uring_cmd,支持在用户态定义新的 io_uring 操作码。此次要被 io_uring 开创一个内核服务阶段,即可写 SQE 后通过 CQE 获取结果。这为文件系统、网络栈、甚至 GPU 计算提供了统一的异步入口。
六、内核协作与 SQE 批量优化
io_uring 充分利用内核中的 IO 列队例行等待·可以对一个方向中的多个 I/O 请求进行注册,内核对这些请求进行排序和合并,显著提高顺序存储的 IO 吞吐和 IOPS,内核层的分析和优化,本质上将多次 IO 打包成 2 次(排队)。
七、io_uring 与 epoll 性能对比
- epoll:基于事件回调模型,仅适用于网络事件处理,文件 IO 仍需调用系统调用
- io_uring:对比 epoll+I/O 复用,在高并发 read/write 场景下 IOPS 提升约 70%,延迟降低约 30%
- IODING(IORING_SETUP_IOPOLL):轮询模式完全向内核态分离开销,避免用户态切换和中断开销
- 混合方案:在网络+文件混合 IO 场景下,可以 io_uring + epoll 配合使用,io_uring 处理文件 IO,epoll 处理网络事件
八、生产实践场景与踩坑清单
8.1 适用场景
- 高性能存储引擎:RocksDB、ScyllaDB 等已经在系统调用层引入 io_uring,从而将 fsync 和 read 延迟降至微秒级
- 低延迟网络服务器:Nginx 通过 io_uring 模式处理文件 IO,在可运行时环境下多事集
- 数据库 WAL 写入:通过 IOSQE_IO_LINK 将 write + fsync 链接提交,保证写入顺序和持久化
8.2 踩坑清单
- SQE 资源溢出:不正确的队列深度配置可能导致 SQE 短缺,布置时要确保 ring 大小和并发工作的匹配
- 内核版本要求:基础功能需要 Linux 5.1+,完整的 io_uring_cmd 需 6.6+,生产环境都应检查内核版本
- 关联划线处理:高并发场景下,硬件的关联划线与 ring buffer 的磨合也会带来一定开销,在设计同时将工作写入 SQE 的硬件关联划线
- Fixed buffer 生命周期:注册的缓冲区必须保持有效直到 io_uring_unregister_buffers 被调用,否则内核访问无效内存会导致系统崩溃
- IO 合并限制:并非所有设备和文件系统都支持 IO 合并,需针对特定存储介质测试合并效果
九、总结
io_uring 是 Linux 异步 I/O 的未来向量和。除了在高速存储和应用服务器中已有广泛应用外,io_uring_cmd 和本地 PSI(Proactor 异步架构)的开发也在加速。随着内核版本的逐步升级,io_uring 将有望完全替代 epoll 成为 Linux IO 处理的事实标准。对于追求极致 IO 性能的系统来说,拥抱 io_uring 已不再是选择题,而是必答题。

发表评论 取消回复