2. io_uring 核心数据结构与 API 入门
io_uring 的高性能并非来自某个“魔法函数”,而是来自一套精心设计的内存共享协议:内核与用户态通过 mmap 共享提交队列(SQ)和完成队列(CQ),用户态把请求写进 SQ,内核处理后把结果写进 CQ,双方通过环形缓冲区上的头尾指针同步进度,从而把系统调用次数压到最低。理解这套协议,关键就是掌握三个系统调用和两个结构体。
三个系统调用:建立、提交、注册
io_uring_setup 负责创建 io_uring 实例。原型为 int io_uring_setup(unsigned entries, struct io_uring_params *p)。第一个参数 entries 是期望的队列深度,内核会向上取整到 2 的幂;第二个参数既是入参也是出参,调用前可设置 flags(如 IORING_SETUP_SQPOLL 启用内核轮询线程、IORING_SETUP_IOPOLL 启用块设备轮询),调用返回后内核会填充 sq_entries、cq_entries、features 以及各 mmap 区域的偏移量。返回值是一个文件描述符,后续所有操作都围绕它展开。
拿到 fd 后,用户态需要做三次 mmap:用 IORING_OFF_SQ_RING 映射 SQ 环形区,用 IORING_OFF_CQ_RING 映射 CQ 环形区,用 IORING_OFF_SQES 映射 SQE 数组。前两者给出的是“控制区”,包含头尾指针和数组基址;后者给出的是真正存放请求的 SQE 数组。三者缺一不可。
io_uring_enter 是提交与收割的入口,原型为 int io_uring_enter(int fd, unsigned to_submit, unsigned min_complete, unsigned flags, sigset_t *sig)。to_submit 表示本次要提交的 SQE 数量;min_complete 表示希望等待完成的 CQE 数量,配合 IORING_ENTER_GETEVENTS 使用即可实现“提交并等待”;flags 还可带 IORING_ENTER_SQ_WAKEUP 唤醒 SQPOLL 线程。若只提交不等待,把 min_complete 设为 0 即可。
io_uring_register 用于把长期不变的对象提前注册进内核,减少每次提交时的重复开销。典型用途包括:注册固定缓冲区(IORING_REGISTER_BUFFERS),让内核预先锁定内存并建立映射,之后 SQE 用缓冲区索引而非地址;注册固定文件(IORING_REGISTER_FILES),避免每次操作都做 fd 查找与引用计数;以及注册事件 fd(IORING_REGISTER_EVENTFD),把完成事件转为 eventfd 通知,便于与 epoll 等机制集成。
io_uring_sqe:请求的载体
SQE 描述“要做什么”。核心字段包括:opcode 指定操作类型(如 IORING_OP_READV、IORING_OP_WRITEV、IORING_OP_ACCEPT、IORING_OP_RECV、IORING_OP_SEND);flags 控制行为(如 IOSQE_FIXED_FILE 表示 fd 是注册索引,IOSQE_IO_LINK 表示与下一条链接);fd 是目标文件描述符或注册索引;off 是文件偏移;addr 与 len 指向用户缓冲区及其长度;user_data 是用户自定义的 64 位标识,完成时会原样回传,是关联请求与响应的关键;buf_index 在使用固定缓冲区时指定索引。多个字段通过 union 复用,因此必须按 opcode 语义填写,不能随意赋值。
io_uring_cqe:完成的回执
CQE 描述“结果是什么”,结构简单但信息密度高:user_data 与提交时的 SQE 对应,用于匹配;res 是操作返回值,成功时为字节数或 0,失败时为负的 errno(如 -EAGAIN、-ECONNRESET);flags 携带附加信息,例如 IORING_CQE_F_BUFFER 表示使用了缓冲区选择组,IORING_CQE_F_MORE 表示后续还有完成事件。判断一次操作是否成功,就是看 res 是否小于 0。
一次完整的提交与收割
把上述结构串起来,一次典型操作如下:应用从 SQ 的 tail 位置取一个空闲 SQE 槽,填写 opcode、fd、addr、len 和 user_data,然后递增 tail;接着调用 io_uring_enter(fd, 1, 1, IORING_ENTER_GETEVENTS, NULL),内核看到 tail 前进,取出 SQE 执行,完成后把 CQE 写入 CQ 并推进 CQ 的 tail;应用检查 CQ 的 head 与 tail 是否相等,不等则取出 CQE,读取 res 判断成败,再推进 head。整个过程用户态只做内存读写和一次系统调用,这正是 io_uring 低开销的来源。
参数与返回值速查
| 对象 | 关键字段/参数 | 含义与取值要点 |
|---|---|---|
| io_uring_setup | entries | 期望队列深度,内核向上取整到 2 的幂 |
| io_uring_setup | params.flags | SQPOLL / IOPOLL 等特性开关 |
| io_uring_enter | to_submit | 本次提交的 SQE 数量 |
| io_uring_enter | min_complete | 期望等待的 CQE 数量,配合 GETEVENTS |
| io_uring_register | opcode | REGISTER_BUFFERS / FILES / EVENTFD 等 |
| io_uring_sqe | opcode | READV / WRITEV / ACCEPT / RECV / SEND 等 |
| io_uring_sqe | user_data | 64 位用户标识,完成时原样回传 |
| io_uring_cqe | res | 成功为字节数或 0,失败为负 errno |
| io_uring_cqe | flags | F_BUFFER / F_MORE 等附加状态 |
user_data 当成“随便填的序号”而忽略其关联作用,会导致高并发下无法正确匹配请求与完成;另外,SQE 中多个字段是 union 复用,必须严格按 opcode 语义填写,否则内核可能解析出错误参数。