[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的核心是一个两级队列架构,旨在最大限度地减少锁争用并利用硬件并行性。

  1. 第一级:软件暂存队列 (Software Staging Queues)

    • blk-mq为系统中的每个CPU核心都创建一个独立的软件队列(struct blk_mq_ctx)。
    • 当一个进程在某个CPU上发起I/O时,请求(bio)会被提交到该CPU专属的软件队列中。
    • 由于每个CPU操作自己的队列,因此在提交路径上几乎没有锁争用,完美地解决了传统单队列模型的最大瓶颈。
  2. 第二级:硬件分派队列 (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 通过以下设计解决了这个问题:

  1. 消除单一队列锁:用多个无锁或几乎无锁的队列取代单一的全局锁队列。
  2. 提升扩展性:使 I/O 吞吐能力能够随着 CPU 核心数和硬件队列数的增加而有效提升。
  3. 适应现代硬件:为 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_tagsstruct 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 中的生命周期

  1. 提交阶段 (Submission):

    • 上层代码(如文件系统)调用 submit_bio() 来提交一个 bio
    • blk_mq_submit_bio() 被调用。它会获取当前 CPU 对应的 blk_mq_ctx
    • 系统从 blk_mq_tags 中分配一个 request 结构体和一个唯一的标签。
    • bio 的信息被填充到 request 中。
    • request 被放入当前 CPU 的 blk_mq_ctx 软件队列中。
  2. 分发阶段 (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 的标签一同发送给存储设备。
  3. 完成阶段 (Completion):

    • 存储硬件完成 I/O 操作,并触发一个中断。
    • 驱动程序的中断处理函数被执行。它从硬件获取已完成命令的标签。
    • 驱动程序使用标签调用 blk_mq_complete_request()blk_mq_end_request() 来通知 blk-mq 框架。
    • blk-mq 使用标签快速找到对应的 request 结构体。
    • blk-mq 执行收尾工作:释放标签,调用 biobi_end_io 回调函数,最终释放 request 结构体。

五、 关键源码函数分析

  • blk_mq_init_queue(struct blk_mq_tag_set *set):
    blk-mq 模式下创建和初始化一个 request_queue 的核心函数。它负责分配和设置 blk_mq_ctxblk_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. 软中断下半部):

这是一个经典的、为了最大化系统响应性和吞吐量而设计的两阶段中断处理模型:

  1. 上半部 (Top Half - The Hardware Interrupt): 当块设备(如SD卡控制器)完成一个I/O操作时, 它会触发一个硬件中断。内核的中断处理程序(ISR)会立即执行。这个”上半部”的代码被设计得极其快速和精简。它只做最少的工作: 找到是哪个request完成了, 然后调用llist_add将其**无锁地(lock-lessly)**推入当前CPU核心的完成链表blk_cpu_done中, 最后触发一个BLOCK_SOFTIRQ软中断。完成这些后, 它就立即退出, 让CPU可以去响应其他更重要的中断。
  2. 下半部 (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
/*
* __latent_entropy 属性是一个给内核随机数生成器(RNG)的提示.
* 它表明这个函数的执行时机(软中断的触发时间)与硬件事件(I/O完成)相关,
* 具有一定的不可预测性. 内核的RNG可以利用这些时间戳的抖动作为熵(entropy)的来源之一,
* 来提高随机数的质量.
*/
static __latent_entropy void blk_done_softirq(void)
{
/*
* 调用核心处理函数 blk_complete_reqs.
* this_cpu_ptr(&blk_cpu_done) 是一个高效的宏,
* 用于获取当前正在执行的CPU核心的 blk_cpu_done 链表头的指针.
* 这确保了每个CPU只处理自己核上的完成列表, 实现了完美的扩展性.
*/
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
/*
* blk_complete_reqs: 处理一个完成链表中的所有请求.
* @list: 指向一个CPU的 blk_cpu_done 完成链表的头.
*/
static void blk_complete_reqs(struct llist_head *list)
{
/*
* 这一行是整个函数最高效、最关键的部分, 包含两个原子操作:
* 1. llist_del_all(list):
* 这是一个无锁链表操作, 它会原子地将 list 指向的整个链表"摘下",
* 并返回一个指向被摘下链表的第一个节点的指针. list 本身则被原子地置为空.
* 这个操作极快, 它使得硬件中断的"上半部"可以立即开始向这个(现在为空的)
* 链表中添加新的已完成请求, 而不会与我们接下来要做的处理发生任何冲突.
*
* 2. llist_reverse_order(...):
* 无锁链表(llist)本质上是一个LIFO(后进先出)的栈. 这意味着最新完成的请求
* 位于链表头. 然而, 为了公平起见, I/O请求应该尽可能按照FIFO(先进先出)
* 的顺序来完成. 这个函数会高效地将摘下的LIFO链表反转成FIFO顺序.
*
* 最终, 'entry' 指向一个与全局完成列表完全分离的、按FIFO顺序排列的本地请求链表.
*/
struct llist_node *entry = llist_reverse_order(llist_del_all(list));
struct request *rq, *next;

/*
* 使用 llist_for_each_entry_safe 宏来安全地遍历我们本地的请求链表.
*/
llist_for_each_entry_safe(rq, next, entry, ipi_list)
/*
* 这是最终的完成分派(dispatch)步骤:
* - rq->q: 获取该请求所属的请求队列(request_queue).
* - ->mq_ops: 获取该队列的多队列操作函数表.
* - ->complete(rq): 调用由块设备驱动程序(如SD/MMC驱动, NVMe驱动)
* 在初始化时注册的 specific complete 函数.
*
* 这个 `complete` 函数会执行所有最终的清理工作, 例如:
* - 将数据从DMA缓冲区复制到最终的内存位置.
* - 更新请求的状态, 报告成功或失败.
* - 唤醒正在睡眠等待此I/O完成的进程.
* - 释放 `request` 结构体本身.
*/
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设备驱动程序铺平了道路。

关键机制初始化:

  1. 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”链表中的所有请求, 包括唤醒等待的进程、释放资源等。这种”中断上半部/下半部”的机制确保了硬件中断的路径尽可能短, 提高了系统响应性。
  2. 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
/*
* __init 宏表示这是一个仅在内核启动时执行的初始化函数.
*/
static int __init blk_mq_init(void)
{
int i;

/*
* 遍历系统中所有"可能"存在的CPU (由NR_CPUS配置决定).
* 在单核系统上, 这个循环只会执行一次 (i=0).
*/
for_each_possible_cpu(i)
/*
* 为每个CPU的 blk_cpu_done 变量初始化一个无锁链表(llist)的头.
* 这个链表用于暂存已由硬件完成, 等待软中断处理的I/O请求.
*/
init_llist_head(&per_cpu(blk_cpu_done, i));
for_each_possible_cpu(i)
/*
* 为每个CPU初始化一个"Call Single Data"(CSD)结构.
* 当需要在一个远程CPU上完成请求时, 会调用 __blk_mq_complete_request_remote 函数.
* 在单核系统上, 这个机制不会被使用.
*/
INIT_CSD(&per_cpu(blk_cpu_csd, i),
__blk_mq_complete_request_remote, NULL);
/*
* 注册块设备层的软中断(softirq)处理函数.
* 当硬件中断完成后, 会触发 BLOCK_SOFTIRQ, 内核稍后会调用 blk_done_softirq.
* blk_done_softirq 会处理 blk_cpu_done 链表中的请求. 这是I/O完成的核心机制.
*/
open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);

/*
* 注册一个CPU热插拔(hotplug)状态的回调.
* 当一个CPU被宣告"死亡"(dead)且不可恢复时, blk_softirq_cpu_dead 会被调用,
* 用于清理与该CPU相关的软中断资源. 在单核系统上不会被调用.
*/
cpuhp_setup_state_nocalls(CPUHP_BLOCK_SOFTIRQ_DEAD,
"block/softirq:dead", NULL,
blk_softirq_cpu_dead);
/*
* 注册CPU"死亡"状态的多实例回调.
* 当CPU死亡时, blk_mq_hctx_notify_dead 会被调用, 通知所有 blk-mq 驱动
* 去清理与该CPU相关的硬件上下文(hctx)队列.
*/
cpuhp_setup_state_multi(CPUHP_BLK_MQ_DEAD, "block/mq:dead", NULL,
blk_mq_hctx_notify_dead);
/*
* 注册CPU"上线/下线"状态的多实例回调.
* 当CPU上线时, blk_mq_hctx_notify_online 会被调用, 以激活该CPU的硬件队列.
* 当CPU下线时, blk_mq_hctx_notify_offline 会被调用, 以停用并迁移该CPU的队列.
*/
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() 将此函数注册为在内核启动的"子系统初始化"阶段被调用.
* 这个阶段确保了CPU和中断子系统已经就绪.
*/
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
/**
* blk_mq_rq_ctx_init - 初始化一个 request(关联 q/ctx/hctx、标签、标志位与调度器状态)
* @data: 分配期上下文(包含 q/ctx/hctx、cmd_flags、rq_flags 等)
* @tags: tag 集合
* @tag: 分配到的 tag
*
* Return: 初始化完成的 request 指针
*/
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; /* 能力/路径门控: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); /* 关键行:并发可见性,避免读取到脏 deadline */
req_ref_set(rq, 1); /* 关键行:请求对象基础引用计数 */

if (rq->rq_flags & RQF_USE_SCHED) { /* 能力门控:启用 I/O scheduler 的扩展字段初始化 */
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
/**
* __blk_mq_alloc_requests_batch - 批量分配多个 request 并压入 cached_rqs
* @data: 分配期上下文(包含 nr_tags、cached_rqs 等)
*
* Return: 弹出并返回一个 request;若完全失败则返回 NULL
*/
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); /* 分支策略:尽可能一次获取多个 tag */
if (unlikely(!tag_mask)) { /* 分支策略:本轮无 tag;若一个都未拿到则直接失败 */
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); /* 关键行:非调度器标签时维护 hctx 活跃计数 */

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
/**
* __blk_mq_alloc_requests - 为队列分配 request(支持批量与单个、支持 NOWAIT 与重试)
* @data: 分配期上下文(输入/输出 ctx/hctx、rq_flags、nr_tags 等)
*
* Return: 成功返回 request;失败在 NOWAIT 场景返回 NULL,等待场景内部重试直到拿到 tag
*/
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; /* 能力门控: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)) { /* 分支策略:flush/passthrough 走直达路径,不进入调度器 */
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); /* 关键行:无调度器时标记 hctx tag 竞争态 */
}

if (data->flags & BLK_MQ_REQ_RESERVED)
data->rq_flags |= RQF_RESV; /* 能力门控:保留请求标记 */

if (data->nr_tags > 1) { /* 分支策略:尝试批量分配以填充 plug 缓存 */
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) { /* 分支策略:tag 不可用 */
if (data->flags & BLK_MQ_REQ_NOWAIT)
return NULL; /* NOWAIT:不睡眠直接失败 */
msleep(3); /* 关键行:等待/退让,处理 hctx 失活与迁移窗口 */
goto retry;
}

if (!(data->rq_flags & RQF_SCHED_TAGS))
blk_mq_inc_active_requests(data->hctx); /* 关键行:单个请求路径的 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
/**
* blk_mq_rq_cache_fill - 批量分配 request 填充 plug 缓存并返回一个 request
* @q: request_queue
* @plug: 当前任务的 plug 上下文
* @opf: 操作标志(含 REQ_OP 等)
* @flags: blk-mq 分配标志
*
* Return: 成功返回 request;失败返回 NULL(并回退队列进入计数)
*/
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); /* 关键行:失败时配平 enter/exit */
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
/**
* blk_mq_alloc_cached_request - 优先从当前任务 plug 缓存获取 request,必要时触发批量填充
* @q: request_queue
* @opf: 操作标志(含 REQ_OP 等)
* @flags: blk-mq 分配标志
*
* Return: 成功返回 request;无可用缓存或不满足复用条件时返回 NULL
*/
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) /* 分支策略:hctx 类型不一致则不复用 */
return NULL;
if (op_is_flush(rq->cmd_flags) != op_is_flush(opf)) /* 分支策略:flush 语义不一致则不复用 */
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
/**
* blk_mq_alloc_request - 分配一个 request(优先 plug 缓存,否则进入队列并分配 tag)
* @q: request_queue
* @opf: 操作标志(含 REQ_OP 等)
* @flags: blk-mq 分配标志
*
* Return: 成功返回 request;失败返回 ERR_PTR(负错误码)
*/
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); /* 关键行:失败时配平 enter/exit */
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
/**
* @brief 按 (hctx_type, cpu) 在 tag_set 映射表中选择硬件队列
* @param q request queue
* @param type hctx 类型索引
* @param cpu CPU 编号
* @return 对应的硬件队列上下文指针
*/
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])); /* 从 mq_map[cpu] 取队列编号并转换为 hctx */
}

