Block multi-queue 架构解析(二)流程与机制

主要流程

request_queue初始化

块设备初始化时通过blk_mq_init_queue()创建request_queue并初始化,主要功能包含:

  • request_queue与块设备的blk_mq_tag_set相互绑定,根据blk_mq_tag_set设置一些参数。
  • 创建软硬件队列及进行绑定。
  • 设置io请求入口函数make_request_fn 为 blk_mq_make_request()。

IO提交/转换request

IO请求的入口为blk_mq_make_request(),其中首先判断IO是否可以跟其他request合并,若无法合并再将IO转换为request进一步处理。

request获取

request是事先在硬件队列的tags或者sched_tags中分配好的,通过blk_mq_get_request()获取,期间有可能会因获取不到request而被io_schedule,进入iowait状态。(扩展阅读IOwait 到底在wait什么)

request加入队列

IO在blk_mq_make_request()中转换成request后,根据不同情况做处理,最终去向主要有三种:加入plug队列,加入调度队列,直接派发。

request派发

blk_mq_run_hw_queue() 用来启动一个硬件队列的request派发,触发派发的情况很多,大致归纳下是:需要直接处理request,plug/调度队列flush,硬件队列启动/停止,blk_mq_get_tags中获取不到tags,kyber_domain_wake等等。

调用__blk_mq_run_hw_queue() 从可能的三个队列中选取request进行派发,他们是:硬件队列的dispatch,request_queue的调度器队列,硬件队列关联的软件队列的rq_list。最终在blk_mq_dispatch_rq_list()中调用q->mq_ops->queue_rq()完成向下层驱动派发request。

request完成

硬件驱动完成一个IO请求后,调用scsi_cmnd->scsi_done回调,针对block multi-queue 实现了统一接口scsi_mq_done()。处理IO完成中断的CPU与发起request的CPU可能不一致,scsi_mq_done()中的策略是尽量在发起request的CPU上处理request完成。

最终都会调用q->mq_ops->complete回调对request进行完成处理,统一实现的接口是scsi_softirq_done()。其中主要调用路径是scsi_softirq_done()->scsi_finish_command()->scsi_io_completion()->scsi_end_request()->__blk_mq_end_request()... ,执行request提交过程中注册的各种回调,最终释放request,完成一次IO请求。

机制

plug/unplug

Linux块设备层早期就引入了plug/unplug机制,为每个task分配一个plug list,当task提交IO时不立马处理,而是积攒一定量再统一加入下一级派发队列,减少对派发队列的竞争,从而提高性能,block multi-queue也继承了这一机制,与单队列的实现基本一致,网络上已有大量分析,这里就不再赘述。

IO 合并

request由bio转化而来,最初一个request内只包含一个bio,若有新提交的bio与已经在排队等待处理的request物理上连续,就直接将该bio合入request。推荐这篇文章https://blog.csdn.net/juS3Ve/article/details/80576965,已经分析的十分透彻了。block multi-queue 新增的部分是,当不使用调度器时request会在软件队列的rq_list上等待派发,提交IO时也会尝试与rq_list中的request进行合并。对于block mulit-queue架构,可能发生IO合并的队列有三个:plug list ,调度器内部队列(使用调度器),软件队列的rq_list(不使用调度器)。

软硬件队列映射

在之前的数据结构分析中提到block multi-queue的软硬件映射关系是由硬件相关的blk_mq_tag_set->map决定,设置其映射关系的函数为blk_mq_map_queues()。

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
int blk_mq_map_queues(struct blk_mq_queue_map *qmap)
{
    unsigned int *map = qmap->mq_map;
    unsigned int nr_queues = qmap->nr_queues;
    unsigned int cpu, first_sibling, q = 0;

    for_each_possible_cpu(cpu)
        map[cpu] = -1;

    /*
     * Spread queues among present CPUs first for minimizing
     * count of dead queues which are mapped by all un-present CPUs
     */
    for_each_present_cpu(cpu) {//优先映射online的cpu
        if (q >= nr_queues)//若硬件队列数量不够直接跳出
            break;
        map[cpu] = queue_index(qmap, nr_queues, q++);
    }

    for_each_possible_cpu(cpu) {//检查所有cpu
        if (map[cpu] != -1)
            continue;
        /*
         * First do sequential mapping between CPUs and queues.
         * In case we still have CPUs to map, and we have some number of
         * threads per cores then map sibling threads to the same queue
         * for performance optimizations.
         */
        if (q < nr_queues) {//若硬件队列仍有剩余直接顺序映射
            map[cpu] = queue_index(qmap, nr_queues, q++);
        } else {
            first_sibling = get_first_sibling(cpu);    //查找是否有同core的cpu
            if (first_sibling == cpu)    //未找到,顺序选择一个硬件队列
                map[cpu] = queue_index(qmap, nr_queues, q++);
            else    //找到,与之映射同一硬件队列
                map[cpu] = map[first_sibling];
        }
    }

    return 0;
}

具体映射策略是:

  • 优先保障online的cpu映射到不同的硬件队列
  • 若cpu数量大于硬件队列,将同一硬件核的cpu映射到同一硬件队列
  • 没有同硬件核的cpu顺序轮转映射硬件队列

并发管理

block multi-queue通过控制request的获取,来限制一个硬件队列在block层最大并发request数量。由之前的数据结构分析可知,request是预先分配好的保存在struct blk_mq_tags结构中。运行时根据是否使用调度器,决定从硬件队列的tags或是sched_tags中获取request,所以最大并发request数也要分是否使用调度器来讨论。获取request的函数为blk_mq_get_request()。

使用调度器流程:

  • 找到当前软件队列映射的硬件队列
  • 调用调度器limit_depth方法(若有实现),设置shallow_depth
  • sched_tags有空闲的request且分配的request总数
  • 若分配不成功,调用blk_mq_run_hw_queue()启动request派发,再重试获取request,期间可能进入iowait

不使用调度器流程:

  • 找到当前软件队列映射的硬件队列
  • 若硬件blk_mq_tag_set支持共享,增加tags的active_queues
  • 若硬件blk_mq_tag_set支持共享,且当前硬件队列已分配的request超过均分上限,分配失败
  • tags有空闲的request,分配成功
  • 若分配不成功,调用blk_mq_run_hw_queue()启动request派发,再重试获取request,期间可能进入iowait

总结一下,每个硬件队列的最大并发request数是:

使用调度器:request_queue的nr_request,与调度器limit_depth方法返回值的较小值。

不使用调度器且不支持共享tag_set:硬件队列queue_depth。

不使用调度器支持共享tag_set:queue_depth个request,各active_queue均分。

调度器

block multi-queue最开始是不支持调度器的,调度器只在单队列中生效,随着单队列代码逐步移出内核,而调度器对于hdd这种慢速设备又很有必要,调度器框架也加入了block multi-queue。从代码也能看出multi-queue框架中是否使用调度器,处理流程差异很大。Linux5.x后支持mq-deadline,bfq,kyber三种调度器,其中只有kyber有针对multi-queue框架中的多软硬队列做了处理,也被称为是真正意义上的多队列调度器。