Linux c 2.4 io uring
一、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系统调用通知内核,但可以一次提交批量请求。
第二步:内核异步执行
内核收到通知后:
-
从 SQ 中取出请求索引。
-
根据索引找到对应的 SQE,执行异步 I/O。
-
对于网络 I/O,内核使用 fast-poll 机制在内部轮询;对于磁盘 I/O,可能交给内核工作线程。
-
操作完成后,内核将结果写入 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、代理、游戏后端)
-
为什么适合:网络服务器的核心是海量连接的
accept、recv、send。io_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)
-
为什么适合:容器镜像的提取、快照、文件系统操作(
open、readdir、stat)都适合批量异步执行。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 密集场景。
更多推荐




所有评论(0)