blk_mq_get_hctx_type 从操作与标志推导hctx类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 根据 opf(操作类型与标志)选择 hctx_type
* @param opf 操作类型(REQ_OP_*)与标志(如 REQ_POLLED)
* @return 推导得到的 hctx_type
*/
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) /* 轮询请求优先走 POLL 队列,需队列已启用轮询能力 */
type = HCTX_TYPE_POLL;
else if ((opf & REQ_OP_MASK) == REQ_OP_READ) /* 纯读请求可分流到 READ 队列,其余保持 DEFAULT */
type = HCTX_TYPE_READ;
return type;
}

blk_mq_map_queue 按请求类型在ctx中选择对应硬件队列

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief 根据 opf 在软件 ctx 中选择对应类型的硬件队列
* @param opf 操作类型(REQ_OP_*)与标志(如 REQ_POLLED)
* @param ctx 软件队列上下文
* @return 匹配的硬件队列上下文指针
*/
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)]; /* 用类型索引选择该 ctx 绑定的 hctx */
}

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; /* 同步等待点:end_io 触发 complete() 后,等待方才能继续往下走 */
blk_status_t ret; /* 完成状态回传:由底层结束请求时写入,blk_execute_rq() 最终返回它 */
};

/**
* @brief 同步执行请求时的 end_io 回调:回填状态并唤醒等待者
* @param rq 已完成的 request,要求 rq->end_io_data 指向有效的 struct blk_rq_wait
* @param ret 本次完成的块层状态码,由底层完成路径传入
* @return 返回值语义:不要求通用结束路径额外接管资源释放,由调用者按自身生命周期管理 request
*
* 补充说明:
* - 通过 rq->end_io_data 绑定到栈上 wait 对象,complete() 唤醒 blk_execute_rq() 的等待点
* - wait->ret 的写入与 complete() 的顺序,保证等待方被唤醒后能读到最终状态
*/
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
/**
* @brief 判断一个 request 是否属于 poll 类型硬件队列
* @param rq 待判断的 request,依赖 rq->mq_hctx 及其 type 字段已正确初始化
* @return true 表示该请求绑定在 HCTX_TYPE_POLL 上,可走轮询完成路径;false 则走普通等待
*
* 补充说明:
* - 这是轮询能力的门控点:没有 mq_hctx 或类型不匹配都直接否决,避免误走轮询导致空转
* - 该判断结果会影响 blk_execute_rq() 的等待方式选择(poll vs sleep)
*/
bool blk_rq_is_poll(struct request *rq)
{
if (!rq->mq_hctx) /* 请求尚未绑定硬件上下文时,无法进行 hctx 级轮询,直接走非轮询路径 */
return false;
if (rq->mq_hctx->type != HCTX_TYPE_POLL) /* 仅 poll 类型 hctx 才支持该模型,其他类型轮询收益不成立 */
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
/**
* @brief 在 poll 模型下主动轮询硬件队列,直到对应 completion 变为 done
* @param rq 需要等待完成的 request,要求 blk_rq_is_poll(rq) 为 true
* @param wait 与该 request 绑定的 completion,end_io 回调 complete() 后应变为 done
* @return 无
*
* 补充说明:
* - 每轮只触发一次 blk_hctx_poll(),把“是否继续等待”的判断留给 completion_done()
* - cond_resched() 提供显式让出点,避免轮询期间长期占用 CPU 影响系统其他任务推进
*/
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(); /* 若调度器需要切换任务则让出 CPU,防止轮询等待把系统“卡”在这一处 */
} while (!completion_done(wait)); /* 只要 end_io 还没 complete(),就继续轮询推进 */
}

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
/**
* @brief 把一个已准备好的 passthrough request 插入队列执行,并同步等待其完成
* @param rq 目标 request,要求已完成必要字段初始化且属于 passthrough
* @param at_head 为 true 时插入队列头部以更快被派发;否则按普通顺序插入
* @return 返回底层完成时提供的 blk_status_t 状态
*
* 补充说明:
* - 通过 rq->end_io / rq->end_io_data 进行一次性回调绑定,回调负责写回状态并 complete()
* - 等待方式按 blk_rq_is_poll() 分流:poll 类型主动轮询 + 让出点;非 poll 类型走阻塞等待
* - wait 为栈对象,函数返回前必须等到完成信号到达,避免 end_io 在栈释放后写入造成内存破坏
*/
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)); /* 该接口假设 request 已完全准备好走直通路径,避免调度/合并语义干扰 */

