多队列块 IO 排队机制 (blk-mq)

多队列块 IO 排队机制是一种 API,它使快速存储设备能够通过排队并将 IO 请求同时提交给块设备,从而实现每秒大量的输入/输出操作 (IOPS),并受益于现代存储设备提供的并行性。

介绍

背景

从内核开发之初,磁盘驱动器一直是事实上的标准。块 IO 子系统的目标是为那些在执行随机访问时有高惩罚的设备实现尽可能好的性能,而瓶颈是机械移动部件,比存储堆栈中的任何层都慢得多。这种优化技术的一个例子是根据硬盘磁头当前位置对读/写请求进行排序。

然而,随着固态驱动器和非易失性存储器的发展,这些存储器没有机械部件,也没有随机访问惩罚,并且能够执行高并行访问,堆栈的瓶颈已经从存储设备转移到操作系统。为了利用这些设备设计中的并行性,引入了多队列机制。

以前的设计有一个单队列来存储块 IO 请求,并使用单个锁。由于缓存中的脏数据以及多个处理器只有一个锁的瓶颈,这在 SMP 系统中扩展性不佳。当不同的进程(或同一进程,移动到不同的 CPU)想要执行块 IO 时,这种设置也会遇到拥塞。与此相反,blk-mq API 生成多个队列,每个队列都有 CPU 本地的单独入口点,从而消除了对锁的需求。关于其工作原理的更深入解释将在以下部分(操作)中介绍。

操作

当用户空间对块设备执行 IO(例如,读取或写入文件)时,blk-mq 会采取行动:它将存储和管理对块设备的 IO 请求,充当用户空间(以及文件系统,如果存在)和块设备驱动程序之间的中间件。

blk-mq 有两组队列:软件暂存队列和硬件调度队列。当请求到达块层时,它将尝试最短路径:直接将其发送到硬件队列。但是,在两种情况下它可能不会这样做:如果该层附加了 IO 调度程序,或者如果我们想尝试合并请求。在这两种情况下,请求都将发送到软件队列。

然后,在软件队列处理完请求后,它们将被放置在硬件队列中,硬件可以直接访问并处理这些请求的第二阶段队列。但是,如果硬件没有足够的资源来接受更多请求,blk-mq 会将请求放在一个临时队列中,以便将来在硬件能够处理时发送。

软件暂存队列

如果块 IO 子系统没有将请求直接发送到驱动程序,它会将请求添加到软件暂存队列(由 struct blk_mq_ctx 表示)。一个请求是一个或多个 BIO。它们通过数据结构 struct bio 到达块层。然后,块层将从中构建一个新的结构,即 struct request,该结构将用于与设备驱动程序通信。每个队列都有自己的锁,队列的数量由每个 CPU 或每个节点定义。

暂存队列可用于合并相邻扇区的请求。例如,扇区 3-6、6-7、7-9 的请求可以合并为一个 3-9 的请求。即使对 SSD 和 NVM 的随机访问与顺序访问的响应时间相同,分组的顺序访问请求也会减少单个请求的数量。这种合并请求的技术称为插入。

除此之外,可以通过 IO 调度程序对请求进行重新排序,以确保系统资源的公平性(例如,确保没有应用程序遭受饥饿)和/或提高 IO 性能。

IO 调度程序

块层实现了多个调度程序,每个调度程序都遵循启发式方法来提高 IO 性能。它们是“可插入的”(如即插即用),可以在运行时使用 sysfs 选择。您可以在此处阅读有关 Linux 的 IO 调度程序的更多信息。调度仅发生在同一队列中的请求之间,因此不可能合并来自不同队列的请求,否则会导致缓存抖动,并且需要为每个队列加锁。调度完成后,请求就有资格发送到硬件。可以选取的调度程序之一是 NONE 调度程序,这是最直接的调度程序。它只会将请求放在进程正在运行的任何软件队列上,而不会进行任何重新排序。当设备开始处理硬件队列中的请求(也称为运行硬件队列)时,映射到该硬件队列的软件队列将根据其映射依次被清空。

