一次 read 调用就要陷入内核一次。几万并发连接同时读写时,系统调用与上下文切换本身会占掉可观 CPU。io_uring 把提交和完成放进两块共享内存环,让一次调用带走一批请求,这就是它要解决的问题。
io_uring 要解决什么问题:从「一次调用一个 I/O」到共享环
Linux 早就有异步 IO 接口 libaio,但它只对 O_DIRECT 和少数场景有效,缓冲读写与 socket 都不在支持范围。LWN 在 2020 年的回顾里写得很直接:AIO 需要底层专门支持,因此在两类核心场景之外一直不好用。
io_uring 在 2019 年 5 月的 5.1 内核合入。它不再用一次系统调用描述一个操作,而是在用户态与内核之间放两块环:提交环写请求描述,完成环读结果。
共享内存带来两个直接后果。请求描述不必复制进内核,完成结果也不必复制出来;多个请求可以攒着,用一次 io_uring_enter 提交。
它省下的是固定开销,不是磁盘或网卡的物理时间。这个区别决定了后面所有关于「快不快」的判断。
环里有什么:SQE、CQE 与两次 mmap
创建实例只调用一次 io_uring_setup(entries, ¶ms),返回一个文件描述符。entries 是队列深度的下限,内核可能给得更多;params 回填实际条目数与各段偏移。
随后对这个 fd 做 mmap。提交侧先映射环本身,再单独映射 SQE 数组;完成侧的环与 CQE 数组通常连在一起。io_uring(7) 手册说明,5.4 起内核会给出 IORING_FEAT_SINGLE_MMAP,两次 mmap 就够,更早的内核需要三次。
两块环的结构并不对称。CQ 环直接索引 CQE 数组;SQ 环与 SQE 之间多一层间接数组,环里存数组下标,数组里才是 SQE 下标。手册的原话是提交侧「有一个间接数组」,这让内核可以按自己的顺序消费。
SQE 是定长结构:opcode 说明做什么,fd、addr、len、off 描述对象,user_data 原样回传。CQE 只有三个字段:user_data、res、flags。错误不走 errno,res 直接放负的错误码,比如 -EINVAL。
还有一个常被忽略的约定:提交上去的请求可以按任意顺序完成。要对应哪次提交,只能靠 user_data,不能靠数组下标。
一次读请求的时序:从 tail 到 CQE
把一次 4 KB 读走一遍,顺序大致如下。
- 取 SQ 的 tail,用 ring_mask 取模得到槽位,填入 opcode、fd、缓冲区地址、长度与偏移。
- 把槽位下标写进间接数组,tail 加一,用 release 语义写回,保证内核看到新 tail 时数据已经就绪。
- 调用 io_uring_enter 通知内核取件。若开了 SQPOLL,这一步交给内核线程。
- 内核按 opcode 执行。能立刻完成的直接完成;可能阻塞的路径返回 -EAGAIN,请求被打包交给 io-wq 工作线程,去掉 NOWAIT 后同步执行。
- 完成后内核写入一个 CQE,填好 res 与 user_data,CQ 的 tail 前进。
- 应用从 CQ 的 head 读取,处理完把 head 写回。CQ 满时内核先在内部暂存,等有空间再放入。
第 4 步是理解性能的关键。io-wq 让 io_uring 能覆盖缓冲读写与文件系统操作,代价是会阻塞的请求最终仍由线程池同步执行。LWN 对 io-wq 的设计有专门分析,其中提到同一文件的多个缓冲请求不应并发,否则会在锁上互相拖住。
数据库场景常把 fsync 也放进环里,一批日志提交后统一等完成,减少往返次数。站内SQLite WAL 模式的写入流程里,fsync 的时机同样决定了提交延迟。
缓冲区与 iovec 必须在完成前保持有效。SQPOLL 下连提交时用到的指针也要留到完成;5.4 及更早的内核要求更严,提交数据结构在整个过程中都不能变动。
三个开关:注册缓冲区、SQPOLL 与 IOPOLL
这三个机制经常被笼统说成「提速」,作用点其实不同。
| 机制 | 省掉什么 | 代价与限制 |
|---|---|---|
| 注册缓冲区与文件 | 每次请求的地址固定与 fd 查找 | 占用锁定内存;生命周期由应用自己管理 |
| SQPOLL | io_uring_enter 调用 | 内核线程持续轮询耗 CPU;空闲超过 sq_thread_idle 毫秒会置 IORING_SQ_NEED_WAKEUP,应用仍需 enter 唤醒 |
| IOPOLL | 完成中断 | 忙等耗 CPU;仅对 O_DIRECT 生效;设备需支持轮询,NVMe 要加载 poll_queues 参数 |
io_uring_setup(2) 记载,SQPOLL 在 5.11 之前要求先注册文件,否则提交会拿到 EBADF;5.13 之后普通用户也能使用。手册对它的评价相当克制,说这类开关听起来像自动加速,但要不要用必须按场景评估。
注册缓冲区配合 IORING_OP_READ_FIXED 使用,省掉每次请求的地址固定;注册文件把 fd 查表换成数组下标。代价是应用要自己维护生命周期,用错时报 EBADF 或 EFAULT。
上手与观测:版本、sysctl 与 fio 对照
先看内核版本:uname -r。5.1 才有这套接口,实际使用建议 5.15 以上,6.1 之后特性与修复更完整。
再看开关:cat /proc/sys/kernel/io_uring_disabled。0 是默认,所有进程可建实例;1 只允许特权进程或 io_uring_group 组成员;2 全部拒绝,io_uring_setup 返回 -EPERM。内核文档写明该 sysctl 在 6.6 引入,且只影响新建实例,已经在跑的进程不受影响。
容器里要单独确认。Docker 在 2023 年 11 月把 io_uring_* 从默认 seccomp 允许列表移除,容器内创建实例会失败。遇到 ENOSYS 或 EPERM,先查 seccomp 配置与宿主机 sysctl,再怀疑程序。
验证不要只跑 demo。fio 自带 io_uring 引擎,固定文件、块大小与队列深度,只换引擎就能对照:
fio --name=r --filename=/data/test --size=1G --rw=randread --direct=1 --ioengine=io_uring --iodepth=32 --numjobs=4
再把 ioengine 换成 libaio 与 psync 各跑一遍。三次结果要在同一台机器、同一时段、同一方向上比较,否则变量太多。
同时看 CPU 分布。%sys 高说明系统调用占比大;iowait 高说明设备是瓶颈;两者都不高而吞吐上不去,多半卡在队列深度或应用串行。随机读有时瓶颈根本不在接口层,站内Everything 的 MFT 索引原理就说明,把随机访问换成顺序访问往往比换引擎更有效。
追踪调用:strace -f -e io_uring_setup,io_uring_enter,io_uring_register ./程序。只看到 setup 没有 enter,说明请求走了 SQPOLL;频繁 EPERM,回到 sysctl 与 seccomp 两步。
五个常见误区
- 以为换上就一定更快。小请求、队列深度 1、页缓存命中时,共享环的收益会被建环、内存屏障和批处理不足吃掉。手册对 SQPOLL 的建议是逐案评估,这个态度适用于整套接口。
- 以为完成顺序跟着提交顺序。手册明确说请求可以任意顺序完成。流式 socket 上同一时刻挂多个 send 或多个 recv 并不安全,要顺序就用 IOSQE_IO_LINK 串起来。
- 以为 seccomp 能逐条拦住环里的操作。LWN 在 2020 年就指出,seccomp 过滤器不适用于 io_uring 提交的操作。这也是容器运行时选择整个接口一起禁用的原因。
- 以为 io_uring_disabled=2 会立刻掐断在跑的服务。内核文档写明只阻止新建,已有实例仍可使用。要让存量进程也受影响,得重启或滚动替换。
- 以为缓冲区提交后就能复用。读写缓冲、iovec 乃至 SQPOLL 下的提交期指针都要活到 CQE。提前复用可能读到脏数据,或拿到 EFAULT。
写代码时直接用 liburing 比手搓 mmap 与内存屏障稳妥,它把建环、提交、收割包成了几个函数,示例目录里也有现成的复制与压测程序。
适合自己动手的人:写存储、代理或数据库周边工具,愿意用 liburing 改代码,也能对同一份负载做对照测试。只想让网页快一点,先看缓存、连接复用与图片体积,收益通常更直接。
排错顺序建议固定:内核版本 → sysctl → 容器 seccomp → 引擎对照 → 队列深度与 worker → 文件系统与设备。每改一项记下时间与参数,回滚才有依据。
相关地址
以下为文中接口与文档的官方入口,版本、字段与默认值以这些页面为准。
- io_uring(7) 手册页(man7.org):https://man7.org/linux/man-pages/man7/io_uring.7.html
- io_uring_setup(2) 手册页(man7.org):https://man7.org/linux/man-pages/man2/io_uring_setup.2.html
- io_uring_disabled 与 io_uring_group(Linux 内核文档):https://docs.kernel.org/admin-guide/sysctl/kernel.html
- Linux 5.1 发布说明(Kernel Newbies):https://kernelnewbies.org/Linux_5.1
- The rapid growth of io_uring(LWN):https://lwn.net/Articles/810414/
- Redesigned workqueues for io_uring(LWN):https://lwn.net/Articles/803070/
- Operations restrictions for io_uring(LWN):https://lwn.net/Articles/825759/
- liburing(GitHub):https://github.com/axboe/liburing
- fio 文档与 ioengine 列表(fio):https://fio.readthedocs.io/en/latest/fio_doc.html
- 默认 seccomp 配置移除 io_uring_*(moby PR #46762):https://github.com/moby/moby/pull/46762
© 版权声明
文章版权归作者所有,转载需注明出处。未声明原创的皆为网络收集,如有侵权请邮箱lishiwenhuagx@sina.com联系下架处理。




