一、iouring 的本质是什么?

它底层维护了两个在用户态和内核态之间共享内存的环形缓冲区:

. 提交队列(Submission Queue, SQ)
  • 谁写:用户态程序

  • 谁读:内核

  • 内容:用户提交的 I/O 请求的索引(不是请求本身,请求在 SQ 条目数组中)

2. 完成队列(Completion Queue, CQ)
  • 谁写:内核

  • 谁读:用户态程序

  • 内容:已完成的 I/O 操作结果(返回值、错误码、用户数据等)

二、完整的执行流程

第一步:用户态提交请求
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);      // 1. 从共享内存获取一个空闲 SQE
io_uring_prep_recv(sqe, fd, buf, len, 0);                // 2. 填写操作类型和参数
memcpy(&sqe->user_data, &my_info, sizeof(my_info));      // 3. 附加自定义数据
io_uring_submit(&ring);                                   // 4. 通知内核
  • 前 3 步都是纯用户态操作,在共享内存里写数据,无系统调用

  • 第 4 步 io_uring_submit 才真正通过 io_uring_enter 系统调用通知内核,但可以一次提交批量请求。

第二步:内核异步执行

内核收到通知后:

  1. 从 SQ 中取出请求索引。

  2. 根据索引找到对应的 SQE,执行异步 I/O。

  3. 对于网络 I/O,内核使用 fast-poll 机制在内部轮询;对于磁盘 I/O,可能交给内核工作线程。

  4. 操作完成后,内核将结果写入 CQ(返回值、user_data 等)。

第三步:用户态收割结果
io_uring_wait_cqe(&ring, &cqe);               // 阻塞等待,或
int n = io_uring_peek_batch_cqe(&ring, cqes, 128);  // 批量收割,零系统调用!

三、为什么性能这么高?—— 五大关键机制

1. 共享内存,零拷贝

io_uring_setup 初始化时,内核会分配 SQ/CQ 的内存区域,并 mmap 映射到用户空间。用户程序直接通过指针读写,不需要拷贝数据进内核。

2. 批量提交与批量收割

你可以连续调用多次 io_uring_get_sqe 准备 N 个请求,然后一次 io_uring_submit 全部提交。收割时,io_uring_peek_batch_cqe 也能一次取出多个完成事件。这极大摊薄了系统调用开销。

3. 真正异步,线程不阻塞

epoll 是同步非阻塞:它只通知你“可读了”,但 read 本身还是需要你亲自调用,数据拷贝期间线程是占用的。
io_uring 是 Proactor 模式:你提交一个请求后就完事了,内核在后台完成所有数据拷贝,完成后通知你。你的线程可以继续干其他活。

4. 可选的内核轮询模式(SQPOLL)

开启 IORING_SETUP_SQPOLL 后,内核会启动一个专用线程,主动轮询 SQ 中是否有新请求。这样用户态连 io_uring_submit 的系统调用都省了,完全在用户态写内存就完成提交。

5. 减少上下文切换

传统 I/O 模型:用户态 → 内核态(read) → 返回用户态 → 数据处理 → 用户态 → 内核态(write)→ 返回用户态,来回切换。
io_uring:用户态写入 SQ → 内核处理 → 用户态读取 CQ,全程用户态操作占主导,切换次数大幅减少。

四、一张图总结

用户态                                              内核态
  │                                              │
  ├─ 写入 SQ (共享内存)              │
  ├─ 更新 SQ tail ───→              ├─ 读取 SQ,异步执行 I/O
  │                                                 ├─ 工作线程/fast-poll 处理
  │                                                 ├─ 结果写入 CQ (共享内存)
  ├─ 读取 CQ ←───────────── │
  ├─ 批量收割结果,更新 CQ head
  │
  └─ 零系统调用 / 极少系统调用

五、相比 epoll 的直观对比

操作 epoll io_uring
等待事件 epoll_wait(系统调用) 直接读 CQ 内存(零系统调用)
执行读/写 recv/send(系统调用) 内核异步完成,用户仅收割结果
一次完整收发 至少 3 次系统调用 可做到 1 次(批量提交)甚至 0 次(SQPOLL)
数据拷贝 recv 从内核拷到用户 提前注册缓冲区,可零拷贝

