Linux内核学习轨迹第七部: 多队列块层blk-mq深度拆解(第四节)

发布时间:2026/6/13 20:58:23

Linux内核学习轨迹第七部: 多队列块层blk-mq深度拆解(第四节) 4. 多队列块层blk-mq深度拆解Linux 5.0内核开始blk-mqMulti-Queue Block IO Queueing Mechanism多队列块IO排队机制已成为块层的默认实现Linux 6.0内核完全移除了传统单队列块层框架所有块设备驱动必须基于blk-mq开发。blk-mq是为适配多核CPU、NVMe SSD等多队列硬件量身设计的彻底解决了传统单队列的全局自旋锁竞争瓶颈实现了IO请求的全链路并行处理把NVMe设备的IOPS和延迟性能提升了一个数量级是现代Linux IO栈的核心基石。4.1 传统单队列块层的致命痛点传统单队列块层的核心设计是单个全局IO请求队列全局自旋锁所有CPU核心发起的IO请求都要进入同一个队列必须拿到全局锁才能操作队列在多核场景下存在三个致命缺陷完全无法适配现代硬件严重的全局锁竞争多核CPU同时发起IO时全局自旋锁会导致大量CPU时间浪费在锁等待上CPU核心数越多锁竞争越严重。实测8核以上CPU场景下单队列的锁开销会超过30%16核以上场景锁开销甚至超过50%CPU算力被严重浪费。无法适配多队列硬件现代NVMe SSD通常支持64/128个独立硬件提交队列每个队列可以直接和一个CPU核心交互完全并行处理IO。单队列框架无法发挥硬件的多队列并行能力硬件性能被严重浪费NVMe设备的百万级IOPS能力被锁竞争限制到十万级。NUMA架构性能损耗多NUMA节点的服务器上跨节点的内存访问、全局锁竞争会导致IO延迟急剧增加。单队列框架无法做NUMA亲和性优化跨节点的IO访问延迟比同节点高2倍以上。blk-mq的核心设计目标就是彻底解决这三个问题通过「Per-CPU软件队列 多硬件队列」的全并行架构消除全局锁最大化发挥多核CPU和多队列硬件的性能。4.2 blk-mq的核心两层架构blk-mq的架构分为上下两层彻底解耦了IO的调度分发与硬件提交中间通过IO调度器连接完整架构如下┌─────────────────────────────────────────────────────────────────┐ │ 通用块层BIO提交、合并、封装为request请求 │ └───────────────────────────────┬─────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 【软件队列层】Per-CPU软件队列 │ CPU0 → blk_mq_ctx[0] CPU1 → blk_mq_ctx[1] ... CPUn → blk_mq_ctx[n] │ 每个CPU核心对应一个独立软件队列独立自旋锁多核完全无锁竞争 └───────────────────────────────┬─────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ IO调度器层mq-deadline/kyber/bfq/none对request请求调度优化 │ └───────────────────────────────┬─────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 【硬件队列层】硬件提交队列 │ 硬件队列0 → blk_mq_hw_ctx[0] 硬件队列1 → blk_mq_hw_ctx[1] ... │ 每个硬件队列对应存储设备的一个物理提交队列可绑定CPU核心 └───────────────────────────────┬─────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 块设备驱动NVMe/ahci/RAID卡驱动把IO请求下发给物理硬件 │ └─────────────────────────────────────────────────────────────────┘各层核心职责详解1.软件队列层struct blk_mq_ctx每个CPU核心对应一个独立的软件队列每个队列有自己的自旋锁完全消除了多核之间的全局锁竞争。发起IO的CPU核心只会操作自己对应的软件队列不会和其他CPU核心产生锁冲突多核场景下的并行能力得到了质的提升。软件队列的核心职责缓存当前CPU核心发起的IO请求做批量合并、统计然后提交给IO调度器。2.IO调度器层blk-mq框架原生支持可插拔的IO调度器调度器可以选择在软件队列层每个CPU独立调度或硬件队列层全局调度执行对IO请求做排序、合并、优先级调度、公平性控制适配不同的硬件和业务场景。3.硬件队列层struct blk_mq_hw_ctx每个硬件队列对应存储设备的一个物理提交队列现代NVMe SSD通常有64/128个硬件队列每个队列可以独立和PCIe总线交互完全并行处理IO。硬件队列可以和指定的CPU核心绑定实现中断亲和性避免跨CPU/跨NUMA节点的访问开销最大化降低IO延迟。硬件队列的核心职责接收调度器分发的IO请求下发给块设备驱动处理硬件中断完成IO闭环。4.3 blk-mq核心数据结构拆解本章节基于Linux 6.6 LTS内核源码定义在include/linux/blk-mq.h拆解blk-mq最核心的4个数据结构剔除调试、统计类非核心字段聚焦生产环境与原理理解的核心内容。4.3.1 驱动操作函数集struct blk_mq_ops这是块设备驱动必须实现的blk-mq标准接口是blk-mq层和驱动之间的桥梁驱动通过实现这套接口接入blk-mq框架核心函数如下struct blk_mq_ops { // 【核心入口】驱动处理IO请求的入口函数blk-mq把request请求下发给驱动 blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd); // IO完成回调函数硬件IO完成后驱动调用blk_mq_complete_request()触发这个回调 void (*complete)(struct request *rq, blk_status_t error); // 初始化硬件队列上下文设备初始化时调用 int (*init_hctx)(struct blk_mq_hw_ctx *hctx, void *driver_data, unsigned int hctx_idx); // 清理硬件队列上下文 void (*exit_hctx)(struct blk_mq_hw_ctx *hctx, unsigned int hctx_idx); // 初始化软件队列上下文 int (*init_ctx)(struct blk_mq_ctx *ctx, unsigned int ctx_idx); // 清理软件队列上下文 void (*exit_ctx)(struct blk_mq_ctx *ctx, unsigned int ctx_idx); // 检查硬件队列是否繁忙用于调度分发 int (*busy)(struct blk_mq_hw_ctx *hctx); // 设备暂停时的处理函数 void (*quiesce)(struct request_queue *q); // 设备恢复时的处理函数 void (*unquiesce)(struct request_queue *q); };核心函数说明queue_rq是驱动最核心的函数blk-mq层把准备好的request请求通过这个函数下发给驱动驱动把请求转换为硬件指令下发给物理设备complete是IO完成的回调函数驱动在硬件IO完成后通过这个函数通知blk-mq层blk-mq层再通知上层通用块层和文件系统。4.3.2 IO请求队列struct request_queuestruct request_queue是块设备IO请求的核心管理结构每个gendisk对应一个request_queue是整个块设备IO栈的核心枢纽包含了blk-mq的所有配置、队列、调度器、驱动操作函数集核心字段如下struct request_queue { // blk-mq驱动操作函数集 const struct blk_mq_ops *mq_ops; // 硬件队列上下文数组每个元素对应一个硬件队列 struct blk_mq_hw_ctx **queue_hw_ctx; // 硬件队列的总数量 unsigned int nr_hw_queues; // 软件队列上下文数组每个元素对应一个CPU核心的软件队列 struct blk_mq_ctx __percpu *queue_ctx; // 软件队列的总数量等于CPU核心数 unsigned int nr_queues; // 块设备的IO调度器 struct elevator_type *elevator; // 调度器的私有数据 void *elevator_data; // 所属的gendisk实例 struct gendisk *disk; // 队列的引用计数 refcount_t refs; // 队列的读写信号量 struct rw_semaphore rq_rwsem; // 设备的IO参数最大IO大小、逻辑块大小、物理块大小 unsigned int max_sectors; unsigned int logical_block_size; unsigned int physical_block_size; // 队列深度每个硬件队列的最大请求数 unsigned int nr_requests; // 队列的标志位只读、非阻塞、可插拔等 unsigned long queue_flags; // 队列的冻结计数用于设备热插拔、暂停IO int mq_freeze_depth; // 所属的NUMA节点 int numa_node; // 驱动私有数据 void *queuedata; };4.3.3 硬件队列上下文struct blk_mq_hw_ctxstruct blk_mq_hw_ctx是blk-mq对硬件提交队列的抽象每个硬件队列对应一个实例管理该硬件队列的所有状态、调度器、CPU亲和性、统计信息核心字段如下struct blk_mq_hw_ctx { // 所属的request_queue struct request_queue *queue; // 硬件队列的编号 unsigned int hctx_idx; // 该硬件队列对应的驱动私有数据 void *driver_data; // 该硬件队列的调度器实例 struct elevator_queue *sched; // 待下发给驱动的请求链表 struct list_head dispatch; // 保护硬件队列的自旋锁 spinlock_t lock; // 该硬件队列绑定的CPU核心掩码 const struct cpumask *cpumask; // 该硬件队列所属的NUMA节点 int numa_node; // 该硬件队列的中断处理函数 irq_handler_t handler; // 硬件队列的队列深度 unsigned int queue_depth; // 硬件队列的繁忙标记 atomic_t busy; // 该硬件队列的IO统计信息 struct blk_mq_stats stats; // 延迟下发的工作队列 struct delayed_work run_work; // 超时处理工作队列 struct work_struct timeout_work; };4.3.4 软件队列上下文struct blk_mq_ctxstruct blk_mq_ctx是Per-CPU软件队列的抽象每个CPU核心对应一个实例管理该CPU核心发起的所有IO请求完全独立的自旋锁多核之间无锁竞争核心字段如下struct blk_mq_ctx { // 所属的request_queue struct request_queue *queue; // 所属的CPU核心编号 unsigned int cpu; // 所属的NUMA节点 int numa_node; // 该CPU核心的IO请求链表待提交给调度器 struct list_head rq_list; // 保护该软件队列的自旋锁 spinlock_t lock; // 该软件队列对应的硬件队列实现软件队列到硬件队列的映射 struct blk_mq_hw_ctx *hctx; // 该CPU核心的IO统计信息 struct blk_mq_ctx_stats stats; // 引用计数 refcount_t refs; // 随机数种子用于IO调度 unsigned int rand; };4.4 blk-mq IO请求的完整执行流程衔接第3章的BIO生命周期BIO被封装为request请求后进入blk-mq层的完整执行流程分为6个阶段全链路无全局锁实现完全并行处理1. request请求入队Per-CPU软件队列 → 2. IO调度器处理 → 3. 软件队列到硬件队列的映射分发 → 4. 硬件队列下发给驱动 → 5. 驱动下发硬件执行 → 6. IO完成与请求释放阶段1request请求入队Per-CPU软件队列通用块层把BIO封装为struct request请求根据发起IO的CPU核心找到对应的Per-CPU软件队列struct blk_mq_ctx拿到该软件队列的独立自旋锁把request请求加入到软件队列的rq_list链表尾部释放自旋锁整个过程仅操作当前CPU的软件队列和其他CPU核心完全无锁竞争。阶段2IO调度器处理blk-mq层调用对应IO调度器的入队函数把request请求加入到调度器的队列中调度器对request请求进行排序、合并、优先级调度、延迟处理优化IO访问模式调度器把处理完成的request请求加入到待分发的调度队列中等待下发到硬件队列。阶段3软件队列到硬件队列的映射分发blk-mq通过软件队列到硬件队列的映射机制把软件队列的request请求分发到对应的硬件队列核心映射规则默认映射规则每个CPU核心的软件队列固定映射到一个硬件队列实现CPU核心和硬件队列的绑定避免跨CPU/跨NUMA节点的访问NUMA优化映射多NUMA节点场景下节点内的CPU核心的软件队列仅映射到同节点的PCIe设备对应的硬件队列完全避免跨NUMA节点的IO访问大幅降低延迟负载均衡映射如果某个硬件队列过于繁忙blk-mq会自动把请求分发到空闲的硬件队列实现负载均衡避免单个硬件队列成为瓶颈。映射完成后request请求被加入到对应硬件队列的dispatch分发链表中等待下发给驱动。阶段4硬件队列下发给驱动拿到硬件队列的自旋锁遍历dispatch链表中的request请求调用驱动实现的queue_rq函数把request请求逐个下发给驱动驱动返回处理结果成功下发请求从dispatch链表中移除等待硬件执行完成设备繁忙请求保留在dispatch链表中等待后续重试释放自旋锁完成请求下发。阶段5驱动下发硬件执行驱动解析request请求中的所有BIO获取IO的起始扇区、大小、内存缓冲区、操作类型把request请求转换为硬件可识别的指令比如NVMe的SQE提交队列项、SCSI的CDB命令把指令写入硬件的提交队列通过PCIe总线下发给物理存储设备硬件执行IO操作完成后触发MSI-X硬件中断通知CPU IO完成。阶段6IO完成与请求释放驱动在中断处理函数中解析硬件的完成队列检查IO执行结果设置request的状态调用blk_mq_complete_request()函数触发blk-mq层的IO完成处理blk-mq层调用驱动的complete回调函数然后调用BIO的bi_end_io回调函数通知上层通用块层和文件系统IO完成释放request请求的引用计数当引用计数为0时释放request结构体和对应的BIO资源IO请求的生命周期结束。4.5 blk-mq的核心设计优势全链路无锁并行设计Per-CPU软件队列独立硬件队列的设计彻底消除了传统单队列的全局锁竞争多核CPU场景下IO性能随CPU核心数线性扩展16核以上场景性能提升5倍以上。原生适配多队列硬件完全匹配NVMe SSD的多队列硬件设计每个硬件队列可以独立和CPU核心交互完全并行处理IO把NVMe设备的百万级IOPS能力完全释放出来。NUMA架构原生优化支持NUMA节点亲和性同节点的CPU核心、硬件队列、PCIe设备绑定完全避免跨NUMA节点的内存访问和锁竞争多节点服务器场景下IO延迟降低50%以上。灵活可插拔的调度器框架支持在软件队列层、硬件队列层灵活插入IO调度器适配不同的硬件和业务场景比如NVMe SSD用none无调度器机械硬盘用mq-deadline调度器桌面场景用bfq调度器。低延迟高IOPS减少了上下文切换和锁等待的开销IO平均延迟降低30%以上99.9%长尾延迟降低10倍以上完美适配低延迟高并发的业务场景比如数据库、高频交易、分布式存储。热插拔与高可用支持原生支持设备热插拔、队列动态调整、硬件故障处理适配企业级存储的高可用需求。4.6 工程实践与调优指南4.6.1 核心调优参数所有调优参数都在/sys/block/[设备名]/queue/目录下对应struct request_queue的核心字段参数路径核心作用调优建议nr_requests每个硬件队列的最大队列深度NVMe SSD建议设置为1024~2048机械硬盘建议设置为256~512过大会导致延迟升高过小会导致IOPS上不去hw_sector_size硬件扇区大小只读不可修改4K对齐的核心依据max_sectors_kb单个IO的最大大小NVMe SSD建议设置为2048机械硬盘建议设置为1024提升顺序IO吞吐量read_ahead_kb预读大小机械硬盘顺序读场景建议设置为1024NVMe SSD随机读场景建议设置为0关闭预读schedulerIO调度器NVMe SSD建议设置为none/kyber机械硬盘建议设置为mq-deadline桌面/安卓场景建议设置为bfqrq_affinity中断亲和性设置为2强制IO完成中断在发起IO的CPU核心上执行降低延迟提升缓存命中率4.6.2 生产环境最佳实践1.NVMe SSD多队列优化开启NVMe设备的多队列支持确保硬件队列数量等于CPU核心数实现每个CPU核心对应一个独立硬件队列关闭irqbalance服务手动绑定每个硬件队列的中断到对应的CPU核心避免中断在CPU核心之间漂移导致缓存失效延迟升高设置rq_affinity2保证IO发起和完成都在同一个CPU核心上最大化L1/L2缓存命中率降低延迟。2.NUMA架构优化多NUMA节点服务器上把存储设备的PCIe控制器绑定到对应的NUMA节点仅该节点的CPU核心可以访问对应的硬件队列完全避免跨节点IO访问业务进程绑定到同NUMA节点的CPU核心确保进程发起的IO都在同节点内处理跨节点IO延迟会升高2倍以上。3.队列深度调优随机IO场景数据库、KV存储队列深度不宜过大建议1024以内过大会导致IO排队延迟升高99.9%长尾延迟恶化顺序IO场景大数据、视频存储队列深度可以调大到2048提升IO合并的概率最大化吞吐量低延迟场景高频交易队列深度建议设置为256~512减少排队延迟保证低延迟。4.IO调度器选型NVMe SSD绝大多数场景用none无调度器因为NVMe设备内部有自己的FTL调度算法内核调度器会带来额外开销低延迟随机读写场景用kyber调度器控制IO延迟机械硬盘HDD必须用mq-deadline调度器通过排序减少磁头寻道开销提升吞吐量桌面/嵌入式场景用bfq调度器保证前台应用的IO优先级提升交互体验。4.6.3 常见避坑指南irqbalance服务的坑irqbalance会自动调整硬件中断的CPU亲和性导致中断在CPU核心之间漂移缓存频繁失效IO延迟升高。NVMe SSD场景建议关闭irqbalance手动绑定中断到固定CPU核心。硬件队列数量设置不当硬件队列数量超过CPU核心数会导致多个CPU核心竞争同一个硬件队列的锁反而带来锁竞争开销。最佳实践是硬件队列数量等于CPU核心数或者等于NUMA节点内的CPU核心数。跨NUMA节点IO访问多NUMA节点服务器上进程在节点0的CPU核心上运行却访问节点1的PCIe存储设备会导致跨节点内存访问IO延迟升高2倍以上。必须保证进程、CPU核心、存储设备在同一个NUMA节点内。队列深度过大的坑很多人误以为队列深度越大IOPS越高实际上过大的队列深度会导致IO在队列中排队延迟急剧升高尤其是99.9%长尾延迟会恶化10倍以上。必须根据业务场景合理设置队列深度不是越大越好。NVMe SSD用传统调度器的坑很多老教程建议NVMe SSD用deadline调度器实际上现代NVMe SSD用none无调度器性能最好因为设备内部的FTL已经做了IO调度内核调度器只会带来额外的CPU开销没有任何收益。

相关新闻