rq->end_io_data = &wait; /* 把栈上等待对象挂到 request 上,供 end_io 回调定位并写回状态 */
rq->end_io = blk_end_sync_rq; /* 绑定一次性完成回调:写 wait->ret 并 complete(&wait.done) */

blk_account_io_start(rq); /* 提交前记录 I/O 统计起点,使后续完成统计能覆盖本次同步执行 */
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); /* poll 场景主动推进完成,配合让出点降低忙等副作用 */
else
blk_wait_io(&wait.done); /* 非 poll 场景阻塞等待完成,同时考虑超长 I/O 与后台监测机制的交互 */

return wait.ret; /* 返回 end_io 回调写回的最终 blk_status_t */
}
EXPORT_SYMBOL(blk_execute_rq);

__blk_mq_run_dispatch_ops blk_mq_run_dispatch_ops 读侧锁选择与派发操作包裹

作用与实现要点

  • 这组宏把“执行一段派发相关的代码块”统一包进读侧保护里:根据 tag_set 是否允许阻塞,自动选择 SRCURCU,让 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
/**
* @brief 根据队列 tag_set 的阻塞能力选择 SRCU/RCU 读锁,并在锁内执行 dispatch_ops
* @param q 目标 request_queue,用于读取 q->tag_set->flags 与 q->tag_set->srcu
* @param check_sleep 为真时允许对 dispatch_ops 中潜在睡眠点做运行期标注检查
* @return 无
*
* 补充说明:
* - 当 tag_set 可能走到会睡眠的路径时用 SRCU,避免在读侧临界区内睡眠破坏 RCU 语义
* - dispatch_ops 在锁内执行,确保其读取到的共享对象不会在并发回收/替换中变成悬挂指针
*/
#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
/**
* @brief 用默认策略运行 dispatch_ops:进入 __blk_mq_run_dispatch_ops 且开启睡眠点标注检查
* @param q 目标 request_queue
* @param dispatch_ops 需要在读侧锁内执行的派发代码块
* @return 无
*
* 补充说明:
* - 统一把 check_sleep 设为 true,调用点若明确“这段不会睡”可改用底层宏并传 false
* - 该宏本身不改变 dispatch_ops 的逻辑,只负责把它放进合适的读侧保护里
*/
#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
/**
* @brief 绕过调度器与软队列,把请求直接插入 hctx->dispatch 等待派发
* @param rq 待插入的 request,要求 rq->mq_hctx 已正确指向目标硬件队列
* @param flags 插入行为标志(如是否插入到 dispatch 头部)
* @return 无
*
* 补充说明:
* - dispatch 链表受 hctx->lock 保护,插入时需要互斥以避免并发派发/并发插入破坏链表
* - 头插/尾插决定该请求在 dispatch 中的相对优先级,影响后续派发时的先后顺序
*/
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); /* 保护 dispatch 链表结构与相关状态,避免与派发侧并发操作导致链表损坏 */
if (flags & BLK_MQ_INSERT_AT_HEAD)
list_add(&rq->queuelist, &hctx->dispatch); /* 需要更快被派发时头插,减少在 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
/**
* @brief 把一批 request 按 hctx 类型入队到对应 ctx 软件队列,并触发硬件队列运行
* @param hctx 目标硬件队列上下文,用于决定 rq_lists 索引与后续 run_hw_queue
* @param ctx 目标软件队列上下文,list 中每个 rq 都应属于该 ctx
* @param list 待插入的请求链表(使用 rq->queuelist 串起),函数可能消费并清空它
* @param run_queue_async 是否以异步方式推进硬件队列运行,可能被 REQ_NOWAIT 升级为 true
* @return 无
*
* 补充说明:
* - 若硬件队列不忙且允许同步推进,先尝试直接下发以减少一次软队列往返
* - 扫描 list 时打 trace,并根据 REQ_NOWAIT 决定是否必须异步推进,避免在不可睡眠语义下阻塞
* - 入队后调用 blk_mq_hctx_mark_pending(),让派发侧能从位图快速发现该 ctx 有待处理请求
*/
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)); /* 直发失败的请求会留在 list 里,后续再走软队列入队 */
if (list_empty(list))
goto out; /* list 已被直发路径全部消费时,跳过软队列入队与 pending 标记,降低锁竞争 */
}

