引言:高性能 I/O 的历史痛点
在 Linux 系统编程中,I/O 性能一直是制约应用吞吐量的关键瓶颈。从传统的 read()/write() 阻塞系统调用,到 epoll() 事件通知机制,再到原生的 Linux AIO (io_submit()),每一代技术都在试图解决"如何高效地在用户态与内核之间传递 I/O 请求"这一核心问题。然而,这些方案各有难以逾越的局限:
epoll 的困境:epoll 本质上只是一个事件通知器,它告诉你"某个 fd 可读了",但实际的数据读写仍然需要你发起系统调用。对于高并发存储或网络服务,这意味着每个 I/O 操作仍然需要两次系统调用(epoll_wait read/write),上下文切换开销不可忽视。
Linux AIO 的缺陷:原生的 POSIX AIO(如 io_submit())虽然名义上支持异步,但其 design 存在诸多限制:仅支持 O_DIRECT 文件(绕过页缓存),不支持网络 I/O,完成事件经常需要回退到阻塞模式,而且在不同文件系统上行为不一致。
2019 年 Linux 5.1 内核的发布,带来了一个根本性的解决方案——io_uring。
一、io_uring 架构总览
io_uring 由 Block I/O 子系统开发者 Jens Axboe 设计,其核心思想极其优雅:在用户态和内核态之间建立一个共享内存环形缓冲区(ring buffer),实现零系统调用的 I/O 请求提交与完成通知。
io_uring 的核心结构包含两个环形缓冲区:
| 组件 | 全称 | 方向 | 用途 |
|---|---|---|---|
| SQ | Submission Queue | 内核态读取 ← 用户态写入 | 存放待提交的 I/O 请求 (SQE) |
| CQ | Completion Queue | 用户态读取 ← 内核态写入 | 存放已完成的 I/O 结果 (CQE) |
关键在于:SQ 和 CQ 都是 单生产者-单消费者 的环形缓冲区,且位于共享内存中。这意味着:
提交端:用户态程序将 SQE 写入 SQ 尾指针位置,然后只需更新尾指针(内存写操作),无需进入内核。
完成端:内核将 CQE 写入 CQ 尾指针位置,用户态只需读取 CQ 头指针到尾指针之间的数据(纯内存访问),无需系统调用。
只有当需要内核"注意"SQ 中有新请求时,才需要调用 io_uring_enter() 系统调用。更进一步,通过 IORING_SETUP_SQPOLL 模式,内核会主动轮询 SQ,实现真正的零系统调用 I/O。
二、核心数据结构与系统调用
2.1 初始化:io_uring_setup()
int io_uring_setup(unsigned entries, struct io_uring_params *p);
entries 是 SQ 的请求数量(会被向上取整为 2 的幂,CQ 大小是 SQ 的两倍)。io_uring_params 结构体包含丰富的配置字段和输出信息:
struct io_uring_params {
__u32 sq_entries; // SQ 实际大小(输出)
__u32 cq_entries; // CQ 实际大小(输出)
__u32 flags; // 配置标志(输入)
__u32 sq_thread_cpu; // SQPOLL 线程绑核(输入)
__u32 sq_thread_idle; // SQPOLL 空闲超时(输入,ms)
__u32 features; // 内核支持的特性(输出)
__u32 wq_fd; // io-wq 工作队列 fd(输出)
// ... 内存映射偏移量字段
struct io_sqring_offsets sq_off;
struct io_cring_offsets cq_off;
};
调用成功后返回一个 ring fd,随后通过 mmap() 映射三个内存区域:SQ 环形缓冲区、SQE 数组、CQ 环形缓冲区(CQ 直接附加在 SQ 映射之后)。
2.2 提交请求:SQE 结构
struct io_uring_sqe {
__u8 opcode; // 操作码(IORING_OP_READV 等)
__u8 flags; // IOSQE 标志位
__u16 ioprio; // I/O 优先级
__s32 fd; // 目标文件描述符
union { __u64 off; __u64 addr2; };
union { __u64 addr; __u64 splice_off_in; };
__u32 len; // 数据长度
union {
__kernel_rwf_t rw_flags;
__u32 fsync_flags;
// ... 其他操作特定标志
};
__u64 user_data; // 用户自定义标识,会在 CQE 中原样返回
union {
struct { __u16 buf_index; __u16 buf_group; };
__u64 __pad2[3];
};
};
常用的 opcode 类型:
| 操作码 | 功能 | 典型应用场景 |
|---|---|---|
| IORING_OP_READV | 散布读(preadv) | 从文件多段区域读取 |
| IORING_OP_WRITEV | 聚集写(pwritev) | 向文件多段区域写入 |
| IORING_OP_READ | 定位读(pread) | 单区域读取 |
| IORING_OP_WRITE | 定位写(pwrite) | 单区域写入 |
| IORING_OP_FSYNC | 文件同步 | 确保数据落盘 |
| IORING_OP_ACCEPT | 接受连接 | 异步 accept |
| IORING_OP_CONNECT | 发起连接 | 异步 connect |
| IORING_OP_SENDMSG | 发送消息 | 异步网络发送 |
| IORING_OP_RECVMSG | 接收消息 | 异步网络接收 |
| IORING_OP_TIMEOUT | 超时事件 | 定时器模拟 |
2.3 完成事件:CQE 结构
struct io_uring_cqe {
__u64 user_data; // 对应的 SQE 中设置的 user_data
__s32 res; // 操作返回值(类似系统调用返回值)
__u32 flags; // 标志位(如 IORING_CQE_F_MORE)
};
通过 user_data 字段,用户可以将完成事件与原始请求关联起来,实现高效的请求-响应匹配。
三、liburing 实用编程指南
虽然可以直接使用系统调用操作 io_uring,但官方提供的 liburing 库封装了底层细节,提供了简洁的 C 接口。以下展示如何使用 liburing 构建高性能异步 I/O 程序。
3.1 初始化与队列准备
#include

发表评论 取消回复