io_uring¶
是什么 / 解决什么问题¶
io_uring 是 Linux 5.1+ 引入的异步 I/O 框架。通过用户态与内核共享的环形缓冲区(ring buffer)提交和完成 I/O 请求,避免每次操作都陷入内核。它是第一个真正通用的异步 I/O 接口——支持磁盘、网络、定时器等几乎所有操作。
为什么需要 io_uring¶
| 已有方案 | 局限性 |
|---|---|
| epoll + 非阻塞 | 对普通文件无效;每次 read/write 仍是 syscall |
| Linux AIO (io_submit) | 只支持 O_DIRECT 文件;API 设计差 |
| 线程池模拟 | 线程开销大;调度不可控 |
| POSIX AIO (aio_read) | glibc 用线程池实现,性能差 |
io_uring 一次解决所有问题:文件和网络统一、零拷贝提交、批量操作、无需 syscall。
核心概念¶
用户态 内核态
┌─────────────┐ ┌─────────────┐
│ Submission │ ──提交请求──► │ │
│ Queue (SQ) │ │ io_uring │
└─────────────┘ │ 内核工作线程 │
┌─────────────┐ │ │
│ Completion │ ◄──完成通知── │ │
│ Queue (CQ) │ └─────────────┘
└─────────────┘
SQE = Submission Queue Entry(提交项)
CQE = Completion Queue Entry(完成项)
- SQ (Submission Queue):用户态写入 SQE,告诉内核"我要做什么操作"
- CQ (Completion Queue):内核写入 CQE,告诉用户态"操作完成了,结果是什么"
- 共享内存:SQ/CQ 通过 mmap 共享,读写无需 syscall
使用模式¶
基本流程(使用 liburing)¶
#include <liburing.h>
struct io_uring ring;
// 1. 初始化(队列深度 256)
io_uring_queue_init(256, &ring, 0);
// 2. 获取一个 SQE 槽位
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
// 3. 填充操作(这里是 read)
io_uring_prep_read(sqe, fd, buf, buf_size, offset);
io_uring_sqe_set_data(sqe, my_context); // 关联用户数据
// 4. 提交到内核
io_uring_submit(&ring);
// 5. 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 6. 处理结果
int result = cqe->res; // 等价于 read 的返回值
void *ctx = io_uring_cqe_get_data(cqe);
// 7. 标记 CQE 已消费
io_uring_cqe_seen(&ring, cqe);
// 清理
io_uring_queue_exit(&ring);
批量提交(高吞吐)¶
// 一次提交多个操作
for (int i = 0; i < batch_size; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fds[i], bufs[i], sizes[i], 0);
io_uring_sqe_set_data(sqe, &contexts[i]);
}
// 一次 syscall 提交所有
io_uring_submit(&ring);
// 批量收割完成事件
struct io_uring_cqe *cqes[batch_size];
int completed = io_uring_peek_batch_cqe(&ring, cqes, batch_size);
for (int i = 0; i < completed; i++) {
process_result(cqes[i]);
}
io_uring_cq_advance(&ring, completed);
网络服务器(替代 epoll)¶
// io_uring 可以直接做 accept、recv、send,不需要 epoll
void setup_accept(struct io_uring *ring, int listen_fd) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_multishot_accept(sqe, listen_fd, NULL, NULL, 0);
io_uring_sqe_set_data(sqe, ACCEPT_TAG);
// multishot:一次注册,持续接受连接
}
void setup_recv(struct io_uring *ring, int fd, char *buf, size_t len) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_recv(sqe, fd, buf, len, 0);
io_uring_sqe_set_data(sqe, conn_context);
}
void setup_send(struct io_uring *ring, int fd, char *buf, size_t len) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_send(sqe, fd, buf, len, 0);
io_uring_sqe_set_data(sqe, conn_context);
}
// 事件循环
for (;;) {
io_uring_submit_and_wait(&ring, 1);
struct io_uring_cqe *cqe;
unsigned head;
io_uring_for_each_cqe(&ring, head, cqe) {
void *tag = io_uring_cqe_get_data(cqe);
if (tag == ACCEPT_TAG) {
int conn_fd = cqe->res; // 新连接 fd
setup_recv(&ring, conn_fd, ...);
} else {
handle_io_completion(tag, cqe->res);
}
}
io_uring_cq_advance(&ring, count);
}
文件 I/O(io_uring 的核心优势场景)¶
// 读取文件无需 O_DIRECT,page cache 也能异步
int fd = open("data.bin", O_RDONLY);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, 4096, offset);
io_uring_submit(&ring);
// 不阻塞!即使 page cache miss 也在后台完成
这解决了 epoll 对普通文件无效的问题。
行为与语义¶
操作类型支持(部分列表)¶
| 类别 | 操作 |
|---|---|
| 文件 | read, write, readv, writev, fsync, fallocate |
| 网络 | accept, connect, recv, send, recvmsg, sendmsg |
| 高级 | splice, tee, shutdown, close, openat, statx |
| 定时 | timeout, link_timeout |
| 其他 | nop, cancel, poll_add (兼容 epoll 语义) |
SQE 标志¶
// 链式操作:上一个成功才执行下一个
sqe->flags |= IOSQE_IO_LINK;
// 例:read → process → write 形成链
// 固定文件:避免每次操作的 fd 查找开销
io_uring_register_files(&ring, fds, nr_fds);
sqe->flags |= IOSQE_FIXED_FILE;
// drain:等前面所有操作完成再执行
sqe->flags |= IOSQE_IO_DRAIN;
Setup 标志¶
// SQPOLL 模式:内核轮询 SQ,用户态提交无需 syscall
struct io_uring_params params = { .flags = IORING_SETUP_SQPOLL };
io_uring_queue_init_params(256, &ring, ¶ms);
// 此模式下 io_uring_submit() 不进内核(如果内核线程还在轮询)
// 注意:SQPOLL 会占用一个 CPU 核心持续轮询
CQE 结果¶
cqe->res // 操作结果,语义等同于对应 syscall 的返回值
// read: 读取字节数 或 -errno
// accept: 新 fd 或 -errno
cqe->flags // 额外标志(如 IORING_CQE_F_MORE 表示 multishot 还有后续)
性能考量¶
io_uring vs epoll 性能对比¶
| 维度 | epoll + 非阻塞 | io_uring |
|---|---|---|
| syscall 次数 | 每次 read/write 都是 syscall | 批量提交,可能 0 syscall (SQPOLL) |
| 磁盘文件 | 不支持异步 | 完整异步支持 |
| 上下文切换 | epoll_wait 必须进内核 | SQPOLL 模式可避免 |
| 网络吞吐 | 高 | 更高(减少 syscall 开销) |
| 延迟 | 低 | 更低(SQPOLL 消除了进入内核的延迟) |
| 复杂度 | 中 | 高 |
什么时候 io_uring 更快¶
- 大量小 I/O 操作 — syscall 开销占比高时,批量提交优势明显
- 混合文件+网络 — 统一模型,无需线程池处理文件 I/O
- 超低延迟场景 — SQPOLL 避免用户/内核切换
- 高 IOPS 存储 — NVMe SSD 的 IOPS 瓶颈在软件而非硬件
什么时候 epoll 够用¶
- 连接数中等(< 万级)且请求不密集
- 纯网络服务(无磁盘 I/O)
- 内核版本 < 5.1
- 追求代码简洁性
调优参数¶
// 队列深度:根据并发量调整
io_uring_queue_init(4096, &ring, 0); // 高并发场景
// 注册固定 buffer,避免每次 I/O 的 page pin/unpin
struct iovec iovs[N] = { ... };
io_uring_register_buffers(&ring, iovs, N);
io_uring_prep_read_fixed(sqe, fd, buf, len, offset, buf_index);
// 注册固定文件,减少 fd 引用计数操作
int fds[M] = { ... };
io_uring_register_files(&ring, fds, M);
常见陷阱与 FAQ¶
1. 忘记检查内核版本¶
不同 io_uring 功能需要不同最低版本:
| 功能 | 最低版本 |
|---|---|
| 基本 io_uring | 5.1 |
| SQPOLL | 5.4 |
| 注册固定 buffer | 5.1 |
| multishot accept | 5.19 |
| send_zc (零拷贝发送) | 6.0 |
// 运行时检测
struct io_uring_probe *probe = io_uring_get_probe_ring(&ring);
if (!io_uring_opcode_supported(probe, IORING_OP_SEND_ZC)) {
// 回退到普通 send
}
2. SQ 满了导致 get_sqe 返回 NULL¶
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
// SQ 满了,先提交再重试
io_uring_submit(&ring);
sqe = io_uring_get_sqe(&ring);
}
3. CQE 不及时消费导致溢出¶
CQ 大小默认是 SQ 的 2 倍。如果不及时消费 CQE,新完成的操作会被丢弃:
// 定期消费 CQE
io_uring_for_each_cqe(&ring, head, cqe) {
// 处理
}
io_uring_cq_advance(&ring, count);
// 或设置更大的 CQ
params.flags |= IORING_SETUP_CQSIZE;
params.cq_entries = 8192;
4. SQPOLL 需要权限¶
// SQPOLL 需要 CAP_SYS_ADMIN 或设置了 sq_thread_idle
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 1000; // 空闲 1s 后停止轮询,有新提交自动恢复
5. 链式操作中间失败¶
观测与调试¶
# 查看进程是否使用 io_uring
ls /proc/<pid>/fdinfo/ | xargs grep -l io_uring
# strace 看 io_uring syscall
strace -e trace=io_uring_setup,io_uring_enter,io_uring_register -p <pid>
# 查看 io_uring 内核统计
cat /proc/<pid>/fdinfo/<uring_fd>
# 输出含 SqEntries, CqEntries, SqThreadCpu 等
# bpftrace 统计 io_uring 提交频率
bpftrace -e '
tracepoint:io_uring:io_uring_submit_sqe {
@ops[args->opcode] = count();
}'
# 查看 io_uring 完成延迟
bpftrace -e '
tracepoint:io_uring:io_uring_complete {
@latency = hist(nsecs - @start[args->ctx]);
}'
延伸阅读¶
man 7 io_uring— 概述man 2 io_uring_setup/io_uring_enter/io_uring_register— 原始 syscall- liburing 源码 — 官方用户态库
- Lord of the io_uring — 入门教程
- io_uring and networking in 2023 — 最新进展