list_for_each_entry(rq, list, queuelist) {
BUG_ON(rq->mq_ctx != ctx); /* 这批请求必须属于同一个 ctx,否则入错队列会导致派发与完成路径失配 */
trace_block_rq_insert(rq); /* 提供观测点,便于在拥塞/延迟问题时定位“请求何时入队” */
if (rq->cmd_flags & REQ_NOWAIT)
run_queue_async = true; /* NOWAIT 语义要求尽量不在当前路径发生潜在阻塞,把推进工作交给异步运行更稳妥 */
}

spin_lock(&ctx->lock); /* 保护 ctx->rq_lists[type] 与 pending 位图更新,避免并发入队/出队造成链表与位图不一致 */
list_splice_tail_init(list, &ctx->rq_lists[type]); /* 把整批请求挂到对应类型的软件队列尾部,并把 list 置空,转移所有权 */
blk_mq_hctx_mark_pending(hctx, ctx); /* 告诉派发侧:该 hctx 上的这个 ctx 有待处理请求,避免只看 dispatch 误判为空 */
spin_unlock(&ctx->lock);
out:
blk_mq_run_hw_queue(hctx, run_queue_async); /* 统一在末尾触发推进:同步或异步由 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
/**
* @brief 按请求类型与队列配置选择入队位置,并保证后续派发能够观察到该请求
* @param rq 待插入的 request,要求 rq->q / rq->mq_ctx / rq->mq_hctx 已初始化
* @param flags 插入行为标志(如是否需要头插)
* @return 无
*
* 补充说明:
* - passthrough 与 flush 直接进入 hctx->dispatch,确保它们不会被调度器/软队列路径延后
* - 若启用 I/O 调度器,则把请求交给调度器的 insert_requests(),由调度器决定排序与合并策略
* - 若未启用调度器,则把请求插入 ctx->rq_lists[hctx->type] 并标记 pending,让派发侧从位图发现该 ctx 有工作
*/
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); /* 直通请求需要优先进 dispatch,避免被调度器/软队列延后而影响“解堵”或控制类命令时序 */
} else if (req_op(rq) == REQ_OP_FLUSH) {
blk_mq_request_bypass_insert(rq, BLK_MQ_INSERT_AT_HEAD); /* flush 直接进 dispatch 且头插,减少被普通 I/O 排队拖慢的概率 */
} else if (q->elevator) {
LIST_HEAD(list);

WARN_ON_ONCE(rq->tag != BLK_MQ_NO_TAG); /* 交给调度器前不应持有普通 tag,避免调度器标签/预算逻辑与驱动侧 tag 管理互相踩踏 */

list_add(&rq->queuelist, &list); /* 用临时 list 包装成“批量插入”形态,复用调度器的 insert_requests() 接口 */
q->elevator->type->ops.insert_requests(hctx, &list, flags); /* 调度器决定插入位置与可能的排序/合并策略 */
} else {
trace_block_rq_insert(rq); /* 无调度器时仍打点,便于对照派发侧延迟判断是否发生在入队之前 */

spin_lock(&ctx->lock); /* 保护 ctx 软件队列与 pending 标记,避免并发派发时观察到半更新状态 */
if (flags & BLK_MQ_INSERT_AT_HEAD)
list_add(&rq->queuelist, &ctx->rq_lists[hctx->type]); /* 头插提升在该 ctx 队列中的优先级,常用于需要尽快推进的控制类/重试类请求 */
else
list_add_tail(&rq->queuelist,
&ctx->rq_lists[hctx->type]); /* 尾插保持顺序,减少对已有队列时序的扰动 */
blk_mq_hctx_mark_pending(hctx, ctx); /* 入队后立刻标记 pending,避免派发侧只看 ctx_map 为空而跳过该 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
/**
* @brief 在不经调度器与软队列的情况下尝试把请求直接交给驱动处理
* @param hctx 目标硬件队列上下文,决定由哪个队列入口提交给驱动
* @param rq 待下发的 request
* @param last 表示这是否是当前批次的最后一个请求,驱动可据此决定是否立刻提交缓存的请求
* @return 返回驱动 queue_rq() 给出的 blk_status_t,供上层决定继续直发还是回退/结束
*
* 补充说明:
* - 对 *_RESOURCE 返回码会把请求退回到可重试队列,并把 dispatch_busy 置忙,避免持续无效直发
* - 对非资源类错误直接结束请求,避免错误请求长期占队等待
*/
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); /* 资源不足时标记 busy,避免上层频繁走“直发-失败”造成空转 */
__blk_mq_requeue_request(rq); /* 把请求放回可重试路径,等待驱动/设备资源恢复后再从更高优先级队列推进 */
break;
default:
blk_mq_update_dispatch_busy(hctx, false); /* 非资源类错误不应让队列长期保持 busy,以免影响其他可正常下发的请求 */
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
/**
* @brief 为一个 request 同时获取 dispatch budget 与驱动 tag,满足直发前置资源条件
* @param rq 目标 request,成功时会在 rq 上记录 budget_token 并持有驱动 tag
* @return true 表示两类资源都已就绪;false 表示不满足直发条件且已做必要的资源回滚
*
* 补充说明:
* - budget 先于 tag 获取,失败时要么直接返回,要么在 tag 失败后归还 budget,避免预算泄漏
* - 成功返回意味着后续可以安全调用 queue_rq(),并且驱动侧能用 tag 进行并发/完成管理
*/
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); /* 把预算 token 绑定到请求上,便于后续完成或回退时按同一 token 配平释放 */
if (!blk_mq_get_driver_tag(rq)) {
blk_mq_put_dispatch_budget(rq->q, budget_token); /* tag 拿不到就不能下发,必须归还预算避免“占着名额但发不出去” */
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
/**
* @brief 尝试把单个 request 直接下发给驱动;不满足条件时回落到入队并触发后续派发
* @param hctx 关联的硬件队列上下文
* @param rq 待处理的 request
* @return 无
*
* 补充说明:
* - 若队列停止或处于 quiesce,直发不再可靠,改为入队等待恢复后再派发
* - 若需要走调度器或拿不到 budget/tag,则走常规入队,并用 REQ_NOWAIT 决定 run_hw_queue 的异步推进方式
* - 资源不足时把请求放进 hctx->dispatch 以获得更高重试优先级,并尽快跑队列推进
*/
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); /* 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); /* 资源不足时放入 dispatch 高优先级队列,等资源恢复后优先被重试以缩短阻塞时间 */
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
/**
* @brief 在批量派发场景下尝试直发一个 request,并把“是否需要停止继续派发”编码到返回值里
* @param rq 待处理的 request
* @param last 表示这是否为当前批次的最后一个请求,供驱动决定提交时机
* @return BLK_STS_OK 表示可以继续处理后续请求;*_RESOURCE 表示资源不足需要上层回退;其他错误表示该请求已被结束
*
* 补充说明:
* - 停止/quiesce 分支返回 OK,是为了让批量循环继续把请求挂回队列而不是把它当成资源错误打断整批处理
* - budget/tag 获取失败在批量场景直接返回 RESOURCE,让上层把当前请求转入 bypass 队列并提前结束批量直发
*/
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; /* 预算或 tag 不足时把压力信号上抛给批量循环,由其统一走退避与重排策略 */
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
/**
* @brief 把 rq_list 中的请求尽量直接下发给驱动,并在跨 hctx 或提前退出时显式 commit 已排队请求
* @param rqs 待派发请求列表,列表中请求可能属于不同 hctx
* @return 无
*
* 补充说明:
* - queued 统计“自上次 commit 以来返回 OK 的请求数”,用于在切换 hctx 或提前退出时做一次 commit 边界收束
* - 一旦遇到资源不足,把当前请求放进 dispatch 高优先级队列并停止继续直发,避免在资源紧张时把 CPU 花在反复失败上
* - out: 分支只在 ret != OK 时 commit,是为了覆盖“未处理到 last=true”的提前退出情形,避免驱动侧等待批尾信号而延迟提交
*/
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); /* 切换到新 hctx 前先把旧 hctx 上已排队的请求提交出去,避免跨队列混在一起导致驱动延迟提交 */
queued = 0;
}
hctx = rq->mq_hctx;
}