一句话总结io_uring 通过共享内存环形队列 + 批量操作 + 真正异步执行,把“提交 I/O”和“收割结果”都变成了几乎零成本的内存读写,彻底榨干了现代硬件和 Linux 内核的性能潜力。

测试。

  • -s:服务器 IP

  • -p:服务器端口

  • -t:线程数(并发连接数)

  • -c:连接数(代码中未使用)

  • -n:总请求数

iouring明显性能要比epoll高

io_uring 并不是用来替代一切的工具,它的设计决定了它在极高 IOPS、需要批量异步操作、渴望减少系统调用开销的场景中最为耀眼。以下是 io_uring 最适合的几个领域:


1. 高并发网络服务器(HTTP、代理、游戏后端)

  • 为什么适合:网络服务器的核心是海量连接的 acceptrecvsendio_uring 可以一次提交/收割成百上千个操作,系统调用次数降到极限,真正异步不阻塞线程。

  • 典型例子:Nginx 的 io_uring 实验模块、Redpanda(Kafka 替代品)、ScyllaDB 的网络层。

  • 你的代码场景:你写的 echo 服务器在 50 连接 × 10000 请求下,如果用 io_uring,吞吐量会明显优于 epoll,CPU 也更低。

2. 数据库和存储引擎(RocksDB、Ceph、MySQL)

  • 为什么适合:数据库需要大量异步文件 I/O(读写 SSTable、WAL 日志、数据文件)。传统的 libaio 只支持 O_DIRECT 且接口复杂;io_uring 支持缓存 I/O、可批量提交,能无缝异步化所有存储操作。

  • 典型例子:RocksDB 已经原生支持 io_uring 后端;Ceph 的 BlueStore 使用 io_uring 加速 OSD 操作;PostgreSQL 也在探索 io_uring 替换 POSIX AIO。

3. 消息队列和流处理系统(Kafka、Pulsar)

  • 为什么适合:消息系统需要同时处理大量磁盘顺序读写(分区日志)和网络收发。io_uring 能够统一异步处理文件和网络 I/O,极大简化架构并提升性能。

  • 典型例子:Redpanda 就是基于 Seastar + io_uring 构建的 Kafka 兼容系统,单节点吞吐量是 Kafka 的几倍。

4. 容器运行时和虚拟化(runc、QEMU)

  • 为什么适合:容器镜像的提取、快照、文件系统操作(openreaddirstat)都适合批量异步执行。io_uring 还能避免在操作大文件时阻塞主线程。

  • 典型例子:runc 使用 io_uring 加速容器启动时的文件复制;Kata Containers 用 io_uring 优化 VMM 与宿主机通信。

5. 需要零拷贝或固定缓冲区的场景

  • 为什么适合io_uring 支持 IORING_REGISTER_BUFFERS 预先注册一批内存,避免每次 I/O 都映射用户页,适合长期持有相同大小缓冲区的应用(如转发代理、日志采集)。

  • 零拷贝发送IORING_OP_SEND_ZC):直接利用 NIC 的 zero-copy 能力发送文件内容,适合文件服务器、CDN 边缘节点。

6. 大规模并发非阻塞 Web 应用(静态文件、API 网关)

  • 为什么适合:业务逻辑主要是转发和文件读取,且连接数极大。io_uring 能让大量 sendfile 或 splice 操作异步完成,几乎不消耗 CPU。

  • 典型例子:Cloudflare 的 Pingora 框架就用 io_uring 来加速内部代理。


什么时候不适合用 io_uring?

  • 内核版本老:生产环境内核低于 5.1,无法使用。

  • 极简单的单连接调试工具:优势发挥不出来,反而增加复杂度。

  • 高频、小对象的 CPU 密集型计算:如果瓶颈在用户态逻辑(如 JSON 解析、加密运算),io_uring 省下的 I/O 开销占比很小,引入意义不大。

  • 跨平台需求:只有 Linux 支持,macOS/Windows 需用 kqueue/IOCP。

  • 团队不熟悉异步模型:debug 难度和开发成本高于简单的 epoll 回调。


总结一句话

适合 io_uring 的地方:需要处理海量 I/O 请求,且希望用最少的系统调用和 CPU 开销完成网络和存储操作的高性能服务器。

不适合的地方:旧内核、简单应用、跨平台、用户态 CPU 密集场景。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