你写的每一行 read()、write()、accept(),本质上都是一次"敲门"——应用程序从用户空间翻墙进入内核空间,请内核替你干完活再翻回来。每次翻墙(context switch)都要保存寄存器、切换页表、刷新 TLB,Spectre/Meltdown 补丁之后这一次"敲门"变得更贵了。在高并发服务器上,一秒钟要敲几百万次门,光是敲门本身就把 CPU 烧光了。
State & Flow 频道这支《How io_uring Actually Works — The Async I/O Interface Replacing epoll》要讲的就是:Linux 在 2019 年悄悄塞进内核(5.1 版)的那个叫 io_uring 的接口,怎么用一对"共享内存环形队列",把敲门的动作几乎彻底省掉。它不是 epoll 的小修小补,而是 Linux 二十年来系统编程范式最大的一次转向——从"就绪通知(readiness)"走向"完成通知(completion)",从"系统调用驱动"走向"消息队列驱动"。Redis、Node.js、Nginx、Netty 都在往它身上迁。看完你才会理解,为什么 Google 在 2022 年的内核漏洞悬赏里,有 60% 的提交都是针对 io_uring 的攻击——一个强大到能绕过内核门卫的东西,自然也危险到值得所有人盯防。
| 项目 | 内容 |
|---|---|
| 视频标题 | How io_uring Actually Works — The Async I/O Interface Replacing epoll |
| 频道 | State & Flow(系统编程深度解读频道) |
| 时长 | 深度技术长片(约 15–25 分钟量级) |
| 播放量 | 技术圈高频传播(受 IP 限制未能拉取精确数字;已通过 YouTube oEmbed 官方接口核实视频与频道真实存在) |
| 核心主题 | io_uring 用共享内存环形队列取代 epoll,靠消除系统调用开销压榨极限 I/O 性能 |
| 关键人物 | Jens Axboe(io_uring 作者,Linux 内核 I/O 子系统维护者) |
视频的开场定位很准:要懂 io_uring,得先明白它要替代的"系统调用"到底有多贵。很多人以为 read() 就是读个文件、很便宜,但从 CPU 的角度看,这是一次完整的"跨域调用"。你的程序跑在用户态(ring 3),文件系统、网络栈、磁盘驱动都住在内核态(ring 0),中间隔着一道硬件级的特权边界。每次系统调用,CPU 要把用户态寄存器压栈、切到内核栈、扫描系统调用表、执行完再原路返回,顺带还要处理地址空间切换带来的 TLB 失效。
最致命的是 2018 年 Spectre / Meltdown 之后,Linux 不得不给每次用户态↔内核态切换加补丁(KPTI 等),把原本就贵的上下文切换又往上推了一大截。视频用 epoll 的事件循环举例:处理一个 socket 事件,至少要 epoll_wait()(系统调用①)拿到就绪 fd,再 read()(系统调用②)真正读数据,如果是边沿触发还得反复 read() 直到 EAGAIN。一个事件两到三次系统调用,并发一上来,CPU 大部分时间不是在干活,而是在反复敲门。
这是视频原话强调的点,也是社区共识。我补充一句:这也是为什么"把逻辑搬进内核"(如 eBPF、XDP)近年来这么火——既然敲门贵,那就少敲、或者干脆进去不出来了。
视频把 epoll 的根本局限讲得很透:epoll 只告诉你"这个 fd 准备好了",它自己并不干 I/O 的活。你拿到通知后,还得自己调 read()/write() 去搬数据。这就带来两层开销。
第一层是上面说的"通知 + 搬运"两段式系统调用。第二层更隐蔽:epoll 内部维护一棵红黑树来管理你关心的 fd 列表(interest list),每次 epoll_ctl 增删改 fd 都要加锁操作这棵树。对于连接很稳定的服务器这没事,但对于"每秒新建/销毁成千上万个连接"的场景(比如短连接代理、高频握手),红黑树的锁竞争会变成瓶颈。还有一个结构性问题:epoll 基本只管网络/管道这类可轮询的 fd,对普通缓冲文件读它帮不上忙——磁盘 I/O 在 epoll 模型里天生尴尬。
视频据此把 io_uring 定位为"完成型(completion-based)"接口:你不再问内核"谁准备好了",而是直接说"把这个文件读进这个 buffer,搞完告诉我"。从"问就绪"到"领任务",这是范式的根本切换。
这是全视频信息密度最高的一段,也是 io_uring 区别于一切前辈的核心设计。io_uring 在内核和应用之间铺了两个环形缓冲区(ring buffer),通过 mmap() 直接映射到同一块物理内存:
关键在于"共享内存"四个字。既然两个 ring 都映射在双方都能直接访问的物理页上,那么应用写 SQE、内核写 CQE,都不需要系统调用——双方通过读写这块共享内存就能交流,像两人共用一块白板传纸条。同步靠的是无锁设计:SQ 和 CQ 都是经典的单生产者单消费者(SPSC)环形队列,只用原子整数索引(head/tail)加内存屏障(memory barrier)就能保证正确性,全程不加锁。
一个 SQE 里打包了完整的请求信息:操作码(如 IORING_OP_READ、IORING_OP_SEND)、文件描述符、目标 buffer 地址、传输长度、偏移量、标志位。内核拿到 SQE 就知道该干嘛,不需要再回头问应用。
这段视频讲得最细。我的延伸理解是:io_uring 本质上是把"系统调用"这个同步 RPC,改造成了一个异步消息队列(mailbox)。应用是消息生产者,内核是消费者兼结果回填者,两者靠共享内存解耦——这正是高性能系统的经典套路(DPDK、SPDK 也是类似哲学:既然内核是瓶颈,那就绕过它)。
io_uring_enter 办完 N 件事光有共享内存还不够。在标准模式下,应用往 SQ 里塞了 SQE 之后,还得调一次 io_uring_enter() 来"摇铃"通知内核"有活干了"。但你完全可以先把几十上百个请求攒在 SQ 里,最后只摇一次铃——这一次系统调用,就能提交一大批 I/O。这叫批量提交(batching)。
视频强调,仅这一招就已经是巨大进步:epoll 是"每个事件一次以上系统调用",io_uring 是"一大批请求一次系统调用",系统调用次数被摊薄到几乎可忽略。对于高吞吐场景(比如数据库刷盘、代理转发),这种放大效应是数量级的。
我的补充:批量是 io_uring 性能曲线能持续往上走的关键。系统调用的开销是"固定成本",请求数量是"可变收益",请求越多,单位请求摊到的敲门成本越低。这和 LSM 树、网卡中断合并、TCP 的 Nagle 是一个道理——把固定开销摊到更多有效工作上。
如果说批量提交是"少敲门",那 IORING_SETUP_SQPOLL 模式就是"根本不敲门"。开启 SQPOLL 后,内核会单独 spawn 一个内核线程(名字通常是 io_uring-sq),它像保安一样持续轮询那块共享的提交队列。应用只要往 SQ 里写 SQE,这个内核线程立刻就能看到并去执行,全程应用不需要调用任何系统调用。
视频把这一点作为高潮:在 SQPOLL 模式下,应用每秒做几百万次读、写、网络收发,系统调用计数可以是零。用户态↔内核态切换的 CPU 成本被彻底抹掉。代价是要常驻一个内核线程、占一点 CPU 自旋,所以它最适合"持续高吞吐、很少空闲"的工作负载,而不是那种偶尔来个请求的低频服务。
视频原话点出了这种"零系统调用"的震撼感。我要诚实补充一句平衡:SQPOLL 有它最甜的工况,但对吞吐不稳定、突发型负载,自旋轮询反而浪费 CPU,这时反而不如普通模式——工具选型看工况,不存在银弹。
视频还讲了两个常被忽略但极其重要的优化。第一个是注册文件(registered files)。传统系统调用每收到一个 fd,内核都要去进程的文件表里查它、增加引用计数、干完活再减引用计数——高并发下这个 bookkeeping 的锁竞争很伤。用 io_uring_register() 提前注册一批 fd,内核把它们缓存好,之后请求里直接用"索引"引用,绕开整张文件表。
第二个是注册缓冲区(registered buffers)。传统 I/O 每次都要把用户态内存页映射进内核、pin 住(防止被换出)、传输完再 unpin,重复劳动。注册 buffer 后,内核永久 pin 住这些页,后续 I/O 直接在已经 pin 好的物理内存上做,达到真正的零拷贝(true zero-copy)。
这两招是 io_uring 和 DPDK/SPDK 这类"内核旁路(kernel bypass)"技术对齐的关键。我的判断是:io_uring 的野心不只是"更好的 epoll",而是想成为留在内核里、却能逼近用户态旁路性能的中间路线——既不用像 DPDK 那样接管整块网卡、重写协议栈,又能拿到大部分性能收益。它填的是"每次 I/O 都付 4μs 内核开销"和"彻底拔掉内核"之间的巨大空白。
视频给出了简短但重要的历史脉络。Linux 其实早在 2.5 时代就有异步 I/O(AIO),但它天生残废:只有配合 O_DIRECT 直读、且文件已预分配时才真正异步,其余情况(包括普通缓冲读、socket、管道)调用 io_submit 时会阻塞在系统调用里——表面异步,实际同步。它也不支持 socket,没法统一网络和磁盘。这意味着用旧 AIO 既享受不到页缓存,又得忍受复杂的 O_DIRECT 语义。
Jens Axboe(Linux 块 I/O 子系统维护者)在 2019 年的 Linux 5.1 里引入 io_uring,正是为了"重做一遍异步 I/O",让它对网络、磁盘、普通文件、管道全部真正异步,并顺手解决了 page cache 的兼容问题。liburing 是他维护的 C 封装库,屏蔽了裸系统调用和内存屏障的坑。
视频提到的这段历史很重要——它解释了 io_uring 为什么一开始就野心很大。我的延伸观察:io_uring 的影响已经溢出 Linux(Windows 在做类似的 IoRing,macOS 的 kqueue/AIO 也常被拿来对比),它正在成为"现代异步 I/O 该长什么样"的事实标准。
这是视频里最该被认真对待、也最容易被"安利向"内容带过的一段。能力越大,攻击面越大。io_uring 让用户态通过共享内存直接触发高权限内核操作,等于在内核边界上开了无数条新的攻击路径。Google 安全团队在 2023 年 6 月公开:2022 年他们收到的内核漏洞悬赏里,约 60% 都是针对 io_uring 的利用。后果是连锁的:io_uring 在 Android 上对应用层禁用,在 ChromeOS 和 Google 自家服务器上整体关闭,Docker 也随之把 io_uring 相关系统调用从默认 seccomp 配置里拉黑。新版内核(5.15 起)逐步收紧了权限控制,允许管理员用 seccomp 限制、或全局禁用某些 opcode。
视频也诚实地留了一个平衡观点:io_uring 不是在所有场景都比 epoll 快。在高连接、轻数据量的"流式(streaming)"模式下,由于多了一层 ring 的间接性,某些基准里 io_uring 反而比 epoll 慢(liburing 仓库的 issue #536 就记录过这类结论)。对于连接稳定、I/O 单次数据量小的简单服务,epoll 依然是成熟、可移植、足够快的选择。
我特别欣赏视频没有把 io_uring 吹成万能解。真正的工程判断是:连接高 churn + 高吞吐 + 需要批量/零拷贝 → io_uring;连接稳定 + 简单可移植 → epoll 仍是好选择。盲目迁移只会把好的 epoll 代码改坏。
flowchart TB
subgraph US["用户空间 User Space"]
APP["应用程序<br/>生产 SQE · 消费 CQE"]
end
subgraph SM["共享内存 mmap(双方直连,无系统调用)"]
SQ["Submission Queue SQ<br/>应用写 · 内核读<br/>无锁 SPSC"]
CQ["Completion Queue CQ<br/>内核写 · 应用读<br/>无锁 SPSC"]
end
subgraph KS["内核空间 Kernel Space"]
SQT["SQPOLL 内核线程<br/>持续轮询 SQ(零调用模式)"]
IO["I/O 执行引擎<br/>磁盘 + 网络 socket 全异步"]
end
APP -- "1 写入 SQE 请求包" --> SQ
SQ -- "2 读取(轮询/被唤醒)" --> SQT
SQT -- "3 派发执行" --> IO
IO -- "4 写回 CQE 结果" --> CQ
CQ -- "5 应用读取结果" --> APP
flowchart LR
subgraph EP["epoll:每次事件都敲门"]
E1["epoll_wait<br/>系统调用①"] --> E2["read / write<br/>系统调用②③"] --> E3["再拷贝数据"]
end
subgraph IU["io_uring:批量 / 不敲门"]
I1["攒 N 个 SQE<br/>写共享内存"] --> I2["一次 enter 摇铃<br/>或 SQPOLL 零调用"] --> I3["内核异步执行<br/>结果回 CQ"]
end
EP -. "范式切换<br/>就绪通知 → 完成通知" .-> IU
第一,重新审视"系统调用很便宜"这个直觉。 写业务代码的人很少盯着系统调用数,但在高并发、高吞吐场景里,它就是隐藏的税。学会用 perf、strace -c 数一数你的服务每秒敲了多少次门,你可能会被吓到。io_uring 给的最大认知升级是:调用的本质是"跨域通信",能用共享内存/消息队列替代的,就别用同步 RPC。
第二,别急着把生产 epoll 代码改成 io_uring。 迁移是有代价的:异步语义会让控制流变复杂(回调/状态机/生命周期),buffer 生命周期管理容易出错,而且容器环境里 io_uring 可能被 seccomp 禁掉(Docker 默认就是)。正确的姿势是先评估工况——连接高 churn + 高 IOPS + 批量/零拷贝收益明显,才值得上;普通的 API 网关,epoll + 一个成熟 runtime(tokio、Netty、libuv)完全够用。
第三,优先用封装,不要裸写系统调用。 直接碰 io_uring 的内存屏障和 ring 指针极易写错。生产里请走 liburing(C)、tokio-uring(Rust)、Netty 的 io_uring transport(Java)、或 seastar(C++)这类成熟抽象。先吃透"完成模型"的思维方式,再谈底层调优。
第四,安全先于性能。 如果你运维容器/多租户环境,务必确认宿主内核版本 ≥ 5.15、并审计 seccomp 策略是否合理放行/禁用 io_uring。一个能零系统调用驱动内核的接口,在你拿到性能的同时,也把更大的责任塞进了你手里。
第五,把 io_uring 当成一个"思维框架"而不只是工具。 它的三个核心思想——共享内存解耦、批量摊薄固定成本、轮询替代中断(doorbell)——可以迁移到你自己的系统设计里:高吞吐网关用批处理、用共享内存 IPC 替代管道、用轮询+背压替代频繁唤醒。理解了它,你再看 DPDK、io_uring、eBPF、SPDK,会发现它们讲的是同一个故事。
man 7 io_uring 与 man 7 io_uring_sqpoll —— Linux 手册页,最权威的接口语义参考,读完能直接上手。examples/ 里的 demo 比读十篇博客都管用,issue #536 里还有 io_uring vs epoll 的真实基准。io_uring 用一对共享内存的无锁环形队列,把"系统调用"这种昂贵的同步敲门,改造成了应用与内核之间的异步消息队列——它不是更快一点的 epoll,而是 Linux 二十年来 I/O 编程范式的一次根本转向。
本文由「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 等场景全覆盖。