硬件调度队列

硬件队列(由 struct blk_mq_hw_ctx 表示)是设备驱动程序用来映射设备提交队列(或设备 DMA 环形缓冲区)的结构,并且是低级设备驱动程序取得请求所有权之前的块层提交代码的最后一步。要运行此队列,块层将从关联的软件队列中删除请求,并尝试将其调度到硬件。

如果无法将请求直接发送到硬件,它们将被添加到请求的链表(hctx->dispatch)中。然后,下次块层运行队列时,它将首先发送位于 dispatch 列表中的请求,以确保公平地调度那些首先准备好发送的请求。硬件队列的数量取决于硬件及其设备驱动程序支持的硬件上下文的数量,但不会超过系统内核的数量。在此阶段不进行重新排序,并且每个软件队列都有一组硬件队列用于发送请求。

注意

块层和设备协议都不能保证请求完成的顺序。这必须由文件系统等更高层处理。

基于标签的完成

为了指示哪个请求已完成,每个请求都由一个整数标识,范围从 0 到调度队列大小。此标签由块层生成,然后由设备驱动程序重用,从而无需创建冗余标识符。当请求在驱动程序中完成时,标签将发送回块层以通知其完成。这样就无需进行线性搜索来找出已完成的 IO。

进一步阅读

源代码文档

enum blk_eh_timer_return

超时处理程序应如何继续

常量

BLK_EH_DONE

块驱动程序已完成命令或将在稍后完成。

BLK_EH_RESET_TIMER

重置请求计时器并继续等待请求完成。

struct blk_mq_hw_ctx

面向硬件块设备的硬件队列的状态

定义:

struct blk_mq_hw_ctx {
    struct {
        spinlock_t lock;
        struct list_head        dispatch;
        unsigned long           state;
    };
    struct delayed_work     run_work;
    cpumask_var_t cpumask;
    int next_cpu;
    int next_cpu_batch;
    unsigned long           flags;
    void *sched_data;
    struct request_queue    *queue;
    struct blk_flush_queue  *fq;
    void *driver_data;
    struct sbitmap          ctx_map;
    struct blk_mq_ctx       *dispatch_from;
    unsigned int            dispatch_busy;
    unsigned short          type;
    unsigned short          nr_ctx;
    struct blk_mq_ctx       **ctxs;
    spinlock_t dispatch_wait_lock;
    wait_queue_entry_t dispatch_wait;
    atomic_t wait_index;
    struct blk_mq_tags      *tags;
    struct blk_mq_tags      *sched_tags;
    unsigned int            numa_node;
    unsigned int            queue_num;
    atomic_t nr_active;
    struct hlist_node       cpuhp_online;
    struct hlist_node       cpuhp_dead;
    struct kobject          kobj;
#ifdef CONFIG_BLK_DEBUG_FS;
    struct dentry           *debugfs_dir;
    struct dentry           *sched_debugfs_dir;
#endif;
    struct list_head        hctx_list;
};

成员

{未命名结构体}

匿名

保护调度列表。

调度

用于准备调度到硬件但由于某些原因(例如,缺少资源)无法发送到硬件的请求。一旦驱动程序可以发送新请求,此列表中的请求将首先发送,以便更公平地调度。

状态

BLK_MQ_S_* 标志。定义硬件队列的状态(活动、计划重新启动、停止)。

run_work

用于安排硬件队列在稍后时间运行。

cpumask

此 hctx 可运行的可用 CPU 的映射。

next_cpu

由 blk_mq_hctx_next_cpu() 用于从 cpumask 中轮询选择 CPU。

next_cpu_batch

在切换到下一个 CPU 之前,批处理中剩余的工作数量计数器。

flags

BLK_MQ_F_* 标志。定义队列的行为。

sched_data

指向附加到请求队列的 IO 调度程序拥有的指针。如何使用此指针由 IO 调度程序决定。

queue

指向拥有此硬件上下文的请求队列的指针。

fq

