多核系统上引入多队列SSD

1.摘要

存储设备的IO性能已从之前的数百IOPS加速到今天的数十万IOPS,并预计在未来几年内达到数千万IOPS。这一急剧演变主要归功于NAND-FLASH(闪存)器件及其数据并行设计的引入。

使用传统的机械存储设备(HDD),IO的延迟和吞吐量就受到这种旋转式存储设备的物理特性影响。通常HDD通过旋转磁盘盘片进行顺序访问速度较快,而通过移动磁头的随机访问却很慢,一代又一代的IO密集型算法和系统就是基于这两个基本特征而设计。固态硬盘(SSD)的出现正在改变这两个存储的性能特征,因为SSD的顺序IO和随机IO之间的延迟差异很小。固态硬盘的IO延迟为数十微秒,而硬盘为数十毫秒。SSD磁盘中的大量内部数据并行度实现了许多并发IO操作,从而使单个设备能够实现接近一百万次每秒IO(IOPS)的随机访问,而传统的磁性硬盘上仅有数百次IOPS。

2. IO瓶颈

现代存储设备的吞吐量现在通常受到其硬件(即,SATA/SAS或PCI-E)和软件接口的限制。硬件性能的如此快速飞跃暴露了以前未被注意到的软件级别的瓶颈,包括操作系统层和应用层。如今,在Linux环境下,单CPU内核可以支持80万IOPS左右的IO提交率。无论使用多少核来提交IO,操作系统块层都不能扩展到超过一百万IOPS。这对今天的固态硬盘来说可能够快了,但对明天的固态硬盘来说就不够快了。

由于目前操作系统中存在的性能瓶颈,一些应用程序和设备驱动程序已经选择绕过Linux块层来提高性能。此选择增加了驱动程序和硬件实现的复杂性。更具体地说,它在容易出错的驱动程序实现中增加了重复代码,并删除了通用操作系统存储层提供的通用功能,如IO调度和服务质量流量整形。
因此不放弃块层,又能提高存储性能,成为重要问题。

3. Linux块层传统实现

操作系统块层负责将IO请求从应用程序传送到存储设备。块层是一种粘合剂,一方面允许应用程序以统一的方式访问不同的存储设备,另一方面为存储设备和驱动程序提供来自所有应用程序的单一入口点。它是一个便捷库,可以对应用程序隐藏存储设备的复杂性和多样性,同时提供对应用程序有价值的公共服务。此外,数据块层实施IO公平性、IO错误处理、IO统计和IO调度,以提高性能并帮助保护最终用户免受其他应用程序或设备驱动程序的不良或恶意实施的影响。

应用程序通过内核系统调用提交IO,将其转换为称为块IO的数据结构。每个数据块IO包含IO地址、IO大小、IO形态(读或写)或IO类型(同步/异步)2等信息。然后,将其传输到libaio(用于异步IO)或直接传输到数据块层(用于将其提交到数据块层的同步IO)。一旦IO请求被提交,相应的数据块IO就被缓冲在临时区域中,该临时区域被实现为一个队列,表示为请求队列。

在这里插入图片描述

Linux块层支持可插拔IO调度器:NOOP、deadline和CFQ,它们都可以在此临时区域内操作IO。块层还提供了一种处理IO完成的机制:每次设备驱动程序中的IO完成时,该驱动程序都会调用堆栈来调用块层中的通用完成函数。然后,块层调用libaio库中的IO完成函数,或者从同步读或写系统调用返回,后者向应用程序提供IO完成信号。

在当前块层中,中转区由请求队列结构表示。每个块设备实例化一个这样的队列。所有块设备的访问都是统一的,应用程序不需要知道块层内的控制流模式。然而,这种针对每个设备的单一队列设计的结果是,块层不能支持跨设备的IO调度。

传统实现块层三个主要性能开销如下:

  1. 请求队列锁定。

    块层通过IO请求队列实现同步访问独占资源。无论何时向请求队列插入数据块IO或从请求队列中删除数据块IO,都必须获取此锁。
    scsi设备的请求处理函数scsi_request_fn如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
    * Function: scsi_request_fn()
    *
    * Purpose: Main strategy routine for SCSI.
    *
    * Arguments: q - Pointer to actual queue.
    *
    * Returns: Nothing
    *
    * Lock status: IO request lock assumed to be held when called.
    */
    static void scsi_request_fn(struct request_queue *q)
    __releases(q->queue_lock)
    __acquires(q->queue_lock)
    {
    struct scsi_device *sdev = q->queuedata;
    struct Scsi_Host *shost;
    struct scsi_cmnd *cmd;
    struct request *req;
    ...
    }

进入函数时已经保证request_queue已经被lock。该函数名称后面多了releases,acquires两个宏定义,在大型项目中为了保证代码质量,会加入很多防御性编程代码,这样能够即时的把问题暴露出来。例如:

1
2
3
4
5
6
7
// 函数本身是非线程安全的,需要在外边上锁保护
void dothing_unsafe()
{
ASSERT(lock == 1); // 确保函数调用时,锁是成功加上的。
... // 执行函数逻辑
ASSERT(lock == 0); // 最后返回时,需要确保lock已被释放,否则说明代码逻辑存在bug
}

例子中dothing_unsafe中典型的防御性编程的代码就是通过assert调用,保证函数在调用时lock是持有状态的。但是assert本身调用会带来除函数逻辑以外的额外开销,因此会对性能造成影响。影响包括几个方面,一个是assert中的if判断有可能会对流水线并行造成不好的影响,这个影响可以通过gcc的__builtin_expect内置函数,提前告知编译器代码生成来规避。另外一个不好的影响是会增加可执行代码的大小,影响指令cache的局部性。

但其实更好的做法是,通过编译期的静态检查,将问题提前暴露出来,而不是留到运行期再发现问题。例如还是上述的例子,通过sparse的静态检查,可以这样写:

1
2
3
4
5
6

// 函数本身是非线程安全的,需要在外边上锁保护
void dothing_unsafe() __releases(lock)
{
... // 执行函数逻辑
}

其中acquires(x) 和releases(x),acquire(x) 和release(x) 必须配对使用,都和锁有关。

  • 每当通过IO提交操作请求队列时,必须获取该锁。

  • 当I/O提交时,数据块层进行优化,如 plugging蓄洪(在将I/O发送到硬件之前先让I/O累积,以提高缓存效率)

  • IO重新排序和公平调度都必须获取请求队列锁,才能继续操作。

  1. 硬件中断

较高的IOPS数会导致成比例的高中断数。当今的大多数存储设备都是这样设计的,即一个内核(CPU0)负责处理所有硬件中断,并将它们作为软中断转发到其他内核。 因此,单个核可能花费相当多的时间来处理这些中断、上下文切换以及影响应用程序可能依赖的数据局部性的L1和L2高速缓存。然后,其他CPU核心也必须使用IPI(处理器间中断)来执行IO完成例程。因此,在许多情况下,仅完成一个IO就需要两次中断和上下文切换。

  1. 远程内存访问

当强制跨CPU核心(或NUMA体系结构中的跨套接字)进行远程内存访问时,请求队列锁争用会加剧。每当IO在与发出IO的内核不同的内核上完成时,就需要这样的远程内存访问。
在这种情况下,获取请求队列上的锁以从请求队列移除块IO引起对存储在上次获取该锁的核的高速缓存中的锁状态的远程存储器访问,然后在两个核上将高速缓存线标记为共享。更新时,副本将从远程缓存中显式失效。如果多个核正在主动发出IO并因此竞争此锁,则与此锁关联的缓存线将在这些核之间持续反弹。

4.多队列

通过使用具有不同功能的两级队列,将单个请求队列锁上的锁争用分布到多个队列,如图所示。
在这里插入图片描述

软件暂存队列。数据块IO请求现在维护在一个或多个请求队列的集合中,而不是将IO转移到单个软件队列中进行调度。可以配置这些分段队列,使得系统上的每个套接字或每个内核都有一个这样的队列。因此,在具有4个插槽和每个插槽6个核心的NUMA系统上,临时区域可能包含最少4个队列,最多24个队列。如果单个队列上的争用不是瓶颈,则请求队列的可变特性会减少锁的扩散。由于许多CPU体系结构为每个套接字(通常也是NUMA节点)提供了大型共享L3缓存,因此每个处理器套接字只有一个队列可以在不利于缓存的重复数据结构和锁争用之间进行很好的权衡。

硬件调度队列。IO进入分段队列后,我们引入了一个新的中间队列层,称为硬件分派队列。使用这些队列,计划分派的数据块IO不会直接发送到设备驱动程序,而是发送到硬件分派队列。硬件分派队列的数量通常与设备驱动程序支持的硬件上下文的数量相匹配。设备驱动程序可以选择支持消息信号中断标准MSI-X[25]所支持的1到2048个队列。因为在块层中不支持IO排序,所以任何软件队列都可以馈送任何硬件队列,而不需要维护全局排序。这允许硬件实现直接映射到NUMA节点或CPU的一个或多个队列,并提供从应用程序到硬件的快速IO路径,而无需访问任何其他节点上的远程内存。

5.多队列内核数据结构

单队列架构发起IO传输的核心函数是blk_queue_bio()。Multi queue多队列核心IO传输函数是blk_mq_make_request()。Multi queue多队列架构引入了struct blk_mq_tag_set、struct blk_mq_tag、struct blk_mq_hw_ctx、struct blk_mq_ctx等数据结构。最初阅读这些代码时,感觉比单队列复杂多了,很容易绕晕。

1 struct blk_mq_ctx代表每个CPU独有的软件队列;

