io_uring 的原理
原始问题:io_uring 的原理是啥 · 2026-06-30
io_uring 原理深度解析
📋 问题定位
- 子系统: Linux 内核异步 IO 框架(Block/File IO 层)
- 引入版本: Linux 5.1(作者 Jens Axboe)
- 核心动机: 解决传统异步 IO(AIO)和 epoll 模型的痛点——减少系统调用开销与数据拷贝
🎯 核心设计理念:共享内存 + 无锁环形队列
io_uring 最革命性的想法是:通过内核与应用共享的环形缓冲区来传递 IO 请求和完成事件,将传统的 syscall → 内核处理 → 返回 模式,变成 应用写入 SQ → 内核消费 → 内核写入 CQ → 应用消费 的提交队列(SQ)与完成队列(CQ)双环模式。
这使得绝大多数场景下无需任何系统调用即可发起 IO——这才是性能提升的根本来源。
🏗️ 关键数据结构
1. 三个核心共享区域
用户态 内核态
═══════════════════════════════ ═══════════════════════════════
┌──────────────────────┐ ┌──────────────────────┐
│ SQ (Submission │ ◄────► │ sq_ring → sqes │
│ Queue Entries) │ mmap │ (环形数组) │
├──────────────────────┤ ├──────────────────────┤
│ CQ (Completion │ ◄────► │ cq_ring → cqes │
│ Queue Entries) │ mmap │ (环形数组) │
├──────────────────────┤ ├──────────────────────┤
│ SQ 数组 (SQEs) │ ◄────► │ 内存中实际 SQE │
└──────────────────────┘ └──────────────────────┘
| 结构 | 含义 | 作用 |
|---|---|---|
| SQ | Submission Queue | 应用写入要发起的 IO 请求(索引指向 SQE) |
| SQE | Submission Queue Entry | 实际的 IO 请求描述(struct io_uring_sqe,包含 fd/op/offset/flag 等) |
| CQ | Completion Queue | 内核写入已完成的 IO 结果 |
| CQE | Completion Queue Entry | 完成结果描述(struct io_uring_cqe,包含 res/user_data) |
2. SQE 结构体(liburing 头文件中定义)
struct io_uring_sqe {
__u8 opcode; /* IORING_OP_READV/WRITEV/ACCEPT/... */
__u8 flags; /* IOSQE_* flags */
__u16 ioprio; /* I/O priority */
__s32 fd; /* file descriptor */
__u64 off; /* offset in file */
__u64 addr; /* iovec/buffer address */
__u32 len; /* iovec count / buffer len */
union {
__kernel_rwf_t rw_flags;
...
};
__u64 user_data; /* 用户自定义数据,返回时带回 */
__u16 buf_index; /* buffer group index (for provided buffers) */
...
};
3. CQE 结构体
struct io_uring_cqe {
__u64 user_data; /* 对应 SQE 中填的 user_data */
__s32 res; /* 返回结果(类似 read/write 返回值或 errno) */
__u32 flags;
};
🔄 IO 请求的完整生命周期
以一次 preadv2 为例:
应用线程 内核 IO 线程 (or 系统调用路径)
═══════════ ════════════════════════
1. 获取 SQE 槽位
sqe = io_uring_get_sqe(ring);
2. 填充 SQE
io_uring_prep_readv(sqe, fd, iov, iovcnt, offset);
sqe->user_data = MY_TAG_123;
3. 通知内核有新请求 ──► 4. 内核从 SQ 消费请求
io_uring_submit(ring); io_submit_sqes()
(实际更新 SQ tail, ① 解析 SQE → 创建
通过 smp_store_release kiocb/io_kiocb
写 tail 指针) ② 调用对应操作回调
→ io_read() / io_write()
③ 若可立即完成,直接完成
④ 若需等待,提交给
block 层后返回
5. 轮询/等待完成 ──► 5. 内核将结果写入 CQ
while (1) { io_cqring_fill_event()
// 方式 A: 无锁轮询 cqe->user_data = SQE.user_data
head = *cq_ring->head; cqe->res = bytes_written
if (head != cq_ring->tail) { smp_store_release(tail)
cqe = &cq_ring->cqes[head & mask];
process_result(cqe);
head++;
}
// 方式 B: 用 io_uring_enter()
// 仅在 SQ 为空或需等待时调用 syscall
io_uring_enter(ring, 0, 1, IORING_ENTER_GETEVENTS);
}
⚡ 三种 IO 模式及其演进
| 模式 | 引入版本 | 原理 | 延迟特征 |
|---|---|---|---|
| IRQ 驱动 (默认) | 5.1 | 请求提交给 block 层,硬件完成后硬中断触发,软中断完成 CQE | 中等,有中断开销 |
| SQPOLL | 5.1 | 内核起一个内核线程(io_sq_wq)持续轮询 SQ,应用不需任何 syscall 提交 | 低,无 syscall |
| IOPOLL | 5.1 | 用户线程在 io_uring_enter() 中主动轮询硬件完成队列 |
最低,但占用 CPU |
SQPOLL 的极致场景:应用只需通过共享内存写 SQE,内核线程秒级消费,零系统调用即可完成 IO。这在 Redis、数据库等场景中效果显著。
📊 与传统方案对比
| 维度 | read/pwrite (同步) | epoll + 线程池 | AIO (libaio) | io_uring |
|---|---|---|---|---|
| syscall 次数/IO | 2 | ~2-3 | 2 | 0 ~ 1 (带 SQPOLL 可为 0) |
| 内存拷贝 | 有 | 有 | 有 | 共享内存无拷贝 |
| 支持 buffered IO | ✅ | ✅ | ❌(仅 O_DIRECT) | ✅ |
| 支持网络 IO | ✅ | ✅ | ❌ | ✅ (5.1+) |
| 支持 fsync/statx/… | 各自 syscall | 各自 syscall | ❌ | 所有操作统一接口 |
| 每次 IO 动态分配内存 | 是 | 是 | 是 | ❌ 预分配,运行时零分配 |
💡 关键设计哲学总结
- 减少 syscall 次数:批量化提交,配合 SQPOLL 完全消除
- 减少内存拷贝:mmap 共享环形队列,IO 请求/完成在共享内存传递
- 减少动态分配:SQE/CQE 数组预先分配,运行时不需要
kmalloc/kfree - 减少锁竞争:利用 memory ordering (smp_store_release / smp_load_acquire) 实现单生产者单消费者无锁通信
- 统一接口:read/write/accept/connect/fsync/statx/openat/splice 等 40+ 种操作统一通过一套接口
🔗 延伸阅读
- 源码入口
fs/io_uring.c(5.10+ 后拆分为多个文件:io_uring/目录) - liburing 用户态库:https://github.com/axboe/liburing
- Jens Axboe 的 LWN 文章:https://lwn.net/Articles/776703/
⚠️ 免责声明
本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。