2019 年 5 月,Linux 5.1 合入了一个只有三个系统调用的新接口:io_uring。作者 Jens Axboe 是内核块层的多年维护者,给它的使命简单粗暴——让 Linux 第一次拥有「真正的」异步 IO。这话听着夸张:Linux 不是早就有 AIO 吗?但正是这桩旧案,解释了为什么一个新接口值得全行业等二十年,也解释了为什么 2025 年的 PostgreSQL 18 把它当作头版特性。

一、二十年欠账:异步 IO 的两次失败
第一代是 POSIX AIO,也就是 glibc 提供的 aio_read/aio_write。它压根不是内核功能,而是 glibc 用线程池模拟出来的「伪异步」:每个请求丢给池中线程去同步执行。man 手册自己承认了天花板——「maintaining multiple threads to perform I/O operations is expensive and scales poorly」(维护大量线程做 IO,开销大且扩展性差),并说基于内核 AIO 的重建「尚未成熟」。
第二代是 2005 年前后进入内核的 Linux AIO(libaio,io_submit/io_getevents)。它是真的内核异步,却盖了一座 O_DIRECT 监狱。2017 年 kkourt.io 的一篇实测梳理把现状列得很清楚:
- 头文件里声明了一堆命令,内核实际支持的只有 4 个:
PREAD、PWRITE、PREADV、PWRITEV; - 缓冲 IO 不支持异步,必须配 O_DIRECT,否则请求在
io_submit里就同步执行完了; - 内存压力等场景下
io_submit照样可能阻塞,异步语义破功。
这套接口此后十余年基本停滞。行业于是形成扭曲的分工:数据库自带 buffer pool、本来就绕过页缓存用 O_DIRECT,忍着 libaio 的简陋凑合用;网络服务与普通应用干脆放弃异步 IO,用「epoll 通知 + 同步读写」凑合——后者正是本系列第 5 篇的主角。结果就是:Linux 拥有最活跃的内核生态,却没有一个同时覆盖存储与网络、缓冲与非缓冲的异步 IO 接口。这就是 io_uring 要还的账。
二、io_uring 的答案:把提交与收割搬进共享内存
io_uring 的思路不是「再造一个更好的系统调用」,而是把系统调用这个交互方式本身替换掉。用户态与内核通过 mmap 共享一对环形缓冲:
- SQ(Submission Queue,提交环):应用把 IO 请求描述符(SQE,64 字节)排进队列;
- CQ(Completion Queue,完成环):内核完成后把结果(CQE,16 字节)排回来。
每个 SQE 携带 64 位 user_data,完成时原样带回,应用靠它把结果对回请求;错误以负数 errno 写进 CQE 的 res 字段。整条链路大多数时候不需要陷入内核:提交只是写共享内存,收割只是读共享内存,需要等待或唤醒时才调一次 io_uring_enter。三个系统调用的分工在 io_uring(7) 手册里写得很清楚:io_uring_setup 建环并 mmap,io_uring_enter 提交与等待,io_uring_register 预注册资源。
两个容易被略过的设计细节值得点名。其一,SQ 环里存的不是 SQE 本身而是索引,中间隔着一个间接数组,配合 ring_mask 取模寻址——这让内核与应用各自的游标互不踩脚。其二,完成顺序不保证:请求可以乱序完成(磁盘 IO 本来就是乱序的),有依赖就显式声明 IOSQE_IO_LINK 链。
flowchart LR
subgraph U["用户态"]
APP["应用"] -- "填充 SQE(带 user_data)" --> SQ["SQ 提交环"]
CQ["CQ 完成环"] -- "收割 CQE(res 结果)" --> APP
end
subgraph K["内核态"]
SQ -- "io_uring_enter / SQPOLL" --> ENG["异步执行引擎"]
ENG -- "完成写回" --> CQ
end
与 epoll 的本质差异在这一步就显形了:epoll 只解决「什么时候可以读写」的通知,每次读写仍是一次同步系统调用;io_uring 把操作本体异步化——应用说「帮我把这个文件读进这块内存」,内核做完把结果放回来。等与做,第一次都交给了内核。
省系统调用在 Spectre/Meltdown 缓解措施普及后价值陡增,因为一次系统调用的开销里有相当比例来自安全缓解。io_uring 还提供 IORING_SETUP_SQPOLL:内核起专用线程轮询 SQ,应用连 enter 都免了,提交路径彻底零系统调用;再配合预注册的文件描述符与内存缓冲(fd 换成数组下标,省掉每次操作的查找与引用计数),Axboe 在设计文档里用 fio 给出的同配置对比中,io_uring 的可提交速率远超 libaio——具体数字随内核版本与负载形态变化,但方向从来稳定。
三、从 8 个 opcode 到全能接口
5.1 首发只有 8 个操作:readv/writev、fsync、poll 等,主打存储。此后以「一个合并窗口补一批」的节奏扩张,LWN 2020 年初的观察文章标题就叫「The rapid growth of io_uring」:
| 内核版本 | 新增能力 | 意义 |
|---|---|---|
| 5.1(2019-05) | readv/writev、fsync、poll_add、注册缓冲与文件 | 存储异步起步 |
| 5.3 | sendmsg/recvmsg | 网络第一块拼图 |
| 5.5 | accept/connect、async_cancel、link_timeout | 网络服务成型 |
| 5.6 | openat/close/statx/read/write/epoll_ctl 等 | 文件操作全家桶,opcode 达 30 个 |
| 5.19(2022-07) | multishot accept/recv、buffer ring、零拷贝发送起步 | 网络高性能形态 |
| 6.x 持续 | multishot 全面化、混合 SQE/CQE 等 | 6.12 起 CQE 标志体系定型 |
到 5.6 已有 30 个 opcode,当时就有评论者担心 8 位 opcode 字段(上限 256)不够用,也有评论者感叹这套「提交/完成」模型像 25 年前 Windows NT I/O 模型的翻版——方向殊途同归,只是 Linux 这次用共享内存把交互成本打了下来。
5.19 的两个能力值得单独一提。multishot 让一次提交持续产出多个事件:accept 一次提交收 N 个新连接,recv 一次提交收 N 批到达数据,把「每连接每事件一次提交」也省掉了。provided buffer ring 解决 accept 之后「不知道该把数据读进哪块内存」的鸡生蛋问题:预先圈一片缓冲池,内核读好数据后带着 buffer id 回来,应用用完还池。有了这两件,io_uring 才算补齐了真实协议服务器的拼图。
四、PostgreSQL 18:二十年欠账的清偿
数据库一直是异步 IO 最执着的买家。PostgreSQL 自带 buffer pool,却长期依赖操作系统预读(posix_fadvise)这类隔靴搔痒的提示——内核不懂数据库的访问模式,预读常常打不准。PostgreSQL 17 先铺了 read stream 基建,2025 年 9 月 25 日发布的 PostgreSQL 18 终于落地异步 IO 子系统:新参数 io_method 三选一——sync(旧行为)、worker(后台 IO 工作进程,缺省,io_workers 默认 3)、io_uring(Linux 独占,共享环直连内核)。官方公告给出的口径是读存储场景最高 3 倍提升,覆盖顺序扫描、位图堆扫描与 VACUUM;写路径仍走同步。
pganalyze 在 AWS c7i.8xlarge 上的冷缓存实测(3.5 GB 表、关闭并行查询):
| io_method | 顺序扫描耗时 |
|---|---|
| PG 17 sync(基线) | 15,830.9 ms |
| PG 18 sync | 15,071.1 ms |
| PG 18 worker(3 进程) | 10,052.0 ms |
| PG 18 io_uring | 5,723.4 ms |
io_uring 比同步基线快约 2.8 倍,也明显快过 worker 模式——没有中间进程,请求从 buffer manager 直达内核环。配套调整同样有信息量:effective_io_concurrency 默认值从 1 提到 16,新增 pg_aios 视图观察在途请求。
生态的其他拼图:libuv 自 1.45 起,文件操作在 Linux 上默认改走 io_uring;QEMU 磁盘后端自 5.0(2020)支持 aio=io_uring;Rust 阵营的 monoio(字节跳动)与 glommio(Datadog)把 thread-per-core 架构押在它上面。而 epoll 时代的两位旗手按兵不动:nginx 没有跟进,Redis 的 io_uring 改造长期停留在社区 PR 阶段。切换有真实成本,原因见下一节。
五、双面账单:性能争议与安全风暴
「io_uring 全面快过 epoll」是个经不起推敲的流行叙事,liburing 仓库的 issue #189 是最好的标本。开发者 Alex Hultman 用 40–400 连接的小数据 echo 服务器反复对比,epoll 每一次都赢,放话「拿不出科学证据我就不用」;Redis 类引擎作者 romange 反驳,自己在 5.7 内核上测出过单机 300 万 QPS。Axboe 本人的表态最有信息量:高性能姿势不是把 epoll 代码翻译过来(就绪通知 + 手动 read),而是「直接发请求,让内核内部轮询去处理」——把 readiness 模型硬搬进 completion 模型,两头的好处都拿不到。真机数据也提醒别迷信微基准:Proxmox 生态的 QEMU 存储测试显示,128K 大块高队列深度下 aio=io_uring 反而出现明显劣化。
本文的待证伪假设是「io_uring 在真实业务负载中带来数量级提升」,裁决:数量级差距(10 倍级)只出现在高队列深度的存储微基准与提交路径对比;真实业务里,PostgreSQL 冷缓存读是 2–3 倍,网络服务通常是 ±20% 的量级,方向取决于负载形态——批处理、multishot、SQPOLL 三件凑齐才有甜头(本文作者判断,截至 2026-10-31)。
安全这本账更沉。2023 年 6 月,Google 漏洞奖励团队公布:42 个获赏的 Linux 内核漏洞利用中 60% 涉及 io_uring,相关赏金约 100 万美元(总赏金 180 万);更刺眼的是「所有绕过我们缓解措施的提交都用到了 io_uring 漏洞」。Google 的结论是 io_uring「目前只宜用于可信组件」,随即 ChromeOS 禁用、Android 用 seccomp 把 app 进程挡在外面(未来版本将用 SELinux 收紧到少数系统进程)、GKE AutoPilot 与 Google 生产服务器默认关闭。安全研究者 Ben Hawkes 当时断言「io_uring 的时代可能走向终结」。
内核的回应分两层。防御层:Linux 6.6 合入 kernel.io_uring_disabled 系统参数——0 默认放行、1 仅限 CAP_SYS_ADMIN(另有 io_uring_group 白名单组)、2 完全禁用;Docker 的默认 seccomp 配置长期不包含 io_uring 调用族,容器里启用前先验证。治理层:维护者 Pavel Begunkov 等人对内存记账与请求生命周期做了大规模加固,Axboe 的立场是多数漏洞的根因(早期线程卸载的不安全实现)早已修复,漏洞猎手只是扎堆在最热的新代码上。两边的说法都有依据,工程师的结论只有一条:把它当高危能力一起评估收益与暴露面,基线允许再上。
六、工程落地清单
- 存储密集服务优先评估:数据库、代理缓存、日志引擎收益最大;内核 5.1 即可用,但踩在安全修复密集期之后更稳妥——本文建议以 5.15+ 稳定树为起步线。
- 网络服务别为「快」切换:epoll 仍是默认正确答案,批处理 + multishot + SQPOLL 组合下才有可见收益,且要先过安全基线。
- 把 completion 模型当新范式学:不要把 readiness 代码机械翻译;预注册资源、multishot、buffer ring 才是收益的主要来源。
- 上线前查两道闸门:发行版策略(
kernel.io_uring_disabled)与容器 seccomp 放行;用注册缓冲与零拷贝发送时留意 memlock 限额,不足时报-ENOMEM。 - 用 liburing 而不是裸系统调用:mmap 环的样板代码与内存序细节容易出错,库已经替你踩平。
结语
回到系列的坐标系:第 5 篇讲「等」的解耦——epoll 让内核替你等;这一篇是「做」的解耦——io_uring 让内核连 IO 本身也替你做了,等待与执行都不再消耗你的系统调用。libaio 二十年的教训同样清楚:一个不完整的抽象比没有更糟,它会逼出整个行业的变通与妥协——PostgreSQL 等了二十年才敢切换,就是这个代价的注脚。存储与 IO 这条线到此补完;下一个值得攻坚的,是内存的另一半故事:回收与 OOM。
参考资料
- Efficient IO with io_uring — Jens Axboe——设计原典:环布局、SQPOLL、注册缓冲与 fio 对比。
- io_uring(7) — man7.org——SQ/CQ、间接数组、user_data 与版本特性探测的权威契约。
- The rapid growth of io_uring — LWN——5.1–5.6 opcode 扩张的第一手编年史。
- Linux asynchronous file IO — kkourt.io——libaio 四个 opcode 与 O_DIRECT 监狱的实测梳理。
- PostgreSQL 18 Released! — postgresql.org——AIO 子系统与
io_method的官方公告(2025-09-25)。 - Waiting for Postgres 18: Accelerating Disk Reads with Asynchronous I/O — pganalyze——io_method 三模式冷缓存实测数字。
- Our learnings from 42 Linux kernel exploits — oss-security——Google 漏洞账单一手数据(60%、约 100 万美元)。
- io_uring is slower than epoll · liburing issue #189——epoll 之争的完整现场与 Axboe 的「正确姿势」表态。
读者留言
COMMENTS 暂无还没有留言,来说第一句?