2 struct blk_mq_hw_ctx代表硬件队列,块设备至少有一个;

3 struct blk_mq_tag每个硬件队列结构struct blk_mq_hw_ctx对应一个;

4 struct blk_mq_tag主要是管理struct request(下文简称req)的分配。struct request大家应该都比较熟悉了,单队列时代就存在,IO传输的最后都要把bio转换成request;

5 struct blk_mq_tag_set包含了块设备的硬件配置信息,比如支持的硬件队列数nr_hw_queues、队列深度queue_depth等,在块设备驱动初始化时多处使用blk_mq_tag_set初始化其他成员;

每个CPU对应唯一的软件队列blk_mq_ctx,blk_mq_ctx对应唯一的硬件队列blk_mq_hw_ctx,blk_mq_hw_ctx对应唯一的blk_mq_tag,三者的关系在后续代码分析中多次出现。

以nvme驱动程序初始化代码为例,分析多请求队列的源码,从函数nvme_dev_add开始。
nvme_dev_add()函数中设置blk_mq_tag_set结构的关键成员;分配设置每个硬件队列独有blk_mq_tag结构;分配并设置struct blk_mq_tag_set *set的set->mq_map[]数组,该数组下标是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
/*
* Return: error value if an error occurred setting up the queues or calling
* Identify Device. 0 if these succeeded, even if adding some of the
* namespaces failed. At the moment, these failures are silent. TBD which
* failures should be reported.
*/
static int nvme_dev_add(struct nvme_dev *dev)
{
if (!dev->ctrl.tagset) {

//设置blk_mq_tag_set的blk_mq_ops为nvme_mq_ops
dev->tagset.ops = &nvme_mq_ops;

////设置硬件队列个数
dev->tagset.nr_hw_queues = dev->online_queues - 1;
dev->tagset.timeout = NVME_IO_TIMEOUT;
dev->tagset.numa_node = dev_to_node(dev->dev);

////设置队列深度
dev->tagset.queue_depth =
min_t(int, dev->q_depth, BLK_MQ_MAX_DEPTH) - 1;
dev->tagset.cmd_size = nvme_cmd_size(dev);
dev->tagset.flags = BLK_MQ_F_SHOULD_MERGE;
dev->tagset.driver_data = dev;

//调用blk_mq_alloc_tag_set(),target是struct blk_mq_tag_set tagset
if (blk_mq_alloc_tag_set(&dev->tagset))
return 0;
dev->ctrl.tagset = &dev->tagset;

nvme_dbbuf_set(dev);
} else {
blk_mq_update_nr_hw_queues(&dev->tagset, dev->online_queues - 1);

/* Free previously allocated queues that are no longer usable */
nvme_free_queues(dev, dev->online_queues);
}

return 0;
}
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

/*
* Alloc a tag set to be associated with one or more request queues.
* May fail with EINVAL for various error conditions. May adjust the
* requested depth down, if if it too large. In that case, the set
* value will be stored in set->queue_depth.
*/
int blk_mq_alloc_tag_set(struct blk_mq_tag_set *set)
{
/*
* If a crashdump is active, then we are potentially in a very
* memory constrained environment. Limit us to 1 queue and
* 64 tags to prevent using too much memory.
*/
if (is_kdump_kernel()) {
set->nr_hw_queues = 1;
set->queue_depth = min(64U, set->queue_depth);
}
/*
* There is no use for more h/w queues than cpus.
硬件队列数大于CPU个数
*/
if (set->nr_hw_queues > nr_cpu_ids)
set->nr_hw_queues = nr_cpu_ids;
//按照CPU个数分配struct blk_mq_tag_set需要的struct blk_mq_tags指针数组,每个CPU都有一个blk_mq_tags
set->tags = kzalloc_node(nr_cpu_ids * sizeof(struct blk_mq_tags *),
GFP_KERNEL, set->numa_node);
if (!set->tags)
return -ENOMEM;

ret = -ENOMEM;

//分配mq_map[]指针数组,按照CPU的个数分配nr_cpu_ids个unsigned int类型数据,该数组成员对应一个CPU
set->mq_map = kzalloc_node(sizeof(*set->mq_map) * nr_cpu_ids,
GFP_KERNEL, set->numa_node);
if (!set->mq_map)
goto out_free_tags;

//为每个set->mq_map[cpu]分配一个硬件队列编号。该数组下标是CPU的编号,数组成员是硬件队列的编号
ret = blk_mq_update_queue_map(set);
if (ret)
goto out_free_mq_map;

/*分配每个硬件队列独有的blk_mq_tags结构,根据硬件队列的深度queue_depth分配对应个数的request存到
struct blk_mq_tags * tags->static_rqs[],并设置blk_mq_tags结构的nr_reserved_tags、nr_tags等其他成员*/

ret = blk_mq_alloc_rq_maps(set);
if (ret)
goto out_free_mq_map;
}

如注释,关键是调用函数blk_mq_alloc_rq_maps()分配每个硬件队列独有的blk_mq_tags结构,并初始化其成员,该函数源码如下:

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