ret = blk_mq_request_issue_directly(rq, last);
switch (ret) {
case BLK_STS_OK:
queued++; /* 统计这一段里被驱动接收的请求数,供后续 commit_rqs 作为“提交边界”的依据 */
break;
case BLK_STS_RESOURCE:
case BLK_STS_DEV_RESOURCE:
blk_mq_request_bypass_insert(rq, 0); /* 资源不足时把当前请求放到高优先级重试队列,避免被普通入队路径进一步拖后 */
blk_mq_run_hw_queue(hctx, false); /* 触发后续派发尝试,让资源一旦恢复就能尽快重试 dispatch 队列 */
goto out; /* 提前退出以免继续直发造成更多失败与无效开销,同时保留对已接收请求做 commit 的机会 */
default:
blk_mq_end_request(rq, ret); /* 明确错误直接结束,避免错误请求占用队列位置并反复参与派发 */
break;
}
}

out:
if (ret != BLK_STS_OK)
blk_mq_commit_rqs(hctx, queued, false); /* 提前退出时可能没有走到 last=true,显式 commit 确保已接收请求不会因等待批尾信号而滞留 */
}

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

/**
* @brief 根据本轮是否遇到资源紧张,把 hctx->dispatch_busy 按 EWMA 方式更新
* @param hctx 目标硬件队列上下文,dispatch_busy 记录在该对象中并影响后续派发策略
* @param busy 本轮是否观察到“需要保守派发”的迹象(例如资源不足导致回退/重排)
* @return 无
*
* 补充说明:
* - busy 为真时把样本注入到 EWMA 中,busy 为假时只做衰减,让忙碌痕迹随时间自然消退
* - 使用固定点缩放避免小值在整数运算中被过早舍入为 0,导致派发层误判“完全不忙”
*/
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; /* 既没有历史忙碌痕迹,本轮也没观察到忙,就保持为 0,避免无意义写回 */

