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算法涉及的关键数据结构整理如下,这里只把关键成员列下:
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关键数据结构在内核源码和本文都是以简称出现,如下:
需要记住这些简称,下文经常用到。为了能更清晰的展示这几个数据结构之前错综复杂的关系,看下下边的示意图(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图):
下文我们主要介绍一下bfq算法向IO队列添加IO请求、派发IO请求过程涉及的函数流程。
先把流程图贴下(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图) :
函数先不用深究,先熟悉一下整理流程:一个新进程传输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。注意,entity是bfqq的成员struct bfq_entity entity,entiry和bfqq其实可以理解成是一回事。
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链表。计算bfqq的entity的起始虚拟运行时间entity->start和entity的结束虚拟运行时间entity->finish。最后把entity按照entity->finish插入到bfq调度器的st->active红黑树,并赋值sd->next_in_service=entity。entity在st->active红黑树中按照entity->finish由小到大从左到右排列。下图演示了这个过程(为了演示简单,没有演示entity在红黑树中的树形排列形式,只是演示entity按照entity->finish由小到大从左向右排列):
先看下流程图(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图) :
流程也是相当复杂,先知道流程即可。 派发IO请求常见的入口函数有两个:
现在进入__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指向的新分配的bfqq的entity赋值给了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->service和st->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请求。
如果进程没有继续执行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由小到大从左向右排列)
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请求可派发了。
如果前文提到的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->service。entity->service就是bfqq的entiry已经消耗的配额,如果这个配额超过bfqq的entity的最大配额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