内核block层IO调度器—bfq算法之1整体流程介绍

1 bfq算法基础知识

 bfq是cfq的改进版,bfq说明文档bfq-iosched.txt说bfq具有高吞吐、低延迟等特性,适用于互式应用的低延迟场景。在2021年clk还有人专门介绍了Budget Fair Queueing调度算法(bfq)。bfq是怎么做到高吞吐低延迟的?本文基于centos 8.3,内核版本4.18.0-240.el8,探索下bfq算法,详细源码注释见 GitHub - dongzhiyan-stack/linux-4.18.0-240.el8。

bfq的基本原理是:每个进程都先分配一个bfq调度队列bfq_queue,简称bfqq,bfqq与进程绑定。每个进程的bfqq分配一个初始配额budget,进程每派发一个IO请求,就消耗bfqq的一定配额(消耗的配额与传输的IO请求数据量成正比)。等bfqq的配额消耗光、bfqq上没有IO请求要要传输,则令bfqq到期失效。接着切换到其他进程的bfqq,派发这个新的bfqq上的IO请求。bfq的基本原理不算复杂,它是怎么保证了高吞吐、低延迟的特性呢?本文主要介绍bfq基础数据结构和基本函数流程,高吞吐、低延迟原理分析会在后续文章中介绍。

bfq算法涉及的关键数据结构整理如下,这里只把关键成员列下:

  1. //bfq 总的数据结构bfqd
  2. struct bfq_data {
  3.     struct request_queue *queue;
  4.     struct bfq_queue *in_service_queue;//bfqd当前正在使用的bfqq
  5.     int queued;//添加一个IO请求到队列中加1,派发一个IO请求后减1
  6.     int rq_in_driver;//已经派发但还没传输完成的IO请求数
  7.     struct request *waited_rq;//bfq_dispatch_rq_from_bfqq()赋值为派发的IO请求
  8.     u32 peak_rate;//bfq_update_rate_reset()计算peak_ratebfq_calc_max_budget()中根据peak_rate计算bfqd->bfq_max_budget
  9.     struct list_head active_list;//bfq_active_insert()entity插入st->active红黑树后,把entity所属bfqq插入次active_list链表
  10.     struct list_head idle_list;//bfq_idle_insert()entity插入st->idle红黑树后,把entity所属bfqq插入次idle_list链表
  11.     u64 bfq_fifo_expire[2];//IO请求超时时间,__bfq_insert_request()中赋值
  12.     int bfq_max_budget;//影响bfqq的最大配额budget
  13. }
  14. //bfq调度队列,IO请求req添加到里边,关键成员是bfq_entiry,简称bfqq
  15. struct bfq_queue {
  16.     struct bfq_data *bfqd;//指向bfqq所属bfqd
  17.     unsigned short ioprio, ioprio_class;//ioprio_classbfqq所属调度类,ioprio是具体调度类里的优先级
  18.     struct rb_root sort_list;//bfq_add_request()中把IO请求req添加到sort_list链表,bfq_remove_request()中从bfqq->sort_list链表剔除req
  19.     struct request *next_rq;//下一次优先派发的reqbfq_remove_request()中更新,bfq_add_request()中赋值
  20.     struct bfq_entity entity;//bfqq的调度实体
  21.     struct list_head bfqq_list;//bfqqbfqq_list插入bfqdactive_listidle_list链表,见 bfq_active_insert() bfq_idle_insert()
  22.     pid_t pid;//使用bfqq的进程ID
  23. }
  24. //bfq实际的调度实体
  25. struct bfq_entity {
  26.     struct rb_node rb_node;//entityrb_node 插入st->active st->idle 红黑树
  27.     u64 start, finish;//bfqq的起始和结束虚拟运行时间
  28.     u64 min_start;//entity刚插入st->active红黑树时的entity->start,初始entity->start
  29.     int service;//entity已经消耗的配额
  30.     int budget;//entity的总配额
  31.     int weight;//entity的权重
  32.     struct bfq_entity *parent;//指向父entity
  33.     struct bfq_sched_data *sched_data;//调度队列,通过它找到entity所属IO调度类的结构bfq_service_tree
  34.     struct rb_root *tree;//指向entity所处st->active st->idle 红黑树
  35. }

  1. //一个bfq调度类对应一个bfq_service_tree结构
  2. struct bfq_service_tree {
  3.     struct rb_root active;//active属性的entity插入到active这个红黑树
  4.     struct rb_root idle;//idle属性的entity插入到idle这个红黑树
  5.     struct bfq_entity *first_idle;//指向st->idle 红黑树中finish最小的entity
  6.     struct bfq_entity *last_idle;//指向st->idle 红黑树中finish最大的entity
  7.     u64 vtime;//调度器的虚拟运行时间
  8.     unsigned long wsum;//调度器上所有entity的权重之和
  9. }
  1. //代表总的bfq IO调度器,包含3+1bfq调度类结构bfq_service_tree
  2. struct bfq_sched_data {
  3.     struct bfq_entity *in_service_entity;//bfq调度器当前正在使用的entity
  4.     struct bfq_entity *next_in_service;//bfq调度器下一次使用的entity
  5.     //一个bfq调度类对应一个bfq_service_tree结构。service_tree[]数组保存了3+1bfq调度类bfq_service_tree结构
  6.     struct bfq_service_tree service_tree[BFQ_IOPRIO_CLASSES];
  7. };