需要执行刷新操作的请求队列。

driver_data

指向创建此 hctx 的块驱动程序所拥有的数据的指针

ctx_map

每个软件队列的位图。如果位为 1,则该软件队列中存在挂起的请求。

dispatch_from

在未选择调度程序时使用的软件队列。

dispatch_busy

blk_mq_update_dispatch_busy() 使用的数字,用于使用指数加权移动平均算法来决定 hw_queue 是否繁忙。

type

HCTX_TYPE_* 标志。硬件队列的类型。

nr_ctx

软件队列的数量。

ctxs

软件队列的数组。

dispatch_wait_lock

dispatch_wait 队列的锁。

dispatch_wait

当目前没有可用标签时,用于放置请求的等待队列,以便将来再次尝试。

wait_index

用于插入请求的下一个可用 dispatch_wait 队列的索引。

tags

块驱动程序拥有的标签。仅当请求从硬件队列分派时,才会在此集合中分配标签。

sched_tags

I/O 调度程序拥有的标签。如果请求队列有关联的 I/O 调度程序,则会在分配请求时分配一个标签。否则,不使用此成员。

numa_node

存储适配器已连接到的 NUMA 节点。

queue_num

此硬件队列的索引。

nr_active

活动请求的数量。仅当标签集在请求队列之间共享时才使用。

cpuhp_online

用于存储如果 CPU 即将死亡的请求的列表

cpuhp_dead

用于存储如果某个 CPU 死亡的请求的列表。

kobj

用于 sysfs 的内核对象。

debugfs_dir

此硬件队列的 debugfs 目录。命名为 cpu<cpu_number>。

sched_debugfs_dir

调度程序的 debugfs 目录。

hctx_list

如果此 hctx 未使用,则它是 q->unused_hctx_list 中的一个条目。

struct blk_mq_queue_map

将软件队列映射到硬件队列

定义:

struct blk_mq_queue_map {
    unsigned int *mq_map;
    unsigned int nr_queues;
    unsigned int queue_offset;
};

成员

mq_map

CPU ID 到硬件队列索引的映射。这是一个包含 nr_cpu_ids 元素的数组。每个元素的值都在 [queue_offset, queue_offset + nr_queues) 范围内。

nr_queues

要将 CPU ID 映射到的硬件队列的数量。

queue_offset

要映射到的第一个硬件队列。由 PCIe NVMe 驱动程序用于将每个硬件队列类型(enum hctx_type)映射到一组不同的硬件队列。

enum hctx_type

硬件队列的类型

常量

HCTX_TYPE_DEFAULT

所有未以其他方式核算的 I/O。

HCTX_TYPE_READ

仅用于读取 I/O。

HCTX_TYPE_POLL

任何类型的轮询 I/O。

HCTX_MAX_TYPES

hctx 的类型数量。

struct blk_mq_tag_set

可在请求队列之间共享的标签集

定义:

struct blk_mq_tag_set {
    const struct blk_mq_ops *ops;
    struct blk_mq_queue_map map[HCTX_MAX_TYPES];
    unsigned int            nr_maps;
    unsigned int            nr_hw_queues;
    unsigned int            queue_depth;
    unsigned int            reserved_tags;
    unsigned int            cmd_size;
    int numa_node;
    unsigned int            timeout;
    unsigned int            flags;
    void *driver_data;
    struct blk_mq_tags      **tags;
    struct blk_mq_tags      *shared_tags;
    struct mutex            tag_list_lock;
    struct list_head        tag_list;
    struct srcu_struct      *srcu;
};

成员

ops

指向实现块驱动程序行为的函数的指针。

map

一个或多个 ctx -> hctx 映射。每个硬件队列类型(enum hctx_type)都有一个映射,驱动程序希望支持该类型。对映射的大小没有限制,并且在类型之间共享映射是完全合法的。

nr_maps

map 数组中的元素数量。一个介于 [1, HCTX_MAX_TYPES] 范围内的数字。

nr_hw_queues

