.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-zh_CN.rst

:Original: Documentation/block/blk-mq.rst

:翻译:

 柯子杰 kezijie <kezijie@leap-io-kernel.com>

:校译:



================================================
多队列块设备 I/O 排队机制 (blk-mq)
================================================

多队列块设备 I/O 排队机制提供了一组 API，使高速存储设备能够同时在多个队列中
处理并发的 I/O 请求并将其提交到块设备，从而实现极高的每秒输入/输出操作次数
(IOPS)，充分发挥现代存储设备的并行能力。

介绍
====

背景
----

磁盘从 Linux 内核开发初期就已成为事实上的标准。块 I/O 子系统的目标是尽可能
为此类设备提供最佳性能，因为它们在进行随机访问时代价极高，性能瓶颈主要在机械
运动部件上，其速度远低于存储栈中其他任何层。其中一个软件优化例子是根据硬盘磁
头当前的位置重新排序读/写请求。

然而，随着固态硬盘和非易失性存储的发展，它们没有机械部件，也不存在随机访问代
码，并能够进行高速并行访问，存储栈的瓶颈从存储设备转移到了操作系统。为了充分
利用这些设备设计中的并行性，引入了多队列机制。

原来的设计只有一个队列来存储块设备 I/O 请求，并且只使用一个锁。由于缓存中的
脏数据和多处理器共享单锁的瓶颈，这种设计在 SMP 系统中扩展性不佳。当不同进程
（或同一进程在不同 CPU 上）同时执行块设备 I/O 时，该单队列模型还会出现严重
的拥塞问题。为了解决这些问题，blk-mq API 引入了多个队列，每个队列在本地 CPU
上拥有独立的入口点，从而消除了对全局锁的需求。关于其具体工作机制的更深入说明，
请参见下一节（ `工作原理`_ ）。

工作原理
--------

当用户空间执行对块设备的 I/O（例如读写文件）时，blk-mq 便会介入：它将存储和
管理发送到块设备的 I/O 请求，充当用户空间（文件系统，如果存在的话）与块设备驱
动之间的中间层。

blk-mq 由两组队列组成：软件暂存队列和硬件派发队列。当请求到达块层时，它会尝
试最短路径：直接发送到硬件队列。然而，有两种情况下可能不会这样做：如果该层有
IO 调度器或者是希望合并请求。在这两种情况下，请求将被发送到软件队列。

随后，在软件队列中的请求被处理后，请求会被放置到硬件队列。硬件队列是第二阶段
的队列，硬件可以直接访问并处理这些请求。然而，如果硬件没有足够的资源来接受更
多请求，blk-mq 会将请求放置在临时队列中，待硬件资源充足时再发送。

软件暂存队列
~~~~~~~~~~~~

在这些请求未直接发送到驱动时，块设备 I/O 子系统会将请求添加到软件暂存队列中
（由 struct blk_mq_ctx 表示）。一个请求可能包含一个或多个 BIO。它们通过 struct bio
数据结构到达块层。块层随后会基于这些 BIO 构建新的结构体 struct request，用于
与设备驱动通信。每个队列都有自己的锁，队列数量由每个 CPU 和每个 node 为基础
来决定。

暂存队列可用于合并相邻扇区的请求。例如，对扇区3-6、6-7、7-9的请求可以合并
为对扇区3-9的一个请求。即便 SSD 或 NVM 的随机访问和顺序访问响应时间相同，
合并顺序访问的请求仍可减少单独请求的数量。这种合并请求的技术称为 plugging。

此外，I/O 调度器还可以对请求进行重新排序以确保系统资源的公平性（例如防止某
个应用出现“饥饿”现象）或是提高 I/O 性能。

I/O 调度器
^^^^^^^^^^

块层实现了多种调度器，每种调度器都遵循一定启发式规则以提高 I/O 性能。它们是
“可插拔”的(plug and play)，可在运行时通过 sysfs 选择。你可以在这里阅读更
多关于 Linux IO 调度器知识 `here
<https://www.kernel.org/doc/html/latest/block/index.html>`_。调度只发
生在同一队列内的请求之间，因此无法合并不同队列的请求，否则会造成缓存冲突并需
要为每个队列加锁。调度后，请求即可发送到硬件。可能选择的调度器之一是 NONE 调
度器，这是最直接的调度器：它只将请求放到进程所在的软件队列，不进行重新排序。
当设备开始处理硬件队列中的请求时（运行硬件队列），映射到该硬件队列的软件队列
会按映射顺序依次清空。

硬件派发队列
~~~~~~~~~~~~~

硬件队列（由 struct blk_mq_hw_ctx 表示）是设备驱动用来映射设备提交队列
（或设备 DMA 环缓存）的结构体，它是块层提交路径在底层设备驱动接管请求之前的
最后一个阶段。运行此队列时，块层会从相关软件队列中取出请求，并尝试派发到硬件。

如果请求无法直接发送到硬件，它们会被加入到请求的链表(``hctx->dispatch``) 中。
随后，当块层下次运行该队列时，会优先发送位于 ``dispatch`` 链表中的请求，
以确保那些最早准备好发送的请求能够得到公平调度。硬件队列的数量取决于硬件及
其设备驱动所支持的硬件上下文数，但不会超过系统的CPU核心数。在这个阶段不
会发生重新排序，每个软件队列都有一组硬件队列来用于提交请求。

.. note::

        块层和设备协议都不保证请求完成顺序。此问题需由更高层处理，例如文件系统。

基于标识的完成机制
~~~~~~~~~~~~~~~~~~~

为了指示哪一个请求已经完成，每个请求都会被分配一个整数标识，该标识的取值范围
是从0到分发队列的大小。这个标识由块层生成，并在之后由设备驱动使用，从而避
免了为每个请求再单独创建冗余的标识符。当请求在驱动中完成时，驱动会将该标识返
回给块层，以通知该请求已完成。这样，块层就无需再进行线性搜索来确定是哪一个
I/O 请求完成了。

更多阅读
--------

- `Linux 块 I/O：多队列 SSD 并发访问简介 <http://kernel.dk/blk-mq.pdf>`_

- `NOOP 调度器 <https://en.wikipedia.org/wiki/Noop_scheduler>`_

- `Null 块设备驱动程序 <https://www.kernel.org/doc/html/latest/block/null_blk.html>`_

源代码
======

该API在以下内核代码中:

include/linux/blk-mq.h

block/blk-mq.c