[toc]
block/blk-mq.c 多队列块层核心(Multi-Queue Block Layer Core) 现代存储I/O性能的基石
历史与背景
这项技术是为了解决什么特定问题而诞生的?
blk-mq
(Block Multi-Queue)框架的诞生是为了从根本上重构Linux的块设备层,以适应现代存储硬件的飞速发展,特别是高速闪存设备(SSD、NVMe)的出现。
传统的Linux块层(single-queue block layer)是为机械硬盘(HDD)设计的,其核心是一个per-device的单一请求队列。这种设计在HDD时代是合理的,因为磁盘的机械寻道时间是主要瓶颈。然而,随着能够处理数十万甚至数百万IOPS(每秒I/O操作数)的闪存设备的普及,这个单一队列的设计成为了新的、严重的性能瓶颈:
- 锁争用:在多核CPU系统上,所有核心提交I/O时都必须争抢保护这个单一队列的全局锁。这种锁争用导致CPU无法有效扩展,即使硬件有能力处理更多请求,软件层面也无法将请求高效地提交给硬件。
- 缓存行弹跳:对单一队列的频繁访问导致其数据所在的缓存行在不同CPU核心的缓存之间来回失效和同步,造成了巨大的性能开销。
- 无法发挥硬件并行性:现代NVMe等设备在硬件层面就支持多个并行的提交和完成队列(Submission/Completion Queues)。单队列的软件栈完全无法利用这种硬件并行性。
- I/O调度器成为瓶颈:传统的I/O调度器(如CFQ)设计复杂,也围绕着单一队列工作,在高IOPS场景下其本身也成为了开销。
blk-mq
框架,主要由Jens Axboe主导开发,旨在通过引入多队列模型来解决以上所有问题,从而释放现代存储的全部潜力。
它的发展经历了哪些重要的里程碑或版本迭代?
blk-mq
的演进是Linux I/O栈的一次革命性变革。
- 引入 (Kernel 3.13):
blk-mq
框架首次被合并到内核中,最初只支持少数几个测试驱动。 - 功能完善 (Kernel 3.16):
blk-mq
达到了功能完备的状态,开始支持更多的驱动。 - SCSI多队列 (scsi-mq):在
blk-mq
的基础上,SCSI子系统也被改造为多队列模式,使得SATA、SAS等传统设备也能从blk-mq
中受益。 - 成为默认:随着其稳定性和性能优势得到验证,
blk-mq
逐渐成为新驱动的默认选择,并在RHEL 8等主流企业发行版中成为默认配置。 - 移除单队列层 (Kernel 5.0):
blk-mq
最终完全取代了传统的单队列块层实现,旧的代码被正式从内核中移除,标志着多队列时代的全面到来。
目前该技术的社区活跃度和主流应用情况如何?
blk-mq
是当前Linux内核唯一的块层实现,是整个I/O栈的绝对核心。
- 核心地位:所有块设备驱动(NVMe, SATA, SCSI, virtio-blk等)都构建在
blk-mq
之上。 - 性能基石:它是
io_uring
等最新异步I/O接口能够实现极致性能的基础。 - 社区状态:作为块层的维护者,Jens Axboe和社区持续对其进行优化,以支持新的硬件特性、提升效率(如轮询I/O、写者节流等),并完善其I/O调度器框架。
核心原理与设计
它的核心工作原理是什么?
blk-mq
的核心是一个两级队列架构,旨在最大限度地减少锁争用并利用硬件并行性。
第一级:软件暂存队列 (Software Staging Queues)
blk-mq
为系统中的每个CPU核心都创建一个独立的软件队列(struct blk_mq_ctx
)。- 当一个进程在某个CPU上发起I/O时,请求(
bio
)会被提交到该CPU专属的软件队列中。 - 由于每个CPU操作自己的队列,因此在提交路径上几乎没有锁争用,完美地解决了传统单队列模型的最大瓶颈。
第二级:硬件分派队列 (Hardware Dispatch Queues)
- 这一级队列(
struct blk_mq_hw_ctx
)直接映射到存储硬件实际拥有的物理提交队列。 一个NVMe设备可能有多个硬件队列,而一个SATA设备可能只有一个。 blk-mq
框架负责将软件队列中的请求分派到硬件队列中。通常,会有一个或多个软件队列映射到一个硬件队列。- 这个分派过程是触发实际I/O的最后一步。请求被放入硬件队列后,设备驱动就会拾取它并发送给硬件。
- 这一级队列(
这个两级模型巧妙地将“无锁的per-cpu请求提交”和“与硬件能力匹配的并行分派”结合起来,实现了极高的可伸scaling。
它的主要优势体現在哪些方面?
- 极高的可伸缩性:通过per-cpu队列,
blk-mq
在多核系统上的性能可以近乎线性地随CPU核心数增加而增长。 - 低延迟:消除了锁争用,大大缩短了I/O请求在内核中的处理路径和延迟。
- 充分利用硬件:能够完美匹配并利用现代NVMe等多队列硬件的并行处理能力。
- 对传统硬件亦有提升:即使对于只有单个硬件队列的传统SATA设备,per-cpu的软件队列也依然能通过减少提交端的锁争用而带来性能提升。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 调度器模型的改变:传统的I/O调度器(如CFQ, Deadline)是为单队列设计的,无法直接用于
blk-mq
。为此,blk-mq
引入了一个新的调度器框架,并开发了新的调度器(如mq-deadline
,bfq
,kyber
)。 - 对低性能设备可能过犹不及:对于非常慢的设备(如一些SD卡或虚拟化环境下的慢速磁盘),
blk-mq
带来的多队列开销可能不会带来明显好处,甚至理论上会略有增加。但在现代硬件上,这基本不成问题。 - 配置复杂性:
blk-mq
的内部结构比单队列模型更复杂,虽然对用户透明,但对于内核开发者和深度性能调优者来说,理解其工作原理需要更多知识。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
作为当前唯一的块层实现,它适用于所有与块设备交互的场景。它带来的好处在以下场景中尤为突出:
- 高性能存储(NVMe SSDs):这是
blk-mq
的“原生”应用场景,只有blk-mq
才能发挥出NVMe设备数百万IOPS的性能。 - 多核服务器:在拥有大量CPU核心的服务器上,
blk-mq
的per-cpu设计是防止I/O性能瓶颈的关键。 - 数据库和虚拟化:这些I/O密集型应用能够从
blk-mq
带来的低延迟和高吞吐量中直接受益。
是否有不推荐使用该技术的场景?为什么?
在现代Linux内核中(5.0及以后),已经没有不使用blk-mq
的选项了,因为它已经完全取代了旧的实现。所有块设备I/O都会流经blk-mq
框架。
对比分析
请将其 与 其他相似技术 进行详细对比。
blk-mq
(多队列) vs. 传统单队列块层
特性 | blk-mq (Multi-Queue) |
传统单队列块层 (Single-Queue Legacy) |
---|---|---|
核心数据结构 | Per-CPU的软件队列 + Per-Hardware-Queue的硬件队列。 | Per-Device的单一请求队列。 |
锁模型 | 提交路径几乎无锁。锁的粒度非常细。 | 一个全局的、per-device的粗粒度锁,保护整个请求队列。 |
CPU伸缩性 | 极高。性能随CPU核心数线性增长。 | 差。在多核环境下迅速因锁争用而达到瓶颈。 |
硬件并行性 | 完全支持。可以直接映射到硬件的多个队列。 | 不支持。无法利用硬件的多队列能力。 |
I/O调度器 | 使用专门的mq 调度器(mq-deadline , bfq , kyber , none )。 |
使用传统调度器(noop , deadline , cfq )。 |
适用设备 | 通用,尤其针对高速闪存设备(NVMe)进行了优化。 | 主要为机械硬盘(HDD)设计。 |
当前状态 | 当前标准(自Kernel 5.0起为唯一实现)。 | 已被移除(自Kernel 5.0起)。 |
block/blk-mq.c
:多队列块设备 I/O 框架实现
block/blk-mq.c
是 Linux 内核中实现多队列块层(Multi-Queue Block Layer, blk-mq
)的核心源文件。该框架的设计目标是为了在现代多核处理器和高速存储设备(如 NVMe SSD)上实现高 I/O 性能和高扩展性。
一、 核心功能
blk-mq.c
的核心功能是提供一个全新的、可扩展的 I/O 调度框架,以替代传统的单队列块设备层。它通过将 I/O 提交和完成路径分解到多个队列中,从根本上减少了锁竞争,使得 I/O 性能能够随 CPU 核心数的增加而线性增长。
该文件包含了 blk-mq
框架的初始化、运行时管理、I/O 提交流程和完成回报处理的所有核心逻辑。
二、 解决的技术问题
传统的 Linux 块设备层为每个块设备维护一个单一的请求队列(request_queue
)。所有 CPU 对该设备的 I/O 请求都必须通过获取一个全局锁来访问此队列。在多核系统和高 IOPS 设备上,这个单一的锁会成为严重的性能瓶颈,导致 CPU 无法充分利用硬件能力。
blk-mq
通过以下设计解决了这个问题:
- 消除单一队列锁:用多个无锁或几乎无锁的队列取代单一的全局锁队列。
- 提升扩展性:使 I/O 吞吐能力能够随着 CPU 核心数和硬件队列数的增加而有效提升。
- 适应现代硬件:为 NVMe 等具有多个硬件提交队列的设备提供一个原生、高效的软件模型。
三、 核心设计与数据结构
blk-mq
的设计核心是一个两级队列结构:软件队列和硬件队列。
1. 软件提交队列 (Software Submission Queues)
- 数据结构:
struct blk_mq_ctx
- 设计:
blk-mq
为每个 CPU 核心创建一个或多个软件提交队列 (ctx
)。当一个运行在特定 CPU 上的进程发起 I/O 请求时,该请求会被放入该 CPU 对应的ctx
队列中。 - 目的: 因为每个 CPU 操作自己的本地队列,所以在 I/O 提交路径上几乎没有跨 CPU 的锁竞争。这极大地降低了 I/O 提交时的延迟。
2. 硬件分发队列 (Hardware Dispatch Queues)
- 数据结构:
struct blk_mq_hw_ctx
- 设计: 硬件分发队列 (
hctx
) 直接映射到存储设备物理上的命令队列。一个hctx
可以服务于一个或多个ctx
。 - 目的:
hctx
是软件层和硬件驱动之间的接口。当hctx
被运行时,它会从其关联的ctx
队列中拉取请求,然后通过驱动程序提供的操作函数将这些请求分发给硬件。
3. 请求跟踪与管理 (Request Tracking)
- 数据结构:
struct blk_mq_tags
和struct blk_mq_tag_set
- 设计:
blk-mq
使用一个基于标签(tag)的系统来跟踪在途的 I/O 请求。每个request
在被分配时都会获得一个唯一的整数标签。这个标签会伴随请求一直传递到硬件。 - 目的: 当硬件完成一个操作时,它会通过中断通知驱动程序哪个标签的命令已完成。驱动程序可以使用这个标签在 O(1) 时间复杂度内快速定位到原始的
struct request
结构体,从而高效地处理 I/O 完成事件。
4. 驱动程序接口
- 数据结构:
struct blk_mq_ops
- 设计:
blk-mq
是一个通用框架,它定义了一组标准的操作函数,具体的设备驱动程序必须实现这些函数。queue_rq
: 将一个request
提交给硬件。这是blk-mq
调用驱动程序的核心函数。complete
: 由驱动程序在 I/O 完成时调用,用于通知blk-mq
框架。init_hctx
/exit_hctx
: 用于初始化和清理硬件队列相关的资源。
- 目的: 将通用的队列管理逻辑与特定的硬件操作逻辑解耦。
四、 I/O 请求在 blk-mq
中的生命周期
提交阶段 (Submission):
- 上层代码(如文件系统)调用
submit_bio()
来提交一个bio
。 blk_mq_submit_bio()
被调用。它会获取当前 CPU 对应的blk_mq_ctx
。- 系统从
blk_mq_tags
中分配一个request
结构体和一个唯一的标签。 bio
的信息被填充到request
中。- 该
request
被放入当前 CPU 的blk_mq_ctx
软件队列中。
- 上层代码(如文件系统)调用
分发阶段 (Dispatch):
- 在某个时间点(例如,队列中有足够多的请求,或者延时触发),
__blk_mq_run_hw_queue()
被调用,开始处理一个硬件队列 (hctx
)。 hctx
会轮流检查其关联的所有ctx
软件队列。- 如果
ctx
中有请求,hctx
会将它们移动到自己的分发列表里。 - I/O 调度器(如
mq-deadline
,bfq
)在此时介入,对分发列表中的request
进行排序或调度。 blk-mq
框架遍历调度后的request
列表,对每一个request
调用驱动程序注册的.queue_rq()
函数。- 驱动程序在
.queue_rq()
函数中生成硬件命令,并将命令与request
的标签一同发送给存储设备。
- 在某个时间点(例如,队列中有足够多的请求,或者延时触发),
完成阶段 (Completion):
- 存储硬件完成 I/O 操作,并触发一个中断。
- 驱动程序的中断处理函数被执行。它从硬件获取已完成命令的标签。
- 驱动程序使用标签调用
blk_mq_complete_request()
或blk_mq_end_request()
来通知blk-mq
框架。 blk-mq
使用标签快速找到对应的request
结构体。blk-mq
执行收尾工作:释放标签,调用bio
的bi_end_io
回调函数,最终释放request
结构体。
五、 关键源码函数分析
blk_mq_init_queue(struct blk_mq_tag_set *set)
:blk-mq
模式下创建和初始化一个request_queue
的核心函数。它负责分配和设置blk_mq_ctx
和blk_mq_hw_ctx
数组。blk_mq_submit_bio(struct bio *bio)
:bio
进入blk-mq
系统的主要入口点。负责获取request
并将其排入软件队列。__blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx)
:
驱动硬件队列运行的核心逻辑。它负责从软件队列获取请求,并调用驱动的.queue_rq
函数。blk_mq_dispatch_rq_list(struct blk_mq_hw_ctx *hctx, struct list_head *list)
:
负责运行 I/O 调度器并实际调用驱动.queue_rq
的函数。blk_mq_complete_request(struct request *rq)
:
驱动在 I/O 完成时调用的标准函数,用于启动请求的完成处理流程。blk_mq_tag_get(struct blk_mq_hw_ctx *hctx)
/blk_mq_tag_put(struct blk_mq_hw_ctx *hctx, unsigned int tag, unsigned int sw_tag)
:
用于从blk_mq_tags
池中分配和释放请求标签的函数。
六、 总结
block/blk-mq.c
文件实现了 Linux 的高性能块设备 I/O 框架。其技术核心是采用了两级队列模型(per-CPU 软件队列和硬件映射队列)来最小化锁竞争,并利用基于标签的请求跟踪机制实现高效的 I/O 完成处理。这个框架为现代多核系统和高速存储硬件提供了必需的可扩展性和高性能,是当前 Linux I/O 栈的基石。
I/O Completion Softirq: I/O请求完成(completion)路径的”下半部”(bottom half)
这两个函数共同构成了Linux blk-mq
(多队列块I/O)子系统中I/O请求完成(completion)路径的”下半部”(bottom half)。它们是整个I/O流程的终点, 负责在硬件已经完成读写操作后, 执行所有必要的软件清理工作, 并通知上层代码(如文件系统或应用程序)I/O已完成。
核心原理 (中断上半部 vs. 软中断下半部):
这是一个经典的、为了最大化系统响应性和吞吐量而设计的两阶段中断处理模型:
- 上半部 (Top Half - The Hardware Interrupt): 当块设备(如SD卡控制器)完成一个I/O操作时, 它会触发一个硬件中断。内核的中断处理程序(ISR)会立即执行。这个”上半部”的代码被设计得极其快速和精简。它只做最少的工作: 找到是哪个
request
完成了, 然后调用llist_add
将其**无锁地(lock-lessly)**推入当前CPU核心的完成链表blk_cpu_done
中, 最后触发一个BLOCK_SOFTIRQ
软中断。完成这些后, 它就立即退出, 让CPU可以去响应其他更重要的中断。 - 下半部 (Bottom Half - The Softirq): 内核会在稍后一个更安全、更合适的时机(通常是在从中断返回之前, 或者在内核线程中)来执行所有待处理的软中断。此时,
blk_done_softirq
函数就会被调用。这个”下半部”运行在允许中断的上下文中, 可以执行比上半部复杂得多的、耗时更长的操作, 而不会阻塞新的硬件中断。
这两个函数就是这个”下半部”的实现。
blk_done_softirq
: 软中断入口点
这是一个非常简短的函数, 它的唯一作用就是作为BLOCK_SOFTIRQ
软中断的官方入口点, 并将工作分派给实际的处理函数。
1 | /* |
blk_complete_reqs
: 批量处理已完成的请求
这是真正执行清理工作的核心函数。它被设计用来高效地处理一批(a batch of)已完成的请求。
1 | /* |
blk_mq_init: 初始化多队列块I/O(Block MQ)子系统
此函数是Linux内核多队列块I/O层(Multi-Queue Block Layer, blk-mq
)的全局初始化入口。blk-mq
是现代Linux内核中用于与块设备(如SD卡, NVMe SSDs, SATA驱动器)交互的高性能框架。它的核心原理是为系统中的每个CPU核心都提供独立的I/O提交队列, 从而极大地减少了在多核系统上因争夺单一I/O队列锁而产生的瓶颈。
blk_mq_init
的作用就是建立并激活blk-mq
框架最基础的、与CPU相关的底层基础设施, 特别是I/O请求的完成(completion)处理机制和CPU生命周期(热插拔)管理。它为所有后续注册的blk-mq
设备驱动程序铺平了道路。
关键机制初始化:
Per-CPU 完成处理 (Completion Handling):
- Lock-less Done Lists:
init_llist_head(&per_cpu(blk_cpu_done, i))
为每个CPU核心创建一个无锁(lock-less)链表。当硬件完成一个I/O操作并通过中断通知CPU时, 中断处理程序(ISR)会将完成的请求以极快的速度、无锁地添加到当前CPU的”done”链表中。 - Remote Completion CSD:
INIT_CSD(...)
初始化了一个”Call Single Data”结构, 这是内核中用于高效执行跨核函数调用(IPI)的机制。它用于处理一个请求在CPU-A上提交, 但其完成中断却在CPU-B上触发的场景。此时, CPU-B可以使用这个机制来”通知”CPU-A去处理这个完成的请求。 - Softirq:
open_softirq(BLOCK_SOFTIRQ, blk_done_softirq)
是整个完成路径的核心。它注册了一个软中断(softirq)处理函数blk_done_softirq
。硬件中断处理程序在将请求放入”done”链表后, 只会触发一个BLOCK_SOFTIRQ
软中断就立即退出。内核会在稍后安全、合适的时机执行blk_done_softirq
。这个函数会真正地处理”done”链表中的所有请求, 包括唤醒等待的进程、释放资源等。这种”中断上半部/下半部”的机制确保了硬件中断的路径尽可能短, 提高了系统响应性。
- Lock-less Done Lists:
CPU热插拔支持 (CPU Hotplug):
cpuhp_setup_state_*
系列函数注册了一系列回调, 用于在CPU被动态地添加到系统(online)或从系统中移除(offline/dead)时, 对blk-mq
的per-cpu数据结构进行相应的管理。这确保了当一个CPU上线时, 它的队列会被正确初始化; 当它下线时, 其队列中任何待处理的请求都会被迁移到其他CPU, 并且其占用的资源会被干净地释放, 从而保证了系统在动态CPU拓扑变化下的健壮性。
代码逐行解析
1 | /* |