struct  bfq_data是bfq总的数据结构,struct bfq_queue 是bfq调度队列,IO请求正是添加到它的sort_list成员上。struct bfq_entity是bfq算法调度实体,struct bfq_entity是struct bfq_queue的一个成员,因此我觉得struct bfq_entity和bfq_queue是一回事,只是各自作用不同。struct bfq_entity的成员struct bfq_sched_data *sched_data指向代表总的bfq IO调度器的数据结构struct bfq_sched_data。而struct bfq_sched_data的成员struct bfq_service_tree service_tree[BFQ_IOPRIO_CLASSES]数组,其成员struct bfq_service_tree对应一个bfq调度类。

说明一下,bfq_data、bfq_queue、bfq_entity、bfq_service_tree、bfq_sched_data 这些bfq关键数据结构在内核源码和本文都是以简称出现,如下:

  • 1:bfq_data简称bfqd
  • 2:bfq_queue简称bfqq
  • 3:bfq_entity简称entity
  • 4:bfq_service_tree简称st
  • 5:bfq_sched_data简称sd
  • 6:IO请求request结构简称req

需要记住这些简称,下文经常用到。为了能更清晰的展示这几个数据结构之前错综复杂的关系,看下下边的示意图(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图):

内核block层IO调度器—bfq算法之1整体流程介绍_第1张图片

 下文我们主要介绍一下bfq算法向IO队列添加IO请求、派发IO请求过程涉及的函数流程。

2:新的IO请求插入到bfq 算法队列

先把流程图贴下(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图) :

内核block层IO调度器—bfq算法之1整体流程介绍_第2张图片

函数先不用深究,先熟悉一下整理流程:一个新进程传输IO请求肯定要先执行submit_bio()函数把IO请求插入到bfq的算法队列,函数流程是:submit_bio()->generic_make_request()->blk_mq_make_request()->blk_mq_sched_insert_request()->bfq_insert_requests(),bfq_insert_requests()是入口函数。之后流程简述如下:

1:进入bfq_insert_requests-> bfq_insert_request()函数,在里边执行bfq_init_rq()->bfq_get_bfqq_handle_split()->bfq_get_queue(),为新进程分配一个bfqq。注意,每个新的派发IO请求的进程都要分配一个bfqq。

2:回到bfq_insert_request()函数,执行到__bfq_insert_request()->bfq_add_request()函数,执行elv_rb_add(&bfqq->sort_list, rq)把IO请求req添加到bfqq->sort_list链表,并把IO请求req赋值给bfqq->next_rq,bfqq->next_rq正是bfqq下次优先派发的IO请求。

