<!-- title: 不敲门才快：io_uring干掉系统调用 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=Yw-O5QWkhsQ -->

# 不敲门才快：io_uring 如何用共享内存干掉系统调用

## 导语

你写的每一行 `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 的根本局限讲得很透：**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()` 直接映射到同一块物理内存：

- **Submission Queue（SQ，提交队列）**：只有应用能写，内核只读。应用把每一个 I/O 请求（叫 SQE，Submission Queue Entry）写进去。
- **Completion Queue（CQ，完成队列）**：只有内核能写，应用只读。内核把每个请求的结果（CQE）写进去。

关键在于"共享内存"四个字。既然两个 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 是一个道理——**把固定开销摊到更多有效工作上**。

### 五、SQPOLL：连最后一次门都不敲

如果说批量提交是"少敲门"，那 **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 AIO 的救赎，也是统一存储与网络的接口

视频给出了简短但重要的历史脉络。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 代码改坏。

## 知识结构图

```mermaid
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
```

```mermaid
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，会发现它们讲的是同一个故事。

## 延伸学习路径

1. **《Efficient IO with io_uring》** —— Jens Axboe 本人 2019 年的设计论文（PDF），讲清动机、数据结构、性能评估，是所有解读的源头。
2. **`man 7 io_uring` 与 `man 7 io_uring_sqpoll`** —— Linux 手册页，最权威的接口语义参考，读完能直接上手。
3. **liburing 源码仓库（github.com/axboe/liburing）** —— 作者维护的 C 封装库，看 `examples/` 里的 demo 比读十篇博客都管用，issue #536 里还有 io_uring vs epoll 的真实基准。
4. **Red Hat Developer: Why you should use io_uring for network I/O** —— 厂商视角的实战指南，讲清网络场景的接入方式。
5. **《Lord of the io_uring》（unixism.net）** —— 社区公认最好的入门教程，从零带写一个 io_uring 程序，循序渐进。

## 一句话总结

io_uring 用一对共享内存的无锁环形队列，把"系统调用"这种昂贵的同步敲门，改造成了应用与内核之间的异步消息队列——它不是更快一点的 epoll，而是 Linux 二十年来 I/O 编程范式的一次根本转向。

## 参考来源

1. [How io_uring Actually Works — The Async I/O Interface Replacing epoll（State & Flow）](https://www.youtube.com/watch?v=Yw-O5QWkhsQ) — 本次深度解读的核心视频（已通过 YouTube oEmbed 接口核实真实存在）
3. [Beyond Epoll: Why io_uring is Revolutionizing Linux I/O Performance（Micrologics, 2026-06-21）](https://micrologics.org/blog/beyond-epoll-why-io_uring-is-revolutionizing-linux-io-performance) — epoll 瓶颈分析、SQPOLL、注册文件/缓冲区、生产采纳与代码对比
4. [Efficient IO with io_uring（Jens Axboe, 2019）](https://kernel.dk/io_uring.pdf) — io_uring 作者的设计论文，性能评估一手资料
5. [io_uring(7) — Linux manual page（man7.org）](https://man7.org/linux/man-pages/man7/io_uring.7.html) 与 [io_uring_sqpoll(7)](https://www.man7.org/linux/man-pages/man7/io_uring_sqpoll.7.html) — 官方接口语义与 SQPOLL 模式

---
> 本文由「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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
