一、从 AIO 到 io_uring:Linux 异步 IO 架构演进
Linux 异步 IO 的发展经历了漫长的演进过程。早期的 POSIX AIO(libaio)存在诸多限制:仅支持 O_DIRECT 文件 IO、提交/完成需要系统调用、不支持网络 IO。为了彻底解决这些问题,Linux 5.1(2019年)正式引入了 io_uring,由 Jens Axboe(Linux 块设备层维护者)设计,目标是构建一个统一、高性能、可扩展的异步 IO 框架。
io_uring 的核心设计思想是用户态与内核态共享环形缓冲区(Ring Buffer),将系统调用开销降到最低:正常路径下,提交和完成操作零系统调用(仅通过共享内存),只有在需要通知内核时才调用 io_uring_enter()。
二、io_uring 核心架构与数据结构
2.1 双环形缓冲区:SQ 与 CQ
io_uring 的核心是两片共享内存区域:
- Submission Queue (SQ):提交队列,用户态写入 SQE(Submission Queue Entry),内核消费
- Completion Queue (CQ):完成队列,内核写入 CQE(Completion Queue Entry),用户态消费
SQE 和 CQE 都是一组数组,通过头尾指针实现 lock-free 的生产者-消费者模型。SQ 和 CQ mmap 到用户态,用户直接操作共享内存,避免了传统 IO 路径上的 copy_to_user/copy_from_user。
2.2 io_uring_params 与 io_uring_setup()
初始化流程通过 io_uring_setup(unsigned entries, struct io_uring_params *p) 完成:
struct io_uring_params p;
memset(&p, 0, sizeof(p));
// 可选设置 flags: SETUP_IOPOLL / SETUP_SQPOLL / SETQPOLL_SQ_AFF 等
int ring_fd = io_uring_setup(256, &p);
// mmap SQ 和 SQE 数组
sq_ptr = mmap(0, p.sq_off.array + p.sq_entries * sizeof(unsigned),
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_SQ_RING);
// mmap SQE 数组
sqes = mmap(0, p.sq_entries * sizeof(struct io_uring_sqe),
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_SQES);
// mmap CQ
cq_ptr = mmap(0, p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe),
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
ring_fd, IORING_OFF_CQ_RING);
io_uring_params 结构体包含 p.sq_off 和 p.cq_off 两个偏移量描述,用于定位 SQ/CQ ring 中的各个字段头尾指针位置。这种设计允许内核未来扩展字段而不破坏 ABI。
2.3 SQE 与 CQE 结构
每个 SQE(64 字节)包含:操作码(8 个 opcodes)、flags、fd、offset、addr、len、user_data(用户透传标识符)等。user_data 字段是关键——它会在 CQE 中原样返回,用于关联请求与响应,实现类似 request-response 模式。
CQE(16 字节)包含:user_data(对应 SQE)、res(结果码)、flags。
三、三种工作模式
3.1 中断驱动模式(默认)
用户通过 io_uring_enter() 通知内核处理 SQ 中的条目,内核完成后将 CQE 放入 CQ。适合中低频 IO 场景,CPU 开销最低。
3.2 内核轮询模式(IORING_SETUP_IOPOLL)
内核线程主动轮询设备完成事件,消除了中断延迟。适用于 NVMe SSD、持久内存等低延迟存储设备,可将 IO 延迟降低到微秒级。代价是占用一个 CPU 核心做轮询。
3.3 内核提交轮询模式(IORING_SETUP_SQPOLL)
这是 io_uring 最强大的模式:内核线程自动消费 SQ 中的条目,用户态完全不需要调用 io_uring_enter()——真正实现零系统调用的异步提交。
struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL;
p.sq_thread_idle = 2000; // 2秒空闲后线程休眠(毫秒)
int fd = io_uring_setup(256, &p);
SQPOLL 模式下,内核线程会检查 SQ 头尾指针差异,自动拾取新 SQE。只有在 SQ 满或需要刷写时,用户态才需要调用 io_uring_enter()。
注意:SQPOLL 线程需要与 io_uring 实例相同的 UID,或具有 CAP_SYS_ADMIN 权限。可通过 p.sq_thread_cpu 绑定 CPU 核心。
四、高级特性详解
4.1 预注册文件(Fixed Files)
每次 IO 操作都需要 fd → file 的查找(涉及 RCU 锁和引用计数),频繁的系统调用开销可观。io_uring 允许预先注册文件表,将 fd 替换为索引:
// 注册文件
int fds[] = {file_fd, socket_fd};
io_uring_register_files(&ring, fds, 2);
// 在 SQE 中使用固定索引
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
sqe->fd = 0; // 使用注册表中索引0的文件
sqe->flags = IOSQE_FIXED_FILE; // 必须设置此 flag
注册文件后,内核直接从固定数组中获取 file 解构,跳过 fd lookup,性能提升约 5-10%。
4.2 预注册缓冲区(Fixed Buffers / Registered Buffers)
O_DIRECT 要求内存对齐的缓冲区,每次 IO 需要 pin/_unpin 页面。io_uring 的预注册缓冲区功能解决了这个问题:
// 注册一块或多块缓冲区
struct iovec iov = { .buf = buffer, .len = BUFFER_SIZE };
io_uring_register_buffers(&ring, &iov, 1);
// SQE 中使用
sqe->addr = (unsigned long)iov.iov_base;
sqe->len = iov.iov_len;
sqe->buf_index = 0; // 注册的缓冲区索引
sqe->flags |= IOSQE_BUFFER_SELECT; // 对于 readv/select 操作
缓冲区注册后,内核提前 pin 用户页面,IO 过程中无需额外的 get_user_pages 调用。这对于高频 O_DIRECT 操作(如数据库 WAL 写入)是质的飞跃。
4.3 链式 SQE(Linked SQEs)
通过 IOSQE_IO_LINK flag,可以将多个 SQE 链接成顺序执行链:
// 场景:先读取 header,再根据 header 读取 body
struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe1, fd, &hdr_iov, 1, 0);
sqe1->user_data = OP_READ_HDR;
sqe1->flags |= IOSQE_IO_LINK; // 标记为链接
struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe2, fd, &body_iov, 1, HEADER_SIZE);
sqe2->user_data = OP_READ_BODY;
// sqe2 不会在前面 LINK 断开前开始执行
链式 SQE 保证了操作的原子性和顺序性——前一个失败则后续全部失败(可通过 IOSQE_IO_HARDLINK 硬链接强制保证)。
4.4 网络异步操作(Accept/Connect/Send/Recv)
io_uring 完整支持网络异步化:
// 异步 Accept
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, (struct sockaddr *)&client_addr,
&addr_len, 0);
sqe->user_data = OP_ACCEPT;
// 异步 Recv(multishot:一次提交多次接收)
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sqe, client_fd, NULL, 0, 0);
sqe->buf_group = BUF_GROUP_ID; // 使用预分配的 buffer group
sqe->flags |= IOSQE_BUFFER_SELECT;
Multishot Accept 是 5.19+ 引入的特性:一次 accept 提交,内核在每次新连接到达时自动产生 CQE,大幅减少重复提交的系统调用。
五、Buffer Select 与 Predefined Buffers
5.19+ 引入了 Buffer Group 机制,配合 IOSQE_BUFFER_SELECT,可以在 recv 操作时由内核自动从预分配池中选取缓冲区:
// 注册一个 buffer group
struct io_uring_buf_reg reg = {
.ring_addr = (unsigned long)br,
.ring_entries = 128,
.bgid = 0
};
io_uring_register_buf_ring(&ring, ®, 0);
// 填充 128 个 4KB 缓冲区
for (int i = 0; i < 128; i++) {
br->bufs[i] = make_buffer(BUF_SIZE, i);
}
这实现了真正的零拷贝网络 IO:数据直接从内核页缓存/Socket 接收,用户态预先分配的缓冲区被直接使用。对高吞吐 HTTP/gRPC 服务性能提升显著。
六、完整实战:echo server 基于 io_uring
#include <liburing.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define QUEUE_DEPTH 256
#define BUF_SIZE 1024
enum { OP_ACCEPT, OP_READ, OP_WRITE };
struct conn_info {
int fd;
unsigned type;
char buf[BUF_SIZE];
unsigned buf_len;
};
int main() {
// 1. 初始化 io_uring
struct io_uring ring;
struct io_uring_params p = {0};
p.flags = IORING_SETUP_SQPOLL; // 零系统调用模式
p.sq_thread_idle = 1000; // 1秒空闲休眠
io_uring_queue_init_params(QUEUE_DEPTH, &ring, &p);
// 2. 注册固定缓冲区
struct iovec iov = { .iov_base = malloc(BUF_SIZE), .iov_len = BUF_SIZE };
io_uring_register_buffers(&ring, &iov, 1);
// 3. 设置监听 socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = { .sin_family=AF_INET, .sin_port=htons(8080),
.sin_addr.s_addr=INADDR_ANY };
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
// 4. 提交初始 accept
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
struct conn_info *info = malloc(sizeof(struct conn_info));
info->fd = listen_fd; info->type = OP_ACCEPT;
io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);
sqe->user_data = (unsigned long long)info;
io_uring_submit(&ring);
// 5. 事件循环
while (1) {
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
struct conn_info *c = (struct conn_info *)cqe->user_data;
switch (c->type) {
case OP_ACCEPT: {
int client_fd = cqe->res;
// 重新提交 accept
sqe = io_uring_get_sqe(&ring);
struct conn_info *new_acc = malloc(sizeof(*new_acc));
new_acc->fd = listen_fd; new_acc->type = OP_ACCEPT;
io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);
sqe->user_data = (unsigned long long)new_acc;
// 提交 read
sqe = io_uring_get_sqe(&ring);
c->fd = client_fd; c->type = OP_READ;
io_uring_prep_recv(sqe, client_fd, c->buf, BUF_SIZE, 0);
sqe->user_data = (unsigned long long)c;
io_uring_submit(&ring);
break;
}
case OP_READ:
if (cqe->res <= 0) { close(c->fd); free(c); break; }
c->buf_len = cqe->res;
c->type = OP_WRITE;
sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, c->fd, c->buf, c->buf_len, 0);
sqe->user_data = (unsigned long long)c;
io_uring_submit(&ring);
break;
case OP_WRITE:
c->type = OP_READ;
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, c->fd, c->buf, BUF_SIZE, 0);
sqe->user_data = (unsigned long long)c;
io_uring_submit(&ring);
break;
}
io_uring_cqe_seen(&ring, cqe);
}
return 0;
}
七、性能数据与对比
Jens Axboe 和多个独立基准测试表明,io_uring 的性能优势显著:
| 场景 | 同步 IO | POSIX AIO | io_uring (default) | io_uring (SQPOLL) |
|---|---|---|---|---|
| 随机读 IOPS (NVMe) | 180K | 280K | 680K | 950K |
| 延迟(P99,μs) | 22 | 15 | 8 | 5 |
| CPU 开销(IOPS/核) | 高 | 中 | 低 | 极低 |
| 网络并发连接 | 10K | 不支持 | 100K+ | 100K+ |
| 零系统调用路径 | 否 | 否 | 部分 | 是 |
关键数据(NVMe SSD,深度=32,随机读 4KB):
- io_uring SQPOLL 模式可达同步 IO 5.3 倍 IOPS
- P99 延迟从同步 IO 的 22μs 降至 5μs
- CPU 使用率降低 40-60%(相同吞吐下)
- io_uring 已成为 RocksDB、PostgreSQL 16、MySQL 8.0+、Nginx(可选模块)、Redis(io_threads)高性能设计的底层
八、生产环境最佳实践
8.1 内核版本选择
- 基础功能:Linux 5.1+
- Fixed Buffers/SQPOLL:5.17+(推荐)
- Multishot Accept、Buffer Select:5.19+
- 完整企业级特性:5.19+ 或 6.x
8.2 内存与队列深度
QUEUE_DEPTH 的选择需要平衡:过小导致提交饥饿,过大增加内存和延迟。生产建议:存储 IO 使用 64-256,网络使用 256-1024。CQ 深度默认 = SQ 深度 * 2(
8.3 错误处理与优雅关闭
多路复用场景下,需要处理ETIME(SQPOLL 超时)、EBUSY(提交队列满)、ECANCELED(链接取消)等信号。SQPOLL 模式注意 sq_thread_idle 参数防止 CPU 浪费。
8.4 与 epoll 配合使用
epoll + io_uring 是常见搭配:epoll 监听 listen fd 和可写事件,io_uring 处理实际数据读写。或使用 IORING_OP_POLL_ADD 在 io_uring 内部注册 epoll-equivalent 事件,统一事件循环。
8.5 调试与监控
perf trace -e 'io_uring:*':跟踪 io_uring 事件IORING_SETUP_ATTACH_RWUSER:允许附加到已有 ring 的诊断/proc/<pid>/io_uring:查看进程的 io_uring 实例信息(6.0+)IORING_REGISTER_RING_FDS:暴露 ring fd 给外部监控工具
九、生态项目与未来方向
io_uring 已经催生了多个关键项目:
- tokio-uring:Rust tokio 异步运行时对 io_uring 的支持
- glommio:完全基于 io_uring 的 Rust 异步运行时,追求极致 IO 性能
- uring-sys / liburing:官方 C 绑定,简化 API 使用
- SPDK/io_uring:SPDK 已可选通过 io_uring 提供用户态驱动
- PostgreSQL 16:IO_METHOD = io_uring,大幅提升了 WAL 写入和 checkpoint 性能
- Kver:内核态 eBPF + io_uring 融合方案,用 eBPF 处理 IO 路由决策
未来方向包括:io_uring 与 DPU/SmartNIC offload(将 IO 卸载到硬件)、io_uring passthrough(NVMe 命令直通,绕过块层)、io_uring over VFIO(与 SPDK 共享设备模型)。
十、总结
io_uring 不是简单的 AIO 替代品——它是 Linux IO 架构的一次范式转移。通过共享内存环形缓冲区、预注册资源、内核轮询三大机制,io_uring 真正实现了异步 IO 的设计初衷:最小化系统调用,最小化拷贝,最小化延迟。
无论是构建低延迟存储引擎、高吞吐网络服务还是实时数据分析,理解并善用 io_uring 都是 Linux 系统工程师必须掌握的核心技能。随着 Kernel 6.x 持续增强稳定性和企业级特性,io_uring 正成为 Linux 高性能 IO 的默认选择。

发表评论 取消回复