3:bfq_add_request()函数中,继续执行bfq_bfqq_handle_idle_busy_switch()->bfq_add_bfqq_busy()->bfq_activate_bfqq()->bfq_activate_requeue_entity()->__bfq_activate_requeue_entity()->__bfq_activate_entity()函数,在__bfq_activate_entity()函数中计算entity的起始虚拟运行时间entity->start,一般情况entity->start来自bfq调度器的虚拟运行时间st->vtime。注意,entitybfqq的成员struct bfq_entity entityentirybfqq其实可以理解成是一回事。

4:__bfq_activate_entity()函数中执行bfq_update_fin_time_enqueue()函数,在里边执行bfq_calc_finish(entity, entity->budget) 计算entity的结束虚拟运行时间。计算方法是entity->finish = entity->start +bfq_delta(service, entity->weight),entity->weight是entity的权重,service此时是entity的配额entity->budget。bfq_delta(service, entity->weight)可以阶段理解成entity->budget*N/ entity->weight,N是是一个常数。

5:bfq_update_fin_time_enqueue()函数最后,执行bfq_active_insert()entity按照entity->finish插入调度器st->active红黑树。在st->active红黑树上的entity按照entity->finish由小到大从左向右排列。

6:回到bfq_activate_requeue_entity ()函数,执行bfq_update_next_in_service(),有概率对sd->next_in_service赋值entity。sd->next_in_service是将来派发IO请求时优先选择的bfqq的entity。

总结一下进程把IO请求添加到bfq算法队列过程:为进程分配一个bfqq,把IO请求添加到bfqq->sort_list链表。计算bfqqentity的起始虚拟运行时间entity->startentity的结束虚拟运行时间entity->finish。最后把entity按照entity->finish插入到bfq调度器的st->active红黑树,并赋值sd->next_in_service=entityentityst->active红黑树中按照entity->finish由小到大从左到右排列。下图演示了这个过程(为了演示简单,没有演示entity在红黑树中的树形排列形式,只是演示entity按照entity->finish由小到大从左向右排列):

内核block层IO调度器—bfq算法之1整体流程介绍_第3张图片

3:派发bfq算法队列上的IO请求

先看下流程图(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图) :

内核block层IO调度器—bfq算法之1整体流程介绍_第4张图片

 流程也是相当复杂,先知道流程即可。  派发IO请求常见的入口函数有两个:

  1. blk_finish_plug()->blk_flush_plug_list()->blk_mq_flush_plug_list()->blk_mq_sched_insert_requests()->__blk_mq_delay_run_hw_queue()->__blk_mq_run_hw_queue()->blk_mq_sched_dispatch_requests()->__blk_mq_sched_dispatch_requests()->blk_mq_do_dispatch_sched()->bfq_dispatch_request()->__bfq_dispatch_request()
  2. submit_bio()->generic_make_request()->blk_mq_make_request()->blk_flush_plug_list()…….-> bfq_dispatch_request()->__bfq_dispatch_request()

现在进入__bfq_dispatch_request()函数:

1:执行bfq_select_queue()函数,bfqq = bfqd->in_service_queue是NULL,而goto new_queue分支,执行bfq_set_in_service_queue()函数。在该函数里,执行到bfq_get_next_queue()函数,执行entity = sd->next_in_service和sd->in_service_entity = entity。相当于sd->in_service_entity=sd->next_in_service,这样第一节的sd->next_in_service指向的新分配的bfqqentity赋值给了sd->in_service_entity,作为bfq调度器当前使用的entity

接着执行bfq_update_next_in_service()选择一个新的entiry赋值给sd->next_in_service,作为下次派发IO请求使用的bfqq的entiry。但是此时现在st->active红黑树是空的,于是bfq_update_next_in_service()->bfq_lookup_next_entity()没有找到entiry,返回NULL,则sd->next_in_service = next_in_service=NULL。

