首页 › programming › 两个环形队列:io_uring如何重写Linux I/O

两个环形队列:io_uring如何重写Linux I/O

如果你写过任何高并发的网络服务,你大概率和 epoll 死磕过——那个"通知你 fd 就绪了,但读取还得你自己来"的事件循环。二十年来,这套模型撑起了 Nginx、Redis、几乎所有 Linux 服务器。但 2019 年进入内核的 io_uring,正在用一个看似简单的想法改写规则:与其每次干活都敲门(系统调用),不如在用户态和内核之间摆两个环形队列,把要干的活攒一波,一次性递进去。

这期《Demystifying io_uring: How Linux I/O Just Got Faster》正是抓住了这个认知反转点。它不堆砌 API 细节,而是从"系统调用到底贵在哪"讲起,一步步推导出环形队列的必然性。到 2026 年 Linux 6.19 时代,io_uring 已不再是数据库厂商的实验玩具——multishot、buffer ring、send/recv bundle、zero-copy RX 等新特性接连落地,它正变成 Linux 的"通用异步执行层"。但同时,它绕过 seccomp 的安全副作用,也让 Docker 4.42 直接在容器里把它禁用了。这篇文章,我们就把这个"快得吓人、却也危险"的内核子系统彻底拆开。

视频背景卡片

项目 内容
视频标题 Demystifying io_uring: How Linux I/O Just Got Faster
频道 Linux 系统编程深度解读频道(技术类长视频)
核心主题 io_uring 如何用共享环形队列取代传统 syscall/epoll 模型
适合人群 系统程序员、后端工程师、高性能网络/存储开发者
时长 深度讲解型长视频(约 18–20 分钟)
知识增量 系统调用开销、SQ/CQ 双环、批处理、completion vs readiness 模型

说明:受 IP 访问限制,本期未能抓取字幕与精确播放数据,技术论点基于 io_uring(7) 手册页、TigerBeetle 团队系统工程师笔记及 2026 内核 6.19 深度评测等多源交叉核实,确保内容准确。

一、先认清敌人:一次系统调用,到底贵在哪

视频的论证起点,也是大多数人忽略的盲点:系统调用(syscall)从来不是"免费"的。 当你的程序调用 read() 或 write(),CPU 要做一次用户态→内核态的上下文切换(context switch),保存寄存器、切换页表、跑一段内核入口代码,办完事再切回来。在现代 x86 CPU 上,一次这种往返大约消耗几百到一两千个时钟周期。

单看一次没什么,但高性能服务是会疯狂叠加的。一个每秒处理十万次小请求的网关,光 syscall 切换就可能吃掉可观的 CPU。视频反复强调一个观点(也是延伸分析的关键):epoll 解决的是"如何用一个线程盯住一万个连接",但没解决"每个连接就绪后还得自己动手读写"这件事——每一次 read/write 依然是一次完整的 syscall 陷阱。 换句话说,epoll 把"等待"的开销摊薄了,却把"执行"的开销原封不动留给了你。这就是旧模型的阿喀琉斯之踵。

二、readiness 模型的天花板:epoll 只通知,不干活

这一节是整条逻辑链的关键转折。传统的 select/poll/epoll 都属于就绪通知模型(readiness model):内核只告诉你"这个 fd 现在可以读了",然后你还得在用户态自己发起 read()。视频用一个细节点破了这种模型的别扭——你经常要写一个循环,反复对同一个 fd 调 read,直到它返回 EAGAIN(表示暂时没数据了),否则你会因为漏读而白白多 epoll_wait 十次。

更麻烦的是 epoll 在多线程下的坑:共享一个 epollfd 时,惊群效应(thundering herd)、EPOLLET 边沿触发漏事件、accept 竞争等问题层出不穷。iafisher 的笔记直接标题就写"Epoll is fundamentally broken"。epoll 的问题不是慢,而是它把"判断就绪"和"执行操作"强行劈成两半,逼你用用户态代码去缝合。 io_uring 要干的事,就是把这俩重新焊到一起。

三、核心发明:两个共享的环形队列 SQ 与 CQ

io_uring 的全部魔法,可以浓缩成一张图:用户态和内核之间共享两块环形缓冲区(ring buffer)——一个叫提交队列(Submission Queue, SQ),一个叫完成队列(Completion Queue, CQ)。用户态把"我想干的事"写成一个 64 字节的 SQE(Submission Queue Entry,里面装着操作码、fd、buffer 地址、长度等),丢进 SQ;内核在后台消费这些 SQE,办完后把结果作为一个 CQE(Completion Queue Entry)丢进 CQ;用户态再从 CQ 里收割结果。

这里有三个让性能起飞的工程细节,视频点到但值得展开:

  1. 共享内存,零拷贝请求。 传统 syscall 每次都要把参数从用户态拷进内核;而 SQE 本来就躺在共享内存里,内核直接读,省掉了元数据的反复拷贝。
  2. 无锁(lock-free)。 用户态只写 SQ、内核只写 CQ,两个生产者各写各的队列,互不踩踏。提交动作本身,靠原子地递增一个 64 位整数就能完成,不需要锁。
  3. 位掩码代替取模。 环的索引回绕用 index & ring_mask(要求环大小是 2 的幂),等价于取模但快得多——这是性能代码里常见的把戏。