ewma *= BLK_MQ_DISPATCH_BUSY_EWMA_WEIGHT - 1; /* 先做衰减项,让旧的忙碌程度随轮次逐步变小 */
if (busy)
ewma += 1 << BLK_MQ_DISPATCH_BUSY_EWMA_FACTOR; /* 本轮遇到忙的迹象时注入一个缩放后的“1”,让忙碌信号能在整数域里留下足够分辨率 */
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
/**
* @brief 回退一个 request 前释放直发资源并清理可重试所需状态
* @param rq 需要回退的 request,要求 rq->q 有效且 request 仍处于可重试生命周期内
* @return 无
*
* 补充说明:
* - 先释放驱动 tag,避免资源不足场景下继续占用并发额度
* - 若请求已进入 started 状态,则把状态回退到 idle 并清除超时标志,防止重试路径继承旧状态
*/
static void __blk_mq_requeue_request(struct request *rq)
{
struct request_queue *q = rq->q;

blk_mq_put_driver_tag(rq); /* 释放驱动侧并发标识,避免回退后仍占着 tag 让其他请求更难拿到资源 */

trace_block_rq_requeue(rq); /* 记录一次回退事件,便于观测资源不足/回退频率与时序 */
rq_qos_requeue(q, rq); /* 通知 rq_qos 层该请求发生回退,便于节流/延迟模型按“未真正完成”修正内部状态 */

if (blk_mq_request_started(rq)) {
WRITE_ONCE(rq->state, MQ_RQ_IDLE); /* started 的请求回退后需要重新走派发状态机,用 WRITE_ONCE 避免并发观察到撕裂的状态写入 */
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
/**
* @brief 把 request 放入队列的 requeue_list,交给后台 requeue work 重新插入与重派发
* @param rq 需要回退的 request
* @param kick_requeue_list 为 true 时立即触发消费 requeue_list 的后台机制,加快重试
* @return 无
*
* 补充说明:
* - 先调用 __blk_mq_requeue_request() 做资源与状态清理,确保挂入 requeue_list 的请求处于可重试形态
* - 再通过 blk_mq_sched_requeue_request() 让调度器层感知回退,避免调度器与派发队列状态失配
* - requeue_list 由 requeue_lock 保护,并使用 irqsave 形式兼容可能来自中断相关上下文的回退路径
*/
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); /* 保护 requeue_list/flush_list,避免并发回退与后台 work 同时改链表 */
list_add_tail(&rq->queuelist, &q->requeue_list); /* 把请求挂到统一回退链表,延后到 work 中做批量重新插入与派发 */
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
/**
* @brief 后台 work:批量取出 requeue_list/flush_list 的请求并重新插入,再触发硬件队列运行
* @param work 指向队列 request_queue 内的 requeue_work.work,用于反推出所属 request_queue
* @return 无
*
* 补充说明:
* - 先把共享链表 splice 到本地链表再处理,缩短持锁时间并降低回退现场的锁竞争
* - RQF_DONTPREP 的请求直接进 hctx->dispatch,避免块层合并/重准备破坏驱动已建立的私有状态
* - 普通回退请求头插入队,减少“刚回退就再次排在末尾”导致的重试延迟
*/
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); /* 后台 work 也需要与回退入口互斥,避免同时 splice/插入导致链表不一致 */
list_splice_init(&q->requeue_list, &rq_list); /* 把共享 requeue_list 搬到本地 rq_list,并把共享链表清空,降低后续处理时的持锁需求 */
list_splice_init(&q->flush_list, &flush_list); /* flush_list 同理,统一在 work 中做重新插入,避免在 flush 相关路径里做重活 */
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); /* 驱动已启动过的请求可能携带驱动私有数据,直接放进 dispatch 避免块层合并/重准备改写其形态 */
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); /* flush 请求按其自身插入规则分流,具体是否进入 dispatch 由 blk_mq_insert_request 内部决定 */
}

blk_mq_run_hw_queues(q, false); /* 重新插入完成后统一触发一次运行,让这些回退/flush 请求尽快进入派发与下发流程 */
}

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
/**
* @brief 判断指定硬件队列是否仍有待后续运行路径处理的请求来源
* @param hctx 硬件队列上下文,内部包含 dispatch 链表、ctx_map 位图以及调度器相关状态
* @return true 表示至少还有一处待处理来源,后续仍应保留运行该 hctx 的机会;false 表示当前看不到待处理工作
*
* 补充说明:
* - 该函数只做聚合判断,不直接派发请求,也不修改请求所有权
* - 返回值会被外围 run queue 路径消费,用来决定是否继续推进该 hctx
*/
static bool blk_mq_hctx_has_pending(struct blk_mq_hw_ctx *hctx)
{
return !list_empty_careful(&hctx->dispatch) || /* 上次因资源或时机原因没能继续下发的请求还挂在 dispatch 上,再次运行时要优先补发,避免先到的请求长期滞留 */
sbitmap_any_bit_set(&hctx->ctx_map) || /* 映射到本 hctx 的某个 software queue 仍被标成非空,说明后续 flush/dispatch 还有请求可取 */
blk_mq_sched_has_work(hctx); /* 绑定 I/O 调度器时还要把调度器私有队列算进去,否则会把“调度层仍有请求”误判成空队列 */
}

blk_mq_hctx_mark_pending 标记某个软件队列在该硬件队列下仍有请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @brief 把指定 software queue 标成在当前硬件队列类型下仍有待处理请求
* @param hctx 硬件队列上下文,持有按 ctx 编号组织的待处理位图
* @param ctx software queue 上下文,其 index_hw[] 给出在不同 hctx 类型下对应的位图槽位
* @return 无返回值
*
* 补充说明:
* - 该标记让后续 flush busy ctx 或 run queue 路径能快速发现这个 ctx,不必每次遍历所有 software queue
* - 这里只维护“可被取走”的可见状态,不改变请求生命周期与引用计数
*/
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]; /* 按当前 hctx 类型取该 ctx 在位图中的槽位,读队列/轮询队列等不同类型各自独立定位 */

if (!sbitmap_test_bit(&hctx->ctx_map, bit)) /* 只有该 ctx 第一次对本 hctx 暴露出待处理请求时才置位,避免反复写同一位造成无意义更新 */
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
/**
* @brief 清除指定 software queue 在当前硬件队列类型下的待处理位
* @param hctx 硬件队列上下文,内部保存待处理 software queue 的位图
* @param ctx software queue 上下文,其 index_hw[] 必须与置位时使用同一映射关系
* @return 无返回值
*
* 补充说明:
* - 该清理动作与置位路径成对出现,表示当前这个 ctx 已经不需要再被本 hctx 作为待处理来源扫描
* - 清位只影响后续可见性;若之后又插入新请求,插入路径会重新把该位设回去
*/
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]; /* 取回与置位时相同的槽位,保证撤销的是同一个 software queue 在同一类 hctx 上的待处理来源 */

