一、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/write180KN/A5.5 微秒2
POSIX AIO90K120K11.0 微秒4
epoll + 线程池350K480K8.0 微秒3~5
io_uring (default)420K720K4.2 微秒0~1
io_uring (SQPOLL)520K980K2.8 微秒0
io_uring (SQPOLL+FIXED)580K1.2M1.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。

应对措施:

  1. 生产环境使用 CONFIG_IO_URING_DISABLE 全局禁用,或按进程组通过 LSM(SELinux/AppArmor)控制
  2. 不可信代码使用 seccomp-bpf 阻止 io_uring 系统调用
  3. 保持内核版本大于等于 6.1 获取关键安全补丁

七、io_uring 新版本特性(6.x 内核)

内核版本io_uring 新特性关键改进
6.1Zero-Copy sendmsg / IORING_OP_SENDMSGUDP 小包发包延迟低至 2 微秒
6.3IORING_SETUP_ATTACH(内核端 ring)多线程共享 ring,免提交锁
6.5Socket Buffer Recycling + IORING_OP_SPLICE零拷贝管道加速
6.7Multi-shot accept (IORING_ACCEPT_MULTISHOT)单次 accept 注册,批量返回新连接
6.9Pre-mapped fixed buffer (IORING_REGISTER_PBUF)专用缓冲池、DMA 地址固定
6.11async 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 异步编程的基础设施级接口。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.350221s