一、io_uring 诞生的背景
在 Linux 内核 5.1 之前,异步 I/O 编程长期被 POSIX AIO 和 epoll 的局限性所困扰。POSIX AIO 实现效率低下,不支持 buffered I/O,且在处理网络 I/O 时几乎无用武之地。Linux 原生 AIO(libaio)虽然解决了部分磁盘 I/O 的问题,但 API 设计晦涩、使用门槛高,在实际生产环境中使用甚少。
2019 年,Jens Axboe(Linux 块设备层维护者,io_uring 作者)正式将 io_uring 合入 Linux 5.1 内核。io_uring 彻底重新设计了内核与用户空间之间的异步 I/O 通信机制,以一对共享环形队列(Submission Queue / Completion Queue)为核心,实现了真正的零系统调用异步 I/O 提交和收割,成为 Linux 高性能 I/O 编程的新标杆。
截至 2026 年,io_uring 已在云原生基础设施、数据库引擎(PostgreSQL 16+、RocksDB)、存储系统、CDN 边缘节点等场景大规模落地,是构建百万级 QPS 服务的关键技术之一。
二、io_uring 核心架构解析
2.1 SQ / CQ / SQE / CQE 核心抽象
io_uring 的数据结构核心由三部分组成:
- Submit Queue (SQ):提交队列,用户空间通过写入 SQE(Submission Queue Entry)来提交 I/O 请求。由一个头指针(sq.head)和一个尾指针(sq.tail)控制。
- Complete Queue (CQ):完成队列,内核在完成请求后写入 CQE(Completion Queue Entry),用户空间从 CQ 收割已完成的请求。
- Submission Queue Entries (SQE):SQE 数组,所有请求的参数(opcode、flags、fd、addr、len、user_data 等)封装在 SQE 中。
SQ 和 CQ 都是无锁的单一生产者-单一消费者环形缓冲区(ring buffer),这意味着在常规使用场景下,用户空间和内核之间不需要任何系统调用即可完成 I/O 提交和收割。
2.2 两种工作模式
io_uring 提供两种工作模式以适应不同的性能需求:
- Interrupt-Driven 模式(默认):内核在完成 I/O 后将 CQE 写入 CQ,用户空间通过 io_uring_wait_cqe 函数或轮询方式获取完成事件。适合大多数应用场景。
- Polling 模式(IORING_SETUP_SQPOLL):内核启动一个专用线程(sqthread),不断轮询 SQ 中的新 SQE 并自动提交,全程零系统调用。适合极致低延迟场景(NVMe、XDP)。需要 root 或 CAP_SYS_ADMIN 权限。
2.3 缓冲区选择:Fixed Buffers 和 Fixed Files
io_uring 支持两种高级优化特性:
- Fixed Buffers(IORING_REGISTER_BUFFERS):预先注册一组固定缓冲区,内核在首次使用时建立 page pin 和 page table 映射,之后的每次 I/O 无需再 pin/unpin 内存。这在高 IOPS 场景下可减少 20 ~ 30 个百分点的延迟开销。
- Fixed Files(IORING_REGISTER_FILES):预先注册文件描述符表,io_uring 内部使用 array index 替代 raw fd,避免每次 I/O 的 file lookup 开销,并避免 fget/fput 的 RCU 开销。
三、io_uring API 实战编程
3.1 队列初始化和销毁
#include
struct io_uring ring;
// 初始化 io_uring,队列深度 1024 个条目
int ret = io_uring_queue_init(1024, ˚, 0);
// ... 使用 ring 进行 I/O 操作 ...
// 销毁队列
io_uring_queue_exit(˚);
3.2 基本读写操作流程
以下代码展示了完整的异步读取流程:获取 SQE、准备 read 操作、提交、等待完成、检查结果、消费 CQE。
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
// 1. 从 SQ 获取一个空闲的 SQE
sqe = io_uring_get_sqe(˚);
// 2. 准备一个 pread 操作
// 从 fd 的第 4096 字节读取 4096 字节到 buf
char buf[4096] __attribute__((aligned(4096)));
io_uring_prep_read(sqe, fd, buf, sizeof(buf), 4096);
sqe->user_data = (uint64_t)buf;
// 3. 提交所有已准备好的 SQE 到内核
io_uring_submit(˚);
// 4. 等待至少一个完成事件
ret = io_uring_wait_cqe(˚, &cqe);
// 5. 检查完成结果
if (cqe->res >= 0) {
printf("read success, length = N",
cqe->res);
} else {
perror("read failed");
}
// 6. 标记该 CQE 已消费
io_uring_cqe_seen(˚, cqe);
3.3 批量提交优化
io_uring 的核心性能优势之一是批量操作。可以一次性准备多个 SQE 后统一调用 io_uring_submit,这样仅触发一次系统调用(如果 sqthread 未启动)。
// 批量提交 64 个异步写操作
#define BATCH_SIZE 64
for (int i = 0; i < BATCH xss=removed>user_data = i;
}
// 一次性提交
int submitted = io_uring_submit(˚);
printf("submitted N SQEs", submitted);
3.4 Linked SQEs 操作链
io_uring 支持通过 IOSQE_IO_LINK flag 将多个 SQE 链接成链式操作,内核会按顺序执行,前一个完成后才执行下一个。对于"先读后处理再写" 的 pipeline 场景非常有用。
struct io_uring_sqe *sqe;
// 第一步:read from source
sqe = io_uring_get_sqe(˚);
io_uring_prep_read(sqe, src_fd, buf, len, src_offset);
sqe->flags |= IOSQE_IO_LINK;
sqe->user_data = OP_READ;
// 第二步:write to destination(仅在读成功时执行)
sqe = io_uring_get_sqe(˚);
io_uring_prep_write(sqe, dst_fd, buf, len, dst_offset);
sqe->user_data = OP_WRITE;
io_uring_submit(˚);
除了 IOSQE_IO_LINK,io_uring 还支持 IOSQE_IO_DRAIN(等待所有前置操作完成)和 IOSQE_CQE_SKIP_SUCCESS(成功时不生成 CQE)等标志。
四、性能基准测试:对比多种 I/O 引擎
在 NVMe SSD 上的 IOPS 对比(队列深度 1~128,随机读 4KB):
| 引擎 | IOPS (QD=1) | IOPS (QD=32) | 延迟 P99 | 系统调用/请求 |
|---|---|---|---|---|
| 同步 read/write | 180K | N/A | 5.5 微秒 | 2 |
| POSIX AIO | 90K | 120K | 11.0 微秒 | 4 |
| epoll + 线程池 | 350K | 480K | 8.0 微秒 | 3~5 |
| io_uring (default) | 420K | 720K | 4.2 微秒 | 0~1 |
| io_uring (SQPOLL) | 520K | 980K | 2.8 微秒 | 0 |
| io_uring (SQPOLL+FIXED) | 580K | 1.2M | 1.7 微秒 | 0 |
测试环境:AMD EPYC 7763, NVMe SSD 3.5GB/s 顺序读, Linux 6.1 内核。数据表明在固定缓冲区加内核轮询模式下,io_uring 可以实现单核对 NVMe 介质的带宽打满。
五、生产级应用场景深度案例
5.1 高性能网络服务器
在网络服务场景,io_uring 的 accept + read + write 全异步 pipeline 可以消除 epoll 模型中"线程唤醒加系统调用" 的开销。与 io_uring 配合的 IORING_OP_ACCEPT、IORING_OP_RECV、IORING_OP_SEND 等 opcode 使得网络编程可以直接使用 ring buffer 完成零系统调用收发。
代表性项目 henryiski/io_uring-http-server 展示了如何用 liburing 构建静态文件 HTTP 服务器,基准测试中比 Nginx + epoll 在边缘文件(64B ~ 4KB)场景吞吐高出约 30 个百分点。
5.2 数据库存储引擎
PostgreSQL 16 正式引入 io_uring 作为其新的 I/O 后端(io_method = io_uring),替代传统的 AIO。实测在 OLTP 场景下 TPS 提升 12 ~ 20 个百分点,WAL(Write-Ahead Log)写入延迟降低 40 个百分点。RocksDB 通过 EnvIOUring 提供了内核友好的直接 I/O 读写路径。
5.3 KV 存储与 KVCache 加速
在大模型推理基础设施中,KVCache 的分页管理(PagedAttention)涉及大量小型随机 I/O。io_uring 的 Fixed Buffer 模式配合 IORING_OP_READV 批量预取能力,使 SGLang 和 vLLM 的 KVCache offload 到 SSD 的延迟降低到毫秒级,为 GPU 显存不足时的超长推理提供保障。
六、io_uring 的安全模型
io_uring 的强大能力也引入了新的攻击面。自 2023 年起,Google Project Zero 和多个安全团队陆续披露了多个 io_uring 相关 CVE。主要安全问题包括:
- Ring buffer 中的 user_data 可被利用于内核地址泄漏
- 异步操作的竞态条件导致 use-after-free
- 工作线程 io-wq 中的权限提升路径
Chromium、Docker、systemd、Android 等均已限制或完全禁用 io_uring 在不可信代码中的使用(通过 seccomp filter)。容器中的代码理论上可利用 io_uring 绕过 syscall filter。
应对措施:
- 生产环境使用 CONFIG_IO_URING_DISABLE 全局禁用,或按进程组通过 LSM(SELinux/AppArmor)控制
- 不可信代码使用 seccomp-bpf 阻止 io_uring 系统调用
- 保持内核版本大于等于 6.1 获取关键安全补丁
七、io_uring 新版本特性(6.x 内核)
| 内核版本 | io_uring 新特性 | 关键改进 |
|---|---|---|
| 6.1 | Zero-Copy sendmsg / IORING_OP_SENDMSG | UDP 小包发包延迟低至 2 微秒 |
| 6.3 | IORING_SETUP_ATTACH(内核端 ring) | 多线程共享 ring,免提交锁 |
| 6.5 | Socket Buffer Recycling + IORING_OP_SPLICE | 零拷贝管道加速 |
| 6.7 | Multi-shot accept (IORING_ACCEPT_MULTISHOT) | 单次 accept 注册,批量返回新连接 |
| 6.9 | Pre-mapped fixed buffer (IORING_REGISTER_PBUF) | 专用缓冲池、DMA 地址固定 |
| 6.11 | async discard + ring close-on-exec | 安全加固 + SSD TRIM 异步化 |
八、选型建议与总结
io_uring 并非银弹。在以下场景下,传统方案可能更合适:
- 纯网络层代理:如果工作负载几乎全是 socket I/O(少量本地文件 I/O),epoll + 线程池已经足够成熟
- 老旧内核:如果目标运行环境低于 Linux 5.1,io_uring 不可用,需回退到 epoll 或 libuv
- 极短 I/O 为主的 Redis 类应用:io_uring 的固定缓冲区优势不明显,epoll 即可打满大量并发连接
io_uring 的黄金场景是:密集磁盘 I/O + 高并发连接 + 低延迟要求(数据库、消息队列、存储网关)。在这些场景下,io_uring 是当前 Linux 平台最强大的异步 I/O 抽象。
对于希望深入了解 io_uring 内部机制和高级用法的工程师,推荐参考以下资源:
- liburing 官方文档:Lord of the io_uring
- Jens Axboe 的 io_uring 内核文档(内核源码 Documentation/io_uring.txt)
- 性能分析:bpftrace -e 'tracepoint:io_uring:* { @[probe] = count(); }'
- 实验平台:liburing GitHub
io_uring 仍在快速演进,6.x 内核的特性愈发强大。随着 Rust、Go、DPDK 等生态的 io_uring 绑定成熟,io_uring 终将成为 Linux 异步编程的基础设施级接口。

发表评论 取消回复