多队列块 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 之前,批处理中剩余的工作数计数器。
标志
BLK_MQ_F_* 标志。定义队列的行为。
sched_data
指向附加到请求队列的 IO 调度程序拥有的数据的指针。如何使用此指针取决于 IO 调度程序。
队列
指向拥有此硬件上下文的请求队列的指针。
fq
需要执行刷新操作的请求队列。
driver_data
指向创建此 hctx 的块驱动程序拥有的数据的指针。
ctx_map
每个软件队列的位图。如果位为打开,则该软件队列中存在待处理请求。
dispatch_from
当未选择调度程序时要使用的软件队列。
dispatch_busy
blk_mq_update_dispatch_busy() 使用的数字,用于决定硬件队列是否正忙于使用指数加权移动平均算法。
类型
HCTX_TYPE_* 标志。硬件队列的类型。
nr_ctx
软件队列的数量。
ctxs
软件队列数组。
dispatch_wait_lock
dispatch_wait 队列的锁。
dispatch_wait
当目前没有可用的标记时,将请求放入其中的等待队列,以便将来等待另一次尝试。
wait_index
下一个可用的 dispatch_wait 队列的索引,用于插入请求。
标记
块驱动程序拥有的标记。只有在从硬件队列调度请求时才分配此集中的标记。
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;
struct rw_semaphore update_nr_hwq_lock;
};
成员
操作
指向实现块驱动程序行为的函数的指针。
映射
一个或多个 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 节点。
超时
请求处理超时(以节拍为单位)。
标志
零个或多个 BLK_MQ_F_* 标志。
driver_data
指向创建此标记集的块驱动程序拥有的数据的指针。
标记
标记集。每个硬件队列一个标记集。具有 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) 时用作锁。
update_nr_hwq_lock
同步更新 nr_hw_queues 与添加/删除磁盘和切换电梯。
-
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 的预算令牌
超时
在请求超时时调用。
轮询
调用以轮询特定标记的完成。
complete
将请求标记为完成。
init_hctx
在硬件队列的块层端设置完毕时调用,允许驱动程序分配/初始化匹配结构。
exit_hctx
Ditto 用于退出/拆卸。
init_request
为块层分配的每个命令调用,允许驱动程序设置驱动程序特定数据。
大于或等于 queue_depth 的标记用于设置刷新请求。
exit_request
Ditto 用于退出/拆卸。
cleanup_rq
在释放尚未完成的一个请求之前调用,通常用于释放驱动程序私有数据。
忙
如果设置,则返回此队列当前是否忙。
map_queues
这允许驱动程序通过覆盖构建 mq_map 的设置时函数来指定自己的队列映射。
show_rq
由 debugfs 实现用于显示有关请求的驱动程序特定信息。
-
enum mq_rq_state blk_mq_rq_state(struct request *rq)¶
读取请求的当前 MQ_RQ_* 状态
参数
struct request *rq
目标请求。
-
bool blk_mq_add_to_batch(struct request *req, struct io_comp_batch *iob, bool is_error, void (*complete)(struct io_comp_batch*))¶
将请求添加到完成批处理
参数
struct request *req
要添加到批处理的请求
struct io_comp_batch *iob
要添加请求的批处理
bool is_error
如果请求失败并出现错误,则指定 true
void (*complete)(struct io_comp_batch *)
请求的 completaion 处理程序
描述
批处理完成仅在没有 I/O 错误且没有特殊的 ->end_io 处理程序时有效。
返回
当请求已添加到批处理时为 true,否则为 false
-
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
请求队列。
注意
此函数不会阻止调用 struct 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
指向要启动的请求的指针
描述
设备驱动程序使用的函数,用于通知块层即将处理请求,因此 blk 层可以执行适当的初始化,例如启动超时计时器。
-
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 队列,以便我们可以在将来再次尝试发送它。插入此队列的请求具有更高的优先级。
参数
struct bio *bio
Bio 指针。
描述
从 q 和 bio 构建请求结构并发送到设备。如果发生以下情况,请求可能不会直接排队到硬件:* 此请求可以与另一个请求合并 * 我们想将请求放置在插件队列中以供将来可能合并 * 此队列中有一个活动的 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_set,克隆的 bios 从中分配
gfp_t gfp_mask
bio 的内存分配掩码
int (*bio_ctr)(struct bio *, struct bio *, void *)
为每个克隆 bio 调用的设置函数。成功返回
0
,失败返回非0
。void *data
要传递给 bio_ctr 的私有数据
描述
将 rq_src 中的 bios 克隆到 rq,并将 rq_src 的属性复制到 rq。此外,原始 bios 指向的页面不会被复制,克隆的 bios 只是指向相同的页面。因此,克隆的 bios 必须在原始 bios 之前完成,这意味着调用者必须在 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() 的引用。
上下文
可以睡眠