io_uring深度实战:Linux异步I/O的革命性演进
一、前言:为什么我们需要 io_uring
在Linux内核5.1(2019年5月)引入的io_uring,彻底改变了Linux异步I/O编程模型。相比之前的AIO(Asynchronous I/O)和同步阻塞I/O,io_uring提供了真正的零拷贝、零系统调用的异步I/O能力,性能提升了数倍甚至数十倍。
io_uring是由Jens Axboe主导开发的。他的目标很简单:解决Linux AIO长期存在的问题——仅支持O_DIRECT模式下的文件I/O,不支持网络I/O,API使用复杂,性能不达预期。
二、io_uring 核心架构
io_uring的创新在于使用了两个共享的环形缓冲区(Ring Buffer)来实现用户态与内核态之间的零系统调用通信:
- 提交队列(Submission Queue, SQ):用户态向SQ中写入请求描述符(SQE),内核态从SQ中取任务
- 完成队列(Completion Queue, CQ):内核态将完成的请求结果写入CQ,用户态从CQ中读取结果
- 提交队列条目数组(SQE Array):作为SQE的实际存储区域,SQ中的索引指向这个数组
- 完成队列条目数组(CQE Array):与SQ类似,CQE也在另一个数组中
这种设计的精髓在于:用户态和内核态通过内存映射(mmap)直接访问这些环形队列,在初始设置完成后,提交和完成操作都不需要系统调用。
三、深入理解 io_uring 数据结构
3.1 io_uring 结构体
struct io_uring {
struct io_uring_sq sq; /* 提交队列 */
struct io_uring_cq cq; /* 完成队列 */
unsigned int flags;
int ring_fd; /* uring实例的文件描述符 */
/* ... */
};
3.2 提交队列条目(io_uring_sqe)
每个SQE代表一个待提交的I/O操作:
struct io_uring_sqe {
__u8 opcode; /* 操作码:IORING_OP_READV/WRITEV/SEND/RECV等 */
__u8 flags; /* 标志位:IOSQE_FIXED_FILE/ASYNC等 */
__u16 ioprio; /* I/O优先级 */
__s32 fd; /* 文件描述符 */
union { __u64 off; __u64 addr2; };
union { __u64 addr; __u64 splice_off_in; };
__u32 len; /* 操作长度 */
union { __u32 rw_flags; __u32 fsync_flags; };
__u64 user_data; /* 用户数据,在CQE中返回 */
__u8 buf_index; /* 缓冲区组索引 */
__u16 personality;
union { __s32 splice_fd_in; };
__u64 __pad2[2];
};
3.3 完成队列条目(io_uring_cqe)
struct io_uring_cqe {
__u64 user_data; /* 对应SQE的user_data */
__s32 res; /* 操作结果(返回值) */
__u32 flags; /* 标志位 */
};
四、liburing 库实战
虽然可以直接使用系统调用与io_uring交互,但liburing库封装了所有底层操作,提供了更友好的API。
4.1 安装 liburing
# Ubuntu/Debian
apt install liburing liburing-dev
# 或从源码编译
git clone https://github.com/axboe/liburing.git
cd liburing
./configure && make && sudo make install
4.2 基础使用:读取文件
#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct io_uring ring;
/* 1. 初始化io_uring,队列深度256 */
io_uring_queue_init(256, &ring, 0);
/* 2. 获取一个SQE并填充(准备读操作) */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
int fd = open("test.txt", O_RDONLY);
char *buf = malloc(4096);
io_uring_prep_read(sqe, fd, buf, 4096, 0);
io_uring_sqe_set_data(sqe, buf);
/* 3. 提交请求 */
io_uring_submit(&ring);
/* 4. 等待并获取完成事件 */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
/* 5. 处理结果 */
if (cqe->res > 0) {
printf("Read %d bytes\n", cqe->res);
} else {
printf("Error: %d\n", cqe->res);
}
/* 6. 标记为已消费 */
io_uring_cqe_seen(&ring, cqe);
/* 7. 清理 */
free(buf);
close(fd);
io_uring_queue_exit(&ring);
return 0;
}
4.3 批量提交:链式操作
/* 链式操作:先读后写,写依赖读完成 */
struct io_uring_sqe *sqe1 = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe1, src_fd, buf, len, 0);
sqe1->flags |= IOSQE_IO_LINK;
struct io_uring_sqe *sqe2 = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe2, dst_fd, buf, len, 0);
sqe2->flags |= IOSQE_IO_LINK;
io_uring_submit(&ring);
/* 等待两个完成事件 */
struct io_uring_cqe *cqe;
for (int i = 0; i < 2; i++) {
io_uring_wait_cqe(&ring, &cqe);
io_uring_cqe_seen(&ring, cqe);
}
五、高级特性
5.1 固定缓冲区(Fixed Buffers)
预先注册一批缓冲区,在I/O操作中直接使用,避免每次分配/释放内存的开销:
struct iovec iovecs[32];
/* 分配32个4KB的缓冲区 */
for (int i = 0; i < 32; i++) {
iovecs[i].iov_base = aligned_alloc(4096, 4096);
iovecs[i].iov_len = 4096;
}
/* 注册固定缓冲区 */
io_uring_register_buffers(&ring, iovecs, 32);
/* 使用固定缓冲区读取 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, fd, NULL, 4096, 0, 0);
io_uring_submit(&ring);
5.2 固定文件(Fixed Files)
预先注册文件描述符表,避免每次open/close的系统调用开销:
int fds[10];
/* ... 打开fd并填充数组 ... */
io_uring_register_files(&ring, fds, 10);
/* 使用fixed file(fd设为索引,而非实际fd) */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, 0, buf, len, 0); /* 索引0对应fds[0] */
sqe->flags |= IOSQE_FIXED_FILE;
io_uring_submit(&ring);
5.3 轮询模式(Polling)
io_uring支持内核轮询模式(IORING_SETUP_IOPOLL),内核线程主动检查SQ中的新工作:
struct io_uring_params params = {0};
params.flags = IORING_SETUP_IOPOLL;
io_uring_queue_init_params(256, &ring, ¶ms);
注意:IOPOLL需要设备驱动支持(如NVMe),不支持时会回退到中断模式。
5.4 SQPOLL:SQ轮询线程
IORING_SETUP_SQPOLL创建一个内核线程持续收割提交队列,用户态完全不需要系统调用:
struct io_uring_params params = {0};
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 2000;
io_uring_queue_init_params(256, &ring, ¶ms);
/* 之后零系统调用提交 */
while (1) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
/* 填充sqe... */
if (++count >= 16) {
io_uring_submit(&ring);
count = 0;
}
}
六、io_uring 在网络I/O中的应用
io_uring不仅限于文件I/O,还支持send/recv/sendmsg/recvmsg等网络操作。
6.1 基础网络I/O示例
/* TCP发送数据 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, sockfd, send_buf, len, 0);
sqe->user_data = OP_SEND;
io_uring_submit(&ring);
/* TCP接收数据 */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, sockfd, recv_buf, buf_size, 0);
sqe->user_data = OP_RECV;
io_uring_submit(&ring);
6.2 Recv Multi-Shot:高效持续接收
/* 注册一个持久接收请求,每次收到数据都会产生CQE */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sqe, client_fd, NULL, 0, 0);
sqe->buf_group = 0; /* 使用缓冲区组0 */
sqe->flags |= IOSQE_BUFFER_SELECT;
io_uring_submit(&ring);
/* 一个请求自动产生多个CQE,极大减少提交次数 */
七、性能对比与分析
在典型的高性能存储场景下(NVMe SSD),io_uring与替代方案的性能对比:
| 方案 | IOPS | 延迟 | CPU占用 |
|---|---|---|---|
| 同步读(pread) | ~150K | ~7μs | 高(1核100%) |
| POSIX AIO | ~100K | ~10μs | 中 |
| io_uring(内联) | ~700K | ~1.4μs | 低 |
| io_uring(固定缓冲区) | ~1.2M | ~0.8μs | 极低 |
| io_uring(SQPOLL+IOPOLL) | ~2.5M | ~0.4μs | 极低 |
io_uring的性能优势来源:
- 零系统调用:SQPOLL模式下用户态提交完全不需要syscall
- 零拷贝:固定缓冲区直接使用注册的内存页
- 批处理:内核可以同时接收多个SQE,减少上下文切换
- 无锁设计:SQ和CQ使用原子操作,无需加锁
八、操作取消与超时
8.1 操作取消
/* 取消一个已提交但尚未完成的操作 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_cancel(sqe, target_user_data, 0);
io_uring_submit(&ring);
8.2 运行超时
/* 为操作设置超时 */
struct __kernel_timespec ts = { .tv_sec = 1, .tv_nsec = 0 };
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_timeout(sqe, &ts, 0, 0);
io_uring_submit(&ring);
九、在项目中集成 io_uring
9.1 编译配置
# Makefile
CFLAGS = -O2 -Wall
LDFLAGS = -luring
io_uring_app: main.c
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
9.2 系统内核版本要求
- Linux 5.1:基础io_uring支持(read/write/open/close等)
- Linux 5.5:IORING_FEAT_FAST_POLL(快速轮询)
- Linux 5.6:IORING_OP_SENDMSG/RECVMSG,固定缓冲区增强
- Linux 5.15:Multishot accept和recv,最低推荐版本
- Linux 5.19:零拷贝网络发送(IORING_SEND_ZC)
- Linux 6.1+:完整的高级特性支持
十、未来展望
io_uring仍在快速演进中:
- io_uring_uring:将io_uring用于io_uring注册fd本身的自引用模式
- zero-copy networking:内核态零拷贝发送的持续完善
- buffer groups增强:更灵活的缓冲区池生命周期管理
- io_uring 与 eBPF协同:利用eBPF进行更精细的I/O调度控制
- 用户态I/O框架生态:基于io_uring的存储引擎(RocksDB、SPDK等)日趋成熟
总结
io_uring代表了Linux I/O模型设计的最高水准:通过共享内存环形队列实现用户态与内核态的高效通信;通过固定缓冲区/文件进一步减少系统调用;通过SQPOLL和IOPOLL追求极致的零syscall体验。对于任何对I/O性能有较高要求的应用场景——数据库引擎、网络代理、游戏服务器、AI推理服务——io_uring都值得深入研究和实践。
掌握io_uring,不仅是掌握一个API,更是对操作系统内核与用户态协作模式的重新思考。

发表评论 取消回复