/*
* Allocate the request maps associated with this tag_set. Note that this
* may reduce the depth asked for, if memory is tight. set->queue_depth
* will be updated to reflect the allocated depth.
*/
static int blk_mq_alloc_rq_maps(struct blk_mq_tag_set *set)
{
unsigned int depth;
int err;

depth = set->queue_depth;
do {

/*分配每个硬件队列独有的blk_mq_tags结构,根据硬件队列的深度queue_depth分配对应个数的request存到
struct blk_mq_tags * tags->static_rqs[],并设置blk_mq_tags结构的nr_reserved_tags、nr_tags等其他成员*/
err = __blk_mq_alloc_rq_maps(set);
if (!err)// __blk_mq_alloc_rq_maps分配成功返回0,这里就直接break了,只循环一次
break;

set->queue_depth >>= 1;
if (set->queue_depth < set->reserved_tags + BLK_MQ_TAG_MIN) {
err = -ENOMEM;
break;
}
} while (set->queue_depth);

if (!set->queue_depth || err) {
pr_err("blk-mq: failed to allocate request map\n");
return -ENOMEM;
}

if (depth != set->queue_depth)
pr_info("blk-mq: reduced tag depth (%u -> %u)\n",
depth, set->queue_depth);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

static int __blk_mq_alloc_rq_maps(struct blk_mq_tag_set *set)
{
int i;

for (i = 0; i < set->nr_hw_queues; i++)
/*分配每个硬件队列独有的blk_mq_tags结构,根据硬件队列的深度queue_depth分配对应个数的request存到
struct blk_mq_tags * tags->static_rqs[],并设置blk_mq_tags结构的nr_reserved_tags、nr_tags等其他成员*/

if (!__blk_mq_alloc_rq_map(set, i))
goto out_unwind;

return 0;

out_unwind:
while (--i >= 0)
blk_mq_free_rq_map(set->tags[i]);

return -ENOMEM;
}
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

static bool __blk_mq_alloc_rq_map(struct blk_mq_tag_set *set, int hctx_idx)
{
int ret = 0;

/*分配并返回硬件队列专属的blk_mq_tags结构,分配设置其成员nr_reserved_tags、nr_tags、rqs、static_rqs。主要
是分配struct blk_mq_tags *tags的tags->rqs[]、tags->static_rqs[]这两个req指针数组。hctx_idx是硬件队列编号,每
一个硬件队列独有一个blk_mq_tags结构*/

set->tags[hctx_idx] = blk_mq_alloc_rq_map(set, hctx_idx,
set->queue_depth, set->reserved_tags);
if (!set->tags[hctx_idx])
return false;
/*针对hctx_idx编号的硬件队列,分配set->queue_depth个req存于tags->static_rqs[i]。具体是分配N个page,将page
的内存一片片分割成req结构大小。然后tags->static_rqs[i]记录每一个req首地址,接着执行磁盘底层驱
动初始化函数,建立request与nvme队列的关系吧*/


ret = blk_mq_alloc_rqs(set, set->tags[hctx_idx], hctx_idx,
set->queue_depth);
if (!ret)
return true;

blk_mq_free_rq_map(set->tags[hctx_idx]);
set->tags[hctx_idx] = NULL;
return false;
}

blk_mq_alloc_rq_map()函数只分配struct blk_mq_tags *tags的tags->static_rqs[]这个req指针数组,实际分配req是在blk_mq_alloc_rqs()函数,源码如下。

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

struct blk_mq_tags *blk_mq_alloc_rq_map(struct blk_mq_tag_set *set,
unsigned int hctx_idx,
unsigned int nr_tags,
unsigned int reserved_tags)
{
struct blk_mq_tags *tags;
int node;

node = blk_mq_hw_queue_to_node(set->mq_map, hctx_idx);
if (node == NUMA_NO_NODE)
node = set->numa_node;

//分配一个每个硬件队列结构独有的blk_mq_tags结构,设置其成员nr_reserved_tags和nr_tags,分配
//blk_mq_tags的bitmap_tags、breserved_tags结构
tags = blk_mq_init_tags(nr_tags, reserved_tags, node,
BLK_MQ_FLAG_TO_ALLOC_POLICY(set->flags));
if (!tags)
return NULL;


//分配nr_tags个struct request *指针赋于tags->rqs[],不是分配struct request结构
tags->rqs = kzalloc_node(nr_tags * sizeof(struct request *),
GFP_NOIO | __GFP_NOWARN | __GFP_NORETRY,
node);
if (!tags->rqs) {
blk_mq_free_tags(tags);
return NULL;
}
//分配nr_tags个struct request *指针赋予tags->static_rqs[]
tags->static_rqs = kzalloc_node(nr_tags * sizeof(struct request *),
GFP_NOIO | __GFP_NOWARN | __GFP_NORETRY,
node);
if (!tags->static_rqs) {
kfree(tags->rqs);
blk_mq_free_tags(tags);
return NULL;
}

return tags;
}

接着是初始化的后半部分,在 blk_mq_init_queue()中完成。该函数主要分配块设备的运行队列request_queue,接着分配每个CPU专属的软件队列并初始化,分配硬件队列并初始化,然后建立软件队列和硬件队列的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct request_queue *blk_mq_init_queue(struct blk_mq_tag_set *set)
{
struct request_queue *uninit_q, *q;

//分配struct request_queue并初始化
uninit_q = blk_alloc_queue_node(GFP_KERNEL, set->numa_node);
if (!uninit_q)
return ERR_PTR(-ENOMEM);

//分配每个CPU专属的软件队列,分配硬件队列,对二者做初始化,并建立软件队列和硬件队列联系
q = blk_mq_init_allocated_queue(set, uninit_q);
if (IS_ERR(q))
blk_cleanup_queue(uninit_q);

return q;
}
EXPORT_SYMBOL(blk_mq_init_queue);

blk_mq_init_queue函数整体来说,是创建request_queue运行队列并初始化其成员,分配每个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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

struct request_queue *blk_mq_init_allocated_queue(struct blk_mq_tag_set *set,
struct request_queue *q)
{
/* mark the queue as mq asap */
q->mq_ops = set->ops;

q->poll_cb = blk_stat_alloc_callback(blk_mq_poll_stats_fn,
blk_mq_poll_stats_bkt,
BLK_MQ_POLL_STATS_BKTS, q);
if (!q->poll_cb)
goto err_exit;

//为每个CPU分配一个软件队列struct blk_mq_ctx,软件队列结构在这里分配
q->queue_ctx = alloc_percpu(struct blk_mq_ctx);
if (!q->queue_ctx)
goto err_exit;

/* init q->mq_kobj and sw queues' kobjects */
blk_mq_sysfs_init(q);

//分配硬件队列,这看着也是每个CPU分配一个queue_hw_ctx指针
q->queue_hw_ctx = kzalloc_node(nr_cpu_ids * sizeof(*(q->queue_hw_ctx)),
GFP_KERNEL, set->numa_node);
if (!q->queue_hw_ctx)
goto err_percpu;
//赋值q->mq_map,这个数组保存了每个CPU对应的硬件队列编号
q->mq_map = set->mq_map;


/* 1 循环分配每个硬件队列结构blk_mq_hw_ctx并初始化,即对每个struct blk_mq_hw_ctx *hctx硬件队列结构大部
分成员赋初值。重点是赋值hctx->tags=blk_mq_tags,即每个硬件队列唯一对应一个blk_mq_tags,blk_mq_tags来自
struct blk_mq_tag_set 的成员struct blk_mq_tags[hctx_idx]。然后分配hctx->ctxs软件队列指针数组,注意只是指针数
组!
2 为硬件队列结构hctx->sched_tags分配blk_mq_tags,这是调度算法的tags。接着根据为这个blk_mq_tags分配
q->nr_requests个request,存于tags->static_rqs[],这是调度算法的blk_mq_tags的request!
*/

...

INIT_DELAYED_WORK(&q->requeue_work, blk_mq_requeue_work);
INIT_LIST_HEAD(&q->requeue_list);
spin_lock_init(&q->requeue_lock);

//设置rq的make_request_fn为blk_mq_make_request,request处理函数
blk_queue_make_request(q, blk_mq_make_request);

/*
* Do this after blk_queue_make_request() overrides it...
nr_requests被设置为队列深度
*/
q->nr_requests = set->queue_depth;

/*
* Default to classic polling
*/
q->poll_nsec = -1;

if (set->ops->complete)
blk_queue_softirq_done(q, set->ops->complete);


/*依次取出每个CPU唯一的软件队列struct blk_mq_ctx *__ctx ,__ctx->cpu记录CPU编号,还根据CPU编号取出该CPU对应的硬件队列blk_mq_hw_ctx*/
blk_mq_init_cpu_queues(q, set->nr_hw_queues);

//共享tag设置
blk_mq_add_queue_tag_set(set, q);

/*1:根据CPU编号依次取出每一个软件队列,再根据CPU编号取出硬件队列struct blk_mq_hw_ctx *hctx,对硬件队
列结构的hctx->ctxs[]赋值软件队列结构
2:根据硬件队列数,依次从q->queue_hw_ctx[i]数组取出硬件队列结构体struct blk_mq_hw_ctx *hctx,然后对
hctx->tags赋值blk_mq_tags结构,前边的blk_mq_realloc_hw_ctxs()函数已经对hctx->tags赋值blk_mq_tags结构
*/
blk_mq_map_swqueue(q);

...
}
EXPORT_SYMBOL(blk_mq_init_allocated_queue);

简单总结来说,blk_mq_init_allocated_queue函数负责分配每个CPU专属的软件队列,分配硬件队列,对二者做初始化,分配,并建立软件队列和硬件队列联系。该函数中调用的blk_mq_realloc_hw_ctxs()、blk_mq_map_swqueue()是两个重点函数,下文列出了源码注释。

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

static void blk_mq_realloc_hw_ctxs(struct blk_mq_tag_set *set,
struct request_queue *q)
{
int i, j;
struct blk_mq_hw_ctx **hctxs = q->queue_hw_ctx;

blk_mq_sysfs_unregister(q);

/* protect against switching io scheduler */
mutex_lock(&q->sysfs_lock);
for (i = 0; i < set->nr_hw_queues; i++) {
int node;

if (hctxs[i])
continue;

node = blk_mq_hw_queue_to_node(q->mq_map, i);
//分配每一个硬件队列结构blk_mq_hw_ctx
hctxs[i] = kzalloc_node(blk_mq_hw_ctx_size(set),
GFP_KERNEL, node);
...

atomic_set(&hctxs[i]->nr_active, 0);
hctxs[i]->numa_node = node;
hctxs[i]->queue_num = i;

/* 1 为分配的struct blk_mq_hw_ctx *hctx 硬件队列结构大部分成员赋初值。重点是赋值hctx->tags=blk_mq_tags,
即每个硬件队列唯一对应一个blk_mq_tags,blk_mq_tags来自struct blk_mq_tag_set 的成员struct blk_mq_tags[hctx_idx]。
然后分配hctx->ctxs软件队列指针数组,注意只是指针数组!
2 为硬件队列结构hctx->sched_tags分配blk_mq_tags,这是调度算法的tags。接着为这个blk_mq_tags分配
q->nr_requests个request,存于tags->static_rqs[],这是调度算法的blk_mq_tags的request!
*/

if (blk_mq_init_hctx(q, set, hctxs[i], i)) {
...
}
...
//设置硬件队列数
q->nr_hw_queues = i;
mutex_unlock(&q->sysfs_lock);
blk_mq_sysfs_register(q);
}

blk_mq_realloc_hw_ctxs()函数很重要,分配每一个硬件队列具体的数据结构blk_mq_hw_ctx。然后主要为该结构的tags和sched_tags成员,分配赋值每个硬件队列必须的blk_mq_tags。之后进行IO传输前,要从hctx->tags-> static_rqs[]或者hctx->sched_tags-> static_rqs[]分配一个req。该函数中调用的blk_mq_init_hctx()主要是初始化blk_mq_hw_ctx硬件队列成员,分配调度算法hctx->sched_tags需要的blk_mq_tags。

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

static int blk_mq_init_hctx(struct request_queue *q,
struct blk_mq_tag_set *set,
struct blk_mq_hw_ctx *hctx, unsigned hctx_idx)
{
...
//运行队列
hctx->queue = q;
hctx->flags = set->flags & ~BLK_MQ_F_TAG_SHARED;

cpuhp_state_add_instance_nocalls(CPUHP_BLK_MQ_DEAD, &hctx->cpuhp_dead);

//赋值hctx->tags的blk_mq_tags,每个硬件队列对应一个blk_mq_tags,这个tags在__blk_mq_alloc_rq_map()中赋值
hctx->tags = set->tags[hctx_idx];

/*
* Allocate space for all possible cpus to avoid allocation at
* runtime
*/
//为每个CPU分配软件队列blk_mq_ctx指针,只是指针
hctx->ctxs = kmalloc_node(nr_cpu_ids * sizeof(void *),
GFP_KERNEL, node);
if (!hctx->ctxs)
goto unregister_cpu_notifier;

if (sbitmap_init_node(&hctx->ctx_map, nr_cpu_ids, ilog2(8), GFP_KERNEL,
node))
goto free_ctxs;

hctx->nr_ctx = 0;

if (set->ops->init_hctx &&
set->ops->init_hctx(hctx, set->driver_data, hctx_idx))
goto free_bitmap;

/*为硬件队列结构hctx->sched_tags分配blk_mq_tags,一个硬件队列一个blk_mq_tags,这是调度算法的blk_mq_tags,
与硬件队列专属的blk_mq_tags不一样。然后根据为这个blk_mq_tags分配q->nr_requests个request,存于
tags->static_rqs[]*/

if (blk_mq_sched_init_hctx(q, hctx, hctx_idx))
goto exit_hctx;
...
}

blk_mq_map_swqueue()函数主要作用是,根据CPU编号取出硬件队列结构struct blk_mq_ctx *ctx和软件队列结构struct blk_mq_ctx *ctx,然后把软件队列结构赋值给硬件队列结构,即hctx->ctxs[hctx->nr_ctx++] = ctx,相当于完成硬件队列与软件队列的映射。

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
67
68

static void blk_mq_map_swqueue(struct request_queue *q)
{
...
/*
* Map software to hardware queues.
*
* If the cpu isn't present, the cpu is mapped to first hctx.
*/
/*根据CPU编号依次取出每一个软件队列,再根据CPU编号取出硬件队列struct blk_mq_hw_ctx *hctx,对硬件
队列结构的hctx->ctxs[]赋值软件队列结构blk_mq_ctx*/
for_each_present_cpu(i) {

//根据CPU编号取出硬件队列编号
hctx_idx = q->mq_map[i];
/* unmapped hw queue can be remapped after CPU topo changed */
if (!set->tags[hctx_idx] &&
!__blk_mq_alloc_rq_map(set, hctx_idx)) {
/*
* If tags initialization fail for some hctx,
* that hctx won't be brought online. In this
* case, remap the current ctx to hctx[0] which
* is guaranteed to always have tags allocated
*/
q->mq_map[i] = 0;
}
//根据CPU编号取出每个CPU对应的软件队列结构struct blk_mq_ctx *ctx
ctx = per_cpu_ptr(q->queue_ctx, i);
//根据CPU编号取出每个CPU对应的硬件队列struct blk_mq_hw_ctx *hctx
hctx = blk_mq_map_queue(q, i);

cpumask_set_cpu(i, hctx->cpumask);

/*硬件队列关联的第几个软件队列。硬件队列每关联一个软件队列,都hctx->ctxs[hctx->nr_ctx++] = ctx,把
软件队列结构保存到hctx->ctxs[hctx->nr_ctx++],即硬件队列结构的hctx->ctxs[]数组,而ctx->index_hw会先保存
hctx->nr_ctx*/

ctx->index_hw = hctx->nr_ctx;
//软件队列结构以hctx->nr_ctx为下标保存到hctx->ctxs[]
hctx->ctxs[hctx->nr_ctx++] = ctx;
}

mutex_unlock(&q->sysfs_lock);
/*根据硬件队列数,依次从q->queue_hw_ctx[i]数组取出硬件队列结构体struct blk_mq_hw_ctx *hctx,然后对
hctx->tags赋值blk_mq_tags结构*/
queue_for_each_hw_ctx(q, hctx, i) {
/*
* If no software queues are mapped to this hardware queue,
* disable it and free the request entries.
*/
if (!hctx->nr_ctx) {
/* Never unmap queue 0. We need it as a
* fallback in case of a new remap fails
* allocation
*/
if (i && set->tags[i])
blk_mq_free_map_and_requests(set, i);

hctx->tags = NULL;
continue;
}
//i是硬件队列编号,这是根据硬件队列编号i从struct blk_mq_tag_set *set取出硬件队列专属的blk_mq_tags
hctx->tags = set->tags[i];
WARN_ON(!hctx->tags);

...
}
}

每个CPU对应唯一的软件队列blk_mq_ctx,blk_mq_ctx对应唯一的硬件队列blk_mq_hw_ctx,blk_mq_hw_ctx对应唯一的blk_mq_tags。我们在进行发起bio请求后,需要从blk_mq_tags结构的相关成员分配一个tag(其实是一个数字),再根据tag分配一个req,最后才能进行IO派发,磁盘数据传输。

6. request的分配与发送

在多队列io中,blk_mq_make_request函数负责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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

static void blk_mq_make_request(struct request_queue *q, struct bio *bio)
{
//遍历当前进程plug_list链表上的所有req,检查bio和req代表的磁盘范围是否挨着,挨着则把bio合并到req
if (!is_flush_fua && !blk_queue_nomerges(q) &&blk_attempt_plug_merge(q, bio, &request_count, &same_queue_rq))
return;

//在IO调度器队列里查找是否有可以合并的req,找到则可以bio后项或前项合并到req,还会触发二次合并
if (blk_mq_sched_bio_merge(q, bio))
return;

//依次遍历软件队列ctx->rq_list链表上的req,然后看req能否与bio前项或者后项合并
if (blk_mq_merge_bio(q, bio))
return;

/*从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag,然后req =
tags->static_rqs[tag]从static_rqs[]分配一个req,再req->tag=tag。接着hctx->tags->rqs[rq->tag] = rq,一个req必须分配一
个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝试分配tag*/
rq = blk_mq_sched_get_request(q, bio, bio->bi_rw, &data);
if (unlikely(!rq))
return;

plug = current->plug;
if (unlikely(is_flush_fua)) {//如果是flush fua请求
//赋值req扇区起始地址,req结束地址,rq->bio = rq->biotail=bio,并且统计磁盘使用率等数据
blk_mq_bio_to_request(rq, bio);
//将request插入到flush队列
blk_insert_flush(rq);
//启动req磁盘硬件队列异步派送
blk_mq_run_hw_queue(data.hctx, true);

}else if (plug && q->nr_hw_queues == 1){// 如果进程使用plug链表plug IO,并且硬件队列数是1
//赋值req扇区起始地址,req结束地址,rq->bio = rq->biotail=bio,并且统计磁盘使用率等数据
blk_mq_bio_to_request(rq, bio);
//只是先把req添加到plug->mq_list链表上,等后续再一次性把plug->mq_list链表req向块设备驱动派发
list_add_tail(&rq->queuelist, &plug->mq_list)

}else if (plug && !blk_queue_nomerges(q)) {//如果进程使用plug链表plug IO,并且是硬件多队列
//赋值req扇区起始地址,req结束地址,rq->bio = rq->biotail=bio,统计磁盘使用率等数据
blk_mq_bio_to_request(rq, bio);
//将req直接派发到设备驱动,如果块设备驱动层繁忙也会执行blk_mq_run_hw_queue将req异步派发给驱动
blk_mq_try_issue_directly(data.hctx, same_queue_rq);

}else if ((q->nr_hw_queues > 1 && is_sync) || (!q->elevator &&
!data.hctx->dispatch_busy)) {//如果是硬件多队列的write sync操作或者不使用调度器且硬件队列不繁忙
//赋值req扇区起始地址,req结束地址,rq->bio = rq->biotail=bio,并且统计磁盘使用率等数据
blk_mq_bio_to_request(rq, bio);
//将req直接派发到设备驱动,如果块设备驱动层繁忙也会执行blk_mq_run_hw_queue将req异步派发给驱动
blk_mq_try_issue_directly(data.hctx, rq);

}else if (q->elevator) {//使用调度器
//赋值req扇区起始地址,req结束地址,rq->bio = rq->biotail=bio,并且统计磁盘使用率等数据
blk_mq_bio_to_request(rq, bio);
//将req插入IO调度器队列,并执行blk_mq_run_hw_queue()将IO派发到块设备驱动
blk_mq_sched_insert_request(rq, false, true, true);

}else {
//赋值req扇区起始地址,req结束地址,rq->bio = rq->biotail=bio,并且统计磁盘使用率等数据
blk_mq_bio_to_request(rq, bio);
//把req插入到软件队列ctx->rq_list链表
blk_mq_queue_io(data.hctx, data.ctx, rq);
//启动硬件队列上的req异步派发到块设备驱动
blk_mq_run_hw_queue(data.hctx, true);
}
}

每个进程都在task_struct结构体中,维护一个blk_plug *plug;变量,blk_plug定义如下:

1
2
3
4
5
6

struct blk_plug {
struct list_head list; /* requests */
struct list_head mq_list; /* blk-mq requests */
struct list_head cb_list; /* md requires an unplug callback */
};

list:用于缓存请求的队列
mq_list:缓存硬件队列数是1的进程请求,延缓向驱动发送请求
cb_list:回调函数的链表

blk_mq_make_request()函数包含的核心函数较多,基本流程是,
先尝试把bio合并到软件队列或plug队列或调度算法队列。

如果无法合并,则执行blk_mq_sched_get_request()分配tag和req。这里出现”了分配tag”。这是与单队列时代的一个明显区别。

这里先简单介绍一下,多队列IO传输,将bio转换成req(就是sturct request),大体过程是这样的:先根据当前进程所在CPU,找到CPU对应的软件队列blk_mq_ctx(获取过程见blk_mq_get_ctx函数,每个CPU都有一个唯一的软件队列),再根据软件队列blk_mq_ctx得到其映射的硬件队列blk_mq_hw_ctx(获取过程见blk_mq_map_queue函数,每个软件队列对应唯一的硬件队列)。硬件队列blk_mq_hw_ctx结构中有两个关键成员struct blk_mq_tags *tags(针对无调度算法)和struct blk_mq_tags *sched_tags(针对有调度算法)。

有了硬件队列结构blk_mq_hw_ctx,得到其成员struct blk_mq_tags *tags指向的blk_mq_tags结构。blk_mq_tags结构又有几个关键成员:

struct sbitmap_queue bitmap_tags

struct sbitmap_queue breserved_tags

struct request **static_rqs

unsigned int nr_reserved_tags

static_rqs这个指针数组保存了nr_tags个req指针,实际的req结构在前文的__blk_mq_alloc_rq_map->blk_mq_alloc_rqs分配。

struct sbitmap_queue bitmap_tagsstruct sbitmap_queue breserved_tags分析下来有点像ext4 文件系统的inode bitmap,一个bit位表示一个req的使用情况,1为已经分配,0为未分配。

struct sbitmap_queue breserved_tags管理static_rqs[0~ (nr_reserved_tags-1]]这nr_reserved_tags个req,struct sbitmap_queue bitmap_tags管理static_rqs[ nr_reserved_tags ~ nr_tags]这nr_tags- nr_reserved_tags个req。

分配tag和req的一般情况:从struct sbitmap_queue bitmap_tags分析出哪个req是空闲的,返回一个数字,这个数字就是tag,表示了static_rqs[] 中哪个位置的req是空闲的。实际情况tag+ nr_reserved_tags才能表示实际空闲的req在static_rqs[]中的下标。每一个req在派发给驱动时,必须得先分配一个tag。

7.利用bcc工具采集每个cpu核心下对应硬件队列可以存储的请求数

blk_mq_map_queue函数的功能是获取软件队列对应的硬件队列,通过硬件队列结构体blk_mq_hw_ctx中的变量tags->nr_tags和nr_reserved_tags取和获取当前cpu下硬件队列存储的请求数。但是由于内核5.0以上,相关结构体已经改变,没有运行成功,需要在内核4.x上安装bcc运行。

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

from bcc import BPF
from time import sleep

bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
struct proc_key_t {
u32 cpu;
};

struct val_t {
u32 num;
};

BPF_HASH(commbyreq, struct proc_key_t, struct val_t);

int trace_pid_start(struct pt_regs *ctx, struct request_queue *q,
int cpu)
{
struct proc_key_t key = {};
struct val_t val = {};
key.cpu = cpu;
struct blk_mq_hw_ctx * hw = q->queue_hw_ctx[q->mq_map[cpu]];
struct blk_mq_tags * tags = hw->tags;
val.num = tags->nr_tags+tags->nr_reserved_tags;
commbyreq.update(&key, &val);
return 0;
}

"""

# load BPF program
b = BPF(text=bpf_text)
b.attach_kprobe(event="blk_mq_map_queue", fn_name="trace_pid_start")

print("Tracing... Hit Ctrl-C to end.")

# trace until Ctrl-C
while 1:

print("%-6s %-16s" % ("CPU", "NUM"))

# by-PID output
counts = b.get_table("commbyreq")
for k, v in counts:


print("%-6d %-3d " % (k.cpu,
v.num))

counts.clear()

8. blk_mq_sched_get_request()分配tag和req

该函数主要作用是:从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag,然后req = tags->static_rqs[tag]从static_rqs[]分配一个req,再req->tag=tag。接着hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝试分配tag。如果留意的话,这就是上一节介绍的tag和req的分配过程,更详细的步骤看下边的__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

struct request *blk_mq_sched_get_request(struct request_queue *q,
struct bio *bio,
unsigned int op,
struct blk_mq_alloc_data *data)
{
//data->ctx 获取当前进程所属CPU的专有软件队列
data->ctx = blk_mq_get_ctx(q);

//获取软件队列的硬件队列,CPU、软件队列、硬件队列是一一对应关系
data->hctx = blk_mq_map_queue(q, data->ctx->cpu);

if (e) {//使用调度器
//使用调度时设置BLK_MQ_REQ_INTERNAL标志
data->flags |= BLK_MQ_REQ_INTERNAL;
rq = __blk_mq_alloc_request(data, op);
}else{ //无调度器
//同理
rq = __blk_mq_alloc_request(data, op);
}
return rq;
}

__blk_mq_alloc_request函数的大体过程是:从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag,然后req = tags->static_rqs[tag]从static_rqs[]分配一个req,再req->tag=tag。接着hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝试分配tag。函数核心是执行blk_mq_get_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
24
25
26
27
28
29
30
31
32
33
34
35

struct request *__blk_mq_alloc_request(struct blk_mq_alloc_data *data, int rw)
{
/*从硬件队列有关的blk_mq_tags结构体的static_rqs[]数组里得到空闲的request。获取失败则启动硬件IO数据派
发,之后再尝试从blk_mq_tags结构体的static_rqs[]数组里得到空闲的request。注意,这里返回的是空闲的request
在static_rqs[]数组的下标*/
tag = blk_mq_get_tag(data);

if (tag != BLK_MQ_TAG_FAIL) //分配tag成功
{
//有调度器时返回硬件队列的hctx->sched_tags,无调度器时返回硬件队列的hctx->tags
struct blk_mq_tags *tags = blk_mq_tags_from_data(data);
//从tags->static_rqs[tag]得到空闲的req,tag是req在tags->static_rqs[ ]数组的下标
rq = tags->static_rqs[tag]; //这里真正分配得到本次传输使用的req

if (data->flags & BLK_MQ_REQ_INTERNAL) //用调度器时设置
{
rq->tag = -1;
__rq_aux(rq, data->q)->internal_tag = tag;//这是req的tag
}
else
{
//赋值为空闲req在blk_mq_tags结构体的static_rqs[]数组的下标
rq->tag = tag;
__rq_aux(rq, data->q)->internal_tag = -1;
//这里边保存的req是刚从static_rqs[]得到的空闲的req
data->hctx->tags->rqs[rq->tag] = rq;
}

//对新分配的req进行初始化,赋值软件队列、req起始时间等
blk_mq_rq_ctx_init(data->q, data->ctx, rq, rw);
return rq;
}
return NULL;
}

9. request direct直接派发

blk_mq_try_issue_directly() 将req direct直接派发给磁盘设备驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

static void blk_mq_try_issue_directly(struct blk_mq_hw_ctx *hctx,
struct request *rq)
{
/*从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,
然后hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝
试分配tag,循环,直到分配req成功。然后调用磁盘驱动queue_rq接口函数向驱动派发req,启动磁盘数据传输。如果
遇到磁盘驱动硬件忙,则释放req的tag,设置硬件队列忙*/
ret = __blk_mq_try_issue_directly(hctx, rq, false);

//如果硬件队列忙,把req添加到硬件队列hctx->dispatch队列,间接启动req硬件派发
if (ret == BLK_MQ_RQ_QUEUE_BUSY || ret == BLK_MQ_RQ_QUEUE_DEV_BUSY)
blk_mq_request_bypass_insert(rq, true);

/*req磁盘数据传输完成了,增加ios、ticks、time_in_queue、io_ticks、flight、sectors扇区数等使用计数。依次取出req->bio
链表上所有req对应的bio,一个一个更新bio结构体成员数据,执行bio的回调函数。还更新req->__data_len和
req->buffer*/
else if (ret != BLK_MQ_RQ_QUEUE_OK)
blk_mq_end_request(rq, ret);
}

核心是执行 __blk_mq_try_issue_directly()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

static int __blk_mq_try_issue_directly(struct blk_mq_hw_ctx *hctx,
struct request *rq,
bool bypass_insert)
{
/*从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,然后hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,
之后再尝试分配tag,循环。但是如果req已经有tag 了,会直接返回,不用再分配tag*/
blk_mq_get_driver_tag(rq, NULL, false));

/*调用磁盘驱动queue_rq接口函数,根据req设置command,把req添加到q->timeout_list,并且启动q->timeout
定时器,把新的command复制到sq_cmds[]命令队列,这看着是req直接发给磁盘驱动进行数据传输了。如果遇到磁盘
驱动硬件忙,则设置硬件队列忙,还释放req的tag。*/
return __blk_mq_issue_directly(hctx, rq);
}

__blk_mq_try_issue_directly函数的详细流程:从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,然后hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝试分配tag,循环,直到分配req成功。然后调用磁盘驱动queue_rq接口函数向驱动派发req,启动磁盘数据传输。如果遇到磁盘驱动硬件忙,则释放req的tag,设置硬件队列忙。

其实核心就两点:1为req分配tag;2把req派发给磁盘驱动,启动磁盘数据传输。有人可能会问,不是在blk_mq_make_request->blk_mq_sched_get_request 中已经为req分配过tag了,为什么这里还要再分配?关于这一点,我的分析是,如果req已经分配过tag了,执行blk_mq_get_driver_tag函数(下一节讲)会直接返回。但是会存在这种情况,req在派发给磁盘驱动时,磁盘驱动硬件繁忙,派发失败,则会把req加入硬件队列hctx->dispatch链表,然后把req的tag释放掉,则req->tag=-1,等空闲时派发该req。好的,空闲时间来了,再次派发该req,此时就需要执行blk_mq_get_driver_tag为req重新分配一个tag。一个req在派发给驱动时,必须分配一个tag!

__blk_mq_issue_directly是直接将req派发给驱动的核心函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

static int __blk_mq_issue_directly(struct blk_mq_hw_ctx *hctx, struct request *rq)
{
//根据req设置磁盘驱动 command,把req添加到q->timeout_list,并且启动q->timeout,把command复制到nvmeq->sq_cmds[]队列等等
ret = q->mq_ops->queue_rq(hctx, &bd);
switch (ret) {
case BLK_MQ_RQ_QUEUE_OK://成功把req派发给磁盘硬件驱动
blk_mq_update_dispatch_busy(hctx, false);//设置硬件队列不忙,看着就hctx->dispatch_busy = ewma
break;
case BLK_MQ_RQ_QUEUE_BUSY:
case BLK_MQ_RQ_QUEUE_DEV_BUSY://这是遇到磁盘硬件驱动繁忙,req没有派送给驱动
blk_mq_update_dispatch_busy(hctx, true);//设置硬件队列忙
//硬件队列繁忙,则从tags->bitmap_tags或者breserved_tags中按照req->tag这个tag编号释放tag
__blk_mq_requeue_request(rq);
break;
default:
//标记硬件队列不忙
blk_mq_update_dispatch_busy(hctx, false);
break;
}
return ret;
}

基本是调用磁盘驱动层的函数,将req有关的磁盘传输信息发送给驱动,然后会进行磁盘数据传输。如果遇到磁盘驱动硬件忙,则设置硬件队列忙,还释放req的tag。

blk_mq_get_driver_tag() 在req派发给驱动前分配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
bool blk_mq_get_driver_tag(struct request *rq, struct blk_mq_hw_ctx **hctx,
bool wait)
{
/*如果req对应的tag没有被释放,则直接返回完事,其实还有一种情况rq->tag被置-1。就是__blk_mq_alloc_request()
函数分配过tag和req后,如果使用了调度器,则rq->tag = -1。这种情况,rq->tag != -1也成立,但是再直接执行
blk_mq_get_driver_tag()分配tag也没啥意思呀,因为tag已经分配过了。所以感觉该函数主要还是针对req因磁盘硬
件驱动繁忙无法派送,然后释放了tag,再派发时分配tag的情况。*/
if (rq->tag != -1)
goto done;

//判断tag是否预留的,是则加上BLK_MQ_REQ_RESERVED标志
if (blk_mq_tag_is_reserved(data.hctx->sched_tags, rq_aux(rq)->internal_tag))
data.flags |= BLK_MQ_REQ_RESERVED;

/*从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,
然后hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再
尝试分配tag,循环。*/
rq->tag = blk_mq_get_tag(&data);

//对hctx->tags->rqs[rq->tag]赋值
data.hctx->tags->rqs[rq->tag] = rq;
//之所以这里重新赋值,是因为blk_mq_get_tag中可能会休眠,等再次唤醒进程所在CPU就变了,就会重新获取
一次硬件队列保存到data.hctx
*hctx = data.hctx;

//分配成功返回1
return rq->tag != -1;
}

从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,然后hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝试分配tag,循环。

可以发现,该函数本质还是调用前文介绍过的blk_mq_get_tag()去硬件队列的blk_mq_tags结为req分配一个tag。该函数只是分配tag,没有分配req。blk_mq_get_driver_tag()存在的意义是:req派发给磁盘驱动时,遇到磁盘硬件队列繁忙,无法派送,则释放掉tag,req加入硬件hctx->dispatch链表异步派发。等再次派发时,就会执行blk_mq_get_driver_tag()为req分配一个tag。

10. 软件队列ctx->rq_list、硬件hctx->dispatch、IO算调度法队列链表上的req派发

blk_mq_try_issue_directly()类的req direct 派发是针对单个req的,blk_mq_run_hw_queue()是派发类软件队列ctx->rq_list、硬件hctx->dispatch链表、IO调度算法队列上的req的,这是二者最大的区别。

在这里插入图片描述

启动硬件队列上的req派发到块设备驱动。

1
2
3
4
5
6
7
8
9
bool blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx, bool async)//async为true表示异步传输,false表示同步
{
//有req需要硬件传输
if (need_run) {
__blk_mq_delay_run_hw_queue(hctx, async, 0);
return true;
}
return false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void __blk_mq_delay_run_hw_queue(struct blk_mq_hw_ctx *hctx, bool async,//async为true则异步,false则同步传输unsigned long msecs)// msecs决定派发延时
{

//同步传输
if (!async && !(hctx->flags & BLK_MQ_F_BLOCKING)) {
/*各种各样场景的req派发,hctx->dispatch硬件队列dispatch链表上的req派发;有deadline调度算法时红黑树或
者fifo调度队列上的req派发;无IO调度器时,硬件队列关联的所有软件队列ctx->rq_list上的req的派发等等。派发过程
应该都是调用blk_mq_dispatch_rq_list(),磁盘驱动硬件不忙直接启动req传输,繁忙的话则把剩余的req转移到
hctx->dispatch队列,然后启动异步传输*/

__blk_mq_run_hw_queue(hctx);
return;
}

/*启动异步传输,开启kblockd_workqueue内核线程workqueue,异步执行hctx->run_work对应的work函数
blk_mq_run_work_fn, 实际blk_mq_run_work_fn里执行的还是__blk_mq_run_hw_queue*/
kblockd_mod_delayed_work_on(blk_mq_hctx_next_cpu(hctx), &hctx->run_work,msecs_to_jiffies(msecs));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

static void __blk_mq_run_hw_queue(struct blk_mq_hw_ctx *hctx)
{
//硬件队列锁,这时如果是同一个硬件队列,就有锁抢占了
hctx_lock(hctx, &srcu_idx);

/*各种各样场景的req派发,hctx->dispatch硬件队列dispatch链表上的req派发;有deadline调度算法时红黑树或者fifo
调度队列上的req派发;无IO调度算法时,硬件队列关联的所有软件队列ctx->rq_list上的req的派发等等。派发
都是调用blk_mq_dispatch_rq_list(),磁盘驱动硬件不忙直接启动req传输,繁忙的话则把剩余的req转移到
hctx->dispatch队列,然后启动异步传输*/
blk_mq_sched_dispatch_requests(hctx);

hctx_unlock(hctx, srcu_idx);
}

核心是调用blk_mq_sched_dispatch_requests ()函数。blk_mq_sched_dispatch_requests()函数派发各种队列的req。

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
void blk_mq_sched_dispatch_requests(struct blk_mq_hw_ctx *hctx)
{
LIST_HEAD(rq_list);

if (!list_empty_careful(&hctx->dispatch)) {
//把hctx->dispatch链表上的req转移到局部rq_list
list_splice_init(&hctx->dispatch, &rq_list);
}

//如果hctx->dispatch上有req要派发,hctx->dispatch链表上的req已经转移到rq_list
if (!list_empty(&rq_list))
{
//这里设置了hctx->state的BLK_MQ_S_SCHED_RESTART标志位
blk_mq_sched_mark_restart_hctx(hctx);

/*rq_list上的req来自hctx->dispatch硬件派发队列,遍历list上的req,先给req在硬件队列hctx的blk_mq_tags
里分配一个空闲tag,就是建立req与硬件队列的联系吧,然后把req派发给块设备驱动。看着任一个req要启动硬
件传输,都要从blk_mq_tags结构里得到一个空闲的tag。如果磁盘驱动硬件繁忙,还要把list剩余的req转移到
hctx->dispatch,启动异步传输。下发给块设备驱动的req成功减失败总个数不为0返回true。否则false。*/
if (blk_mq_dispatch_rq_list(q, &rq_list, false))
{
if (has_sched_dispatch) //有调度器则接着派发调度器队列上的req
//派发调度器队列上的req
blk_mq_do_dispatch_sched(hctx);
else
//派发硬件队列绑定的所有软件队列上的req
blk_mq_do_dispatch_ctx(hctx);
}
}
else if (has_sched_dispatch){
blk_mq_do_dispatch_sched(hctx); //派发调度器队列上的req
} else if (hctx->dispatch_busy){
blk_mq_do_dispatch_ctx(hctx); //派发硬件队列绑定的所有软件队列上的req
}else{
//把硬件队列hctx关联的软件队列上的ctx->rq_list链表上req转移到传入的rq_list链表
blk_mq_flush_busy_ctxs(hctx, &rq_list);

/*遍历rq_list上的req,先给req在硬件队列hctx的blk_mq_tags里分配一个空闲tag,然后把req派发给块设备
驱动。如果遇到块设备驱动层繁忙,则把req再加入hctx->dispatch异步派发*/
blk_mq_dispatch_rq_list(q, &rq_list, false);
}
}

总结下来,主要是3种情况
1 执行blk_mq_dispatch_rq_list()派发硬件队列hctx->dispatch链表上的req
2执行blk_mq_do_dispatch_sched()派发调度器队列上的req。
3执行blk_mq_do_dispatch_ctx ()函数派发软件队列ctx->rq_list链表上的req。

blk_mq_do_dispatch_sched() 和blk_mq_flush_busy_ctxs()最后也是指定的blk_mq_dispatch_rq_list()函数进行实际的派发。

10.1 blk_mq_do_dispatch_sched()派发调度器队列上的req

这里以deadline调度算法为例。执行deadline算法派发函数,循环从fifo或者红黑树队列选择待派发给传输的req,然后给req在硬件队列hctx的blk_mq_tags里分配一个空闲tag,接着把req派发给块设备驱动。如果磁盘驱动硬件繁忙,则把req转移到hctx->dispatch队列,然后启动req异步传输。硬件队列繁忙或者算法队列没有req了则跳出循环返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

static void blk_mq_do_dispatch_sched(struct blk_mq_hw_ctx *hctx)
{
LIST_HEAD(rq_list);
do {
/*执行deadline算法派发函数,从fifo或者红黑树队列选择待派发的req返回。然后设置新的next_rq,并把
req从fifo队列和红黑树队列剔除,req来源有:上次派发设置的next_rq;read req派发过多而选择的write req;fifo 队列上
超时要传输的req,统筹兼顾,有固定策略*/
rq = e->aux->ops.mq.dispatch_request(hctx);//dd_dispatch_request

//把选择出来派发的req加入局部变量rq_list链表
list_add(&rq->queuelist, &rq_list);
// blk_mq_dispatch_rq_list()才会调度算法队列上的req进行派发
}
while (blk_mq_dispatch_rq_list(q, &rq_list, true))
}

10.2 blk_mq_do_dispatch_ctx ()派发软件队列ctx->rq_list链表上的req

循环遍历hctx硬件队列关联的所有软件队列上ctx->rq_list链表的req,给req在硬件队列hctx的blk_mq_tags里分配一个空闲tag,然后把req派发给块设备驱动。如果遇到块设备驱动层繁忙,则把req再加入hctx->dispatch链表异步派发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

static void blk_mq_do_dispatch_ctx(struct blk_mq_hw_ctx *hctx)
{
//依次遍历hctx硬件队列关联的所有软件队列
do {
//从软件队列ctx->rq_list链表取出req,然后从软件队列中剔除req。接着清除hctx->ctx_map中软件队列对应的标志位???????
rq = blk_mq_dequeue_from_ctx(hctx, ctx);
if (!rq) {
break;
}
//req加入到rq_list
list_add(&rq->queuelist, &rq_list);
//取出硬件队列关联的下一个软件队列
ctx = blk_mq_next_ctx(hctx, rq->mq_ctx);

// blk_mq_dispatch_rq_list()中才完成对rq_list链表上的软件队列的req的派发
} while (blk_mq_dispatch_rq_list(q, &rq_list, true))
}

10.3 blk_mq_dispatch_rq_list()实际完成对各个队列上的req的派发

list来自hctx->dispatch硬件派发队列、软件队列rq_list链表上、调度算法队列等req。遍历list上的req,先给req在硬件队列hctx的blk_mq_tags里分配一个空闲tag,然后调用磁盘驱动queue_rq函数派发req。任一个req要启动硬件传输前,都要从blk_mq_tags结构里得到一个空闲的tag。如果遇到磁盘驱动硬件繁忙,则要把这个派发失败的req再添加list链表,再把list链表上的所有req转移到hctx->dispatch队列,之后启动异步派发时再从hctx->dispatch链表上取出这些req派发。下发给驱动的req成功减失败总个数不为0返回true,其他返回false。

可以发现一个规律,最终肯定调用磁盘驱动的queue_rq函数才能把req派送给磁盘驱动,然后才能进行磁盘数据传输。但是该函数有三个返回值,一是BLK_MQ_RQ_QUEUE_OK,表示派送req成功;BLK_MQ_RQ_QUEUE_DEV_BUSY或者BLK_MQ_RQ_QUEUE_BUSY,表示磁盘驱动硬件繁忙,则无法派送req,需要把req再放入hctx->dispatch链表,之后进行异步派发。异步派发最后还是执行blk_mq_dispatch_rq_list()函数。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
bool blk_mq_dispatch_rq_list(struct request_queue *q, struct list_head *list,
bool got_budget)
{
do {
//从list链表取出一个req
rq = list_first_entry(list, struct request, queuelist);
/*先根据rq->mq_ctx->cpu这个CPU编号从q->mq_map[cpu]找到硬件队列编号,再q->queue_hw_ctx
[硬件队列编号]返回硬件队列唯一的blk_mq_hw_ctx结构体,每个CPU都对应了唯一的硬件队列*/
hctx = blk_mq_map_queue(rq->q, rq->mq_ctx->cpu);

/*从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,然后hctx->tags->rqs[rq->tag] = rq,一个req必须分配一个tag才能IO传输。分配失败则启动硬件IO数据派发,之后再尝试分配tag 。分配成功返回true,一般情况都分配成功*/
if (!blk_mq_get_driver_tag(rq, NULL, false)) {

//获取tag失败,则要尝试开始休眠了,再尝试分配,函数返回时获取tag就成功了
if (!blk_mq_mark_tag_wait(&hctx, rq)) {
blk_mq_put_dispatch_budget(hctx);
//如果还是分配tag失败,但是硬件队列有共享tag标志
if (hctx->flags & BLK_MQ_F_TAG_SHARED)
no_tag = true;//设置no_tag标志位
//直接跳出循环,不再进行req派发
break;
}
}

//从list链表剔除req
list_del_init(&rq->queuelist);
//根据req设置nvme_command,把req添加到q->timeout_list,并且启动q->timeout,把新的cmd复制到
nvmeq->sq_cmds[]队列。真正把req派发给驱动,启动硬件nvme硬件传输
ret = q->mq_ops->queue_rq(hctx, &bd);//nvme_queue_rq
switch (ret) {
case BLK_MQ_RQ_QUEUE_OK://传输完成,queued++表示传输完成的req
queued++;
break;
case BLK_MQ_RQ_QUEUE_BUSY:
case BLK_MQ_RQ_QUEUE_DEV_BUSY:
if (!list_empty(list)) {
nxt = list_first_entry(list, struct request, queuelist);
blk_mq_put_driver_tag(nxt);
}
//磁盘驱动硬件繁忙,要把本次派送的req再添加到list链表
list_add(&rq->queuelist, list);
//tags->bitmap_tags中按照req->tag把req的tag编号释放掉,与blk_mq_get_driver_tag()获取tag相反
__blk_mq_requeue_request(rq);
break;
default:
pr_err("blk-mq: bad return on queue: %d\n", ret);
case BLK_MQ_RQ_QUEUE_ERROR:
errors++;//下发给驱动时出错errors加1,这种情况一般不会有吧,除非磁盘硬件有问题了
rq->errors = -EIO;
blk_mq_end_request(rq, rq->errors);
break;
}
//如果磁盘驱动硬件繁忙,break跳出do...while循环
if (ret == BLK_MQ_RQ_QUEUE_BUSY || ret == BLK_MQ_RQ_QUEUE_DEV_BUSY)
break;

}while (!list_empty(list));

//list链表不空,说明磁盘驱动硬件繁忙,有部分req没有派送给驱动
if (!list_empty(list)) {
//这里是把list链表上没有派送给驱动的的req再移动到hctx->dispatch链表
list_splice_init(list, &hctx->dispatch);

/*因为硬件队列繁忙没有把hctx->dispatch上的req全部派送给驱动,则下边就再执行一次
blk_mq_run_hw_queue()或者blk_mq_delay_run_hw_queue(),再进行一次异步派发,就那几招,一个套路*/

//测试hctx->state是否设置了BLK_MQ_S_SCHED_RESTART位,blk_mq_sched_dispatch_requests()就会设置这个标志位
needs_restart = blk_mq_sched_needs_restart(hctx);
if (!needs_restart ||(no_tag && list_empty_careful(&hctx->dispatch_wait.task_list)))
//再次调用blk_mq_run_hw_queue()启动异步req派发true表示允许异步
blk_mq_run_hw_queue(hctx, true);

//如果设置了BLK_MQ_S_SCHED_RESTART标志位,并且磁盘驱动硬件繁忙导致了部分req没有来得及传输完
else if (needs_restart && (ret == BLK_MQ_RQ_QUEUE_BUSY))
//调用blk_mq_delay_run_hw_queue,但这次是异步传输,即开启kblockd_workqueue内核线程派发req
blk_mq_delay_run_hw_queue(hctx, BLK_MQ_RESOURCE_DELAY);

//更新hctx->dispatch_busy,设置硬件队列繁忙
blk_mq_update_dispatch_busy(hctx, true);
//返回false,说明硬件队列繁忙
return false;
}

if (ret == BLK_MQ_RQ_QUEUE_BUSY || ret == BLK_MQ_RQ_QUEUE_DEV_BUSY)
return false;//返回false表示硬件队列忙

/*queued表示成功派发给驱动的req个数,errors表示下发给驱动时出错的req个数,二者加起来不为0才返回非。下发给驱动的req成功减失败总个数不为0返回true*/
return (queued + errors) != 0;
}

11. request plug形式的派发

blk_flush_plug_list()派发plug->mq_list链表上的req。

1
2
3
4
5
6
7
8

void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{
if (!list_empty(&plug->mq_list))
blk_mq_flush_plug_list(plug, from_schedule);
if (list_empty(&plug->list))
return;
}

核心是执行blk_mq_flush_plug_list函数。

函数核心流程:每次循环,取出plug->mq_list上的req,添加到ctx_list局部链表。如果每两次取出的req都属于一个软件队列,只是把这些req添加到局部ctx_list链表,该函数最后执行blk_mq_sched_insert_requests把ctx_list链表上的req进行派发。如果前后两次取出的req不属于一个软件队列,则立即执行blk_mq_sched_insert_requests()将ctx_list链表已经保存的req进行派发,然后把本次循环取出的req继续添加到ctx_list局部链表。

简单来说,blk_mq_sched_insert_requests()只会派发同一个软件队列上的req。该函数req的派发,如果有调度器,则把req先插入到IO算法队列;如果无调度器,会尝试执行blk_mq_try_issue_list_directly直接将req派发给磁盘设备驱动。最后再执行blk_mq_run_hw_queue()把剩余的因各种原因未派发的req进行同步或异步派发。

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
void blk_mq_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{
LIST_HEAD(ctx_list);//ctx_list临时保存了当前进程plug->mq_list链表上的部分req
unsigned int depth;
//就是令list指向plug->mq_list的吧
list_splice_init(&plug->mq_list, &list);
//对plug->mq_list链表上的req进行排序吧,排序规则基于req的扇区起始地址
list_sort(NULL, &list, plug_ctx_cmp);

//循环直到plug->mq_list链表上的req空
while (!list_empty(&list))
{
//plug->mq_list取一个req
rq = list_entry_rq(list.next);
//从链表删除req
list_del_init(&rq->queuelist);

/*this_ctx是上一个req的软件队列,rq->mq_ctx是当前req的软件队列。二者软件队列相等则if不成立,只是
把req添加到局部ctx_list链表。如果二者软件队列不等,则执行if里边的blk_mq_sched_insert_requests把
局部ctx_list链表上的req进行派送。然后把局部ctx_list链表清空,重复上述循环*/
if (rq->mq_ctx != this_ctx)
{
if (this_ctx)
{
//派发this_ctx链表上的req
blk_mq_sched_insert_requests(this_q, this_ctx,&ctx_list,from_schedule);
//this_ctx赋值为req软件队列
this_ctx = rq->mq_ctx;
this_q = rq->q;
//遇到不同软件队列的req,depth清0
depth = 0;
}
}
depth++;
//把req添加到局部变量ctx_list链表,看着是向ctx_list插入一个req,depth深度就加1
list_add_tail(&rq->queuelist, &ctx_list);
}

/*如果plug->mq_list上的req,rq->mq_ctx都指向同一个软件队列,前边的blk_mq_sched_insert_requests
执行不了,则在这里执行一次,将ctx_list链表上的req进行派发。还有一种情况,是plug->mq_list链表上的
最后一个req也只能在这里派发*/
if (this_ctx) {
//派发this_ctx链表上的req
blk_mq_sched_insert_requests(this_q, this_ctx, &ctx_list,from_schedule);
}
}

11.1 blk_mq_sched_insert_requests真正派发plug->mq_list链表上的req

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

void blk_mq_sched_insert_requests(struct request_queue *q,
struct blk_mq_ctx *ctx,
struct list_head *list, bool run_queue_async)
{
if (e && e->aux->ops.mq.insert_requests) //使用调度器
{
/*尝试将req合并到q->last_merg或者调度算法的hash队列的临近req。合并不了的话,把req插入到deadline
调度算法的红黑树和fifo队列,设置req在fifo队列的超时时间。还插入elv调度算法的hash队列*/
e->aux->ops.mq.insert_requests(hctx, list, false);
}
else
{
//硬件队列不能忙,没用IO调度器,不能异步处理,if才成立
if (!hctx->dispatch_busy && !e && !run_queue_async) {
//将list链表上的req进行直接派发
blk_mq_try_issue_list_directly(hctx, list);
//如果list空,说明所有的req都派发磁盘驱动了,直接返回收工
if (list_empty(list))
return;
}
// 到这里,说明list链表上还有剩余的req没有派发硬件队列传输,则需把list链表上的剩下的所
//有req插入到到软件队列ctx->rq_list链表
blk_mq_insert_requests(hctx, ctx, list);
}

//再次启动硬件IO数据派发
blk_mq_run_hw_queue(hctx, run_queue_async);
}

如果有IO调度算法,则把list(来自plug->mq_list)链表上的req插入elv的hash队列,mq-deadline算法的还要插入红黑树和fifo队列。如果没有使用IO调度算法,则执行blk_mq_try_issue_list_directly函数,在该函数中:取出list链表的上的req,从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于req->tag,然后调用磁盘驱动queue_rq接口函数把req派发给驱动。如果遇到磁盘驱动硬件忙,则设置硬件队列忙,还释放req的tag。然后把这个失败派送的req插入hctx->dispatch链表,此时如果list链表空则执行blk_mq_run_hw_queue同步派发req(这个过程见blk_mq_request_bypass_insert),接着就return返回了。

因为此时磁盘驱动硬件忙,不能再继续把list剩余的req再强制进行派发了,则执行blk_mq_insert_requests函数把这些剩余未派发的req插入到软件队列ctx->rq_list链表上,然后执行blk_mq_run_hw_queue再进行req同步或异步派发。下文重点介绍函数blk_mq_try_issue_list_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

void blk_mq_try_issue_list_directly(struct blk_mq_hw_ctx *hctx,struct list_head *list)
{
//list临时保存了当前进程plug->mq_list链表上的部分req,遍历该链表上的req
while (!list_empty(list))
{
//从plug->mq_list链表取出一个req
struct request *rq = list_first_entry(list, struct request,queuelist);
//从list链表剔除req
list_del_init(&rq->queuelist);
//真正req派送在这里
ret = blk_mq_request_issue_directly(rq);

/*如果ret为BLK_MQ_RQ_QUEUE_OK,说明只是把req派发给磁盘驱动。如果是BLK_MQ_RQ_QUEUE_BUSY或
者BLK_MQ_RQ_QUEUE_DEV_BUSY,则说明遇到磁盘驱动硬件繁忙,直接break。如果req是其他值,说明这个req传输
完成了,则执行blk_mq_end_request()进行IO统计*/

if (ret != BLK_MQ_RQ_QUEUE_OK)
{
if (ret == BLK_MQ_RQ_QUEUE_BUSY ||ret == BLK_MQ_RQ_QUEUE_DEV_BUSY)
{
//磁盘驱动硬件繁忙,把req添加到硬件队列hctx->dispatch队列,如果list链表空为true,则同步启动req硬件派发
blk_mq_request_bypass_insert(rq,list_empty(list));
//注意,磁盘驱动硬件的话,直接直接跳出循环,函数返回了
break;
}

//该req磁盘数据传输完成了,增加ios、ticks、time_in_queue、io_ticks、flight、sectors扇区数等使用
//计数。依次取出req->bio链表上所有req对应的bio,一个一个更新bio结构体成员数据,执行bio的回调函数。
//还更新req->__data_len和req->buffer。
blk_mq_end_request(rq, ret);
}

}
}

依次遍历list(来自plug->mq_list)链表上的req,执行blk_mq_request_issue_directly()派发该req。在该函数中,从硬件队列的blk_mq_tags结构体的tags->bitmap_tags或者tags->nr_reserved_tags分配一个空闲tag赋于rq->tag,调用磁盘驱动queue_rq接口函数把req派发给驱动。如果遇到磁盘驱动硬件忙,则设置硬件队列忙,还释放req的tag,然后把这个派送失败的req插入hctx->dispatch链表,此时如果list链表空则执行blk_mq_run_hw_queue同步派发req,之后就从blk_mq_request_issue_directly函数返回。如果遇到req传输完成则执行blk_mq_end_request()统计IO使用率等数据并唤醒进程。

blk_mq_request_issue_directly函数呢,调用__blk_mq_try_issue_directly()执行具体的req派发工作。

1
2
3
4
5
6
7
8
9
10
11
12
13

int blk_mq_request_issue_directly(struct request *rq)
{
//req所在的软件队列
struct blk_mq_ctx *ctx = rq->mq_ctx;
//与ctx->cpu这个CPU编号对应的硬件队列
struct blk_mq_hw_ctx *hctx = blk_mq_map_queue(rq->q, ctx->cpu);

//硬件队列加锁
hctx_lock(hctx, &srcu_idx);
ret = __blk_mq_try_issue_directly(hctx, rq, true);
hctx_unlock(hctx, srcu_idx);
}

进程plug->mq_list链表上的req派送,一个个是先执行blk_mq_try_issue_list_directly直接将req派送给磁盘驱动进行数据传输。如果遇到磁盘驱动硬件繁忙,还是要把req加入hctx->dispatch链表。接着还要把plug->mq_list链表上剩余未派发的req加入软件队列ctx->rq_list链表上。最后执行blk_mq_run_hw_queue()再把hctx->dispatch链表和ctx->rq_list链表上的req进行同步或者异步派发。

12. 总结

软件队列ctx->rq_list链表、硬件队列hctx->dispatch链表、IO调度算法队列的相关链表、req plug模式的plug->mq_list,blk_mq_try_issue_list_directly、__blk_mq_try_issue_directly direct 派发模式。

硬件队列hctx->dispatch链表,正常情况下它用不到。只有在req派发时,发现磁盘驱动硬件繁忙,暂时没时间处理该req,又不能不管,只能先把req添加到硬件队列hctx->dispatch链表。接着执行blk_mq_run_hw_queue类的函数,该函数会在磁盘驱动硬件空闲时,从hctx->dispatch链表取出刚才没来得及派发的req,再次尝试派发给磁盘设备驱动。

软件队列ctx->rq_list链表,向该链表插入req的情况是:发起bio请求过程的blk_mq_make_request()->blk_mq_queue_io();执行blk_mq_sched_insert_requests派发req时:情况1,如果硬件队列繁忙或者使用了调度器或者异步派发,不能执行blk_mq_try_issue_list_directly()直接将req派发给设备驱动的情况下,那就执行blk_mq_insert_requests()将派发req的临时list链表上的req插入到软件队列ctx->rq_list链表;情况2,硬件队列空闲且没有使用调度器且同步派发,则执行blk_mq_try_issue_list_directly()将临时list链表上的req派发给磁盘设备驱动。但派发过程遇到了磁盘驱动硬件繁忙,只能被迫返回,接着还是执行blk_mq_insert_requests()将list链表上剩下未派发的req插入到ctx->rq_list链表。之后执行blk_mq_run_hw_queue类函数,在磁盘驱动硬件空闲时,从软件队列ctx->rq_list链表取req,再次尝试派发给设备驱动。

IO调度算法队列的相关链表,这是执行设置了IO调度算法的情况下,肯定要先把派发的req插入的IO算法队列相关链表进行处理。它的派发见blk_mq_make_request()和blk_mq_sched_insert_requests()中调用的blk_mq_sched_insert_request函数。

req plug模式的plug->mq_list链表。就是当前进程集聚了很多bio在plug->mq_list,然后一下次全部派发给磁盘设备驱动。它的发起函数是blk_flush_plug_list。基本原理是,先取出plug->mq_list链表上的req,如果设置了IO调度器,则把req插入到IO算法队列。否则,则先执行blk_mq_try_issue_list_directly()将这些req直接派发给磁盘设备驱动。如果无法直接派发给磁盘设备驱动,就先把req添加到软件队列ctx->rq_list链表,等稍后执行blk_mq_run_hw_queue类函数,再次尝试派发这些req。

原文链接:https://blog.csdn.net/hu1610552336/article/details/111464548

PS:前往公众号“Linux工坊”,可以查看最新内容!关注即可免费领取面试&Linux技术书籍!

------ 本文结束------
  • 文章标题: 多核系统上引入多队列SSD
  • 本文作者: 你是我的阳光
  • 发布时间: 2021年05月08日 - 22:26:45
  • 最后更新: 2022年11月07日 - 16:45:00
  • 本文链接: https://szp2016.github.io/Linux/多核系统上引入多队列SSD/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
0%