回到bfq_set_in_service_queue(),执行__bfq_set_in_service_queue()里边的bfqd->in_service_queue = bfqq,就是bfqd->in_service_queue赋值当前正在使用的bfqq。回到bfq_select_queue()函数,从bfq_set_in_service_queue()返回值得到本次派发IO请求要使用的bfqq,就是前边分配的那个bfqq。

2:bfq_select_queue()函数执行完,回到__bfq_dispatch_request()函数,执行bfq_dispatch_rq_from_bfqq()。执行rq = bfqq->next_rq得到本次派发的IO请求,service_to_charge = bfq_serv_to_charge(rq, bfqq)计算派发本次的IO请求需要的配额(就是返回值service_to_charge),是依照派发IO请求对应的扇区的字节数结算的。

接着执行bfq_bfqq_served(bfqq, service_to_charge)把本次派发的IO请求需要的配额service_to_charge累加到entity->servicest->vtime。entity->service是bfqq的entity已经消耗的配额,st->vtime是bfq调度器总的虚拟运行时间。

继续,执行bfq_dispatch_remove()->bfq_remove_request()函数,执行bfqq->next_rq = bfq_find_next_rq(bfqd, bfqq, rq)选择下一次派发的IO请求(如果bfqq->sort_list没有IO请求req了,则bfqq->next_rq赋值NULL),并且把当前选中派发的IO请求从bfqq->sort_list链表剔除。最后,if (!(bfq_tot_busy_queues(bfqd) > 1 && bfq_class_idle(bfqq)))成立,直接return rq返回本次派发的IO请求。

3:从__bfq_dispatch_request()一路向上返回,返回本次派发的IO请求,本质就是刚才提到的bfqq->next_rq这个IO请求。

4:bfqq因没有要派发的IO请求而过期失效

如果进程没有继续执行bfq_insert_requests()向bfqq->sort_list链表添加新的IO请求,则bfqq->next_rq是NULL。再次执行__bfq_dispatch_request()派发IO请求时:

1:先进入bfq_select_queue()函数,bfqq = bfqd->in_service_queue不是NULL,接着执行next_rq = bfqq->next_rq选择派发的IO请求。因bfqq->next_rq是NULL,执行reason = BFQQE_NO_MORE_REQUESTS和bfq_bfqq_expire(bfqd, bfqq, false, reason)令bfqq没有派发的IO请求了而令bfqq到期失效。

2:进入bfq_bfqq_expire()函数,执行到__bfq_bfqq_expire()->bfq_del_bfqq_busy()->bfq_deactivate_bfqq()->__bfq_deactivate_entity()函数。在__bfq_deactivate_entity()函数里,执行bfq_calc_finish(entity, entity->service),本质是entity->finish = entity->start +bfq_delta(entity->service, entity->weight),以entiry已经消耗的配额除以entity的权重entity->weight,计算entity的虚拟运行时间,加上entity->start得到entity->finish。entity->finish是entiry的结束虚拟运行时间!

继续,__bfq_deactivate_entity()函数中执行bfq_idle_insert(st, entity)把entity按照entity->finish插入到st->idle红黑树。在st->idle红黑树上的entity按照entity->finish由小到大从左向右排列。bfqq的到期失效后,一般是要先暂存到st->idle红黑树。如下演示了这个过程,st->first_idle指向st->idle 红黑树第一个entity,st->last_idle指向st->idle 红黑树最后一个entity。(为了演示简单,没有演示entity在红黑树中的树形排列形式,只是演示entity按照entity->finish由小到大从左向右排列)

内核block层IO调度器—bfq算法之1整体流程介绍_第5张图片

3 :回到bfq_deactivate_entity()函数,if (sd->next_in_service == entity)不成立,因为sd->next_in_service是NULL。

