[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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
static __latent_entropy void blk_done_softirq(void) {
blk_complete_reqs(this_cpu_ptr(&blk_cpu_done)); }
|
blk_complete_reqs: 批量处理已完成的请求
这是真正执行清理工作的核心函数。它被设计用来高效地处理一批(a batch of)已完成的请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
static void blk_complete_reqs(struct llist_head *list) {
struct llist_node *entry = llist_reverse_order(llist_del_all(list)); struct request *rq, *next;
llist_for_each_entry_safe(rq, next, entry, ipi_list)
rq->q->mq_ops->complete(rq); }
|
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”链表中的所有请求, 包括唤醒等待的进程、释放资源等。这种”中断上半部/下半部”的机制确保了硬件中断的路径尽可能短, 提高了系统响应性。
CPU热插拔支持 (CPU Hotplug):
cpuhp_setup_state_*系列函数注册了一系列回调, 用于在CPU被动态地添加到系统(online)或从系统中移除(offline/dead)时, 对blk-mq的per-cpu数据结构进行相应的管理。这确保了当一个CPU上线时, 它的队列会被正确初始化; 当它下线时, 其队列中任何待处理的请求都会被迁移到其他CPU, 并且其占用的资源会被干净地释放, 从而保证了系统在动态CPU拓扑变化下的健壮性。
代码逐行解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
static int __init blk_mq_init(void) { int i;
for_each_possible_cpu(i)
init_llist_head(&per_cpu(blk_cpu_done, i)); for_each_possible_cpu(i)
INIT_CSD(&per_cpu(blk_cpu_csd, i), __blk_mq_complete_request_remote, NULL);
open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);
cpuhp_setup_state_nocalls(CPUHP_BLOCK_SOFTIRQ_DEAD, "block/softirq:dead", NULL, blk_softirq_cpu_dead);
cpuhp_setup_state_multi(CPUHP_BLK_MQ_DEAD, "block/mq:dead", NULL, blk_mq_hctx_notify_dead);
cpuhp_setup_state_multi(CPUHP_AP_BLK_MQ_ONLINE, "block/mq:online", blk_mq_hctx_notify_online, blk_mq_hctx_notify_offline); return 0; }
subsys_initcall(blk_mq_init);
|
blk_mq_rq_ctx_init __blk_mq_alloc_requests_batch __blk_mq_alloc_requests blk_mq_rq_cache_fill blk_mq_alloc_cached_request blk_mq_alloc_request 请求对象初始化与批量分配缓存策略
作用与实现要点
- 分配路径分层:优先走
plug->cached_rqs 的缓存请求;缓存不足时用 plug->nr_ios 触发批量取 tag + 批量初始化 rq;否则退化为单个 tag 分配。
- 调度器门控与标签策略:
q->elevator 存在时强制 RQF_SCHED_TAGS,并对非 flush / 非 passthrough 请求打开 RQF_USE_SCHED;调度器可通过 limit_depth() 影响可分配深度。
- NOWAIT 与重试策略:
REQ_NOWAIT 会映射为 BLK_MQ_REQ_NOWAIT,tag 不可用时直接失败;非 NOWAIT 场景在 BLK_MQ_NO_TAG 时通过 msleep(3) + goto retry 处理 hctx 迁移/失活窗口。
- 活跃计数与引用计数配平:非
RQF_SCHED_TAGS 时更新 hctx active requests(批量路径用 blk_mq_add_active_requests,单个路径用 blk_mq_inc_active_requests);批量分配后用 percpu_ref_get_many 为队列使用计数补齐引用。
- 请求对象初始化要点:固定字段复位(timeout、统计字段、回调指针等);按
RQF_SCHED_TAGS 决定 tag/internal_tag 的归属;WRITE_ONCE(rq->deadline, 0) 确保 deadline 初始化的并发可见性;req_ref_set(rq, 1) 建立基础引用。
blk_mq_rq_ctx_init 初始化请求对象的上下文字段与调度器相关状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
static struct request *blk_mq_rq_ctx_init(struct blk_mq_alloc_data *data, struct blk_mq_tags *tags, unsigned int tag) { struct blk_mq_ctx *ctx = data->ctx; struct blk_mq_hw_ctx *hctx = data->hctx; struct request_queue *q = data->q; struct request *rq = tags->static_rqs[tag];
rq->q = q; rq->mq_ctx = ctx; rq->mq_hctx = hctx; rq->cmd_flags = data->cmd_flags;
if (data->flags & BLK_MQ_REQ_PM) data->rq_flags |= RQF_PM; rq->rq_flags = data->rq_flags;
if (data->rq_flags & RQF_SCHED_TAGS) { rq->tag = BLK_MQ_NO_TAG; rq->internal_tag = tag; } else { rq->tag = tag; rq->internal_tag = BLK_MQ_NO_TAG; } rq->timeout = 0;
rq->part = NULL; rq->io_start_time_ns = 0; rq->stats_sectors = 0; rq->nr_phys_segments = 0; rq->nr_integrity_segments = 0; rq->end_io = NULL; rq->end_io_data = NULL;
blk_crypto_rq_set_defaults(rq); INIT_LIST_HEAD(&rq->queuelist); WRITE_ONCE(rq->deadline, 0); req_ref_set(rq, 1);
if (rq->rq_flags & RQF_USE_SCHED) { struct elevator_queue *e = data->q->elevator;
INIT_HLIST_NODE(&rq->hash); RB_CLEAR_NODE(&rq->rb_node);
if (e->type->ops.prepare_request) e->type->ops.prepare_request(rq); }
return rq; }
|
__blk_mq_alloc_requests_batch 批量取 tag 并批量初始化请求以填充缓存队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
static inline struct request * __blk_mq_alloc_requests_batch(struct blk_mq_alloc_data *data) { unsigned int tag, tag_offset; struct blk_mq_tags *tags; struct request *rq; unsigned long tag_mask; int i, nr = 0;
do { tag_mask = blk_mq_get_tags(data, data->nr_tags - nr, &tag_offset); if (unlikely(!tag_mask)) { if (nr == 0) return NULL; break; } tags = blk_mq_tags_from_data(data); for (i = 0; tag_mask; i++) { if (!(tag_mask & (1UL << i))) continue; tag = tag_offset + i; prefetch(tags->static_rqs[tag]); tag_mask &= ~(1UL << i); rq = blk_mq_rq_ctx_init(data, tags, tag); rq_list_add_head(data->cached_rqs, rq); nr++; } } while (data->nr_tags > nr);
if (!(data->rq_flags & RQF_SCHED_TAGS)) blk_mq_add_active_requests(data->hctx, nr);
percpu_ref_get_many(&data->q->q_usage_counter, nr - 1); data->nr_tags -= nr;
return rq_list_pop(data->cached_rqs); }
|
__blk_mq_alloc_requests 核心分配逻辑:上下文映射、调度器门控、批量/单个 tag 分配与重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
static struct request *__blk_mq_alloc_requests(struct blk_mq_alloc_data *data) { struct request_queue *q = data->q; u64 alloc_time_ns = 0; struct request *rq; unsigned int tag;
if (blk_queue_rq_alloc_time(q)) alloc_time_ns = blk_time_get_ns();
if (data->cmd_flags & REQ_NOWAIT) data->flags |= BLK_MQ_REQ_NOWAIT;
retry: data->ctx = blk_mq_get_ctx(q); data->hctx = blk_mq_map_queue(data->cmd_flags, data->ctx);
if (q->elevator) { data->rq_flags |= RQF_SCHED_TAGS;
if ((data->cmd_flags & REQ_OP_MASK) != REQ_OP_FLUSH && !blk_op_is_passthrough(data->cmd_flags)) { struct elevator_mq_ops *ops = &q->elevator->type->ops;
WARN_ON_ONCE(data->flags & BLK_MQ_REQ_RESERVED);
data->rq_flags |= RQF_USE_SCHED; if (ops->limit_depth) ops->limit_depth(data->cmd_flags, data); } } else { blk_mq_tag_busy(data->hctx); }
if (data->flags & BLK_MQ_REQ_RESERVED) data->rq_flags |= RQF_RESV;
if (data->nr_tags > 1) { rq = __blk_mq_alloc_requests_batch(data); if (rq) { blk_mq_rq_time_init(rq, alloc_time_ns); return rq; } data->nr_tags = 1; }
tag = blk_mq_get_tag(data); if (tag == BLK_MQ_NO_TAG) { if (data->flags & BLK_MQ_REQ_NOWAIT) return NULL; msleep(3); goto retry; }
if (!(data->rq_flags & RQF_SCHED_TAGS)) blk_mq_inc_active_requests(data->hctx); rq = blk_mq_rq_ctx_init(data, blk_mq_tags_from_data(data), tag); blk_mq_rq_time_init(rq, alloc_time_ns); return rq; }
|
blk_mq_rq_cache_fill 依据 plug->nr_ios 触发批量分配并填充缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
static struct request *blk_mq_rq_cache_fill(struct request_queue *q, struct blk_plug *plug, blk_opf_t opf, blk_mq_req_flags_t flags) { struct blk_mq_alloc_data data = { .q = q, .flags = flags, .shallow_depth = 0, .cmd_flags = opf, .rq_flags = 0, .nr_tags = plug->nr_ios, .cached_rqs = &plug->cached_rqs, .ctx = NULL, .hctx = NULL }; struct request *rq;
if (blk_queue_enter(q, flags)) return NULL;
plug->nr_ios = 1;
rq = __blk_mq_alloc_requests(&data); if (unlikely(!rq)) blk_queue_exit(q); return rq; }
|
blk_mq_alloc_cached_request 从 plug 缓存获取可复用 request 或触发填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
static struct request *blk_mq_alloc_cached_request(struct request_queue *q, blk_opf_t opf, blk_mq_req_flags_t flags) { struct blk_plug *plug = current->plug; struct request *rq;
if (!plug) return NULL;
if (rq_list_empty(&plug->cached_rqs)) { if (plug->nr_ios == 1) return NULL; rq = blk_mq_rq_cache_fill(q, plug, opf, flags); if (!rq) return NULL; } else { rq = rq_list_peek(&plug->cached_rqs); if (!rq || rq->q != q) return NULL;
if (blk_mq_get_hctx_type(opf) != rq->mq_hctx->type) return NULL; if (op_is_flush(rq->cmd_flags) != op_is_flush(opf)) return NULL;
rq_list_pop(&plug->cached_rqs); blk_mq_rq_time_init(rq, blk_time_get_ns()); }
rq->cmd_flags = opf; INIT_LIST_HEAD(&rq->queuelist); return rq; }
|
blk_mq_alloc_request 对外接口:缓存优先、必要时进入队列并走核心分配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
struct request *blk_mq_alloc_request(struct request_queue *q, blk_opf_t opf, blk_mq_req_flags_t flags) { struct request *rq;
rq = blk_mq_alloc_cached_request(q, opf, flags); if (!rq) { struct blk_mq_alloc_data data = { .q = q, .flags = flags, .shallow_depth = 0, .cmd_flags = opf, .rq_flags = 0, .nr_tags = 1, .cached_rqs = NULL, .ctx = NULL, .hctx = NULL }; int ret;
ret = blk_queue_enter(q, flags); if (ret) return ERR_PTR(ret);
rq = __blk_mq_alloc_requests(&data); if (!rq) goto out_queue_exit; } rq->__data_len = 0; rq->phys_gap_bit = 0; rq->__sector = (sector_t) -1; rq->bio = rq->biotail = NULL; return rq; out_queue_exit: blk_queue_exit(q); return ERR_PTR(-EWOULDBLOCK); } EXPORT_SYMBOL(blk_mq_alloc_request);
|
blk_mq_map_queue_type blk_mq_get_hctx_type blk_mq_map_queue 块层按请求类型与CPU选择硬件队列
作用与实现要点
- 两级映射:先把请求
opf 归类为 hctx_type,再用该类型索引 ctx->hctxs[] 得到目标硬件队列上下文。
- 类型优先级:
REQ_POLLED 优先于读写类型判断;否则读请求可单独分流到 HCTX_TYPE_READ,其余落到默认类型。
- CPU 参与映射:
blk_mq_map_queue_type() 直接从 tag_set->map[type].mq_map[cpu] 取队列编号,再转换成 blk_mq_hw_ctx *,把“(type,cpu)”映射到具体硬件队列。
平台关注:单核、无 MMU、ARMv7-M(STM32H750)
本段逻辑与单核/无MMU无直接差异,依赖底层 ops 与系统定时机制
blk_mq_map_queue_type 按类型与CPU映射到硬件队列
1 2 3 4 5 6 7 8 9 10 11 12 13
|
static inline struct blk_mq_hw_ctx *blk_mq_map_queue_type(struct request_queue *q, enum hctx_type type, unsigned int cpu) { return queue_hctx((q), (q->tag_set->map[type].mq_map[cpu])); }
|
blk_mq_get_hctx_type 从操作与标志推导hctx类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
static inline enum hctx_type blk_mq_get_hctx_type(blk_opf_t opf) { enum hctx_type type = HCTX_TYPE_DEFAULT;
if (opf & REQ_POLLED) type = HCTX_TYPE_POLL; else if ((opf & REQ_OP_MASK) == REQ_OP_READ) type = HCTX_TYPE_READ; return type; }
|
blk_mq_map_queue 按请求类型在ctx中选择对应硬件队列
1 2 3 4 5 6 7 8 9 10 11
|
static inline struct blk_mq_hw_ctx *blk_mq_map_queue(blk_opf_t opf, struct blk_mq_ctx *ctx) { return ctx->hctxs[blk_mq_get_hctx_type(opf)]; }
|
blk_end_sync_rq blk_rq_is_poll blk_rq_poll_completion blk_execute_rq 同步直通请求的提交与完成等待梳理
作用与实现要点
- 通过“栈上 completion + end_io 回调 + end_io_data 指针”把异步完成信号改造成可同步等待的结果回传:驱动在结束请求时回填
blk_status_t,并 complete() 唤醒等待方。
- 以
blk_rq_is_poll() 作为能力门控:只有当请求绑定到 HCTX_TYPE_POLL 的硬件上下文时才走主动轮询等待;否则走阻塞等待,避免无意义自旋。
- 轮询等待采用“单次轮询 + 主动让出 CPU”的策略:每轮只触发一次
blk_hctx_poll(),随后 cond_resched() 给调度器插入让出点,既保持低延迟,又避免长时间占用 CPU。
blk_execute_rq() 在提交前用 WARN_ON() 强制约束调用上下文:要求中断可用(否则完成回调/唤醒可能无法到达),并要求请求是 passthrough(表明请求已完全准备好,走直通路径)。
blk_wait_io() 的等待路径会刻意处理“超长 I/O 被 hung task 机制误判”的问题:通过分段等待(或等价策略)降低被后台监测机制打断/告警的概率。
平台关注:单核、无 MMU、ARMv7-M(STM32H750)
- 语义核心仍是“回调发完成、等待方收完成”,但单核下轮询路径更容易独占 CPU,因此
cond_resched() 这一类让出点在系统可用性上更关键;同时 WARN_ON(irqs_disabled()) 体现了该设计默认依赖“中断/调度可运行”来让完成回调推进。若在无 MMU/嵌入式移植环境中缺少完整的阻塞等待与调度语义,需要用等价的等待/让出/中断回调机制替代 blk_wait_io() 与 cond_resched(),并严格保证栈上 wait 的生命周期覆盖到最后一次 complete()。
blk_end_sync_rq 同步等待用的请求完成回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| struct blk_rq_wait { struct completion done; blk_status_t ret; };
static enum rq_end_io_ret blk_end_sync_rq(struct request *rq, blk_status_t ret) { struct blk_rq_wait *wait = rq->end_io_data;
wait->ret = ret; complete(&wait->done); return RQ_END_IO_NONE; }
|
blk_rq_is_poll 判断请求是否绑定到轮询硬件上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
bool blk_rq_is_poll(struct request *rq) { if (!rq->mq_hctx) return false; if (rq->mq_hctx->type != HCTX_TYPE_POLL) return false; return true; } EXPORT_SYMBOL_GPL(blk_rq_is_poll);
|
blk_rq_poll_completion 轮询直到 completion 变为 done
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
static void blk_rq_poll_completion(struct request *rq, struct completion *wait) { do { blk_hctx_poll(rq->q, rq->mq_hctx, NULL, BLK_POLL_ONESHOT); cond_resched(); } while (!completion_done(wait)); }
|
blk_execute_rq 同步提交直通请求并等待完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
blk_status_t blk_execute_rq(struct request *rq, bool at_head) { struct blk_mq_hw_ctx *hctx = rq->mq_hctx; struct blk_rq_wait wait = { .done = COMPLETION_INITIALIZER_ONSTACK(wait.done), };
WARN_ON(irqs_disabled()); WARN_ON(!blk_rq_is_passthrough(rq));
rq->end_io_data = &wait; rq->end_io = blk_end_sync_rq;
blk_account_io_start(rq); blk_mq_insert_request(rq, at_head ? BLK_MQ_INSERT_AT_HEAD : 0); blk_mq_run_hw_queue(hctx, false);
if (blk_rq_is_poll(rq)) blk_rq_poll_completion(rq, &wait.done); else blk_wait_io(&wait.done);
return wait.ret; } EXPORT_SYMBOL(blk_execute_rq);
|
__blk_mq_run_dispatch_ops blk_mq_run_dispatch_ops 读侧锁选择与派发操作包裹
作用与实现要点
- 这组宏把“执行一段派发相关的代码块”统一包进读侧保护里:根据
tag_set 是否允许阻塞,自动选择 SRCU 或 RCU,让 dispatch_ops 在访问共享结构时不至于撞上并发释放/替换。
BLK_MQ_F_BLOCKING 是分流门控:一旦可能走到会睡眠的提交/派发路径,就用 SRCU 读锁,因为它允许读侧在必要时睡眠;否则走 RCU 读锁,以更低开销覆盖“明确不会睡”的快路径。
check_sleep 只影响 might_sleep_if():调用点用它表达“这段 dispatch_ops 里是否允许出现睡眠点”,从而在调试配置下尽早暴露“本来不该睡却睡了”的用法偏差。
srcu_read_lock() 返回的 srcu_idx 必须原样传回 srcu_read_unlock():这不是装饰性的变量,它决定了解锁时对应哪一次读侧进入,漏传或传错会把 SRCU 的读侧计数弄乱,导致后续同步期判断失真。
__blk_mq_run_dispatch_ops 在读侧锁保护下执行派发操作块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
#define __blk_mq_run_dispatch_ops(q, check_sleep, dispatch_ops) \ do { \ if ((q)->tag_set->flags & BLK_MQ_F_BLOCKING) { \ struct blk_mq_tag_set *__tag_set = (q)->tag_set; \ int srcu_idx; \ \ might_sleep_if(check_sleep); \ srcu_idx = srcu_read_lock(__tag_set->srcu); \ (dispatch_ops); \ srcu_read_unlock(__tag_set->srcu, srcu_idx); \ } else { \ rcu_read_lock(); \ (dispatch_ops); \ rcu_read_unlock(); \ } \ } while (0)
|
blk_mq_run_dispatch_ops 默认开启睡眠检查的派发操作包装
1 2 3 4 5 6 7 8 9 10 11 12
|
#define blk_mq_run_dispatch_ops(q, dispatch_ops) \ __blk_mq_run_dispatch_ops(q, true, dispatch_ops) \
|
blk_mq_request_bypass_insert blk_mq_insert_requests blk_mq_insert_request 绕过调度器与软硬队列入队路径梳理
作用与实现要点
- 以 请求类型与队列形态做分流:passthrough 与 flush 直接进入
hctx->dispatch,普通请求则进入调度器(若存在)或进入 ctx->rq_lists[],确保“必须先出队”的请求不会被调度器/软队列稀释优先级。
- 先尝试 直发硬件 的优化分支:当
hctx 不忙且不要求异步运行时,直接把 list 里的请求尝试下发,减少一次入软队列再出软队列的开销;失败残留才回落到软队列。
- 用 一次性 run_queue_async 升级适配 NOWAIT:扫描待插入列表时,只要发现
REQ_NOWAIT 就把 run_queue_async 置为真,避免在“不能睡眠”的语义下走可能阻塞的同步运行路径。
- 用两把锁分别守住两类共享结构:
hctx->lock 保护 hctx->dispatch(硬件侧优先派发的等待队列),ctx->lock 保护 ctx->rq_lists[type] 与 “该 ctx 在该 hctx 上有待处理”的标记,保证派发侧看到一致的链表与 pending 位图。
goto out 的存在是为了在“直发成功把 list 清空”时跳过软队列入队与 pending 标记,减少锁竞争;统一从 out: 调用 blk_mq_run_hw_queue(),把“是否需要继续推进派发”的决定交给硬件队列运行逻辑。
平台关注:单核、无 MMU、ARMv7-M(STM32H750)
- 单核下仍需要这些锁/状态位:它们不仅是“多核互斥”,也承担了中断上下文/软中断/抢占切换与普通上下文之间的互斥与可见性。若在 STM32H750 的移植环境里
spin_lock() 实现为关中断或临界区进入,则必须保证它同时提供必要的内存可见性语义,否则 dispatch 链表与 ctx_map 的更新可能被派发侧观察到不一致,从而出现“入队了但跑队列看不到”的假空队列现象。
blk_mq_request_bypass_insert 将请求直接插入硬件派发表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
static void blk_mq_request_bypass_insert(struct request *rq, blk_insert_t flags) { struct blk_mq_hw_ctx *hctx = rq->mq_hctx;
spin_lock(&hctx->lock); if (flags & BLK_MQ_INSERT_AT_HEAD) list_add(&rq->queuelist, &hctx->dispatch); else list_add_tail(&rq->queuelist, &hctx->dispatch); spin_unlock(&hctx->lock); }
|
blk_mq_insert_requests 批量入队并按需触发硬件队列运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
static void blk_mq_insert_requests(struct blk_mq_hw_ctx *hctx, struct blk_mq_ctx *ctx, struct list_head *list, bool run_queue_async) { struct request *rq; enum hctx_type type = hctx->type;
if (!hctx->dispatch_busy && !run_queue_async) { blk_mq_run_dispatch_ops(hctx->queue, blk_mq_try_issue_list_directly(hctx, list)); if (list_empty(list)) goto out; }
list_for_each_entry(rq, list, queuelist) { BUG_ON(rq->mq_ctx != ctx); trace_block_rq_insert(rq); if (rq->cmd_flags & REQ_NOWAIT) run_queue_async = true; }
spin_lock(&ctx->lock); list_splice_tail_init(list, &ctx->rq_lists[type]); blk_mq_hctx_mark_pending(hctx, ctx); spin_unlock(&ctx->lock); out: blk_mq_run_hw_queue(hctx, run_queue_async); }
|
blk_mq_insert_request 单个请求入队分流:直通/flush/调度器/纯软件队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
static void blk_mq_insert_request(struct request *rq, blk_insert_t flags) { struct request_queue *q = rq->q; struct blk_mq_ctx *ctx = rq->mq_ctx; struct blk_mq_hw_ctx *hctx = rq->mq_hctx;
if (blk_rq_is_passthrough(rq)) { blk_mq_request_bypass_insert(rq, flags); } else if (req_op(rq) == REQ_OP_FLUSH) { blk_mq_request_bypass_insert(rq, BLK_MQ_INSERT_AT_HEAD); } else if (q->elevator) { LIST_HEAD(list);
WARN_ON_ONCE(rq->tag != BLK_MQ_NO_TAG);
list_add(&rq->queuelist, &list); q->elevator->type->ops.insert_requests(hctx, &list, flags); } else { trace_block_rq_insert(rq);
spin_lock(&ctx->lock); if (flags & BLK_MQ_INSERT_AT_HEAD) list_add(&rq->queuelist, &ctx->rq_lists[hctx->type]); else list_add_tail(&rq->queuelist, &ctx->rq_lists[hctx->type]); blk_mq_hctx_mark_pending(hctx, ctx); spin_unlock(&ctx->lock); } }
|
__blk_mq_issue_directly blk_mq_get_budget_and_tag blk_mq_try_issue_directly blk_mq_request_issue_directly blk_mq_issue_direct 直发下发与资源退避重排路径梳理
作用与实现要点
- 以
queue_rq() 的返回码驱动一个小型状态机:BLK_STS_OK 表示请求已被驱动接收;*_RESOURCE 表示驱动/设备侧资源不足,需要把请求退回到可重试的队列并把 dispatch_busy 置忙,降低后续“继续直发”的概率;其他错误则直接结束请求并把 dispatch_busy 复位为不忙。
- 直发路径在真正调用驱动前先做两层门控:硬件队列停止或队列 quiesce 时直接回落到常规入队;需要走调度器(
RQF_USE_SCHED)或拿不到 budget/tag 时也回落入队,避免在资源/策略不满足时硬顶直发。
- budget 与 driver tag 的获取是成对资源:budget 先拿到就要写回到 request,随后若拿不到 tag 必须立刻归还 budget,避免“预算被占用但请求没法下发”的隐性泄漏。
- 批量直发时用
last 与显式 blk_mq_commit_rqs() 控制提交边界:当请求列表跨多个 hctx 或中途因资源不足提前退出时,需要对“已经被驱动接收但尚未提交”的那一段做一次 commit,避免驱动侧因为等不到“最后一个”而延迟提交。
__blk_mq_issue_directly 直接调用驱动 queue_rq 并按返回码处置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
static blk_status_t __blk_mq_issue_directly(struct blk_mq_hw_ctx *hctx, struct request *rq, bool last) { struct request_queue *q = rq->q; struct blk_mq_queue_data bd = { .rq = rq, .last = last, }; blk_status_t ret;
ret = q->mq_ops->queue_rq(hctx, &bd); switch (ret) { case BLK_STS_OK: blk_mq_update_dispatch_busy(hctx, false); break; case BLK_STS_RESOURCE: case BLK_STS_DEV_RESOURCE: blk_mq_update_dispatch_busy(hctx, true); __blk_mq_requeue_request(rq); break; default: blk_mq_update_dispatch_busy(hctx, false); break; }
return ret; }
|
blk_mq_get_budget_and_tag 申请派发预算并获取驱动 tag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
static bool blk_mq_get_budget_and_tag(struct request *rq) { int budget_token;
budget_token = blk_mq_get_dispatch_budget(rq->q); if (budget_token < 0) return false; blk_mq_set_rq_budget_token(rq, budget_token); if (!blk_mq_get_driver_tag(rq)) { blk_mq_put_dispatch_budget(rq->q, budget_token); return false; } return true; }
|
blk_mq_try_issue_directly 尝试直发单个请求,失败则回落入队或高优先级重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
static void blk_mq_try_issue_directly(struct blk_mq_hw_ctx *hctx, struct request *rq) { blk_status_t ret;
if (blk_mq_hctx_stopped(hctx) || blk_queue_quiesced(rq->q)) { blk_mq_insert_request(rq, 0); blk_mq_run_hw_queue(hctx, false); return; }
if ((rq->rq_flags & RQF_USE_SCHED) || !blk_mq_get_budget_and_tag(rq)) { blk_mq_insert_request(rq, 0); blk_mq_run_hw_queue(hctx, rq->cmd_flags & REQ_NOWAIT); return; }
ret = __blk_mq_issue_directly(hctx, rq, true); switch (ret) { case BLK_STS_OK: break; case BLK_STS_RESOURCE: case BLK_STS_DEV_RESOURCE: blk_mq_request_bypass_insert(rq, 0); blk_mq_run_hw_queue(hctx, false); break; default: blk_mq_end_request(rq, ret); break; } }
|
blk_mq_request_issue_directly 作为批量直发子过程的“可返回状态”版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
static blk_status_t blk_mq_request_issue_directly(struct request *rq, bool last) { struct blk_mq_hw_ctx *hctx = rq->mq_hctx;
if (blk_mq_hctx_stopped(hctx) || blk_queue_quiesced(rq->q)) { blk_mq_insert_request(rq, 0); blk_mq_run_hw_queue(hctx, false); return BLK_STS_OK; }
if (!blk_mq_get_budget_and_tag(rq)) return BLK_STS_RESOURCE; return __blk_mq_issue_directly(hctx, rq, last); }
|
blk_mq_issue_direct 批量直发:按 hctx 分段提交,资源不足时退避并保证已排队请求被 commit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
static void blk_mq_issue_direct(struct rq_list *rqs) { struct blk_mq_hw_ctx *hctx = NULL; struct request *rq; int queued = 0; blk_status_t ret = BLK_STS_OK;
while ((rq = rq_list_pop(rqs))) { bool last = rq_list_empty(rqs);
if (hctx != rq->mq_hctx) { if (hctx) { blk_mq_commit_rqs(hctx, queued, false); queued = 0; } hctx = rq->mq_hctx; }
ret = blk_mq_request_issue_directly(rq, last); switch (ret) { case BLK_STS_OK: queued++; break; case BLK_STS_RESOURCE: case BLK_STS_DEV_RESOURCE: blk_mq_request_bypass_insert(rq, 0); blk_mq_run_hw_queue(hctx, false); goto out; default: blk_mq_end_request(rq, ret); break; } }
out: if (ret != BLK_STS_OK) blk_mq_commit_rqs(hctx, queued, false); }
|
blk_mq_update_dispatch_busy 派发忙碌度的EWMA平滑更新
作用与实现要点
- 用一个“只递减得快、递增得慢”的平滑值
hctx->dispatch_busy 表示硬件队列最近一段时间的忙碌程度,让派发层在“是否继续直发/是否该保守派发”这类分支上有一个稳定的参考,而不是被单次 busy 抖动牵着走。
- 计算本质是固定点 EWMA:先把历史值按
WEIGHT-1 衰减,再在 busy 为真时加上一个放大后的“1”,最后除以 WEIGHT 做归一化。FACTOR 把“1 次忙”放大成 1<<FACTOR,避免整数除法把小值过早舍入为 0,导致忙碌迹象一下子消失。
if (!ewma && !busy) return; 让“已经是 0 且本轮也不忙”的路径不写回,既减少写热点,也避免在低负载时把这个值反复触碰造成无意义抖动。
blk_mq_update_dispatch_busy 更新硬件队列的派发忙碌度平滑值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #define BLK_MQ_DISPATCH_BUSY_EWMA_WEIGHT 8 #define BLK_MQ_DISPATCH_BUSY_EWMA_FACTOR 4
static void blk_mq_update_dispatch_busy(struct blk_mq_hw_ctx *hctx, bool busy) { unsigned int ewma;
ewma = hctx->dispatch_busy;
if (!ewma && !busy) return;
ewma *= BLK_MQ_DISPATCH_BUSY_EWMA_WEIGHT - 1; if (busy) ewma += 1 << BLK_MQ_DISPATCH_BUSY_EWMA_FACTOR; ewma /= BLK_MQ_DISPATCH_BUSY_EWMA_WEIGHT;
hctx->dispatch_busy = ewma; }
|
__blk_mq_requeue_request blk_mq_requeue_request blk_mq_requeue_work 请求回退重入队与延迟重派发路径梳理
作用与实现要点
- 回退分两层:
__blk_mq_requeue_request() 只做“把这个 request 从驱动直发资源里撤出来,并把可重试所需的状态清干净”;blk_mq_requeue_request() 再把它挂进 q->requeue_list,把真正的重新插入动作交给后台 work 批量处理,减少在失败现场持锁与做复杂分流的成本。
- tag 与状态位的配平在回退入口处完成:先
blk_mq_put_driver_tag() 释放驱动 tag,避免资源不足时继续占用并发额度;若请求已经开始,则把 rq->state 改回 MQ_RQ_IDLE 并清掉 RQF_TIMED_OUT,让后续重新派发不会被“超时遗留状态”误导。
- 统一通过
requeue_lock 保护 requeue_list/flush_list:回退可能来自多种上下文,使用 spin_lock_irqsave/irqrestore 把并发与中断打断都挡住,保证链表不会被同时改写。
blk_mq_requeue_work() 把共享链表一次性 list_splice_init() 到本地链表再处理,缩短持锁时间;随后根据 RQF_DONTPREP 分流:已经被驱动启动、可能携带驱动私有数据的请求,直接进 hctx->dispatch 避免块层合并/重准备破坏其状态;普通请求则走 blk_mq_insert_request(...AT_HEAD),提高重试请求被尽快重新派发的机会。
kick_requeue_list 是一次性推进开关:调用方可以选择“只排队不立刻触发 work”来合并抖动,或在需要尽快重试时立即 kick,让后台 work 及时把请求重新送回派发路径。
平台关注:单核、无 MMU、ARMv7-M(STM32H750)
- 语义上仍是“回退时释放直发资源并清理状态,再由后台批量重新入队并触发派发”。单核下
spin_lock_irqsave() 往往退化为关中断的临界区,它在这里的意义不只是互斥,还用于避免中断上下文同时触碰 requeue_list/flush_list 导致链表损坏。
- 这段逻辑强依赖后台 work 机制:如果移植环境没有 workqueue/delayed_work,需要提供等价的异步执行点来消费
requeue_list,否则请求会一直挂在队列里无法被重新插入与重派发。
__blk_mq_requeue_request 回退前的资源与状态清理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
static void __blk_mq_requeue_request(struct request *rq) { struct request_queue *q = rq->q;
blk_mq_put_driver_tag(rq);
trace_block_rq_requeue(rq); rq_qos_requeue(q, rq);
if (blk_mq_request_started(rq)) { WRITE_ONCE(rq->state, MQ_RQ_IDLE); rq->rq_flags &= ~RQF_TIMED_OUT; } }
|
blk_mq_requeue_request 将请求挂入队列的回退链表并按需触发后台处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
void blk_mq_requeue_request(struct request *rq, bool kick_requeue_list) { struct request_queue *q = rq->q; unsigned long flags;
__blk_mq_requeue_request(rq);
blk_mq_sched_requeue_request(rq);
spin_lock_irqsave(&q->requeue_lock, flags); list_add_tail(&rq->queuelist, &q->requeue_list); spin_unlock_irqrestore(&q->requeue_lock, flags);
if (kick_requeue_list) blk_mq_kick_requeue_list(q); } EXPORT_SYMBOL(blk_mq_requeue_request);
|
blk_mq_requeue_work 后台批量消费回退链表并重新插入请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
static void blk_mq_requeue_work(struct work_struct *work) { struct request_queue *q = container_of(work, struct request_queue, requeue_work.work); LIST_HEAD(rq_list); LIST_HEAD(flush_list); struct request *rq;
spin_lock_irq(&q->requeue_lock); list_splice_init(&q->requeue_list, &rq_list); list_splice_init(&q->flush_list, &flush_list); spin_unlock_irq(&q->requeue_lock);
while (!list_empty(&rq_list)) { rq = list_entry(rq_list.next, struct request, queuelist); list_del_init(&rq->queuelist);
if (rq->rq_flags & RQF_DONTPREP) blk_mq_request_bypass_insert(rq, 0); else blk_mq_insert_request(rq, BLK_MQ_INSERT_AT_HEAD); }
while (!list_empty(&flush_list)) { rq = list_entry(flush_list.next, struct request, queuelist); list_del_init(&rq->queuelist); blk_mq_insert_request(rq, 0); }
blk_mq_run_hw_queues(q, false); }
|
blk_mq_hctx_has_pending blk_mq_hctx_mark_pending blk_mq_hctx_clear_pending 硬件队列待处理状态判定与位图维护
作用与实现要点
这三段代码不负责真正派发请求,而是把“这个 hctx 现在是否还值得继续 run”压缩成一个很便宜的就绪判断:一类来源是 hctx->dispatch 里已经准备好但上次没能继续下发的请求,一类来源是 ctx_map 里被标成有活要干的 software queue,还有一类来源是调度器私有队列。外围 run queue 逻辑会先看队列是否处在不适合运行的状态,再消费这个判断结果,所以这里本质上是待处理状态的聚合与维护,不直接推进状态机。ctx_map 在文档里就是“每个 software queue 一位,只要该位为 1 就表示这个 software queue 里仍有待处理请求”;dispatch 则是“已经准备派发、但因资源等原因暂时没能送进硬件的请求链表”。([Linux内核文档][1])
blk_mq_hctx_mark_pending() 与 blk_mq_hctx_clear_pending() 围绕 ctx->index_hw[hctx->type] 这一槽位做配对维护,表示“这个特定 ctx 在这个特定队列类型对应的 hctx 上是否仍有请求可取”。先测位再置位,目的是避免重复写同一位;清位则把这个 software queue 从后续扫描候选里移除。这个片段没有直接绑定一次性回调,也没有直接提交 worker、hrtimer 或 trace,它只把后台运行路径需要消费的可运行性摘要维护好。([Linux内核文档][1])
blk_mq_hctx_has_pending 判断硬件队列是否还有待处理来源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
static bool blk_mq_hctx_has_pending(struct blk_mq_hw_ctx *hctx) { return !list_empty_careful(&hctx->dispatch) || sbitmap_any_bit_set(&hctx->ctx_map) || blk_mq_sched_has_work(hctx); }
|
blk_mq_hctx_mark_pending 标记某个软件队列在该硬件队列下仍有请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
static void blk_mq_hctx_mark_pending(struct blk_mq_hw_ctx *hctx, struct blk_mq_ctx *ctx) { const int bit = ctx->index_hw[hctx->type];
if (!sbitmap_test_bit(&hctx->ctx_map, bit)) sbitmap_set_bit(&hctx->ctx_map, bit); }
|
blk_mq_hctx_clear_pending 清除某个软件队列在该硬件队列下的待处理标记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
static void blk_mq_hctx_clear_pending(struct blk_mq_hw_ctx *hctx, struct blk_mq_ctx *ctx) { const int bit = ctx->index_hw[hctx->type];
sbitmap_clear_bit(&hctx->ctx_map, bit); }
|
blk_mq_run_hw_queues blk_mq_delay_run_hw_queues blk_mq_stop_hw_queue blk_mq_stop_hw_queues blk_mq_start_hw_queue blk_mq_start_hw_queues blk_mq_start_stopped_hw_queue blk_mq_start_stopped_hw_queues 硬件队列批量运行、停止与恢复路径梳理
作用与实现要点
这一组函数把 request_queue 级别的“让哪些 hctx 去跑”与单个 hctx 的“停止/恢复”拆开处理。blk_mq_run_hw_queues() 和 blk_mq_delay_run_hw_queues() 先遍历全部硬件队列,再结合 I/O 调度器是否偏向单个 sq_hctx 来决定哪些队列值得触发;若某个 hctx 的 dispatch 链表里已有绕过调度器或上次没能下发完的请求,即使它不是调度器偏好的那一个,也必须单独补跑,避免这些请求长期挂在硬件派发层。Linux blk-mq 文档把 dispatch 定义为“已准备好发往硬件、但因资源等原因暂时没能发送的请求链表”,并说明 run_work 专门用于稍后再运行硬件队列。([内核文档][1])
停止与恢复路径只改 BLK_MQ_S_STOPPED 状态位,不负责把正在路上的派发彻底排空。blk_mq_stop_hw_queue()/blk_mq_stop_hw_queues() 会先取消尚未执行的 run_work,再置位 stopped;源码注释明确说明,调用返回后并不保证 dispatch 已经被排空或彻底阻塞,若调用方需要“等到派发完全静止”这一更强语义,应走 blk_mq_quiesce_queue()。对应地,blk_mq_start_hw_queue()/blk_mq_start_hw_queues() 清掉 stopped 后直接重新 run;blk_mq_start_stopped_hw_queue() 只在确认之前确实是 stopped 时才恢复,并在清位后补一个与 blk_mq_hctx_stopped() 配对的内存屏障,保证后续检查 dispatch 时不会因为可见性顺序错位而漏掉刚准备派发的请求。([codebrowser.dev][2])
能力门控体现在三处:一是 blk_mq_hctx_stopped() 直接拦掉已停队列,二是 blk_queue_sq_sched() 打开后只优先让单队列感知较弱的调度器偏好的 sq_hctx 出队,减少多个硬件队列同时碰同一调度器内部锁带来的缓存抖动与锁竞争,三是 BLK_MQ_F_BLOCKING 会把恢复路径导向可异步执行的 run 方式,避免在当前调用路径里硬跑一个允许阻塞的硬件队列。([codebrowser.dev][2])
平台关注:单核、无 MMU、ARMv7-M(STM32H750)
这段逻辑在语义上不因单核或无 MMU 而改变:BLK_MQ_S_STOPPED 仍然决定某个 hctx 是否允许被 run,run_work 仍然表示“稍后再跑”的后台机制,dispatch 仍然是驱动暂时吃不下请求时的补发链表。需要注意的是,blk_mq_start_stopped_hw_queue() 里的屏障不是为了“只有多核才需要”,而是为了和 blk_mq_hctx_stopped() 中的屏障配对,保证清掉 stopped 之后,后续代码观察 stopped 位与 dispatch 链表时不会出现漏看请求的顺序问题;在单核实现上它可能退化得更轻,但这层顺序语义不能省。无 MMU 也不改变这里的状态位、工作队列和延迟执行语义,真正决定行为差异的仍是底层原子位操作、workqueue/定时机制以及驱动的 .queue_rq() 实现。([codebrowser.dev][3])
blk_mq_run_hw_queues 批量触发请求队列中的硬件队列运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
void blk_mq_run_hw_queues(struct request_queue *q, bool async) { struct blk_mq_hw_ctx *hctx, *sq_hctx; unsigned long i;
sq_hctx = NULL; if (blk_queue_sq_sched(q)) sq_hctx = blk_mq_get_sq_hctx(q); queue_for_each_hw_ctx(q, hctx, i) { if (blk_mq_hctx_stopped(hctx)) continue; if (!sq_hctx || sq_hctx == hctx || !list_empty_careful(&hctx->dispatch)) blk_mq_run_hw_queue(hctx, async); } } EXPORT_SYMBOL(blk_mq_run_hw_queues);
|
blk_mq_delay_run_hw_queues 延迟异步触发全部硬件队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
void blk_mq_delay_run_hw_queues(struct request_queue *q, unsigned long msecs) { struct blk_mq_hw_ctx *hctx, *sq_hctx; unsigned long i;
sq_hctx = NULL; if (blk_queue_sq_sched(q)) sq_hctx = blk_mq_get_sq_hctx(q); queue_for_each_hw_ctx(q, hctx, i) { if (blk_mq_hctx_stopped(hctx)) continue; if (delayed_work_pending(&hctx->run_work)) continue; if (!sq_hctx || sq_hctx == hctx || !list_empty_careful(&hctx->dispatch)) blk_mq_delay_run_hw_queue(hctx, msecs); } } EXPORT_SYMBOL(blk_mq_delay_run_hw_queues);
|
blk_mq_stop_hw_queue 停止单个硬件队列后续运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
void blk_mq_stop_hw_queue(struct blk_mq_hw_ctx *hctx) { cancel_delayed_work(&hctx->run_work);
set_bit(BLK_MQ_S_STOPPED, &hctx->state); } EXPORT_SYMBOL(blk_mq_stop_hw_queue);
|
blk_mq_stop_hw_queues 停止请求队列下全部硬件队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
void blk_mq_stop_hw_queues(struct request_queue *q) { struct blk_mq_hw_ctx *hctx; unsigned long i;
queue_for_each_hw_ctx(q, hctx, i) blk_mq_stop_hw_queue(hctx); } EXPORT_SYMBOL(blk_mq_stop_hw_queues);
|
blk_mq_start_hw_queue 启动单个硬件队列并立即尝试恢复派发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
void blk_mq_start_hw_queue(struct blk_mq_hw_ctx *hctx) { clear_bit(BLK_MQ_S_STOPPED, &hctx->state);
blk_mq_run_hw_queue(hctx, hctx->flags & BLK_MQ_F_BLOCKING); } EXPORT_SYMBOL(blk_mq_start_hw_queue);
|
blk_mq_start_hw_queues 启动请求队列下全部硬件队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
void blk_mq_start_hw_queues(struct request_queue *q) { struct blk_mq_hw_ctx *hctx; unsigned long i;
queue_for_each_hw_ctx(q, hctx, i) blk_mq_start_hw_queue(hctx); } EXPORT_SYMBOL(blk_mq_start_hw_queues);
|
blk_mq_start_stopped_hw_queue 仅对已停止的硬件队列做有序恢复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
void blk_mq_start_stopped_hw_queue(struct blk_mq_hw_ctx *hctx, bool async) { if (!blk_mq_hctx_stopped(hctx)) return;
clear_bit(BLK_MQ_S_STOPPED, &hctx->state);
smp_mb__after_atomic(); blk_mq_run_hw_queue(hctx, async); } EXPORT_SYMBOL_GPL(blk_mq_start_stopped_hw_queue);
|
blk_mq_start_stopped_hw_queues 批量恢复已停止的硬件队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
void blk_mq_start_stopped_hw_queues(struct request_queue *q, bool async) { struct blk_mq_hw_ctx *hctx; unsigned long i;
queue_for_each_hw_ctx(q, hctx, i) blk_mq_start_stopped_hw_queue(hctx, async || (hctx->flags & BLK_MQ_F_BLOCKING)); } EXPORT_SYMBOL(blk_mq_start_stopped_hw_queues);
|
blk_mq_hctx_stopped blk_mq_delay_run_hw_queue blk_mq_hw_queue_need_run blk_mq_run_hw_queue 停止位检查、延迟调度与运行入口梳理
作用与实现要点
这一组代码把“这个硬件队列现在能不能跑、需不需要跑、应该立刻跑还是稍后跑”拆成四层。blk_mq_hctx_stopped() 只回答 stopped 位当前是否还生效,但它不是单纯读一位就结束,而是和恢复路径里的屏障配对,保证“清掉停止位”和“观察 dispatch 是否已有请求”之间不会错序;blk_mq_hw_queue_need_run() 则把 quiesce 状态与待处理来源合并成一次可运行性判断,避免在队列冻结、切换调度器或调整硬件队列数量时继续触碰不安全的派发状态;blk_mq_run_hw_queue() 再根据调用上下文决定当前 CPU 直接跑,还是交给 run_work 异步补跑;blk_mq_delay_run_hw_queue() 是最薄的一层,只负责在队列未停止时把这次运行挂到稍后的 kblockd 工作项里。主线文档对 dispatch、state、run_work 的定义与这里的职责划分是一致的。([codebrowser.dev][1])
这里真正的分支选择重点不在“有没有请求”本身,而在“此刻检查这些请求是否安全”。blk_mq_hw_queue_need_run() 先借 __blk_mq_run_dispatch_ops() 包住判断,再在 blk_queue_quiesced() 为假时才去看 blk_mq_hctx_has_pending(),就是为了避开 quiesce 期间的并发改动;blk_mq_run_hw_queue() 第一次无锁判断若发现不用跑,还会再拿一次 queue_lock 复查,是为了和 blk_mq_unquiesce_queue() 衔接,避免刚好错过“解除 quiesce 后应当重跑”的那个时点。同步运行还有两层门控:中断上下文里不允许走非异步直跑,BLK_MQ_F_BLOCKING 置位时也要承认本次运行可能睡眠。([codebrowser.dev][2])
blk_mq_hctx_stopped 判断硬件队列是否仍处于停止状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
static inline bool blk_mq_hctx_stopped(struct blk_mq_hw_ctx *hctx) { if (likely(!test_bit(BLK_MQ_S_STOPPED, &hctx->state))) return false;
smp_mb();
return test_bit(BLK_MQ_S_STOPPED, &hctx->state); }
|
blk_mq_delay_run_hw_queue 延迟安排单个硬件队列异步运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
void blk_mq_delay_run_hw_queue(struct blk_mq_hw_ctx *hctx, unsigned long msecs) { if (unlikely(blk_mq_hctx_stopped(hctx))) return; kblockd_mod_delayed_work_on(blk_mq_hctx_next_cpu(hctx), &hctx->run_work, msecs_to_jiffies(msecs)); } EXPORT_SYMBOL(blk_mq_delay_run_hw_queue);
|
blk_mq_hw_queue_need_run 判断硬件队列此刻是否值得进入运行路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
static inline bool blk_mq_hw_queue_need_run(struct blk_mq_hw_ctx *hctx) { bool need_run;
__blk_mq_run_dispatch_ops(hctx->queue, false, need_run = !blk_queue_quiesced(hctx->queue) && blk_mq_hctx_has_pending(hctx)); return need_run; }
|
blk_mq_run_hw_queue 触发单个硬件队列运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
void blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx, bool async) { bool need_run;
WARN_ON_ONCE(!async && in_interrupt());
might_sleep_if(!async && hctx->flags & BLK_MQ_F_BLOCKING);
need_run = blk_mq_hw_queue_need_run(hctx); if (!need_run) { unsigned long flags;
spin_lock_irqsave(&hctx->queue->queue_lock, flags); need_run = blk_mq_hw_queue_need_run(hctx); spin_unlock_irqrestore(&hctx->queue->queue_lock, flags);
if (!need_run) return; }
if (async || !cpumask_test_cpu(raw_smp_processor_id(), hctx->cpumask)) { blk_mq_delay_run_hw_queue(hctx, 0); return; }
blk_mq_run_dispatch_ops(hctx->queue, blk_mq_sched_dispatch_requests(hctx)); } EXPORT_SYMBOL(blk_mq_run_hw_queue);
|