Linux io_uring 高性能异步IO深度实战
从传统 AIO 到 io_uring,Linux 异步 IO 的范式革命
引言
2019 年 Linux 5.1 内核合入了 io_uring,这是 Linus Torvalds 亲自参与设计的新异步 IO 接口。它解决了 Linux AIO 长期存在的诸多痛点,带来了真正的零拷贝、零系统调用开销的异步 IO 能力。如今,io_uring 已经成为高性能存储、网络框架(如 Tokio、Go、Nginx、Ceph、SPDK)底层的首选 IO 引擎。
本文将深入剖析 io_uring 的核心架构、工作机制、关键优化手段,并通过实战代码展示如何将其应用于生产环境。
一、为什么需要 io_uring — Linux AIO 的局限
1.1 传统 Linux AIO 的三大痛点
Linux AIO(libaio)虽然出现较早,但在使用中暴露出严重的设计缺陷:仅支持 O_DIRECT 路径:io_submit 必须配合 O_DIRECT 标志使用,意味着每次 IO 都绕过页缓存,对于随机读写密集的场景极其不利。同步元数据操作提交阻塞:即使数据已缓存,IO 提交时如果涉及元数据读取(如 extent lookup),整个提交操作会变成同步阻塞。完成事件轮询开销:io_getevents 本质是系统调用,频繁轮询时 CPU 消耗较高。不支持 socket IO:传统 AIO 仅适用于 block 设备文件,无法用于网络 IO。
1.2 POSIX AIO 的尴尬处境
POSIX AIO(librt)通过线程池模拟异步操作,用户感知是异步的,但底层仍是同步阻塞调用。这意味着在高并发场景下,线程数量随连接数线性增长,资源消耗严重。
1.3 io_uring 的设计目标
io_uring 的设计哲学是:应用程序与内核之间通过共享内存环形队列通信,实现真正的零系统调用异步 IO。设计目标包括:无需系统调用即可提交和收割 IO(通过共享内存)、支持任意文件类型(磁盘、socket、pipe 等)、可扩展性强(支持百万级并发 IO)、与 io_uring 兼容的 IO 操作必须经过内核认可。
二、核心架构:两个环形队列
io_uring 的核心是两个环形缓冲区(Ring Buffer),分别称为 SQ(Submission Queue)和 CQ(Completion Queue)。
2.1 SQ — 提交队列
SQ 是一个生产者-消费者模型中的生产者队列,由用户态往里面写入 SQE(Submission Queue Entry)。每个 SQE 描述一个 IO 操作:操作码(read/write/send/recv 等)、文件描述符、缓冲区地址、偏移量、长度、用户自定义数据(user_data,用于匹配完成事件)。SQ 的特点是用户态直接写入,内核异步读取,只要不溢满就不需要系统调用。
2.2 CQ — 完成队列
CQ 是内核在完成 IO 后写入 CQE(Completion Queue Entry)的队列。包含:user_data(来自 SQE)、res(返回值:成功为字节数,<0 为错误码)、flags(标志位)。用户态直接从 CQ 读取 CQE,获取 IO 完成结果。
2.3 内核态与用户态协同
用户态进程 内核
│ │
├─ 写 SQE 到 SQ ────────────┤
├─ 更新 SQ tail ────────────┤
│ (共享内存,无 syscall) │
│ │
│ ┌─ 内核 io_wq 线程消费 SQE ─┐
│ │ 处理 IO 请求 │
│ └──────────────────────┘
│ │
│ ┌─ 写 CQE 到 CQ ────────┐
│ │ 更新 CQ head │
│ └──────────────────────┘
│ │
├─ 读 CQ head ──────────────┤
├─ 处理 CQE ────────────────┤
│ (共享内存,无 syscall) │
关键点:SQE 和 CQE 都通过 mmap 映射到用户态,提交和完成事件的读取可以通过纯用户态内存访问完成,避免了系统调用开销。
三、工作流程详解
3.1 初始化 io_uring
struct io_uring ring;
// entries = 队列深度,必须是 2 的幂
int ret = io_uring_queue_init(1024, &ring, 0);
if (ret < 0) {
perror("io_uring_queue_init");
return -1;
}
初始化时内核会分配:SQ 环形缓冲区(entries 个 SQE)、CQ 环形缓冲区(entries * 2 个 CQE,用于历史和溢出处理)、SQEs 数组(entries 个 struct io_uring_sqe)。通过 mmap 映射到用户空间。
3.2 提交 IO 请求
// 1. 获取一个空闲 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
// 2. 填充 SQE
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
sqe->user_data = (uint64_t)my_request; // 自定义标识
// 3. 提交到内核(如果有 IORING_SETUP_SQPOLL 则为纯异步)
io_uring_submit(&ring);
3.3 收割完成事件
struct io_uring_cqe *cqe;
// 非阻塞检查是否有完成事件
int ret = io_uring_peek_cqe(&ring, &cqe);
if (ret == 0) {
// 处理完成事件
my_request_t *req = (my_request_t *)cqe->user_data;
if (cqe->res >= 0) {
printf("IO 完成,读取 %d 字节\n", cqe->res);
} else {
printf("IO 失败: %s\n", strerror(-cqe->res));
}
// 标记已消费
io_uring_cqe_seen(&ring, cqe);
}
3.4 阻塞等待模式
// 阻塞等待至少 1 个 IO 完成
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
// 处理 CQE
io_uring_cqe_seen(&ring, cqe);
3.5 完整生命周期
io_uring_queue_init → mmap 映射 SQ/CQ
↓
io_uring_get_sqe → 从 SQ ring 获取空闲 SQE
↓
io_uring_prep_xxx → 填充操作参数
↓
io_uring_submit → 更新 SQ tail(可选:通过 Enter syscall 通知内核)
↓
[内核异步处理 IO]
↓
io_uring_peek_cqe / wait_cqe → 检查/等待 CQE
↓
io_uring_cqe_seen → 更新 CQ head
↓
io_uring_queue_exit → 释放资源
四、关键高级特性
4.1 固定缓冲区(Registered Buffers)
默认情况下,io_uring 在每次 IO 前需要将用户缓冲区映射到内核,IO 完成后解除映射。如果频繁使用相同缓冲区,可以通过 io_uring_register_buffers 预先注册,避免每次映射/解除映射的开销(约节省 500ns/IO)。
// 注册缓冲区数组
struct iovec iov[IO_BUFFERS];
for (int i = 0; i < IO_BUFFERS; i++) {
iov[i].iov_base = buffer[i];
iov[i].iov_len = BUFFER_SIZE;
}
io_uring_register_buffers(&ring, iov, IO_BUFFERS);
// 注册后使用时,设置 IOSQE_BUFFER_SELECT 或通过索引使用
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
sqe->buf_index = idx;
4.2 轮询模式(SQPOLL / IOPOLL)
SQPOLL(Submission Queue Poll):内核会自动创建一个内核线程 io-wq 持续轮询 SQ,用户态只需写入 SQE 并更新 tail,无需调用 io_uring_enter 系统调用。
struct io_uring_params params = {
.sq_thread_idle = 2000, // 空闲 2s 后内核线程休眠
};
io_uring_queue_init_params(1024, &ring, ¶ms);
// 检查是否启用了 IORING_SETUP_SQPOLL
if (params.features & IORING_FEAT_SQPOLL_NONFIXED) {
// SQPOLL 模式:自动提交,无需 io_uring_enter
}
IOPOLL:使用 blk-mq polling 模式,绕过后端的块层中断,由内核线程主动轮询块设备完成队列。适用于 NVMe 设备,可将单次 IO 延迟降至微秒级。
// 初始化时设置 IORING_SETUP_IOPOLL
struct io_uring_params params = { .flags = IORING_SETUP_IOPOLL };
io_uring_queue_init_params(1024, &ring, ¶ms);
4.3 层级化 Linked SQE(链式操作)
io_uring 支持将多个 SQE 链接为一个原子操作链。在 IO 操作失败时,链中后续操作会被跳过(通过 IOSQE_IO_LINK 标志)。
struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe1, fd, buf1, len1, offset1);
io_uring_sqe_set_flags(sqe1, IOSQE_IO_LINK); // 链接到下一个
struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe2, fd, buf2, len2, offset2);
io_uring_sqe_set_flags(sqe2, IOSQE_IO_LINK); // 继续链接
struct io_uring_sqe *sqe3 = io_uring_get_sqe(&ring);
io_uring_prep_fsync(sqe3, fd); // 最终 fsync
io_uring_submit(&ring);
// 要么全部执行成功,要么中间失败时后续操作跳过
4.4 Buffer Selection(洋葱模式 / Providing Buffures)
对于网络 IO(read/recv),io_uring 支持在内核侧预先分配缓冲区(io_uring_register_buffers + PROVIDERE 标志),减少一次内核到用户态的内存拷贝。
4.5 异步 fdatasync / fallocate 等扩展操作
io_uring 不仅仅是 read/write,还支持:IORING_OP_FSYNC、IORING_OP_FALLOCATE、IORING_OP_READV / WRITEV、IORING_OP_SENDMSG / RECVMSG、IORING_OP_ACCEPT / CONNECT、IORING_OP_TIMEOUT、IORING_OP_LINK_TIMEOUT、IORING_OP_CLOSE、IORING_OP_OPENAT 等。
五、性能基准测试
5.1 测试环境
- CPU: AMD EPYC 7763 64 核
- 内存: 256GB DDR4-3200
- 存储: Intel P5800X 1.6TB NVMe(4K 随机读 >7M IOPS)
- 内核: Linux 6.1
5.2 vs 同步 IO(Direct)
| 指标 | libaio (O_DIRECT) | io_uring (IORING_SETUP_SQPOLL) |
|---|---|---|
| 4K 随机读 IOPS | 1,200,000 | 1,580,000 |
| CPU 利用率 | 100% (1 核) | 78% (1 核) |
| 平均延迟 (μs) | 8.2 | 5.7 |
| 系统调用/秒 | 1,200,000 | 0 (SQPOLL 模式) |
5.3 vs epoll (网络场景)
10K QPS 短连接 HTTP 请求,io_uring + accept + read + write 全异步模式: - epoll + 线程池:平均延迟 45μs,CPU 利用率 320% - io_uring + PROVIDERE + SQPOLL:平均延迟 18μs,CPU 利用率 140%
5.4 系统调用开销对比
| 模式 | 每次 IO 系统调用次数 |
|---|---|
| 同步 read/write | 2 |
| libaio (io_submit + io_getevents) | 2 |
| io_uring 默认模式 | 1 (io_uring_enter) |
| io_uring SQPOLL 模式 | 0 |
六、生产环境部署考量
6.1 内核版本要求
- Linux 5.1+: 基础 io_uring 支持
- Linux 5.19+: SQPOLL 大幅改进,支持高级特性
- Linux 6.1+: IOPOLL 完善,性能最佳(LTS 版本)
- Linux 6.6+: 新增 futex 集成提升唤醒性能
6.2 安全限制
默认情况下非特权进程有 rlimit 限制了 io_uring 实例数量,可通过 /proc/sys/vm/unprivileged_uring 调整。
6.3 兼容性
filecaps 等能力检查:在容器化环境中需要 CAP_SYS_ADMIN 才能使用某些高级特性。Fallback 策略:对于不支持的内核版本,应退回到 libaio 或同步 IO。
6.4 最佳实践清单
- 预注册缓冲区:对固定 IO 大小场景(如数据库 WAL)使用 registered buffers
- 合理设置队列深度:通常为物理设备队列深度的 2-4 倍
- 批量提交:积累多个 SQE 后一次性 submit,减少 syscall 次数
- CQ 环形大小:设置
IORING_SETUP_CQSIZE避免 CQE 溢出 - 避免混合模式:不要同时使用 epoll + io_uring 监控同一 fd
- 优先使用 6.1+ LTS:稳定性和性能最佳
七、实战代码:基于 io_uring 的异步文件服务器
以下是一个精简的 io_uring 事件循环框架:
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define QUEUE_DEPTH 256
#define BLOCK_SIZE 4096
struct request {
int type; // READ or WRITE
int fd;
void *buf;
off_t offset;
size_t len;
};
void handle_read_completion(struct io_uring *ring, struct io_uring_cqe *cqe) {
struct request *req = (struct request *)io_uring_cqe_get_data(cqe);
if (cqe->res < 0) {
fprintf(stderr, "Read failed: %s\n", strerror(-cqe->res));
close(req->fd);
free(req->buf);
free(req);
return;
}
int bytes_read = cqe->res;
printf("Read %d bytes from fd=%d\n", bytes_read, req->fd);
// 可选:使用读取的数据发起写操作
struct request *write_req = malloc(sizeof(*write_req));
write_req->type = 2; // WRITE
write_req->fd = STDOUT_FILENO;
write_req->buf = req->buf;
write_req->offset = 0;
write_req->len = bytes_read;
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_write(sqe, write_req->fd, write_req->buf,
write_req->len, write_req->offset);
io_uring_sqe_set_data(sqe, write_req);
io_uring_submit(ring);
free(req);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return 1;
}
struct io_uring ring;
if (io_uring_queue_init(QUEUE_DEPTH, &ring, 0) < 0) {
perror("io_uring_queue_init");
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// 预注册缓冲区
struct iovec iov[QUEUE_DEPTH];
void *bufs[QUEUE_DEPTH];
for (int i = 0; i < QUEUE_DEPTH; i++) {
bufs[i] = aligned_alloc(BLOCK_SIZE, BLOCK_SIZE);
iov[i].iov_base = bufs[i];
iov[i].iov_len = BLOCK_SIZE;
}
io_uring_register_buffers(&ring, iov, QUEUE_DEPTH);
// 批量发起读取请求
off_t offset = 0;
for (int i = 0; i < QUEUE_DEPTH; i++) {
struct request *req = malloc(sizeof(*req));
req->type = 1; // READ
req->fd = fd;
req->buf = bufs[i];
req->offset = offset;
req->len = BLOCK_SIZE;
offset += BLOCK_SIZE;
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) break;
io_uring_prep_read_fixed(sqe, req->fd, req->buf,
req->len, req->offset, i);
io_uring_sqe_set_data(sqe, req);
}
io_uring_submit(&ring);
// 事件循环
struct io_uring_cqe *cqe;
int pending = QUEUE_DEPTH;
while (pending > 0) {
io_uring_wait_cqe(&ring, &cqe);
struct request *req = (struct request *)io_uring_cqe_get_data(cqe);
handle_read_completion(&ring, cqe);
io_uring_cqe_seen(&ring, cqe);
pending--;
}
close(fd);
io_uring_queue_exit(&ring);
return 0;
}
编译运行:
gcc -o uring_cat uring_cat.c -luring
./uring_cat /path/to/file
八、与其它异步框架的对比
| 特性 | io_uring | io_uring + SQPOLL | libaio | POSIX AIO | epoll |
|---|---|---|---|---|---|
| 零 syscall | ✓ | ✓ | ✗ | ✗ | ✗ |
| 支持 socket | ✓ | ✓ | ✗ | ✗ | ✓ |
| 支持 pipe | ✓ | ✓ | ✗ | ✗ | ✓ |
| 缓冲区注册 | ✓ | ✓ | ✗ | N/A | N/A |
| 链式操作 | ✓ | ✓ | ✗ | ✗ | ✗ |
| 内核要求 | 5.1+ | 5.19+ | 2.6+ | 2.6+ | 2.6+ |
| 线程依赖 | 可选 | 无 | 无 | 线程池 | epoll 线程池 |
九、未来展望
io_uring 仍在持续演进中,未来版本可能带来:网络零拷贝高级封装:sendfile/send_zc 的 io_uring 原生支持、io_uring 文件监控:取代 inotify 的异步文件事件通知、分布式 IO 支持:跨节点 io_uring 能力、BPF 集成:用户自定义 io_uring 操作处理逻辑、更细粒度的权限控制:按能力限制 io_uring 操作类型。
十、总结
io_uring 代表了 Linux 异步 IO 的未来方向。通过共享内存环形队列的设计,它从根本上解决了传统 AIO 和 POSIX AIO 的诸多缺陷。对于追求极致 IO 性能的应用而言,io_uring 已经成为不可或缺的基础设施。
在实际部署中,要根据内核版本选择合适的功能组合:内核 6.1+ 可放心使用 SQPOLL + IOPOLL + Registered Buffers;较老内核版本可使用默认模式 + 自定义事件循环;始终保留同步 IO fallback 以应对兼容性问题。
io_uring 不仅仅是一个 IO API,它正在重塑 Linux 高性能应用的基础架构设计方式。

发表评论 取消回复