引言:Linux I/O 的范式转移
长期以来,Linux 开发者习惯于用 epoll 做事件驱动、用线程池做并发 I/O。这套模式在过去二十年支撑了从 Nginx 到 Redis 无数高性能服务。但随着 NVMe 存储和 100GbE 网络的普及,软件栈本身成了瓶颈——每一次 read()/write() 都伴随着用户态/内核态上下文切换、内存拷贝和系统调用开销。
io_uring 由 Jens Axboe(Linux 内核块设备层维护者)在 Linux 5.1 引入,彻底重构了异步 I/O 的接口设计。它通过两个共享环形缓冲区(Submission Queue / Completion Queue)实现用户态与内核态的零系统调用通信,同时支持 Buffers 注册、文件注册等「免分配」机制,将高频 I/O 场景下的系统调用开销趋近于零。
一、核心数据结构:SQ、CQE 与共享内存
io_uring 的核心是两块由用户态和内核态共享的环形缓冲区:
- Submission Queue (SQ):用户态写入 SQE(Submission Queue Entry),描述一个待执行的 I/O 操作(读/写/连接/接受等)。
- Completion Queue (CQ):内核态写入 CQE(Completion Queue Entry),通知用户态某个操作已完成及其结果。
两者的设计都遵循经典的「生产者-消费者」模型:用户态更新 SQ Tail 指针但不直接写 Head,内核态消费后更新 Head;CQ 侧同理。只要不显式设置 IORING_SETUP_SQPOLL,SQ 侧在提交批量操作后可以完全不调系统调用——内核线程会主动轮询 SQ。
二、与 epoll 线程池的性能对比
| 维度 | epoll 线程池 | io_uring |
|---|---|---|
| 系统调用频率 | 每次 I/O 至少 1 次 (read/write) | 批量提交 0 次(SQPOLL 模式) |
| 数据拷贝 | 用户态 ↔ 内核态各一次 | 支持 pre-registered buffers 零拷贝 |
| 文件描述符 | 每次 I/O 触发 fd 查找缓存 miss | 支持 fixed files 跳过 fd 查表 |
在 NVMe 随机读场景下,io_uring 的 IOPS 可达 epoll pread 的 2-3 倍,延迟降低 40%-60%。原因在于:
- 批量 SQE 提交减少了内核态/用户态切换次数
- Registered Buffers 避免了每次
get_user_pages()的开销 - Fixed Files 跳过了文件描述符表锁竞争
- SQPOLL 模式下内核线程持续轮询,消除了 ioctl/enter 系统调用
三、Rust 生态:rio 与 tokio-uring
Rust 对 io_uring 的封装主要有两条路线:
- rio:底层、直接的 liburing 绑定,提供完整的 io_uring API,适合对性能有极致追求的场景。
- tokio-uring:将 io_uring 作为 Tokio 运行时的事件源,让现有 async 代码无需重写即可利用 io_uring 加速 I/O。
下面用 rio 实现一个简单的异步文件读取器:
use rio::{IoUring, Uring};\nuse std::os::unix::io::AsRawFd;\n\nfn main() -

发表评论 取消回复