当前位置:首页 > 文章列表 > 文章 > linux > io_uring 注册缓冲区高并发读写怎样减少重复注册开销为什么结果为空或不符合预期

io_uring 注册缓冲区高并发读写怎样减少重复注册开销为什么结果为空或不符合预期

来源:17golang原创 2026-11-09 11:02:52 0浏览 收藏

我在看高并发读写的 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、注册缓冲区表、worker 与固定读写请求的静态关系说明图
图1:结构说明图展示 ring、注册缓冲区表、worker 与 fixed I/O 请求的职责边界,不是运行截图。
对象主要职责常见误区
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。

固定缓冲区特有的排查顺序可以压缩成四项:

  1. 确认注册成功,并记录注册数组中每个 buffer 的顺序。
  2. 确认 fixed 操作的 buf_index 与实际 buffer 对应,地址和长度没有越界。
  3. 确认提交返回值和 CQE 的 res,负值按负 errno 解释,不用 errno 覆盖它。
  4. 确认缓冲区在 CQE 到来前没有被另一个请求覆盖、复用或释放。
固定缓冲区索引、地址范围、完成队列 CQE 与结果解释的静态关系说明图
图2:关系说明图展示固定缓冲区索引、地址范围、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。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
JVM Class Data Sharing怎样生成类数据归档缩短启动时间为什么结果为空或不符合预期JVM Class Data Sharing怎样生成类数据归档缩短启动时间为什么结果为空或不符合预期
上一篇
JVM Class Data Sharing怎样生成类数据归档缩短启动时间为什么结果为空或不符合预期
io_uring 注册缓冲区高并发读写怎样减少重复注册开销怎样在批处理代码中减少额外分配
下一篇
io_uring 注册缓冲区高并发读写怎样减少重复注册开销怎样在批处理代码中减少额外分配
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    955次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    960次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1071次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1045次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    879次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码