传统异步 I/O 的历史困境
Linux 的异步 I/O(AIO)自内核 2.5 时代引入以来,始终处于一个尴尬的境地。POSIX AIO 由 glibc 在用户空间通过线程池实现,看似异步实则同步阻塞,性能堪忧。而内核原生 AIO(io_submit/io_getevents)虽然真正实现了内核级异步,但限制重重:仅支持 O_DIRECT 直 I/O(绕过页缓存)、不支持 socket 网络操作、每次操作需要系统调用、且要求 I/O 请求大小对齐到块边界。
在 epoll 接管事件通知、协程与用户态线程支配异步编程的时代,内核始终缺少一套统一的、真正的异步操作接口。直到 2019 年,Jens Axboe 带来了 io_uring(最初名为 auring),以最优雅的方式彻底解决了这个困扰 Linux 系统编程二十余年的根本问题。
io_uring 架构设计:共享内存的双环形缓冲区
io_uring 的核心思想极其简洁:在用户态与内核态之间设置两个共享环形缓冲区(ring buffer),彻底消除系统调用在数据传输层面的开销。
┌─────────────────────────┐ ┌─────────────────────────┐
│ 用户态进程 │ │ 内核线程 │
│ │ │ │
│ SQ (Submission Queue) │───写入──▶│ 处理 SQE 中的请求 │
│ SQE 环形缓冲区 │ │ │
│ │ │ 处理完成后写入 CQE │
│ CQ (Completion Queue) │◀──读取───│ CQE 环形缓冲区 │
│ CQE 环形缓冲区 │ │ │
└─────────────────────────┘ └─────────────────────────┘Submission Queue (SQ):用户态将 I/O 请求编码为 Submission Queue Entry(SQE),写入 SQ 环的尾部指针位置,然后更新尾指针。整个过程纯用户态操作,无需系统调用。
Completion Queue (CQ):内核处理完 I/O 请求后,将结果编码为 Completion Queue Entry(CQE),写入 CQ 环。用户态只需读取 CQ 尾指针与内核更新的头部指针之间的差值,即可获得完成事件。
这套设计的精妙之处在于:在 SQPOLL 模式下(内核轮询线程),用户态从提交请求到获取结果的整个生命周期中,完全可以不发起任何系统调用。每次 I/O 操作的开销降低到几次原子内存访问。
核心数据结构详解
1. io_uring_setup — 创建上下文
struct io_uring_params {
__u32 sq_entries;
__u32 cq_entries;
__u32 flags;
__u32 sq_thread_cpu;
__u32 sq_thread_idle;
__u32 features;
};
int io_uring_setup(unsigned entries, struct io_uring_params *p);
// 返回 fd,通过 mmap 映射 SQ/SQE 和 CQ/CQE 到用户空间2. SQE — 30+ 种操作的统一请求格式
struct io_uring_sqe {
__u8 opcode; // IORING_OP_READV/WRITEV/POLL_ADD 等
__u8 flags; // IOSQE_FIXED_FILE, IOSQE_IO_LINK 等
__u16 ioprio;
__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
__u16 buf_index;
__u16 personality;
};SQE 的 opcode 支持 30 余种操作:文件读写(READV/WRITEV)、套接字操作(SENDMSG/RECVMSG)、文件同步(FSYNC)、POLL_ADD(异步 epoll)、固定缓冲区操作(READ_FIXED/WRITE_FIXED)、文件定位(LSEEK)。一套接口覆盖网络、文件、文件系统的全部 I/O 场景。
3. CQE — 统一完成通知
struct io_uring_cqe {
__u64 user_data;
__s32 res;
__u32 flags;
};SQPOLL 模式:零系统调用的终极形态
struct io_uring_params params = {
.flags = IORING_SETUP_SQPOLL,
.sq_thread_cpu = 0,
.sq_thread_idle = 2000,
};
io_uring_setup(256, ¶ms);在 SQPOLL 模式下,内核线程持续轮询 SQ 环,用户态纯内存操作即可完成全部 I/O 生命周期。
Registered Buffers 与 Registered Files
io_uring 允许预注册缓冲区和文件描述符,消除每次 I/O 的页映射和 fd 引用计数开销。
// 注册固定缓冲区
io_uring_register_buffers(&ring, iovecs, 16);
// 使用固定缓冲区读
io_uring_prep_read_fixed(sqe, fd, buf, len, offset, buf_index);
sqe->flags |= IOSQE_FIXED_BUFFER;
// 注册文件
io_uring_register_files(&ring, files_array, 4);
io_uring_prep_read(sqe, file_index, buf, len, offset);
sqe->flags |= IOSQE_FIXED_FILE;链式操作
io_uring 的 IOSQEIO_LINK 标志允许将多个 SQE 按顺序链式执行,无需用户态等待中间结果。
// 链接 3 个操作: 读头部 → 读正文 → 通知
sqe1->flags |= IOSQE_IO_LINK;
sqe2->flags |= IOSQE_IO_LINK;
// 三个 SQE 一次 submit,内核顺序执行
io_uring_submit(&ring);性能对比数据
- IOPS(NVMe 随机读):io_uring+SQPOLL+fixed buffers 可达 170 万+,epoll+read 约 85 万
- P99 延迟:io_uring median 3-5μs,epoll+read 方案 10-20μs
- 系统调用:SQPOLL 模式每批可做到 0 次
- 统一接口:同一套 uring 同时处理 socket 和 file I/O
生产考量
- 内核 5.1 引入,生产级需 5.11+,最佳体验 5.18+
- seccomp 可能拦截 io_uring,容器环境需显式放行
- 多线程写 SQ 需额外同步或使用 per-thread ring
- 固定缓冲区需合理回收策略防止内存膨胀
io_uring 是 Linux 系统编程近十年最具颠覆性的创新,将异步从用户态模拟推进到内核原生支持,将高吞吐从批量缓存推进到零 syscall 核心路径。正在成为百万级 QPS 与微秒级延迟服务的新事实标准。

发表评论 取消回复