拥有此数据结构的块驱动程序支持的硬件队列数量。

queue_depth

每个硬件队列的标签数量,包括保留标签。

reserved_tags

为 BLK_MQ_REQ_RESERVED 标签分配保留的标签数量。

cmd_size

每个请求要分配的额外字节数。块驱动程序拥有这些额外的字节。

numa_node

存储适配器已连接到的 NUMA 节点。

timeout

请求处理超时时间(以 jiffies 为单位)。

flags

零个或多个 BLK_MQ_F_* 标志。

driver_data

指向创建此标签集的块驱动程序所拥有的数据的指针。

tags

标签集。每个硬件队列一个标签集。具有 nr_hw_queues 个元素。

shared_tags

共享标签集。具有 nr_hw_queues 个元素。如果设置,则由所有 tags 共享。

tag_list_lock

序列化 tag_list 访问。

tag_list

使用此标签集的请求队列的列表。另请参阅 request_queue.tag_set_list。

srcu

当请求队列类型为阻塞(BLK_MQ_F_BLOCKING)时,用作锁。

struct blk_mq_queue_data

关于插入队列的请求的数据

定义:

struct blk_mq_queue_data {
    struct request *rq;
    bool last;
};

成员

rq

请求指针。

last

如果它是队列中的最后一个请求。

struct blk_mq_ops

实现块驱动程序行为的回调函数。

定义:

struct blk_mq_ops {
    blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *, const struct blk_mq_queue_data *);
    void (*commit_rqs)(struct blk_mq_hw_ctx *);
    void (*queue_rqs)(struct rq_list *rqlist);
    int (*get_budget)(struct request_queue *);
    void (*put_budget)(struct request_queue *, int);
    void (*set_rq_budget_token)(struct request *, int);
    int (*get_rq_budget_token)(struct request *);
    enum blk_eh_timer_return (*timeout)(struct request *);
    int (*poll)(struct blk_mq_hw_ctx *, struct io_comp_batch *);
    void (*complete)(struct request *);
    int (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
    void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);
    int (*init_request)(struct blk_mq_tag_set *set, struct request *, unsigned int, unsigned int);
    void (*exit_request)(struct blk_mq_tag_set *set, struct request *, unsigned int);
    void (*cleanup_rq)(struct request *);
    bool (*busy)(struct request_queue *);
    void (*map_queues)(struct blk_mq_tag_set *set);
#ifdef CONFIG_BLK_DEBUG_FS;
    void (*show_rq)(struct seq_file *m, struct request *rq);
#endif;
};

成员

queue_rq

从块 IO 排队新的请求。

commit_rqs

如果驱动程序使用 bd->last 来判断何时将请求提交到硬件,则必须定义此函数。如果出现错误导致我们停止发出进一步的请求,则此钩子用于启动硬件(否则最后一个请求会这样做)。

queue_rqs

排队新的请求列表。保证驱动程序每个请求都属于同一个队列。如果驱动程序没有完全清空 rqlist,那么剩余的请求将在返回时由块层单独排队。

get_budget

在排队请求之前预留预算,一旦运行 .queue_rq,驱动程序有责任释放预留的预算。此外,我们必须处理 .get_budget 的失败情况,以避免 I/O 死锁。

put_budget

释放预留的预算。

set_rq_budget_token

存储 rq 的预算令牌

get_rq_budget_token

检索 rq 的预算令牌

timeout

在请求超时时调用。

poll

调用以轮询特定标签的完成情况。

complete

将请求标记为完成。

init_hctx

在硬件队列的块层端设置完毕时调用,允许驱动程序分配/初始化匹配的结构。

exit_hctx

退出/拆卸也一样。

init_request

对于块层分配的每个命令调用,以允许驱动程序设置特定于驱动程序的数据。

大于或等于 queue_depth 的标签用于设置刷新请求。

exit_request

退出/拆卸也一样。

cleanup_rq

在释放尚未完成的请求之前调用,通常用于释放驱动程序私有数据。

busy

如果设置,则返回此队列当前是否繁忙。