4:回到__bfq_bfqq_expire()函数,执行__bfq_bfqd_reset_in_service(),设置bfqd->in_service_queue = NULL。回到bfq_select_queue()函数,执行到bfq_set_in_service_queue()函数,选择下一个运行的bfqq。在该函数里,执行entity = sd->next_in_service和sd->in_service_entity = entity,因sd->next_in_service是NULL,故sd->in_service_entity被赋值NULL。因此bfq_set_in_service_queue()返回的bfqq也是NULL,这说明没有可用的bfqq!接着执行__bfq_set_in_service_queue()函数,本质是bfqd->in_service_queue = bfqq=NULL。

5:最后返回bfq_select_queue()函数,bfq_select_queue()函数返回的bfqq是NULL。返回到__bfq_dispatch_request()函数,直接return NULL。

以上演示了执行__bfq_dispatch_request()派发IO请求时,bfqq因没有IO请求可派发了而过期失效,最后令过期失效的bfqq的entity的插入到st->ilde红黑树。并且因为没有其他bfqq可选择,bfq_select_queue()找不到可使用的bfqq而返回NULL,然后__bfq_dispatch_request()返回NULL,表示没有IO请求可派发了。

5:bfqq因配额耗光而过期失效

如果前文提到的bfqq有大量的IO请求可派发,每次执行__bfq_dispatch_request()都可以找到可派发的IO请求,这样就会bfqq的配额被耗光而导致bfqq到期失效。

__bfq_dispatch_request()->bfq_select_queue()函数中,next_rq = bfqq->next_rq取出本次派发的IO请求,if (next_rq)成立,接着执行if (bfq_serv_to_charge(next_rq, bfqq) >bfq_bfqq_budget_left(bfqq))。bfq_serv_to_charge(next_rq, bfqq)是计算派发本次的IO请求next_rq需要的配额,bfq_bfqq_budget_left(bfqq)是计算bfqq还剩余的配额。如果bfqq剩余的配额小于派发本次的IO请求next_rq需要的配额,则if成立,执行reason = BFQQE_BUDGET_EXHAUSTED,goto expire去执行bfq_bfqq_expire(bfqd, bfqq, false, reason),令bfqq到期失效。这个过程与上一节执行bfq_bfqq_expire()的过程一样,不再赘述。

如果bfqq剩余的配额大于派发本次的IO请求next_rq需要的配额,则 if (bfq_serv_to_charge(next_rq, bfqq) >bfq_bfqq_budget_left(bfqq))不成立,则执行goto keep_queue,直接从bfq_select_queue()返回到__bfq_dispatch_request()函数. __bfq_dispatch_request()函数的执行过程在第2节介绍过,不再赘述。

主要强调一点,bfq_dispatch_rq_from_bfqq()中,执行bfq_serv_to_charge()计算派发本次的IO请求需要的配额。然后执行bfq_bfqq_served()函数,执行里边的entity->service += served和st->vtime += bfq_delta(served, st->wsum),前者是把派发本次的IO请求消耗的配额累计到entity->service,后者是把派发本次的IO请求消耗的配额转换成虚拟运行时间累计到调度器的虚拟运行时间st->vtime。每次派发一个IO请求,派发该IO请求消耗的配额都累加到entity->serviceentity->service就是bfqqentiry已经消耗的配额,如果这个配额超过bfqqentity的最大配额entity->budget,则前边的if (bfq_serv_to_charge(next_rq, bfqq) >bfq_bfqq_budget_left(bfqq))就成立,然后令这个bfqq到期失效,尝试选择一个新的bfqq,派发新的bfqq上的IO请求。

本文主要介绍的bfqq算法,为了方便简单,始终只有一个bfqq,即同时只有一个进程在传输IO请求。实际情况肯定会有多个进程在进程IO传输,则bfq调度器就会有多个bfqq可选择。这个时候bfq的算法价值才可以充分体现出来:低延迟、高吞吐,提高交互式进程的IO性能。这个后文分析吧,感觉有难度。水平有限,如有错误请指正。

【参考】

https://blog.csdn.net/feelabclihu/article/details/105502167

你可能感兴趣的:(block层,linux,block,源码)