[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 blk_rq_is_passthrough blk_op_is_passthrough 同步提交与轮询等待的 blk-mq 请求执行封装

作用与实现要点

  • 同步等待的“回调唤醒”闭环:通过把栈上 wait 指针挂到 rq->end_io_data,并把 rq->end_io 指向 blk_end_sync_rq(),在 I/O 完成时回写 retcomplete(),让 blk_execute_rq() 能可靠退出等待并取回状态。
  • 分支选择原因:轮询 vs 睡眠等待:用 blk_rq_is_poll() 判断该请求的 mq_hctx 是否为 HCTX_TYPE_POLL;若是,则用 blk_hctx_poll() 主动推进完成并配合 cond_resched() 控制忙等副作用;否则走 blk_wait_io() 的阻塞等待路径。
  • 参数与上下文约束WARN_ON(irqs_disabled()) 强制要求可调度/可等待的上下文;WARN_ON(!blk_rq_is_passthrough(rq)) 限定只允许“驱动直通类”请求走这条同步执行封装,避免把普通 I/O 路径误用成同步提交。
  • 一次性栈对象的生命周期门控wait 位于当前栈帧,只有在 complete() 发生后才允许返回;因此必须确保 rq->end_io_data 指向的对象在回调触发前始终有效,否则会产生越界访问风险。
  • 提交推进策略blk_mq_insert_request() 决定插入队首/队尾,blk_mq_run_hw_queue() 负责“踢”硬件队列开始派发,避免只入队不派发导致等待无法满足。

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
struct blk_rq_wait {
struct completion done;
blk_status_t ret;
};

/**
* @brief 同步执行封装的完成回调:回写完成状态并唤醒等待者
* @param rq 当前完成的 request
* @param ret 本次完成的 blk_status_t 结果
* @return 返回 RQ_END_IO_NONE,表示此回调不接管额外的结束处理
*
* @note 该回调依赖 rq->end_io_data 指向有效的 struct blk_rq_wait,
* 其生命周期必须覆盖到 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); /* 作用:满足等待条件并唤醒;后果:若不唤醒则 blk_execute_rq() 可能永久阻塞/忙等 */
return RQ_END_IO_NONE; /* 作用:声明本回调不做额外收尾;约束:避免上层重复处理同一完成事件 */
}

blk_rq_is_poll 判断请求是否走轮询完成路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief 判断 request 是否绑定在 poll 类型的硬件队列上
* @param rq 待判断的 request
* @return true 表示可走轮询推进完成;false 表示应走常规阻塞等待
*/
bool blk_rq_is_poll(struct request *rq)
{
if (!rq->mq_hctx) /* 条件:未绑定 hw_ctx;后果:无法选择正确的 poll 目标,只能判定为非轮询 */
return false;
if (rq->mq_hctx->type != HCTX_TYPE_POLL) /* 条件:hctx 类型不是 POLL;后果:轮询接口可能无意义或浪费 CPU,改走阻塞等待 */
return false;
return true; /* 作用:明确允许轮询推进;后果:后续 blk_execute_rq() 将改用 poll 等待分支 */
}
EXPORT_SYMBOL_GPL(blk_rq_is_poll);

blk_rq_poll_completion 轮询推进完成并适度让出 CPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 使用 blk_hctx_poll() 循环推进硬件队列完成,直到 completion 满足
* @param rq 需要等待完成的 request(用于定位队列与 hctx)
* @param wait completion 对象,用于判断完成条件是否已满足
*
* @note 该路径是“主动推进 + 让出 CPU”的折中:既避免完全睡眠等待,
* 也避免纯忙等占满 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); /* 作用:触发一次轮询推进;原因:让完成回调有机会被执行并 complete() */
cond_resched(); /* 作用:在循环中允许调度;后果:避免长期占用 CPU 导致系统响应性下降 */
} while (!completion_done(wait)); /* 条件:完成条件未满足继续;后果:若条件判断缺失可能提前返回导致 wait->ret 未就绪 */
}

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
/**
* @brief 同步提交一个已准备好的 request:入队、触发派发、等待完成并返回状态
* @param rq 已准备好的 request(由调用方保证字段与 payload 完整)
* @param at_head 为 true 表示插入队首,否则插入队尾
* @return 完成时由 blk_mq_end_request() 传入的 blk_status_t 结果
*
* @note 本接口把等待上下文放在当前栈上,因此返回前必须等到完成回调触发。
*/
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), /* 作用:初始化栈上 completion;后果:若未初始化,等待条件可能永远不成立或误判成立 */
};

WARN_ON(irqs_disabled()); /* 约束:禁止在本地中断关闭时走同步等待;后果:可能无法调度/无法满足等待条件而卡死 */
WARN_ON(!blk_rq_is_passthrough(rq)); /* 约束:只允许驱动直通类请求;原因:该封装依赖特定语义与回调配合,误用会破坏块层路径假设 */

rq->end_io_data = &wait; /* 作用:把回调要写回的上下文挂到 request;约束:wait 在栈上,必须在回调 complete() 前保持有效 */
rq->end_io = blk_end_sync_rq; /* 作用:设置完成回调;后果:若未设置,wait.done 不会被 complete(),等待将无法结束 */

blk_account_io_start(rq); /* 作用:开始 I/O 统计/计账;后果:缺失会导致统计不一致或延迟归因错误 */
blk_mq_insert_request(rq, at_head ? BLK_MQ_INSERT_AT_HEAD : 0); /* 分支策略:按 at_head 选择插入位置;原因:支持“优先派发/正常派发”两类诉求 */
blk_mq_run_hw_queue(hctx, false); /* 作用:触发硬件队列运行;后果:只入队不触发可能导致请求迟迟不派发,等待条件难以满足 */

if (blk_rq_is_poll(rq)) /* 条件:绑定到 POLL 类型 hctx;后果:改走轮询推进,减少睡眠唤醒开销 */
blk_rq_poll_completion(rq, &wait.done);
else
blk_wait_io(&wait.done); /* 条件:非 POLL;后果:走阻塞等待直到 complete(),避免持续占用 CPU */

return wait.ret; /* 作用:向调用方返回完成状态;约束:依赖回调先写入 wait.ret 再 complete() 保证可读一致性 */
}
EXPORT_SYMBOL(blk_execute_rq);

blk_rq_is_passthrough 判断请求是否为驱动直通操作

1
2
3
4
5
6
7
8
9
/**
* @brief 判断 request 的 cmd_flags 是否表示“驱动直通”类操作
* @param rq 待判断的 request
* @return true 表示直通;false 表示非直通
*/
static inline bool blk_rq_is_passthrough(struct request *rq)
{
return blk_op_is_passthrough(rq->cmd_flags); /* 作用:把 request 级别判断下沉到 op 级别;原因:统一掩码与操作码比较规则 */
}

blk_op_is_passthrough 从操作码判定是否为驱动直通

1
2
3
4
5
6
7
8
9
10
/**
* @brief 从 blk_opf_t 中提取 REQ_OP 并判断是否为驱动直通输入/输出
* @param op 带标志位的操作字段
* @return true 表示 REQ_OP_DRV_IN / REQ_OP_DRV_OUT;false 表示其他操作
*/
static inline bool blk_op_is_passthrough(blk_opf_t op)
{
op &= REQ_OP_MASK; /* 作用:仅保留操作码低位;原因:去除其他 flag 干扰,避免把“带标志的非直通”误判为直通 */
return op == REQ_OP_DRV_IN || op == REQ_OP_DRV_OUT; /* 分支含义:仅两类 drv 操作走直通;后果:其他操作会被同步执行封装拒绝(WARN_ON) */
}