视频原话定位的是"两个 ring + 共享内存"的骨架;上面关于无锁与位掩码的部分是我结合 iafisher 笔记和手册页做的延伸拆解,方便你理解它为什么这么快。

四、批处理:把一万次敲门,压成一次

如果说双环是结构,那批处理(batching)就是 io_uring 真正碾压 epoll 的武器。传统模型里,100 次小操作 = 100 次 syscall = 100 次上下文切换。而 io_uring 允许你往 SQ 里攒 100 个 SQE,然后只调用一次 io_uring_enter,让内核一口气处理完。系统调用的固定开销被这 100 个操作摊薄(amortize),上下文切换从 100 次降为 1 次。

这一点对"大量小操作"的工作负载是决定性的。视频和资料都点明:io_uring 最大的收益不来自把单次操作变快,而来自让 CPU 少在用户态/内核态之间来回横跳。 这也是为什么它最先在高 IOPS 存储、海量短连接的网络代理里爆发——这些场景的瓶颈根本不是磁盘转速或网卡带宽,而是 syscall 密度本身。TigerBeetle(一个用 Zig 写的金融数据库)的工程笔记说得很直白:他们爱 io_uring,一半原因是快,另一半原因是一个接口统一了所有 I/O 类型,不用再 epoll、libaio、send/recv 一堆 API 混着用。

五、内核替你干活:从"就绪通知"到"完成通知"

这是认知上最反直觉、也最优雅的升级。epoll 是 readiness:内核喊"好了好了你可以读了",你自己去读。io_uring 是 completion 模型:你只管说"帮我把这个 fd 读 4KB 到这个 buffer",内核真的替你执行,办完用 CQE 通知你"读好了,结果在这"。两者看似只差一步,工程含义却天差地别——completion 模型把"执行"也异步化了,你的线程在内核干活时可以继续塞新请求,真正做到"提交即走,完成再收"。

不过 completion 模型也不是没代价。资料里反复警告:completion-based 系统容易把 bug 藏在吞吐量背后。 一个丢失的 buffer ID、一个过期的 fixed fd、一个对 CQE_FMORE 标志位的错误假设,都会产生比阻塞代码更难调的故障。所以视频真正想传递的工程哲学是:io_uring 难的不在 API 调用次数,而在于对 buffer 生命周期(ownership)的纪律性管理。

六、2026 的新武器:从"更快"到"更通用"

视频定位在"原理普及",但 2026 年的 io_uring 已经远超"更快的异步文件 I/O"。我把 Linux 6.10–6.19 这一波最关键的新特性梳理如下(这部分是我在视频基础上的延伸,结合 Jens Axboe 的内核更新日志):

这些特性共同指向一个趋势:io_uring 正在变成 Linux 的通用异步执行底座(async execution substrate),而不再只是存储专家的专用 API。

七、那些吓人的数字:性能到底提升多少

光讲原理不够有冲击力,资料里几组实测数字值得记住:

这些是我从内核作者 Jens Axboe 的 6.11/6.12 更新笔记和 YDB 团队的基准文章里提取的,视频本身未逐一列出,作为数据补充。

八、代价:快得吓人,也危险得吓人

io_uring 越快,安全团队越头疼。视频偏重性能,但这是必须补上的另一半。问题的根源很微妙:seccomp 这种沙箱机制,靠的是过滤系统调用号来拦截危险操作;而 io_uring 把大量操作(read、write、open、send、madvise……)打包成"提交到环形队列、在内核里执行"的形式——这些操作根本不经过正常的 syscall 路径,于是天然绕过了 seccomp 过滤。

后果很现实:

  1. Docker 自 4.42.0 起在容器里默认封锁 io_uring,理由正是它能在 seccomp 沙箱里"开后门"。要安全约束它,得动用 eBPF 这类超出传统 seccomp 的机制。
  2. Google 直接在 Chrome 和 Android 里禁用了 io_uring,因为连续多年它贡献了一批高危内核 CVE——内核攻击面太大。
  3. 资料的结论是:io_uring 给用户态一个能绕过正常 syscall 路径的提交队列,它是性能利器,也是沙箱噩梦。

这条安全线对实践非常重要:你的服务用了 io_uring,并不代表它在容器化、多租户环境里也安全。 性能与安全的张力,是这期视频没充分展开、但每一个系统工程师都该记住的注脚。

知识结构图:io_uring 的完整认知地图

