背景
Linux 的异步 I/O 一直是个尴尬的话题。
POSIX AIO(aio_read/aio_write)在 glibc 中是用户态线程池模拟,并非真正的内核异步。Linux 原生 AIO(io_submit/io_getevents)虽然在内核中实现,但限制极多——仅支持 O_DIRECT 模式,文件系统需要对齐到扇区大小,小文件场景几乎无法使用,且每个提交仍然涉及多次系统调用。select/poll/epoll 解决了网络 I/O 的事件通知问题,但读写操作本身还是同步阻塞的。
2019 年,Jens Axboe(Linux 块层维护者)在 5.1 内核中引入了 io_uring,彻底改变了这个局面。它不只是一个新的系统调用,而是一套全新的异步 I/O 架构:通过内核与用户态共享环形缓冲区来实现通信,将系统调用开销降到最低,支持缓冲区管理、文件注册、请求链接等高级特性。
截至 2026 年的主流内核,io_uring 已经支持超过 70 种操作码,覆盖文件读写、网络 socket、定时器、futex 等待、NVMe 直通等几乎所有 I/O 场景,成为 Linux 高性能编程的基石。
核心原理
共享环形缓冲区架构
传统系统调用的代价比许多人想象的要高。单次 read() 需要保存/恢复寄存器、切换页表、刷新 TLB、执行 Spectre/Meltdown 缓解代码——在现代内核上,一次空系统调用大约需要 50–150ns,而真正的 I/O 操作还会涉及数据拷贝。io_uring 的设计目标就是在 I/O 密集场景下完全消除这些开销。
io_uring 的核心是两个共享环形缓冲区:
| |
- SQ(Submission Queue):用户把 I/O 请求(SQE)写入 SQ 的 tail,内核从 head 读取。
- CQ(Completion Queue):内核把完成事件(CQE)写入 CQ 的 tail,用户从 head 读取。
通信几乎不依赖系统调用来传输数据本身——只需少量内存屏障保证一致性。
三个系统调用
io_uring 的完整生命周期仅需三个系统调用:
| 系统调用 | 用途 | 备注 |
|---|---|---|
io_uring_setup | 创建 io_uring 实例,初始化 SQ/CQ | 返回 fd,SQ/CQ 通过 mmap 映射到用户空间 |
io_uring_enter | 通知内核处理已提交的 SQE,可选等待完成 | 在 SQ 轮询模式下可以完全不调用 |
io_uring_register | 注册文件描述符、缓冲区等资源 | 减少内核内部查找开销 |
SQE 与 CQE
**提交队列条目(SQE)**描述了要执行的 I/O 操作:
| |
**完成队列事件(CQE)**简洁得多:
| |
因为 I/O 请求可以乱序完成,user_data 用于关联提交和完成。
IORING_SETUP_SQPOLL:零系统调用模式
io_uring 最激进的设计是 SQ 轮询(SQ Polling)。开启 IORING_SETUP_SQPOLL 后,内核会启动一个内核线程持续轮询 SQ,用户只需往共享缓冲区写入 SQE,内核线程自动取走处理——完全不需要调用 io_uring_enter。
这意味着在理想情况下,I/O 操作可以做到零系统调用。对于 IOPS 敏感的场景(如 NVMe SSD、高速网络),这是量级的性能提升。根据 Jens Axboe 的论文数据,在轮询模式下 io_uring 可达 1.7M 4K IOPS,而传统 AIO 仅 608K IOPS(快约 2.8 倍)。
内核 5.17+ 重要优化
IORING_SETUP_COOP_TASKRUN(5.17+):配合IORING_SETUP_SINGLE_ISSUER(6.0+),减少跨核唤醒和锁竞争,显著降低延迟抖动。io_uring_cmd(5.19+):允许 NVMe 驱动通过 io_uring 直接提交命令,实现真正意义上的用户态 NVMe 直通。IORING_SETUP_DEFER_TASKRUN(6.0+):将完成处理推迟到特定时机,进一步减少锁争用。
代码实战
在实际项目中使用 io_uring,永远优先用 liburing——它封装了所有底层细节,且已被 QEMU、SPDK 等项目验证。
示例:用 liburing 实现异步文件读取
以下是一个完整的 C 程序,演示 io_uring 的核心用法:
| |
编译方法:
| |
这个 80 行程序展示了完整的 io_uring 生命周期:初始化 → 获取 SQE → 填充操作 → 提交 → 等待完成 → 消费 CQE → 清理。
关键 API 速查
| liburing API | 说明 |
|---|---|
io_uring_queue_init(depth, ring, flags) | 初始化 io_uring 实例,flags 可传 IORING_SETUP_SQPOLL |
io_uring_get_sqe(ring) | 从 SQ 中获取下一个空闲 SQE |
io_uring_prep_read(sqe, fd, buf, nbytes, offset) | 填充读操作 |
io_uring_prep_write(sqe, fd, buf, nbytes, offset) | 填充写操作 |
io_uring_sqe_set_data(sqe, ptr) | 设置 user_data 为用户指针 |
io_uring_submit(ring) | 将 SQ 中所有 SQE 提交到内核 |
io_uring_wait_cqe(ring, cqe_ptr) | 等待至少一个 CQE 完成 |
io_uring_peek_cqe(ring, cqe_ptr) | 非阻塞地检查 CQE |
io_uring_cqe_seen(ring, cqe) | 标记 CQE 已消费 |
io_uring_queue_exit(ring) | 销毁 io_uring 实例 |
SQ 轮询模式
只需一行改动即可开启 SQ 轮询:
| |
开启后 io_uring_submit 不再执行系统调用(内核线程自动轮询 SQ),适用于毫秒级持续提交的场景(如数据库、代理服务器)。
固定缓冲区(Fixed Buffers)
对于频繁使用的缓冲区,可以通过 IORING_REGISTER_BUFFERS 提前注册,让内核固定其物理页,避免每次 I/O 时对缓冲区进行页锁定和解锁:
| |
固定缓冲区可以减少每次 I/O 的页锁定开销,对高 IOPS 场景有显著收益。
生态现状
以下项目已在实际生产中使用 io_uring:
| 项目 | 领域 | 使用方式 | 状态 |
|---|---|---|---|
| QEMU | 虚拟化 | 通过 liburing 实现 virtio-blk/virtio-fs 后端 I/O | ✅ 默认启用(7.0+) |
| RocksDB | KV 存储 | 通过 MultiRead 接口批量提交点查 | ✅ 生产可用 |
| ScyllaDB | 数据库 | 替换 Seastar 框架的 AIO 后端 | ✅ 生产中 |
| SPDK | 存储 | lib/uring 模块支持 io_uring 作为传输层 | ✅ 可选后端 |
| Nginx | Web 服务 | aio 模块增加 io_uring 支持(patch) | ⚠️ 需要自定义编译 |
| Redis | 缓存 | 社区 fork 支持 io_uring 网络 I/O | ⚠️ 实验性 |
| FIO | 基准测试 | 原生支持 io_uring 引擎 | ✅ 默认内置 |
io_uring 的生态仍在快速扩展。2024–2026 年间,越来越多的项目将其作为默认 I/O 后端,网络 io_uring(IORING_OP_SEND/IORING_OP_RECV)的成熟使得 Web 服务器和代理的采用加速。
今日可执行动作
安装 liburing 并运行上面的示例:
sudo apt install liburing-dev,然后用 gcc 编译运行,体验零拷贝异步 I/O 的完整流程。测量你的应用 I/O 延迟:用
strace -c统计系统调用频率——如果你的应用每秒发起数万次read/write,io_uring 可以在减少 90%+ 系统调用的同时提升吞吐。用 FIO 对比 io_uring 与 AIO 性能:
1 2 3 4# AIO 模式 fio --name=aio-test --ioengine=libaio --rw=randread --bs=4k --size=1G --direct=1 --runtime=30 # io_uring 模式 fio --name=uring-test --ioengine=io_uring --rw=randread --bs=4k --size=1G --direct=1 --runtime=30在 NVMe SSD 上,io_uring 通常比 libaio 快 1.5–3 倍,差异主要在 IOPS 较高时变得更加明显。