map_queues

这允许驱动程序通过覆盖构建 mq_map 的设置时间函数来指定自己的队列映射。

show_rq

由 debugfs 实现使用,以显示有关请求的特定于驱动程序的信息。

enum mq_rq_state blk_mq_rq_state(struct request *rq)

读取请求的当前 MQ_RQ_* 状态

参数

struct request *rq

目标请求。

struct request *blk_mq_rq_from_pdu(void *pdu)

将 PDU 转换为请求

参数

void *pdu

要转换的 PDU(协议数据单元)

返回

请求

描述

驱动程序命令数据紧随请求之后。因此,减去请求大小以返回到原始请求。

void *blk_mq_rq_to_pdu(struct request *rq)

将请求转换为 PDU

参数

struct request *rq

要转换的请求

返回

指向 PDU 的指针

描述

驱动程序命令数据紧随请求之后。因此,添加请求以获取 PDU。

void blk_mq_wait_quiesce_done(struct blk_mq_tag_set *set)

等待直到正在进行的静默完成

参数

struct blk_mq_tag_set *set

要等待的 tag_set

注意

驱动程序有责任确保已在 tag_set 的一个或多个 request_queue 上启动静默。此函数仅等待那些使用 blk_mq_quiesce_queue_nowait 设置了静默标志的 request_queue 上的静默。

void blk_mq_quiesce_queue(struct request_queue *q)

等待所有正在进行的调度完成

参数

struct request_queue *q

请求队列。

注意

此函数不会阻止调用结构体 request 的 end_io() 回调函数。一旦此函数返回,我们确保在通过 blk_mq_unquiesce_queue() 解除队列静止状态之前,不会发生任何调度。

bool blk_update_request(struct request *req, blk_status_t error, unsigned int nr_bytes)

完成多个字节,但不完成请求

参数

struct request *req

正在处理的请求

blk_status_t error

块状态代码

unsigned int nr_bytes

要为 req 完成的字节数

描述

在连接到 req 的多个字节上结束 I/O,但即使 req 没有剩余,也不会完成请求结构。如果 req 有剩余,则会将其设置为下一段范围。

将 blk_rq_bytes() 的结果作为 nr_bytes 传递,保证此函数返回 false

注意

在此函数中,有意忽略 RQF_SPECIAL_PAYLOAD 标志,除了在此函数末尾的一致性检查。

返回

false - 此请求没有任何更多数据 true - 此请求有更多数据

void blk_mq_complete_request(struct request *rq)

结束请求的 I/O

参数

struct request *rq

正在处理的请求

描述

通过调度 ->complete_rq 操作来完成请求。

void blk_mq_start_request(struct request *rq)

开始处理请求

参数

struct request *rq

要启动的请求的指针

描述

设备驱动程序使用的函数,用于通知块层请求即将被处理,以便块层可以执行适当的初始化,例如启动超时计时器。

void blk_execute_rq_nowait(struct request *rq, bool at_head)

插入一个请求到 I/O 调度器以执行

参数

struct request *rq

要插入的请求

bool at_head

在队列的头部或尾部插入请求

描述

将一个完全准备好的请求插入到 I/O 调度器队列的末尾以进行执行。不要等待完成。

注意

如果队列已死,此函数将直接调用 done

blk_status_t blk_execute_rq(struct request *rq, bool at_head)

将请求插入队列以进行执行

参数

struct request *rq

要插入的请求

bool at_head

在队列的头部或尾部插入请求

描述

将一个完全准备好的请求插入到 I/O 调度器队列的末尾以进行执行,并等待完成。

返回

提供给 blk_mq_end_request() 的 blk_status_t 结果。

void blk_mq_delay_run_hw_queue(struct blk_mq_hw_ctx *hctx, unsigned long msecs)

异步运行硬件队列。

参数

struct blk_mq_hw_ctx *hctx

指向要运行的硬件队列的指针。

unsigned long msecs

在运行队列之前要等待的延迟毫秒数。

描述