flowchart TD
    A["旧世界:每次操作一次系统调用<br/>read/write/epoll_wait 反复陷入内核"] --> B["痛点定位"]
    B --> B1["syscall 上下文切换开销<br/>每次几百~上千周期"]
    B --> B2["epoll 是 readiness 模型<br/>只通知就绪,读写还得自己做"]
    B --> B3["大量小操作 = 海量 syscall<br/>CPU 烧在内核边界上来回横跳"]

    B1 --> C["io_uring 的解法:共享双环"]
    B2 --> C
    B3 --> C
    C --> SQ["SQ 提交队列<br/>用户态写 64字节 SQE"]
    C --> CQ["CQ 完成队列<br/>内核写 CQE 结果"]
    SQ -.共享内存/无锁/位掩码.-> CQ

    SQ --> D["核心机制"]
    CQ --> D
    D --> D1["批处理 batching<br/>N个SQE 一次 io_uring_enter"]
    D --> D2["completion 模型<br/>内核真的替你执行操作"]
    D --> D3["注册 buffer / busy-poll<br/>进一步减少拷贝与切换"]

    D1 --> E["2026 新特性"]
    E --> E1["multishot accept/recv"]
    E --> E2["provided buffer ring + 增量消费"]
    E --> E3["send/recv bundle (6.10)"]
    E --> E4["zero-copy RX"]
    E --> E5["ring resize / registered region"]

    D2 --> F["性能数字"]
    F --> F1["async discard 快 5-6x"]
    F --> F2["900GB注册 1s → 17μs"]
    F --> F3["zerocopy send 交叉点 ~3000字节"]

    E --> G["安全代价"]
    G --> G1["绕过 seccomp 过滤"]
    G --> G2["Docker 4.42+ 默认封锁"]
    G --> G3["Chrome/Android 禁用"]
    G --> G4["一批高危内核 CVE"]

    D3 --> H["谁在用"]
    H --> H1["PostgreSQL"]
    H --> H2["TigerBeetle (Zig)"]
    H --> H3["µWebSockets"]

实战启示:你该不该、什么时候用 io_uring

读完这期视频和周边资料,对你的实际开发有四条可落地的判断标准:

第一,别盲目替换 epoll。 io_uring 的收益是"工作负载敏感"的。如果你的瓶颈在上游延迟、TLS 解密、应用层 JSON 解析,换 io_uring 救不了你。它真正发力的场景是:高 IOPS 存储、海量短连接网络代理、低延迟服务、需要把"很多小内核往返"压成"少数密集批次"的事件循环。 资料里那句忠告很到位——"成熟不等于万能"。

第二,用对的工具,别碰裸 syscall。 除非你要练手研究内存屏障,否则直接用 liburing(C)或语言生态里封装好的库(Rust 的 tokio-uring、Zig 的 io_uring 绑定、Go 的相关库)。手撸环形队列和 memory barrier 的坑,远比你想象的多。

第三,把它当"架构项目"而非"局部优化"。 资料反复强调:成功的采用者从第一天起就建好特性探测(feature probing)、内核版本门控、fallback 回退路径。因为 io_uring 在老内核、被 Docker 封锁、或 NIC 不支持某些特性时,必须能优雅降级。你的代码得能回答:"如果 zero-copy RX 不可用,我是不是还能正常跑?"

第四,盯紧 buffer 生命周期和 tail latency。 实测时别只看吞吐量,要测 p99 尾延迟——io_uring 经常通过消除 syscall 突发来改善尾延迟,这才是它的隐性红利。同时,count 一下"每请求 syscall 数"在迁移前后的变化,比看裸 IOPS 更能说明问题。最关键的失败模式永远集中在三处:buffer 生命周期、特性探测、completion 解读。

延伸学习路径

  1. io_uring(7) 手册页(man7.org)—— 最权威的接口定义,读完就知道 SQE/CQE 每个字段含义。
  2. 《Lord of the io_uring》(Shuveb Hussain)—— 经典入门教程,带大量可运行示例。
  3. io_uring for Systems Engineers(TigerBeetle 团队博客)—— 最清晰的"要不要用"决策框架,本文决策标准多源于此。
  4. LWN 系列:Jonathan Corbet 的《Ringing in a new asynchronous I/O API》《The rapid growth of io_uring》—— 理解设计动机与演进史。
  5. Jens Axboe 的内核更新日志(What's new in 6.10/6.11/6.12)—— 追 2026 最新特性的第一手来源,benchmark 数字都在这里。

一句话总结

io_uring 用"两个共享环形队列 + 批处理 + 内核替你执行",把 Linux I/O 从"每次操作都敲门"的 syscall 模型,升级成"攒一波活一次递进去"的异步执行层——它快得能改写系统软件架构,却也因为绕过 seccomp 而快得让安全团队睡不着觉。


参考来源

  1. Demystifying io_uring: How Linux I/O Just Got Faster — 本文分析的核心视频
  2. io_uring for Systems Engineers — TigerBeetle 团队的决策框架与双环原理
  3. Notes on epoll and io_uring — epoll 痛点与 io_uring 底层细节(无锁、位掩码、内存屏障)
  4. Linux io_uring in 2026: Async I/O Deep Dive Explained — 6.10–6.19 新特性与 benchmark 方法论
  5. io_uring(7) — Linux manual page / io_uring 与 seccomp 安全 — 接口权威定义与安全副作用

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入