引言:aio 的困境与 io_uring 的诞生
Linux 异步 I/O(aio)自 2.5 时代便被引入内核,却在生产环境中长期处于"有名无病"的尴尬境地。libaio 的 API 设计存在诸多限制:仅支持 O_DIRECT 文件读写、不支持 socket I/O、每次操作需要两次系统调用(io_submit + io_getevents)、且无法利用 page cache。直到 2019 年 Linux 5.1 引入 io_uring,才算真正为 Linux 生态带来了生产级的高性能异步 I/O 框架。
io_uring 由 Jens Axboe(Linux 块设备子系统维护者)设计,其核心目标是:消除 I/O 路径上不必要的数据拷贝和系统调用开销。在当前版本中,io_uring 已经发展为功能极其丰富的框架,支持网络(TCP/UDP)、磁盘 I/O、pipe、eventfd、poll、symlink 等几乎所有类型的非阻塞/异步操作。
1. 核心架构:共享内存环形缓冲区
io_uring 的革命性设计在于彻底重塑了用户态与内核态之间的通信机制。传统 Linux I/O 路径中,每次操作至少需要一次系统调用(用户态 → 内核态 → 用户态),而 io_uring 通过双环形缓冲区(Shared Ring Buffers)将系统调用频率降到极致。
1.1 三大核心数据结构
io_uring 的核心由三个通过 mmap 映射到用户态的内核数据结构组成:
┌─────────────────────────────────────────────────────────┐
│ io_uring 架构 │
│ │
│ 用户态 │ 内核态 │
│ │ │
│ ┌─────────────┐ │ ┌─────────────────┐ │
│ │ SQ Ring │ ← 提交请求 │ │ SQ 处理线程 │ │
│ │ (提交队列) │ │ │ (kernel thread) │ │
│ └─────────────┘ │ └─────────────────┘ │
│ │ │ │ │
│ │ SQE (Submission │ │ │
│ │ Queue Entry) │ ▼ │
│ ▼ ┌───────┴───────────────────┐ │
│ ┌─────────────┐ │ 内核处理逻辑 │ │
│ │ CQ Ring │ ← 获取完成 │ (vfs_read/write, │ │
│ │ (完成队列) │ │ tcp_sendmsg, ...) │ │
│ └─────────────┘ └───────┬───────────────────┘ │
│ │ │ │ │
│ │ CQE (Completion │ │ │
│ │ Queue Entry) │ ▼ │
│ ▼ ┌───────────────────────────┐ │
│ │ NAPI poll / epoll │ │
│ │ block layer submit │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
参数对照表:
| 名称 | 全称 | 作用 |
|------|------|------|
| SQ Ring | Submission Queue Ring | 用户态写入 SQE 描述符,内核读取的环形缓冲区 |
| CQE | Completion Queue Entry | 单个完成事件(32 字节) |
| CQ Ring | Completion Queue Ring | 内核写入完成事件,用户态读取的环形缓冲区 |
| SQE | Submission Queue Entry | 单个提交请求描述符(64 字节) |
| sqe_head/tail | 环形索引 | 通过 head/tail 指针实现无锁 FIFO |
关键代码:创建 io_uring 实例
#include <liburing.h>
struct io_uring ring;
// 初始化:队列深度 1024,默认参数
int ret = io_uring_queue_init(1024, &ring, 0);
// 高级配置示例:启用 SQPOLL 内核轮询线程
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL; // 内核线程主动轮询 SQ
params.sq_thread_idle = 2000; // 空闲 2s 后线程睡眠
ret = io_uring_queue_init_params(1024, &ring, ¶ms);
2. 零拷贝与零系统调用机制
2.1 SQPOLL 内核轮询模式
IORING_SETUP_SQPOLL 是 io_uring 最核心的优化之一。启用后,内核会创建一个专门的内核线程主动轮询 SQ Ring,用户态提交 SQE 后完全不需要调用 io_uring_enter 系统调用,直到需要收割 CQE 时才查看 CQ Ring。
// SQPOLL 模式下提交 SQE — 纯用户态操作,零系统调用
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
sqe->flags |= IOSQE_FIXED_FILE; // 使用预注册文件
io_uring_submit(&ring); // SQPOLL 下几乎无系统调用
// 收割完成事件
struct io_uring_cqe *cqe;
unsigned head;
io_uring_for_each_cqe(&ring, head, cqe) {
// 处理 cqe->user_data, cqe->res
io_uring_cq_advance(&ring, 1);
}
2.2 固定 Buffer (Fixed Buffers)
传统 I/O 中,每次 read/write 都需要内核建立/撤销 page mapping。io_uring 允许预先注册一组缓冲区,后续操作直接引用索引,避免重复映射:
// 注册固定缓冲区
struct iovec iovecs[32];
for (int i = 0; i < 32; i++) {
posix_memalign(&iovecs[i].iov_base, 4096, 8192);
iovecs[i].iov_len = 8192;
}
io_uring_register_buffers(&ring, iovecs, 32);
// 使用固定缓冲区提交读操作
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_prep_read_fixed(sqe, fd, buf, len, offset, buf_index);
io_uring_submit(&ring);
2.3 固定 Files (Fixed Files)
避免每次 I/O 操作的文件权限检查和 fget/fput 开销:
// 预注册文件描述符表
int files[256] = { fd1, fd2, /* ... */ };
io_uring_register_files(&ring, files, 256);
// 使用索引而非 raw fd
sqe->flags |= IOSQE_FIXED_FILE;
sqe->fd = file_index; // 引用注册表中索引为 file_index 的 fd
3. 高级特性全景
3.1 链式 SQE (Linked SQEs)
io_uring 支持将多个 SQE 标记为链式执行,前一个完成后才执行下一个,且可保证在同一线程上下文:
struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe1, fd, header_buf, HEADER_LEN, 0);
sqe1->flags |= IOSQE_IO_LINK; // 链接标志
struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe2, fd, body_buf, body_len, HEADER_LEN);
sqe2->flags |= IOSQE_IO_LINK;
struct io_uring_sqe *sqe3 = io_uring_get_sqe(&ring);
io_uring_prep_close(sqe3, fd); // 链接链的最后一步
io_uring_submit(&ring);
// 三个操作:读 header → 读 body → 关闭 fd,作为一个原子序列执行
3.2 多 Shot Accept (Multi-shot Accept)
传统 epoll 模式下,每次新连接到达都需要调用 accept()。io_uring 的 IORING_ACCEPT_MULTISHOT 标志允许一次提交 accept SQE 后,内核每次有新连接到达时自动产生 CQE,直到显式取消,大幅减少 accept 系统调用次数:
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, listen_fd, &addr, &addrlen, flags);
sqe->len |= IORING_ACCEPT_MULTISHOT; // 多 shot 模式
io_uring_submit(&ring);
// 内核每次 accept 到新连接都会产生一个 CQE,无需反复提交
3.3 直接转换 (Direct Conversion): poll → read
典型网络服务中,需要先 poll 可读再 read,两次 SQE。io_uring 支持连锁操作:第一个 poll SQE 完成后自动触发第二个 read SQE,用户态全程无感知:
// 连锁:poll fd 可读 → read 数据
sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_poll_add(sqe1, fd, POLLIN);
sqe1->flags |= IOSQE_IO_LINK | IOSQE_ASYNC;
sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe2, fd, buf, len, 0);
sqe2->flags |= IOSQE_FIXED_FILE;
3.4 内核侧轮询 I/O (IORING_SETUP_IOPOLL)
对于 NVMe 设备,用户态轮询模式可以彻底绕过中断和上下文切换:
params.flags = IORING_SETUP_IOPOLL | IORING_SETUP_SQPOLL;
io_uring_queue_init_params(QUEUE_DEPTH, &ring, ¶ms);
// 完全轮询模式:跳过 block layer 中断,直接 busy-poll CQ
// 典型场景:NVMe SSD + 高 IOPS,延迟可降至微秒级
4. 实战:构建高性能异步 I/O 服务
4.1 基于环形缓冲器的零分配设计
生产级 io_uring 服务应避免在热路径上分配内存。以下模式展示了如何通过预注册 buffers + 环形 slot 索引 实现零分配 I/O 循环:
#define BUF_SIZE 8192
#define BUF_COUNT 1024
struct connection {
int fd;
uint16_t buf_index; // 固定 buffer 索引
uint16_t flags;
};
// 预分配缓冲区池
struct iovec bufs[BUF_COUNT];
char pool[BUF_COUNT][BUF_SIZE] __attribute__((aligned(4096)));
for (int i = 0; i < BUF_COUNT; i++) {
bufs[i].iov_base = pool[i];
bufs[i].iov_len = BUF_SIZE;
}
io_uring_register_buffers(&ring, bufs, BUF_COUNT);
// 启动读请求:为每个连接提交起始 read SQE
void submit_read(struct io_uring *ring, struct connection *conn) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read_fixed(sqe, conn->fd, pool[conn->buf_index],
BUF_SIZE, 0, conn->buf_index);
sqe->user_data = (uint64_t)conn; // 通过 user_data 回传上下文
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_submit(ring);
}
// 收割 CQE 并处理
void reap_completions(struct io_uring *ring) {
struct io_uring_cqe *cqe;
unsigned head, completed = 0;
io_uring_for_each_cqe(ring, head, cqe) {
struct connection *conn = (struct connection *)cqe->user_data;
if (cqe->res > 0) {
handle_request(conn, cqe->res);
}
io_uring_cq_advance(ring, 1);
}
}
4.2 与 epoll 模式的性能基准
以下是典型场景下的 io_uring vs epoll 基准测试(NVMe SSD,队列深度 256,单核):
┌──────────────────────────┬──────────────┬──────────────┐
│ 模式 │ IOPS │ 延迟(μs) │
├──────────────────────────┼──────────────┼──────────────┤
│ epoll + libaio │ 180K │ 5.2 │
│ epoll + 同步 read │ 120K │ 8.1 │
│ io_uring (默认) │ 260K │ 3.4 │
│ io_uring (SQPOLL) │ 310K │ 2.7 │
│ io_uring (SQPOLL+IOPOLL) │ 380K │ 1.9 │
└──────────────────────────┴──────────────┴──────────────┘
* 测试环境:Xeon E5-2680v4 × 1核, NVMe SSD, Linux 5.15
5. 陷阱与最佳实践
5.1 SQPOLL 饥饿问题
SQPOLL 内核线程在队列满时会阻塞等待用户态消费 CQE。若用户态长期不收割 CQE,io_uring_enter 调用会 EAGAIN。正确做法是定期调用 io_uring_peek_cqe 或设置合理的 sq_thread_idle 值。
5.2 IOSQE_ASYNC 的代价
标记 IOSQE_ASYNC 会强制操作在内核工作线程中执行,对于本可以立即完成的操作(如 pipe poll、eventfd read),这会引入无法接受的延迟。应避免对快速路径操作使用该标志。
5.3 固定缓冲区的内存占用
固定缓冲区会长期占用物理页面(pinned),无法被 swap 或回收。在内存受限环境中,需谨慎规划缓冲池大小。
5.4 错误处理与兼容性
io_uring 需要 Linux 5.1+ 内核,且部分特性需要更高版本:
Linux 5.1 — 基础 io_uring (read/write/poll)
Linux 5.5 — 支持 socket (connect/accept/send/recv)
Linux 5.6 — 支持 multishot accept、nonblock hint
Linux 5.10 — 支持 IORING_OP_SHUTDOWN、更多协议支持
Linux 5.15 — 完善 SQPOLL idle 清理
Linux 5.19 — 支持 sendmsg/recvmsg、one-shot poll
Linux 6.1 — 全面流量控制、IORING_SETUP_DEFER_TASKRUN
Linux 6.6 — 固定 buffer + read/write 的 zerocopy 优化
6. io_uring 与新兴生态
io_uring 正在重塑 Linux 异步编程格局。主流高性能框架已全面拥抱:
liburing — Jens Axboe 维护的 liburing,C 语言原生库,提供最完整的 io_uring 封装。
tokio-uring — Rust tokio 生态的 io_uring 后端,为 Rust 异步生态带来真正的异步文件 I/O(弥补 tokio 现有实现中 fs 操作阻塞线程池的短板)。
Glommio — 纯 Rust 异步运行时,基于 io_uring 设计,采用 shared-nothing 线程模型。
MySQL 8.0.28+ — 实验性支持 io_uring 的 InnoDB I/O 路径。
PostgreSQL 16+ — 在高级 WAL 写入场景探索 io_uring 优化。
systemd、qemu、nginx (第三方模块) — 已陆续集成 io_uring 支持。
7. 总结:io_uring 的设计哲学
io_uring 的本质是一次成功的系统调用接口重构。它通过三个关键设计突破解决了 Linux I/O 的历史债务:
第一,双环形共享缓冲区 — 将"用户态请求 → 内核执行 → 用户态收割"的同步模型解耦为异步生产者-消费者模型,大幅减少模式切换。
第二,SQPOLL 内核轮询 — 让内核主动 fetch 请求,将系统调用频率从每次 I/O 一次降为接近零(idle 期间由内核线程阻塞等待,不消耗 CPU)。
第三,资源预注册 (Fixed Buffers / Fixed Files) — 将本来每次操作必须执行的 resource mapping 挪到冷路径(初始化阶段),热路径只做纯数据搬运。
io_uring 仍在快速演进中,IORING_SETUP_DEFER_TASKRUN、io_uring_cmd(直通 character device)、FUSE io_uring 支持等新特性持续扩展其边界。对于每一位追求极致 I/O 性能的 Linux 系统程序员来说,io_uring 已经不是可选项,而是必修课。
参考资料
Jens Axboe, io_uring 官方文档: https://kernel.dk/io_uring.pdf
liburing GitHub: https://github.com/axboe/liburing
"The Rapid Growth of io_uring", LWN.net, 2023.
"io_uring and the Future of Linux Asynchronous I/O", EuroBSDCon 2023.

发表评论 取消回复