io_uring 注册缓冲区高并发读写怎样减少重复注册开销为什么结果为空或不符合预期
来源:17golang原创
时间:2026-11-09 11:02:52 208浏览 收藏
我在看高并发读写的 I/O 路径时,最容易误判的一件事是:把“注册缓冲区”也放进每个请求的热路径。io_uring 的 registered buffer 更适合在 ring 初始化阶段完成一次映射和固定,后续请求通过固定缓冲区索引复用它。这样减少的是每次 I/O 的映射、页引用处理和注册管理开销,并不等于所有读写都会自动变快。
官方地址:https://docs.kernel.org/next/filesystems/fuse/fuse-io-uring.html
先注册一组稳定的匿名内存,再用read_fixed或write_fixed指向正确的buf_index;如果结果为空或不符合预期,第一时间看 CQE 的res,再查索引、范围和请求完成前的缓冲区生命周期。
- 注册动作通常属于 ring/worker 初始化,不属于每个请求的循环体。
- 固定 I/O 仍要满足地址和长度落在对应注册缓冲区内,且使用正确的索引。
- CQE 的负值是直接返回的负 errno,不应只把它当成“读到 0 字节”。
先把一次性注册从并发热路径移走
普通读写每次都要把用户缓冲区交给内核处理;注册缓冲区则让内核为一组用户内存建立长期映射,后续固定读写直接引用这张表。Linux 内核文档在 FUSE-over-io-uring 的 buffer pool 说明中也采用了“池先注册、请求重复使用”的思路:同一批页可以继续用于后续的固定 I/O。
因此,错误的结构通常是“收到请求 → register buffers → 提交一次 → unregister buffers”。高并发时这会反复触发资源管理,而且注册与注销还必须避开仍在使用这些内存的在途请求。更稳妥的边界是“创建 ring → 分配缓冲区池 → 注册一次 → 多次提交 → ring 退出时统一清理”。

| 对象 | 主要职责 | 常见误区 |
|---|---|---|
| io_uring ring | 保存提交队列、完成队列和资源表关联 | 以为注册表跨所有 ring 自动共享 |
| iovec | 描述注册时的基址和长度 | 提交时使用超出范围的地址或长度 |
| buf_index | 选择注册表中的固定缓冲区 | 索引与请求实际使用的 buffer 不一致 |
| CQE res | 表示完成字节数或负 errno | 只看指针是否为空,不看返回值 |
固定读写必须同时满足索引和范围
注册成功并不会让普通的 io_uring_prep_read 自动变成固定读。需要使用 io_uring_prep_read_fixed、io_uring_prep_write_fixed 或对应的 fixed 变体,并把 buf_index 传给 SQE。提交的 buf 和 nbytes 必须落在该索引对应的注册区间中;可以只使用注册缓冲区的一部分,但不能越界。
#include#include #include #include // fd 假设已经打开;示例只展示一次注册和一次固定读的边界。 int submit_fixed_read(int fd) { struct io_uring ring; char *buf = NULL; struct iovec iov; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe = NULL; int ret = 0; // 4096 同时满足常见页大小和 aligned_alloc 的尺寸要求。 buf = aligned_alloc(4096, 4096); if (buf == NULL) return -1; memset(buf, 0, 4096); ret = io_uring_queue_init(8, &ring, 0); if (ret user_data = 1; ret = io_uring_submit(&ring); if (ret res res; else ret = cqe->res; io_uring_cqe_seen(&ring, cqe); } cleanup_buffers: // 只有确认在途请求已完成后,才能注销并回收这块内存。 io_uring_unregister_buffers(&ring); cleanup_ring: io_uring_queue_exit(&ring); cleanup_buf: free(buf); return ret; }
这段代码是文章中的最小结构示意,没有在本机执行。实际并发服务通常把固定缓冲区做成池,用空闲槽位分配给请求;槽位必须一直保持占用到对应 CQE 被消费。多个 ring 不应默认共享一张注册表,需要按 ring 管理,或使用内核/库提供的显式克隆能力。
结果为空时先看 CQE,而不是先改并发度
“结果为空”至少有三种不同含义。cqe->res == 0 可能是文件当前位置已经到 EOF;cqe->res 则是负 errno,例如范围、权限、文件状态或资源限制问题;而 CQE 尚未按预期出现,则可能是提交失败、没有取得 SQE,或者请求被错误地归类到另一个 user_data。
固定缓冲区特有的排查顺序可以压缩成四项:
- 确认注册成功,并记录注册数组中每个 buffer 的顺序。
- 确认 fixed 操作的
buf_index与实际 buffer 对应,地址和长度没有越界。 - 确认提交返回值和 CQE 的
res,负值按负 errno 解释,不用errno覆盖它。 - 确认缓冲区在 CQE 到来前没有被另一个请求覆盖、复用或释放。

# 只查看当前进程允许锁定的内存额度,不会替代程序内的 CQE 检查。 ulimit -l # 按应用日志记录三类信息:提交返回值、user_data、cqe.res。 # cqe.res 小于 0 时按负 errno 处理,不能只打印“空结果”。 printf 'submit=%s user_data=%s cqe_res=%s\n' "$submit_ret" "$user_data" "$cqe_res"
注册缓冲区需要长期持有用户内存;在较老内核上还可能受 RLIMIT_MEMLOCK 约束,注册失败时要把资源限制和匿名内存类型一起检查。文件映射、地址偏移和对齐也必须符合当前 I/O 操作的要求,不能因为注册成功就忽略底层文件语义。
什么时候不该强行使用 registered buffer
registered buffer 最适合小块、频繁、反复使用同一组内存的 I/O。如果请求大小很大,映射管理开销相对整个 I/O 时间可能已经很小;如果每个请求的内存都不同,维护一张大注册表还会增加锁定内存和槽位管理成本。此时先用普通读写建立基线,再决定是否把稳定的热点缓冲区迁移为 fixed。
还要把生命周期写成明确的所有权规则:谁拿到槽位,谁负责在 CQE 完成后归还;注销只放在 ring 停止并且没有在途请求的位置。这样才能把“减少重复注册开销”变成可控的资源策略,而不是把内存固定、索引错配和结果解释混在一起。
相关问题
注册成功后为什么普通 read 还是没有收益?
注册表只为 fixed 变体提供已登记的缓冲区。若仍然调用普通 read,先确认是否真的使用了 read_fixed 或相应 fixed 操作,再比较相同负载下的成本。
为什么 CQE 返回负数却没有 errno?
io_uring 把错误直接放进 CQE 的 res,形式是负 errno。应先保存这个值,再按需要转换为可读错误文本,不要只检查线程的全局 errno。
能否每次请求结束就注销 buffer?
只有在确认所有使用它的请求都完成后才可以。逐请求注销会重新引入注册管理开销,也容易和并发请求的生命周期冲突。
资料参考:https://man7.org/linux/man-pages/man7/io_uring_registered_buffers.7.html、https://man7.org/linux/man-pages/man3/io_uring_prep_read_fixed.3.html。
-
426 收藏
-
262 收藏
-
206 收藏
-
387 收藏
-
242 收藏
-
130 收藏
-
367 收藏
-
226 收藏
-
294 收藏
-
369 收藏
-
356 收藏
-
225 收藏
-
104 收藏
-
232 收藏
-
439 收藏
-
479 收藏
-
316 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习