msecs 的延迟异步运行硬件队列。

void blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx, bool async)

开始运行硬件队列。

参数

struct blk_mq_hw_ctx *hctx

指向要运行的硬件队列的指针。

bool async

如果我们想异步运行队列。

描述

检查请求队列是否未处于静止状态,并且是否有待发送的请求。如果为真,则运行队列以将请求发送到硬件。

void blk_mq_run_hw_queues(struct request_queue *q, bool async)

运行请求队列中的所有硬件队列。

参数

struct request_queue *q

指向要运行的请求队列的指针。

bool async

如果我们想异步运行队列。

void blk_mq_delay_run_hw_queues(struct request_queue *q, unsigned long msecs)

异步运行所有硬件队列。

参数

struct request_queue *q

指向要运行的请求队列的指针。

unsigned long msecs

在运行队列之前要等待的延迟毫秒数。

void blk_mq_request_bypass_insert(struct request *rq, blk_insert_t flags)

在调度列表中插入一个请求。

参数

struct request *rq

指向要插入的请求的指针。

blk_insert_t flags

BLK_MQ_INSERT_*

描述

只有当调用者知道我们想绕过目标设备上潜在的 IO 调度器时才应小心使用。

void blk_mq_try_issue_directly(struct blk_mq_hw_ctx *hctx, struct request *rq)

尝试直接将请求发送到设备驱动程序。

参数

struct blk_mq_hw_ctx *hctx

关联硬件队列的指针。

struct request *rq

指向要发送的请求的指针。

描述

如果设备有足够的资源来接受新的请求,则直接将请求发送到设备驱动程序。否则,将其插入 hctx->dispatch 队列,以便我们将来可以再次尝试发送它。插入此队列的请求具有更高的优先级。

void blk_mq_submit_bio(struct bio *bio)

创建并向块设备发送请求。

参数

struct bio *bio

Bio 指针。

描述

qbio 构建请求结构并发送到设备。如果发生以下情况,则请求可能不会直接排队到硬件:* 此请求可以与另一个请求合并 * 我们想将请求放置在插头队列中以便将来可能合并 * 此队列上有一个活动的 IO 调度器

如果 bio 出现错误或在创建请求时出现错误,它将不会将请求排队。

blk_status_t blk_insert_cloned_request(struct request *rq)

堆叠驱动程序提交请求的助手

参数

struct request *rq

正在排队的请求

void blk_rq_unprep_clone(struct request *rq)

释放克隆请求中所有 bios 的辅助函数

参数

struct request *rq

要清理的克隆请求

描述

释放克隆请求 rq 中的所有 bios。

int blk_rq_prep_clone(struct request *rq, struct request *rq_src, struct bio_set *bs, gfp_t gfp_mask, int (*bio_ctr)(struct bio*, struct bio*, void*), void *data)

用于设置克隆请求的辅助函数

参数

struct request *rq

要设置的请求

struct request *rq_src

要克隆的原始请求

struct bio_set *bs

用于分配克隆的 bio 的 bio_set

gfp_t gfp_mask

用于 bio 的内存分配掩码

int (*bio_ctr)(struct bio *, struct bio *, void *)

为每个克隆 bio 调用的设置函数。成功返回 0,失败返回非 0

void *data

要传递给 bio_ctr 的私有数据

描述

rq_src 中的 bio 克隆到 rq,并将 rq_src 的属性复制到 rq。此外,原始 bio 指向的页面不会被复制,克隆的 bio 仅指向相同的页面。因此,克隆的 bio 必须在原始 bio 之前完成,这意味着调用者必须在 rq_src 之前完成 rq

void blk_mq_destroy_queue(struct request_queue *q)

关闭请求队列

参数

struct request_queue *q

要关闭的请求队列

描述

这将关闭由 blk_mq_alloc_queue() 分配的请求队列。所有未来的请求都将失败,返回 -ENODEV。调用者负责通过调用 blk_put_queue() 来删除 blk_mq_alloc_queue() 的引用。

上下文

可以睡眠