sbitmap_clear_bit(&hctx->ctx_map, bit); /* 该位清掉后,本 hctx 的后续扫描不会再把这个 ctx 视为仍有请求,直到插入路径再次显式置位 */
}

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 来决定哪些队列值得触发;若某个 hctxdispatch 链表里已有绕过调度器或上次没能下发完的请求,即使它不是调度器偏好的那一个,也必须单独补跑,避免这些请求长期挂在硬件派发层。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
/**
* @brief 遍历请求队列下的全部硬件队列,并按调度器偏好与派发状态决定是否触发运行
* @param q 目标 request_queue,内部持有 hctx_table 与调度器状态
* @param async 为 true 时尽量走异步运行路径,为 false 时允许当前路径直接尝试运行
* @return 无返回值
*
* 补充说明:
* - 单队列感知较弱的调度器只偏好一个 sq_hctx,其它 hctx 只有在 dispatch 链表里已有待补发请求时才会被额外触发
* - 该函数只负责发起 run,不保证本次调用一定把所有请求都送进驱动
*/
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)) /* 调度器不尊重多硬件队列时,只挑当前 CPU 对应的首选 hctx,避免多个 hctx 同时碰调度器内部锁带来额外竞争 */
sq_hctx = blk_mq_get_sq_hctx(q);
queue_for_each_hw_ctx(q, hctx, i) {
if (blk_mq_hctx_stopped(hctx)) /* 已被驱动或上层显式停掉的 hctx 此时不能再 run,继续遍历其它队列,避免把停止语义绕过去 */
continue;
if (!sq_hctx || /* 没有单一首选 hctx 时,说明当前调度路径允许按常规遍历全部硬件队列 */
sq_hctx == hctx || /* 当前 hctx 正是调度器偏好的那个,应该承担本轮主要派发工作 */
!list_empty_careful(&hctx->dispatch)) /* 即使不是首选 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
/**
* @brief 以延迟异步方式遍历并安排请求队列下的硬件队列运行
* @param q 目标 request_queue,内部持有全部硬件队列与调度器状态
* @param msecs 延迟毫秒数,到期后由 run_work 触发对应 hctx 的运行
* @return 无返回值
*
* 补充说明:
* - 已经挂起 run_work 的 hctx 不会被重复改写延迟,以免多个 hctx 相互改期导致某个 work 一直推迟不执行
* - 该函数只安排稍后运行,不在当前路径直接下发请求
*/
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)) /* 单队列调度器场景下先找首选 hctx,减少无意义地唤醒多个硬件队列 */
sq_hctx = blk_mq_get_sq_hctx(q);
queue_for_each_hw_ctx(q, hctx, i) {
if (blk_mq_hctx_stopped(hctx)) /* stopped 位还在时不应重新挂 run_work,否则会把暂停状态悄悄冲掉 */
continue;
if (delayed_work_pending(&hctx->run_work)) /* 该 hctx 已经有一次延迟运行在路上,此时保留原来的到期时间,避免别的 hctx 反复重设它的延迟造成饥饿 */
continue;
if (!sq_hctx || /* 没有首选 hctx 时,允许按队列遍历为每个 hctx 安排异步运行 */
sq_hctx == hctx || /* 首选 hctx 承担调度器常规出队 */
!list_empty_careful(&hctx->dispatch)) /* 非首选 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
/**
* @brief 取消单个硬件队列尚未执行的延迟运行,并把该队列标成 stopped
* @param hctx 目标硬件队列上下文,内部持有 run_work 与状态位
* @return 无返回值
*
* 补充说明:
* - 这一步常见于驱动资源不足或暂时不满足下发时机时暂停 .queue_rq() 路径
* - 该函数不保证 dispatch 已被排空,也不保证之后不会再观察到已有派发活动;若要获得“彻底静止”的效果,应使用更强的 quiesce 机制
*/
void blk_mq_stop_hw_queue(struct blk_mq_hw_ctx *hctx)
{
cancel_delayed_work(&hctx->run_work); /* 先撤销还没执行的延迟运行,避免刚标记 stopped 就又被旧的 run_work 拉起来 */

set_bit(BLK_MQ_S_STOPPED, &hctx->state); /* 把 stopped 位设上后,后续 run 路径会把这个 hctx 当作暂停对象跳过,但并不回收已经在路上的派发 */
}
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
/**
* @brief 遍历并停止一个 request_queue 关联的所有硬件队列
* @param q 目标 request_queue,hctx_table 中的每个 hctx 都会被逐个置为 stopped
* @return 无返回值
*
* 补充说明:
* - 这是对单个 hctx 停止操作的批量封装
* - 返回后只表示 stopped 位已逐个设置完成,不表示所有 dispatch 已经完全排空
*/
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); /* 逐个 hctx 取消延迟运行并设 stopped 位,把整条 request_queue 的硬件派发入口都关上 */
}
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
/**
* @brief 清除单个硬件队列的 stopped 状态,并立即发起一次运行
* @param hctx 目标硬件队列上下文,恢复后会立刻尝试 run
* @return 无返回值
*
* 补充说明:
* - 该函数不检查之前是否真的处于 stopped,调用方需要自行保证时机
* - 对 blocking 类型 hctx,会把运行请求导向允许阻塞的执行方式,避免在当前路径硬跑可睡眠队列
*/
void blk_mq_start_hw_queue(struct blk_mq_hw_ctx *hctx)
{
clear_bit(BLK_MQ_S_STOPPED, &hctx->state); /* 直接撤掉停止标记,让后续 run 路径重新把这个 hctx 当成可运行对象 */

blk_mq_run_hw_queue(hctx, hctx->flags & BLK_MQ_F_BLOCKING); /* blocking 型 hctx 走可异步的恢复方式,普通 hctx 则可直接尝试派发 */
}
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
/**
* @brief 遍历并恢复一个 request_queue 下的全部硬件队列
* @param q 目标 request_queue,内部每个 hctx 都会执行一次 start
* @return 无返回值
*
* 补充说明:
* - 这是单个 hctx 恢复操作的批量封装
* - 每个 hctx 恢复后都会各自依据 flags 决定 run 的执行方式
*/
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); /* 逐个清 stopped 并立即触发 run,把整条 request_queue 的硬件派发入口重新打开 */
}
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
/**
* @brief 只在目标硬件队列当前确实处于 stopped 时,清除停止位并按顺序恢复运行
* @param hctx 目标硬件队列上下文,只有处于 stopped 才会进入恢复路径
* @param async 为 true 时恢复后异步运行,为 false 时允许当前路径直接尝试运行
* @return 无返回值
*
* 补充说明:
* - 该函数先确认 stopped 再恢复,适合与并发的停止/派发路径配合使用
* - 清除 stopped 位后的内存屏障与 blk_mq_hctx_stopped() 中的屏障配对,避免后续检查 dispatch 时漏看待派发请求
*/
void blk_mq_start_stopped_hw_queue(struct blk_mq_hw_ctx *hctx, bool async)
{
if (!blk_mq_hctx_stopped(hctx)) /* 只有当前确实观察到 stopped 位仍成立才需要恢复,避免无意义地重复清位和重复触发 run */
return;

clear_bit(BLK_MQ_S_STOPPED, &hctx->state); /* 先把停止状态撤掉,让后续 run 路径能够重新接纳这个 hctx */

smp_mb__after_atomic(); /* 这里要保证“清 stopped 位”先于后续 dispatch 检查被观察到,这样另一侧要么看到 stopped 已清,要么看到 dispatch 非空,不会把待发请求漏过去 */
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
/**
* @brief 遍历请求队列下的全部硬件队列,只恢复那些当前仍处于 stopped 的队列
* @param q 目标 request_queue,内部全部 hctx 都会被逐个检查
* @param async 调用方给出的恢复方式偏好;blocking 类型 hctx 会额外强制走可异步的恢复方式
* @return 无返回值
*
* 补充说明:
* - 该函数把“是否已停”和“恢复时采用哪种运行方式”都下放到每个 hctx 的局部判断
* - blocking 类型 hctx 即使调用方传入同步恢复,也会被提升为异步恢复,避免把可阻塞队列压到当前路径
*/
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)); /* 调用方要求异步时直接异步;否则 blocking 型 hctx 也要改走异步,防止恢复路径在当前上下文里撞上可睡眠队列 */
}
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 工作项里。主线文档对 dispatchstaterun_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
/**
* @brief 判断指定硬件队列当前是否仍被 stopped 状态位拦住
* @param hctx 硬件队列上下文,state 中保存 BLK_MQ_S_STOPPED 等状态位
* @return true 表示该 hctx 仍不应继续运行;false 表示停止位当前未生效
*
* 补充说明:
* - 这里的判断要和恢复路径中的屏障配对,避免清掉 stopped 位后漏看 dispatch 上的新请求
* - 返回值只反映停止状态,不代表该队列一定有待派发请求
*/
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(); /* 这里要保证前面加入 dispatch 的写入不会被后面的 stopped 检查越过去,这样恢复路径清位后,要么看到队列已恢复,要么看到 dispatch 非空,不会把待发请求漏掉 */

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
/**
* @brief 在指定延迟后异步安排一个硬件队列运行
* @param hctx 目标硬件队列上下文,run_work 会代表这次稍后执行的运行动作
* @param msecs 延迟毫秒数,传 0 表示尽快异步运行
* @return 无返回值
*
* 补充说明:
* - 已停止的 hctx 不会继续挂新的 run_work,避免暂停语义被旧调度请求冲掉
* - 具体投递到哪个 CPU 由 blk_mq_hctx_next_cpu() 决定,以贴合 hctx 的 CPU 亲和关系
*/
void blk_mq_delay_run_hw_queue(struct blk_mq_hw_ctx *hctx, unsigned long msecs)
{
if (unlikely(blk_mq_hctx_stopped(hctx))) /* stopped 位还在时直接返回,避免把本该暂停的 hctx 又交给后台线程拉起来 */
return;
kblockd_mod_delayed_work_on(blk_mq_hctx_next_cpu(hctx), &hctx->run_work,
msecs_to_jiffies(msecs)); /* 把这次运行挂到选中的 CPU 上执行,延迟值转成 jiffies 后由 run_work 驱动后续派发 */
}
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
/**
* @brief 结合 quiesce 状态与待处理来源,判断当前硬件队列是否需要运行
* @param hctx 目标硬件队列上下文,内部包含 request_queue、dispatch 与 ctx_map 等状态
* @return true 表示此刻可以且值得继续 run;false 表示当前不应运行或没有待处理工作
*
* 补充说明:
* - quiesce 期间不会去读待处理来源,避免在调度器切换或队列重配置时碰到不稳定状态
* - 这里返回 true 只表示值得进入运行路径,不保证本次一定能把请求成功送进驱动
*/
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)); /* 只有队列没被 quiesce,才去聚合 dispatch、ctx_map 和调度层里的待处理来源;否则直接判定当前不该运行 */
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
/**
* @brief 按当前上下文与队列状态决定是否立即运行一个硬件队列,或改为异步安排运行
* @param hctx 目标硬件队列上下文,后续会基于它的 cpumask、flags 和 queue 状态决定运行方式
* @param async 为 true 时优先走异步运行;为 false 时允许当前路径直接派发
* @return 无返回值
*
* 补充说明:
* - 若第一次无锁判断看起来不用跑,会再借 queue_lock 复查一次,避免错过刚解除 quiesce 后应有的重跑
* - 运行方式既受 async 影响,也受当前 CPU 是否属于 hctx 的 cpumask 以及 hctx 是否允许阻塞影响
*/
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); /* blocking 型 hctx 在同步路径里可能睡眠,这里显式把这种语义暴露出来 */

need_run = blk_mq_hw_queue_need_run(hctx);
if (!need_run) {
unsigned long flags;

spin_lock_irqsave(&hctx->queue->queue_lock, flags); /* 第一次无锁观察没看到可运行机会时,再拿 queue_lock 复查一次,防止刚好与 unquiesce 交错而漏掉这次应有的 run */
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)) { /* 调用方要求异步,或当前 CPU 不在这个 hctx 允许运行的掩码里时,不在本地直跑,改交给后台 work 去合适的 CPU 执行 */
blk_mq_delay_run_hw_queue(hctx, 0);
return;
}

blk_mq_run_dispatch_ops(hctx->queue,
blk_mq_sched_dispatch_requests(hctx)); /* 真正进入派发路径,从调度层或 dispatch 链表里取请求往驱动送 */
}
EXPORT_SYMBOL(blk_mq_run_hw_queue);