linux CFQ IO调度算法分析笔记

CFQ调度器是四种IO Scheduler中最复杂的一个,redhat有个文档可以做为入门的文档先了解下 red-hat-enterprise-linux-5-io-tuning-guide.pdf


The cfq scheduler maintains a maximum of 64 internal request queues; each process running on the
system is assigned to any of these queues. Each time a process submits a synchronous I/
O request, it is moved to the assigned internal queue. Asynchronous requests from all processes are
batched together according to their process's I/O priority; for example, all asynchronous requests from
processes with a scheduling priority of "idle" (3) are put into one queue.

During each cycle, requests are moved from each non-empty internal request queue into one dispatch
queue. in a round-robin fashion. Once in the dispatch queue, requests are ordered to minimize disk
seeks and serviced accordingly.


To illustrate: let's say that the 64 internal queues contain 10 I/O request seach, and quantum is set to
8. In the first cycle, the cfq scheduler will take one request from each of the first 8 internal queues.
Those 8 requests are moved to the dispatch queue. In the next cycle (given that there are 8 free slots
in the dispatch queue) the cfq scheduler will take one request from each of the next batches of 8
internal queues.


这里不得不提下io prority,我们可以用ionice来指定io priority,其中有三种class: idle(3), best effort(2), real time(1),具体用法man ionice

real time 和 best effort 内部都有 0-7 一共8个优先级,对于real time而言,由于优先级高,有可能会饿死其他进程,对于 best effort 而言,2.6.26之后的内核如不指定io priority,那就有io priority = cpu nice


关于io优先级,有如下comments:I/O priorities are supported for reads and for synchronous (O_DIRECT, O_SYNC) writes. I/O priorities are not supported for asynchronous writes becausethey are issued outside the context of the program dirtying the memory, and thus program-specific priorities do not apply


言归正传,开始分析cfq的代码

struct cfq_io_context

cfq_io_context可以理解为io_context的子类,代表一个task_struct在cfq里的view,可以看到里面有两个cfq_queue结构的数组,cfq_queue[0]表示进程异步io请求对应的cfq_queue队列,cfq_queue[1]表示进程同步io请求对应的cfq_queue队列


struct cfq_queue

cfq_queue是个进程相关的数据结构,会和一个cfq_io_context关联,sort_list 是这个queue里面 pending requests 构成的红黑树,而 fifo 则是这个sort_list里面的pending requests形成的fifo链表,这个设计有点类似deadline io scheduler。

对于某个进程而言,其io class(rt, be, idle), io priority, io type(sync, async), 进程所属的cgroup 随时都会变化,因此cfq_queue的成员也会跟着变化

struct cfq_rb_root* service_tree

struct rb_node rb_node

其中service_tree指向cfq_data对应的cfq_rb_tree,下面可以看到每个cfq_group对应7个cfq_rb_root为头结点的service_tree,代表了不同的io class, io type;而rb_node则是这个红黑树上的结点

struct rb_node* p_node

struct rb_root* p_root

cfq_data有个成员prio_trees,代表了8个红黑树,每个cfq_queue都根据其io priority对应一个prio_trees成员,ioprio, ioprio_class记录了这些信息

struct cfq_group* cfqg

cfqg记录了cfq_queue对应的cgroup


struct cfq_data

这是和块设备队列相关的一个数据结构。cfq_data的指针是作为一个blkio_cgroup的哈希表的key,而对应的value则是cfq_group;同时也是io_context的radix_tree的一个key,对应的value是cfq_io_context,这样cfq_data,blkio_group和cfq_io_context就一一对应起来了。

cfq_data还有全局性的成员,专门针对异步io的

struct cfq_queue* async_cfqq[2][IOPRIO_BE_NR]

struct cfq_queue* async_idle_cfqq

其中async_cfqq代表了RT/BE各8个优先级的cfq_queue队列,async_idle_cfqq代表了idle的cfq_queue队列。

关于异步同步的请求,有一种说法是,只有page cache write back(pdflush)线程的请求才是内核唯一的异步请求,其他不论用户态是同步还是异步,不论是libaio还是内核native aio,到了调度队列之后都是同步请求??

struct hlist_head cfqg_list

指向该block device上挂载的所有cgroup,对应的hlist_node可以在struct cfq_group的cfqd_node成员中获取

struct rb_root prio_trees[CFQ_PRIO_LISTS]

prio_trees代表了8个红黑树,从priority 0 - 7

struct cfq_rb_root grp_service_tree

struct cfq_group root_cgroup

grp_service_tree为cfq_data所对应的block device上所有的cfq_group构成的红黑树的树根,可以看到cfq_group的成员rb_node就是这个红黑树(cfq_rb_root)的结点

root_cgroup为所有cgroup之中的根cgroup


cfq_group

这是per cgroup对应的数据结构,成员rb_node指向了其在cgroup红黑树中的结点

vdisktime

cfq_group->vdisktime可以理解为一个虚拟磁盘时间

在cfq_group_served函数中,当某个cfq_group被服务完之后,需要更新cfq_group->vdisktime,之后放回到service tree中。cfqg->vdisktime += cfq_scale_slice(charge, cfqg)是更新vdisktime的函数,其中charge是使用掉的time slice时间,cfq_scale_slice实现如下:

static inline u64 cfq_scale_slice(unsigned long delta, struct cfq_group *cfqg)
{
u64 d = delta << CFQ_SERVICE_SHIFT;
d = d * BLKIO_WEIGHT_DEFAULT;
do_div(d, cfqg->weight);
return d;
}

简单的说,weight越大的cfq_group,在消耗了同样的time slice之后,cfq_scle_slice返回的值相对较小,由于service tree每次都选择红黑树中vdisktime最小的cfq_group来服务,这就保证weight值大的cfq_group有更大几率会被再次选择服务

service tree会保存一个红黑树中当前最小的vdisktime值,存在min_vdisktime中


关于busy_queues_avg的解释如下:


* Per group busy queues average. Useful for workload slice calc. We
* create the array for each prio class but at run time it is used
* only for RT and BE class and slot for IDLE class remains unused.
* This is primarily done to avoid confusion and a gcc warning.

struct cfq_rb_root service_trees[2][3]

struct cfq_rb_root service_tree_idle

这两个成员在cgroup的patch出现之后从cfq_data被移到了新的cfq_group结构体中(2.6.33内核),我们可以看下代码里的注释如下:

* rr lists of queues with requests. We maintain service trees for
* RT and BE classes. These trees are subdivided in subclasses
* of SYNC, SYNC_NOIDLE and ASYNC based on workload type. For IDLE
* class there is no subclassification and all the cfq queues go on
* a single tree service_tree_idle.
* Counts are embedded in the cfq_rb_root

这里有一个我一直不明白的地方,service_trees已经是cfq_queue的红黑树根,首先分为BE, RT两大类(IDLE的全部在service_tree_idle里),然后又分为SYNC, SYNC_IDLE, ASYNC这三类,这是很奇怪的地方,因为service_trees本身应该都是NO IDLE的分类了,为什么又来个SYNC_IDLE呢


struct hlist_node cfqd_node

cfqd_node是一个哈希项,其哈希表头hlist_head为cfq_data->cfqg_list,这个cfq_data->cfqg_list的哈希表代表了这个block device上所有的cfq_group


struct blkio_group blkcg

通过blkio_group可以找到对应的blkio_cgroup的结构,关于blkio_cgroup请参考之前关于cgroup的文章。注意这里有两个相似的数据结构:blkio_cgroup和blkio_group,那么这两个数据结构有什么区别呢?我的猜测是,blkio_cgroup对应一个cgroup,可以从blkio_cgroup.css这个cgroup_subsys_state得到对应的cgroup结构。而blkio_group结构则是blkio_cgroup.blkg_list这个哈希表的结点,通过blkio_group.blkcg_node相关联。

注意到blkio_group里有一个成员dev,据此猜测blkio_group是cgroup在不同块设备上应用的数据结构,e.g. 有四个进程ABCD,其中AB在sda上读写,CD在sdb上读写,这样就会有两个blkio_group结构,分别对应sda和sdb,这也和cfq_group对应起来,每个cfq_group对应一个 per cgroup per device的数据结构,因此有一个blkio_group的成员变量把两者关联起来


这些数据结构之间的关系是怎样的?我大概归纳了一下,不一定准确:

cfq_group正如代码中的解释那样,是一个/* This is per cgroup per device grouping structure */ 的结构,比如某个cgroup有两个tasks,一个对/dev/sda写,一个对/dev/sdb写,那么就对应着两个cfq_group结构,但如果两个tasks都是写/dev/sda,那么这两个进程都在同一个cfq_group中。

cfq_queue的属性中,包括workload priority:IDLE, BE, RT,包括workload type:ASYNC, SYNC_NOIDLE, SYNC。同时cfq_queue虽然基于CFQ调度器,但其内部的算法还是基于dead-line的

cfq_group包含了多个红黑树service tree,对应不同workload priority, workload type

一些工具性质的函数:

cfq_find_cfqg(struct cfq_data *cfqd, struct blkio_cgroup *blkcg):如果blkcg是全局的blkio_root_cgroup,则返回cfq_data->root_group,否则首先调用blkiocg_lookup_group在全局的blkio_cgroup的hash列表中查找__key为cfq_data指针的blkio_cgroup结构。通过blkio_cgroup查找cfq_group的方法很简单,通过一个container_of宏就可以。说的通俗一点,就是我们已经通过进程找到了对应的blkio_cgroup,可能是一个根cgroup也可能是某个子cgroup,但这个cgroup的设置未必会有相应的device,e.g. 只设置了blkio.weight而没有设置blkio.dev_weight,这时cfq_data就派上用场了。假设这种场景:一个cgroup中的不同进程读写不同的block device,甚至一个进程读写不同的device,但proportional weight只是针对单个块设备来的,【这里应该理解为什么blkio_cgroup会有多个blkio_group的哈希项了吧】,因此需要通过cfq_data这个key来找到那个blkio_group结构(顺便填入cfq_group->blkg.dev,通过cfq_data->queue->backing_dev_info得来)

cfq_get_cfqg(struct cfq_data *cfqd):首先通过current指向的当前的task_struct,查找其所属的blkio_cgroup,如果当前task_struct还没有加入cgroup,则返回全局的blkio_root_cgroup,其对应的cfq_group结构是cfq_data->root_group。再调用cfq_find_cfqg,由于传入了cfq_data,因此可以找到cfq_data对应的cfq_cgroup结构。如果没有找到则调cfq_alloc_cfqg初始化一个cfq_group结构,再通过current找到blkio_cgroup,最后调用cfq_init_add_cfqg_lists把cfq_data, cfq_group, blkio_cgroup三个结构关联起来。

cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct io_context *ioc,gfp_t gfp_mask):通过ioc得到cfq_data所关联的块设备上进程的cfq_queue,其关键函数为cfq_find_alloc_queue,该函数首先调用cfq_get_cfqg,通过current指向的当前CPU上在跑的进程来找到cfqd所在块设备上的cfq_group;其次调用cfq_cic_lookup来得到块设备对应的cfq_io_context, 从而得到对应的cfq_queue(根据其是否同步请求);最后如果这时cfq_queue为空,则调用kmem_cache_alloc_node重新分配一个cfq_queue,并将其初始化完毕

cfq_init_add_cfqg_lists(struct cfq_data *cfqd,struct cfq_group *cfqg, struct blkio_cgroup *blkcg):把cfqg->blkg这个block_group加到blkcg的哈希表中,这里哈希键值是cfq_data的指针值。同时cfqd这个cfq_data结构也保存了一个哈希表,表头是cfq_data->cfqg_list,该函数会把cfq_group也同时加到这个哈希表里【这里可以看到,blkio_cgroup会保存一个blkio_group的哈希表,每个cfq_data对应一个blkio_group】。同时每个cfq_data也会保存一个哈希表,记录这个cfq_data对应的块设备下的所有cfq_group

cfq_get_io_context(struct cfq_data *cfqd, gfp_t gfp_mask)

/*
* Setup general io context and cfq io context. There can be several cfq
* io contexts per general io context, if this process is doing io to more
* than one device managed by cfq.
*/

上面这段解释了为什么一个io_context里会有多个cfq_io_context,因为一个进程可能同时读写多个设备,这时需要通过cfq_data来确定块设备,从而得到基于这个块设备IO的cfq_io_context

该函数首先调用cfq_cic_lookup查找是否已有cfq_io_context,如果有了就退出,否则调用cfq_alloc_io_context创建一个cfq_io_context,把这个cfq_io_context加入到io_context的radix_tree里(key值为cfq_data指针),如果有必要则调用cfq_ioc_set_ioprio,cfq_ioc_set_cgroup来设置io priority和cgroup

cfq_ioc_set_cgroup(struct io_context *ioc):对于ioc的哈希表ioc->cic_list中的每一个hash node(实际上是cfq_io_context),调用changed_cgroup。 其中changed_cgroup的作用是把cfq_io_context的cfq_queue类型的同步队列设置为NULL,代码中的解释如下

/*
* Drop reference to sync queue. A new sync queue will be
* assigned in new group upon arrival of a fresh request.
*/

cfq_ioc_set_ioprio(struct io_context *ioc):和cfq_ioc_set_cgroup类似,跳过了

cfq_cic_lookup(struct cfq_data *cfqd, struct io_context *ioc):io_context保存了一个radix_tree,其树根为io_context->radix_root。据我猜测,io_context为什么要包含一个cfq_io_context的radix tree呢?可能是因为进程会同时读写多个块设备,因此根据cfq_data的成员cic_index,里面是cfq_data对应的块设备在radix tree里的索引。最后返回io_context中相应块设备对应的cfq_io_context

cfq_cic_link(struct cfq_data *cfqd, struct io_context *ioc,struct cfq_io_context *cic, gfp_t gfp_mask):具体的代码注释讲的很清楚了,跳过

/*
* Add cic into ioc, using cfqd as the search key. This enables us to lookup
* the process specific cfq io context when entered from the block layer.
* Also adds the cic to a per-cfqd list, used when this queue is removed.
*/

cic_to_cfqd(struct cfq_io_context *cic):cfq_io_context的key就是对应的cfq_data

cfq_set_request(struct request_queue *q, struct request *rq, gfp_t gfp_mask):这里可以看到,有struct cfq_data *cfqd = q->elevator->elevator_data 说明cfq_data是基于块设备的。该函数作用是为一个request来分配相应的cfq_io_context, cfq_queue并存到request->elevator_private中。

cfq_scaled_cfqq_slice(struct cfq_data *cfqd, struct cfq_queue *cfqq):通过一系列公式,计算出一个cfq_queue所占用的time_slice。首先计算cfq_cgroup中的平均cfq_queue个数,以及每个cfq_queue的time slice,相乘得到expect_latency为这个cgroup希望得到的time slice;同时调用cfq_group_slice按照权重比例计算出cgroup的time slice;如果这个time slice小于expect_latency,则调整之前根据cfq_queue的优先级计算出的slice,否则返回之前调用cfq_prio_to_slice得到的time slice

cfq_prio_slice cfq_prio_to_slice cfq_scale_slice:这三个函数都是计算队列的服务时间slice time的

cfq_group_slice:cfq_data->grp_service_tree为一个cfq_rb_root为一个红黑树树根,其成员total_weight为这个块设备上所有cgroup的权重值,而cfq_group->weight为该cgroup的权重值,因此该函数返回基于cfq_target_latency,300ms,各个cgroup所占用的slice时间,基于weight的比例。

cfq_set_prio_slice:设置cfq_queue中对应的slice_start, slice_end, allocated_slice

cfq_choose_req(struct cfq_data *cfqd, struct request *rq1, struct request *rq2, sector_t last):看代码中的解释

/*
* Lifted from AS - choose which of rq1 and rq2 that is best served now.
* We choose the request that is closest to the head right now. Distance
* behind the head is penalized and only allowed to a certain extent.
*/

基本上可以认为,同步请求优先异步请求,其次根据请求的位置,按照和AS类似的算法决定优先处理哪个请求

__cfq_group_service_tree_add(struct cfq_rb_root *st, struct cfq_group *cfqg):向service tree插入一个cfq_group,其中红黑树的key被编程为

static inline s64
cfqg_key(struct cfq_rb_root *st, struct cfq_group *cfqg)
{
return cfqg->vdisktime - st->min_vdisktime;
}

这边vdisktime和min_vdisktime是干什么用的,目前我也不清楚

cfq_link_cfqq_cfqg(struct cfq_queue *cfqq, struct cfq_group *cfqg):这个函数本身没啥可说的,但是验证了在CFQ调度器中,所有的异步请求都属于cfq_data->root_group这个cgroup,因此不受指定cgroup的任何限制


#1248 - #1267这段代码,是不需要cgroup调度支持的cfq调度器代码,可以看出简单很多,cfq_get_cfqg只是简单返回cfq_data->root_group


cfq_service_tree_add(struct cfq_data *cfqd, struct cfq_queue *cfqq,bool add_front):该函数目的是把cfq_queue加入到cfq_group对应的service_tree的红黑树中。首先根据io class, io priority来找到cfq_group对应的service_tree,类型为cfq_rb_root,其中插入的key是计算出来的一个起始时间,应该cfq_group是按照这个起始时间来依次处理挂在上面的所有cfq_queue的请求。最后调用cfq_group_notify_queue_add来通知cfq_data

cfq_prio_tree_lookup,cfq_prio_tree_add:这两个函数都是把cfq_queue加到cfq_data里的priority tree的红黑树中,cfq_data共有8个priority tree,对应不同的优先级,而红黑树中的排序基于cfq_queue中第一个请求的sector position

cfq_resort_rr_list,cfq_add_cfqq_rr,cfq_del_cfqq_rr

前者把cfq_queue加到cfq_data中的cgroup对应的service_tree数组,以及cfq_data的priority tree的红黑树中。

后者除了调用cfq_resort_rr_list以外,还递增了cfq_data->busy_queues,cfq_data->busy_sync_queues

最后把cfq_queue移除出service tree,和priority tree,并调用cfq_group_notify_queue_del通知cfq_data

cfq_del_rq_rb,cfq_add_rq_rb:这两个函数操作cfq_queue里面的request请求,把请求从cfq_queue中添加或者删除

cfq_add_rq_rb首先调用elv_rb_add把请求插到cfq_queue->sort_list这个红黑树中,基于请求的起始sector,再调用cfq_dispatch_insert真正把请求下发到底层驱动上,下面再调用cfq_add_cfqq_rr把队列挂到cfq_data代表的块设备上,下面重新选择cfq_queue->next_rq,如果和之前的cfq_queue->next_rq不同,需要改动cfq_queue对应的优先级并调整到队列所在的cfq_data下的priority tree中

cfq_del_rq_rb调用elv_rb_del把请求从cfq_queue->sort_list中删除,如果此时cfq_queue->sort_list为空了,而该队列又在cfq_data的priority tree中,则从红黑树里删除掉

cfq_remove_request(struct request *rq):调用cfq_del_rq_rb从cfq_queue中删除rq

cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio):通过当前current的task_struct来找到io_context,从而调用cfq_cic_lookup来找到cfq_io_context,然后根据bio是否同步找到对应的cfq_queue;下面找到bio之后的第一个sector,然后在cfq_queue->sort_list中基于这个sector查找是否有对应的bio可以被merge

cfq_merge:检查是否可以merge,如何可以修改对应的request,把bio给merge进request

cfq_merged_request:调用cfq_reposition_rq_rb,把相应的request更新为已经merge了bio的request

__cfq_set_active_queue:似乎是初始化cfq_queue,并将其设置为active_queue

__cfq_slice_expired(struct cfq_data *cfqd, struct cfq_queue *cfqq,bool timed_out)

/*
* current cfqq expired its slice (or was too idle), select new one
*/

#1744 - #1751 (kernel.org 3.0.23 cfq-iosched.c) 这里的cfq_queue->slice_resid似乎是还没有用完的slice_time

nr_sync 计算出cfq_group同步队列的个数

cfq_group_served

charge表示已经用掉的配额,在不同模式下意义不同,如果是iops模式,用cfq_queue->slice_dispatch,也就是dispatch的请求个数作为不同的cfq_queue的配额,如果是异步请求则用cfq_queue->allocated_slice,也就是分配给该队列的时间作为配额;否则是同步非iops模式则等同于used_sl (通过cfq_cfqq_slice_usage计算得出)

#975 - #978 cfq_group->vdisktime似乎是改cgroup至今所有用掉的slice总和

如果当前的cfq_group的expire时间(cfq_data->workload_expires)在jiffies之前,那啥也不用做了,不然保存相应的cfq_group的saved_workload_slice,saved_workload,saved_serving_prio

最后调用cfq_resort_rr_list,把cfq_queue插到cfq_group,cfq_data对应的service tree, prio tree的红黑树中

所以代码看下来,我猜测所有的cfq_queue开始时都在cfq_data和其对应的cfq_group的service tree,priority tree中,当轮到这个cfq_queue被处理时,从这些红黑树中被摘下来,等到其slice expired之后,更新一系列参数后被放回原来的红黑树里;再进一步说,如果cfq_queue里已经没有请求了,则会把他们从红黑树里移除掉,如果此时cfq_queue的time slice还没用完,会留着下次再用

这应该就是CFQ大致的工作原理


cfq_dist_from_last,cfq_rq_close

前者计算出request的sector和cfq_data的last_position对应的sector之间相隔的sector数,后者拿这个值和CFQ_CLOSE_THR比较,如果在这个范围内就认为两个request是相近的请求(言下之意不会对磁头转动造成太多的overhead)

cfqq_close(struct cfq_data *cfqd,struct cfq_queue *cur_cfqq)

/*
* First, if we find a request starting at the end of the last
* request, choose it.
*/

首先调用cfq_prio_tree_lookup查找同一个priority tree下的cfq_queue中,有没有起始sector正好在cfq_data->last_position上的,如果有则返回这个离当前磁头位置最近的cfq_queue

/*
* If the exact sector wasn't found, the parent of the NULL leaf
* will contain the closest sector.
*/

如果cfq_prio_tree_lookup没有找到,则返回的parent参数包含了sector差别最小的那个cfq_queue,如果这个cfq_queue->next_rq满足了cfq_rq_close的要求,则返回这个cfq_queue

如果不满足的,开始遍历这个红黑树(只遍历一次),再次判断这个cfq_queue是否满足cfq_rq_close,如果不满足就返回NULL

cfq_choose_wl,choose_service_tree:选择最优的workload class,和选择最优的service tree

choose_service_tree选择的优先级是RT > BE > IDLE,这个决定了cfq_data->serving_prio,然后调用cfq_choose_wl来决定cfq_data->serving_type,

/*
* the workload slice is computed as a fraction of target latency
* proportional to the number of queues in that workload, over
* all the queues in the same priority class
*/

group_slice调用cfq_group_slice,根据group的权重计算出来,而slice则是这个workload里的queues占所有busy_queues_avg的比例而计算得出的

对于同步请求而言,slice在经过一系列的比对之后,会把cfq_data->workload_expires = jiffies + slice,即当前服务的cfq_queue的配额时间被放到cfq_data指定的成员中;而对于异步请求而言,由于异步的优先级比同步要低,会再经过一些处理,具体的请参考代码

/*
* Async queues are currently system wide. Just taking
* proportion of queues with-in same group will lead to higher
* async ratio system wide as generally root group is going
* to have higher weight. A more accurate thing would be to
* calculate system wide asnc/sync ratio.
*/
static struct cfq_queue *cfq_select_queue(struct cfq_data *cfqd)

/*
* Select a queue for service. If we have a current active queue,
* check whether to continue servicing it, or retrieve and set a new one.
*/

这里可以参考之前关于cfq_slice_expired函数的解析,可以看到cfq_data每个时刻只服务一个cfq_queue,就是cfq_data->active_queue

cfq_may_dispatch(struct cfq_data *cfqd, struct cfq_queue *cfqq):判断是否可以向底层驱动分发请求

#2463 如果cfq_queue允许idle一段时间,同时块设备还有异步请求on-the-fly,暂时不分发

#2469 如果要分发的这个cfq_queue是一个异步队列,同时块设备上还有同步请求on-the-fly,暂时不分发

#2472 先给max_dispatch设定个初始值,默认是cfq_quantum/2 = 4

#2473 对于idle级别的cfq_queue,max_dispatch设为1

先看#2542,如果当前cfq_queue->dispatched,即已经分发的请求数目没有超过max_dispatch,如果是同步队列则允许分发,异步队列的话,需要修改下max_dispatch的值并重新和cfq_queue->dispatched比较,具体原因请看代码注释

/*
* Async queues must wait a bit before being allowed dispatch.
* We also ramp up the dispatch depth gradually for async IO,
* based on the last sync IO we serviced

*/

再回到#2479,如果此时cfq_queue->dispatched已经超过了max_dispatch,如果这是个同步cfq_queue,同时此时块设备上只有这个cfq_queue有请求,那么不限制该队列的分发请求数,如下面注释

/*
* If there is only one sync queue
* we can ignore async queue here and give the sync
* queue no dispatch limit. The reason is a sync queue can
* preempt async queue, limiting the sync queue doesn't make
* sense. This is useful for aiostress test.
*/

否则如果只有一个cfq_queue,放大一倍max_dispatch的值,到cfq_data->cfq_quantum = 8

不管怎样,最后还是要比较一次cfq_queue->dispatched和max_dispatch的值,来决定是否给底层驱动分发下一个请求

cfq_exit_io_context:当一个task结束之后,需要对io_context关联的所有cfq_io_context,调用cfq_exit_single_io_context

cfq_exit_single_io_context,__cfq_exit_single_io_context:对cfq_io_context的异步,同步队列,分别调用cfq_exit_cfqq

changed_cgroup(struct io_context *ioc, struct cfq_io_context *cic)

首先无视掉cic对应的异步队列,由此也可以看出其实CFQ里,异步请求是不分cgroup的,下面直接把cfq_io_context里的同步队列设置为NULL,代码中的注释告诉了为什么要这么做

/*
* Drop reference to sync queue. A new sync queue will be
* assigned in new group upon arrival of a fresh request.
*/

cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct io_context *ioc,gfp_t gfp_mask):如果是is_sync表示异步,调用cfq_async_queue_prio返回块设备对应的异步队列;否则调用cfq_find_alloc_queue来创建一个新队列

cfq_get_io_context(struct cfq_data *cfqd, gfp_t gfp_mask)

/*
* Setup general io context and cfq io context. There can be several cfq
* io contexts per general io context, if this process is doing io to more
* than one device managed by cfq.
*/

首先调用get_io_context,获取current指向的task_struct对应的io_context,如果没有则创建一个io_context;下面调用cfq_cic_lookup获取cfq_io_context,如果为空则调用cfq_alloc_io_context创建一个cfq_io_context,并调用cfq_cic_link把cfq_io_context和io_context关联起来;最后调用cfq_ioc_set_ioprio,cfq_ioc_set_cgroup,实际上是对每个cfq_io_context,调用changed_ioprio,changed_cgroup,这些函数是设置个ioprio_changed, cgroup_changed之类的标签

cfq_update_idle_window(struct cfq_data *cfqd, struct cfq_queue *cfqq,struct cfq_io_context *cic)

首先,如果是异步队列或者队列级别为IDLE,不考虑idle(slice_idle, group_idle之类的磁头停留时间)

如果cfq_queue->next_rq->cmd_flags包含了REQ_NOIDLE,不考虑idle;如果slice_idle为0,也不考虑idle;如果io_context->nr_tasks为0,也不考虑idle

最后根据前后是否idle,调用cfq_mark_cfqq_idle_window/cfq_clear_cfqq_idle_window

cfq_should_preempt(struct cfq_data *cfqd, struct cfq_queue *new_cfqq,struct request *rq):判断new_cfqq是否可以抢占

/*
* Check if new_cfqq should preempt the currently active queue. Return 0 for
* no or if we aren't sure, a 1 will cause a preempt.
*/

#3317 如果new_cfqq是idle class的,其最低优先级无法抢占

#3320 如果cfqq,也就是cfq_data->active_queue是idle class的,必定可以被抢占

#3326 不允许非RT抢占RT的cfq_queue

#3333 当前cfqq不是同步队列,那么同步请求所在队列可以抢占

#3336 如果两个队列不属于同一个cgroup,不可抢占

#3339 如果当前cfqq时间片已经用完,可以抢占

#3353 如果请求是基于元数据的,可以抢占

#3359 RT请求可以抢占非RT的请求

cfq_preempt_queue(struct cfq_data *cfqd, struct cfq_queue *cfqq)

/*
* cfqq preempts the active queue. if we allowed preempt with no slice left,
* let it have half of its nominal slice.
*/

cfq_rq_enqueued(struct cfq_data *cfqd, struct cfq_queue *cfqq,struct request *rq)

/*
* Called when a new fs request (rq) is added (to cfqq). Check if there's
* something we should do about it
*/

#3419 - #3421 可以看出cfq_data->rq_queued代表了块设备调度器等待处理的request,cfq_queue->meta_pending代表了每个队列的元数据请求

#3429 - #3451 如果请求所在的队列就是当前cfq_data的active_queue

/*
* Remember that we saw a request from this process, but
* don't start queuing just yet. Otherwise we risk seeing lots
* of tiny requests, because we disrupt the normal plugging
* and merging. If the request is already larger than a single
* page, let it rip immediately. For that case we assume that
* merging is already done. Ditto for a busy system that
* has other work pending, don't risk delaying until the
* idle timer unplug to continue working.
*/

#3452 - #3460 这时意味着要抢占

无论上述哪种情况,都会调用__blk_run_queue,该函数会调用request_queue->request_fn,这个函数由底层驱动初始化,用来从调度队列里获取请求。一般这个函数会调用电梯算法的__elv_next_request,可能会再调用elevator_dispatch_fn。同时也意味着除此之外的情况不需要把请求立刻交给底层驱动


cfq_kick_queue(struct work_struct *work):该函数是一个工作队列的延迟执行函数,被赋值给cfq_data->unplug_work,该函数最后执行__blk_run_queue

void cfq_idle_slice_timer(unsigned long data)

/*
* Timer running if the active_queue is currently idling inside its time slice
*/

我猜测是队列slice_idle时间过去之后,触发的timer执行的函数

如果cfq_cfqq_must_dispatch(cfqq)为true,无脑dispatch掉;如果time slice过期,调用cfq_slice_expired;如果cfq_data还有其他的busy queue,不作为;如果cfq_queue->sort_list不为空,dispatch掉

分发的方式是调用cfq_schedule_dispatch,通过一个工作队列调用cfq_data->unplug_work,这个unplug_work可以看到调用cfq_kick_queue来让底层驱动得到请求

static struct elevator_type iosched_cfq = {
.ops = {
.elevator_merge_fn = cfq_merge,
.elevator_merged_fn =cfq_merged_request,
.elevator_merge_req_fn =cfq_merged_requests,
.elevator_allow_merge_fn =cfq_allow_merge,
.elevator_bio_merged_fn =cfq_bio_merged,
.elevator_dispatch_fn =cfq_dispatch_requests,
.elevator_add_req_fn =cfq_insert_request,
.elevator_activate_req_fn =cfq_activate_request,
.elevator_deactivate_req_fn =cfq_deactivate_request,
.elevator_completed_req_fn =cfq_completed_request,
.elevator_former_req_fn =elv_rb_former_request,
.elevator_latter_req_fn =elv_rb_latter_request,
.elevator_set_req_fn =cfq_set_request,
.elevator_put_req_fn =cfq_put_request,
.elevator_may_queue_fn =cfq_may_queue,
.elevator_init_fn =cfq_init_queue,
.elevator_exit_fn =cfq_exit_queue,
.trim = cfq_free_io_context,
},
.elevator_attrs =cfq_attrs,
.elevator_name ="cfq",
.elevator_owner =THIS_MODULE,

};

以上是CFQ调度器在电梯算法接口中的实现函数,本文对重要的函数进行解析:

cfq_dispatch_requests

该函数是电梯算法用到的最重要的一个函数,底层的设备驱动会调用电梯算法的接口elevator_dispatch_fn来获取下一个执行的请求,也就是调用cfq_dispatch_requests

#2582 可以看到cfq_data指针存在request_queue->elevator->elevator_data中

#2585 如果cfq_data->busy_queues为0,说明没有请求队列,直接返回

#2588 cfq_forced_dispatch后面再讲

#2591 cfq_select_queue前面已经提过,最终是给出一个结果:保留当前cfq_queue还是重新选一个cfq_queue。这里面涉及了诸如time slice是否耗尽,是否需要slice_idle和group_idle,当前cfq_queue里是否还有请求等一系列判断。如果选择了新的一个cfq_queue,那么把这个新的cfq_queue放到cfq_data->active_queue里,把原来的cfq_queue也加入到cfq_data,cfq_group的一系列service tree, prio tree这些红黑树中

#2598 找到了cfq_queue之后,开始调用cfq_dispatch_request,该函数的作用和代码中的注释一样

/*

* Dispatch a request from cfqq, moving them to the request queue

* dispatch list.

*/

#2551 调用cfq_may_dispatch来判断当前是否可以分发给下层驱动请求,该函数的解析请看前一篇

#2557 调用cfq_check_fifo,查看并返回cfq_queue->fifo队列里已经超时的请求。我们前面提到过,cfq_queue内部是按照部分类似deadline的调度算法来进行的。如果没有超时的请求,则返回正常的红黑树的下一个请求next_rq

这里再提下cfq_queue的请求队列,cfq_queue->sort_list是一个红黑树结构,里面是这个队列里排过序的请求。请求排序的key是基于请求的起始sector的,也就是blk_rq_pos(request),具体可以参考elv_rb_add的实现

#2564 是真正把请求下发到底层驱动的实现

/*

* Move request from internal lists to the request queue dispatch list.

*/

static void cfq_dispatch_insert(struct request_queue *q, struct request *rq)

{

struct cfq_data *cfqd = q->elevator->elevator_data;

struct cfq_queue *cfqq = RQ_CFQQ(rq);


cfq_log_cfqq(cfqd, cfqq, "dispatch_insert");


cfqq->next_rq = cfq_find_next_rq(cfqd, cfqq, rq);

cfq_remove_request(rq);

cfqq->dispatched++;

(RQ_CFQG(rq))->dispatched++;

elv_dispatch_sort(q, rq);


cfqd->rq_in_flight[cfq_cfqq_sync(cfqq)]++;

cfqq->nr_sectors += blk_rq_sectors(rq);

cfq_blkiocg_update_dispatch_stats(&cfqq->cfqg->blkg, blk_rq_bytes(rq),

rq_data_dir(rq), rq_is_sync(rq));

}

此时rq要被下发到底层驱动,所以需要找一个和rq磁盘位置最近的请求作为cfq_queue->next_rq,之后调用cfq_remove_request把请求从cfq_queue队列里删除。之后调用elv_dispatch_sort把请求加入到分发队列中并排序


#2608 如果cfq_data->busy_queues大于1,同时该cfq_queue是一个异步队列,同时cfq_queue使用的时间已经超出了cfq_prio_to_maxrq计算出来的时间或者该队列是idle class,都会调用cfq_slice_expired立刻让队列过期


static void cfq_insert_request(struct request_queue *q, struct request *rq)

#define RQ_CIC(rq) ((struct cfq_io_context *) (rq)->elevator_private[0])
#define RQ_CFQQ(rq) (struct cfq_queue *) ((rq)->elevator_private[1])
#define RQ_CFQG(rq) (struct cfq_group *) ((rq)->elevator_private[2])

从上面3个宏定义可以看出,request相关联的cfq_io_context, cfq_queue, cfq_group都是存在request->elevator_private数组中的,因此

#3466 - #3467 通过request_queue->elevator->elevator_data得到cfq_data,通过request得到相关联的cfq_queue,通过request得到相关联cfq_io_context进而得到io_context

#3470 调用cfq_init_prio_data

#2852 如果cfq_cfqq_prio_changed(cfqq)为false,直接返回,因为如果优先级没变化,没必要改变cfq_queue的优先级变量

#2855 通过io_context得到进程的io优先级ioprio_class

#2859 - #2879 ioprio_class有IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE,相应的ioprio也有不同的计算方法

#2885 - #2887 按照ioprio_class, ioprio初始化好cfq_queue

#3473 cfqq->fifo加到request->queuelist末尾

#3474 调用cfq_add_rq_rb把request添加到cfq_queue的红黑树中

#3478 调用cfq_rq_enqueue


cfq_completed_request(struct request_queue *q, struct request *rq)

开始顺序调用cfqd->rq_in_driver--,cfqd->rq_in_flight--, cfqq->dispatched--, cfq_group->dispatched--

如果cfqq是当前cfqd->active_queue

/*
* If this is the active queue, check if it needs to be expired,
* or if we want to idle in case it has no pending requests.
*/

/*
* Idling is not enabled on:
* - expired queues
* - idle-priority queues
* - async queues
* - queues with still some requests queued
* - when there is a close cooperator
*/


cfq_may_queue(struct request_queue *q, int rw):判断请求是否必须要被加入队列

cfq_queue的属性中,包括workload priority:IDLE, BE, RT,包括workload type:ASYNC, SYNC_NOIDLE, SYNC。同时cfq_queue虽然基于CFQ调度器,但其内部的算法还是基于dead-line的

cfq_group包含了多个红黑树service tree,对应不同workload priority, workload type

一些工具性质的函数:

cfq_find_cfqg(struct cfq_data *cfqd, struct blkio_cgroup *blkcg):如果blkcg是全局的blkio_root_cgroup,则返回cfq_data->root_group,否则首先调用blkiocg_lookup_group在全局的blkio_cgroup的hash列表中查找__key为cfq_data指针的blkio_cgroup结构。通过blkio_cgroup查找cfq_group的方法很简单,通过一个container_of宏就可以。说的通俗一点,就是我们已经通过进程找到了对应的blkio_cgroup,可能是一个根cgroup也可能是某个子cgroup,但这个cgroup的设置未必会有相应的device,e.g. 只设置了blkio.weight而没有设置blkio.dev_weight,这时cfq_data就派上用场了。假设这种场景:一个cgroup中的不同进程读写不同的block device,甚至一个进程读写不同的device,但proportional weight只是针对单个块设备来的,【这里应该理解为什么blkio_cgroup会有多个blkio_group的哈希项了吧】,因此需要通过cfq_data这个key来找到那个blkio_group结构(顺便填入cfq_group->blkg.dev,通过cfq_data->queue->backing_dev_info得来)

cfq_get_cfqg(struct cfq_data *cfqd):首先通过current指向的当前的task_struct,查找其所属的blkio_cgroup,如果当前task_struct还没有加入cgroup,则返回全局的blkio_root_cgroup,其对应的cfq_group结构是cfq_data->root_group。再调用cfq_find_cfqg,由于传入了cfq_data,因此可以找到cfq_data对应的cfq_cgroup结构。如果没有找到则调cfq_alloc_cfqg初始化一个cfq_group结构,再通过current找到blkio_cgroup,最后调用cfq_init_add_cfqg_lists把cfq_data, cfq_group, blkio_cgroup三个结构关联起来。

cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct io_context *ioc,gfp_t gfp_mask):通过ioc得到cfq_data所关联的块设备上进程的cfq_queue,其关键函数为cfq_find_alloc_queue,该函数首先调用cfq_get_cfqg,通过current指向的当前CPU上在跑的进程来找到cfqd所在块设备上的cfq_group;其次调用cfq_cic_lookup来得到块设备对应的cfq_io_context, 从而得到对应的cfq_queue(根据其是否同步请求);最后如果这时cfq_queue为空,则调用kmem_cache_alloc_node重新分配一个cfq_queue,并将其初始化完毕

cfq_init_add_cfqg_lists(struct cfq_data *cfqd,struct cfq_group *cfqg, struct blkio_cgroup *blkcg):把cfqg->blkg这个block_group加到blkcg的哈希表中,这里哈希键值是cfq_data的指针值。同时cfqd这个cfq_data结构也保存了一个哈希表,表头是cfq_data->cfqg_list,该函数会把cfq_group也同时加到这个哈希表里【这里可以看到,blkio_cgroup会保存一个blkio_group的哈希表,每个cfq_data对应一个blkio_group】。同时每个cfq_data也会保存一个哈希表,记录这个cfq_data对应的块设备下的所有cfq_group

cfq_get_io_context(struct cfq_data *cfqd, gfp_t gfp_mask)

/*
* Setup general io context and cfq io context. There can be several cfq
* io contexts per general io context, if this process is doing io to more
* than one device managed by cfq.
*/

上面这段解释了为什么一个io_context里会有多个cfq_io_context,因为一个进程可能同时读写多个设备,这时需要通过cfq_data来确定块设备,从而得到基于这个块设备IO的cfq_io_context

该函数首先调用cfq_cic_lookup查找是否已有cfq_io_context,如果有了就退出,否则调用cfq_alloc_io_context创建一个cfq_io_context,把这个cfq_io_context加入到io_context的radix_tree里(key值为cfq_data指针),如果有必要则调用cfq_ioc_set_ioprio,cfq_ioc_set_cgroup来设置io priority和cgroup

cfq_ioc_set_cgroup(struct io_context *ioc):对于ioc的哈希表ioc->cic_list中的每一个hash node(实际上是cfq_io_context),调用changed_cgroup。 其中changed_cgroup的作用是把cfq_io_context的cfq_queue类型的同步队列设置为NULL,代码中的解释如下

/*
* Drop reference to sync queue. A new sync queue will be
* assigned in new group upon arrival of a fresh request.
*/

cfq_ioc_set_ioprio(struct io_context *ioc):和cfq_ioc_set_cgroup类似,跳过了

cfq_cic_lookup(struct cfq_data *cfqd, struct io_context *ioc):io_context保存了一个radix_tree,其树根为io_context->radix_root。据我猜测,io_context为什么要包含一个cfq_io_context的radix tree呢?可能是因为进程会同时读写多个块设备,因此根据cfq_data的成员cic_index,里面是cfq_data对应的块设备在radix tree里的索引。最后返回io_context中相应块设备对应的cfq_io_context

cfq_cic_link(struct cfq_data *cfqd, struct io_context *ioc,struct cfq_io_context *cic, gfp_t gfp_mask):具体的代码注释讲的很清楚了,跳过

/*
* Add cic into ioc, using cfqd as the search key. This enables us to lookup
* the process specific cfq io context when entered from the block layer.
* Also adds the cic to a per-cfqd list, used when this queue is removed.
*/

cic_to_cfqd(struct cfq_io_context *cic):cfq_io_context的key就是对应的cfq_data

cfq_set_request(struct request_queue *q, struct request *rq, gfp_t gfp_mask):这里可以看到,有struct cfq_data *cfqd = q->elevator->elevator_data 说明cfq_data是基于块设备的。该函数作用是为一个request来分配相应的cfq_io_context, cfq_queue并存到request->elevator_private中。

cfq_scaled_cfqq_slice(struct cfq_data *cfqd, struct cfq_queue *cfqq):通过一系列公式,计算出一个cfq_queue所占用的time_slice。首先计算cfq_cgroup中的平均cfq_queue个数,以及每个cfq_queue的time slice,相乘得到expect_latency为这个cgroup希望得到的time slice;同时调用cfq_group_slice按照权重比例计算出cgroup的time slice;如果这个time slice小于expect_latency,则调整之前根据cfq_queue的优先级计算出的slice,否则返回之前调用cfq_prio_to_slice得到的time slice

cfq_prio_slice cfq_prio_to_slice cfq_scale_slice:这三个函数都是计算队列的服务时间slice time的

cfq_group_slice:cfq_data->grp_service_tree为一个cfq_rb_root为一个红黑树树根,其成员total_weight为这个块设备上所有cgroup的权重值,而cfq_group->weight为该cgroup的权重值,因此该函数返回基于cfq_target_latency,300ms,各个cgroup所占用的slice时间,基于weight的比例。

cfq_set_prio_slice:设置cfq_queue中对应的slice_start, slice_end, allocated_slice

cfq_choose_req(struct cfq_data *cfqd, struct request *rq1, struct request *rq2, sector_t last):看代码中的解释

/*
* Lifted from AS - choose which of rq1 and rq2 that is best served now.
* We choose the request that is closest to the head right now. Distance
* behind the head is penalized and only allowed to a certain extent.
*/

基本上可以认为,同步请求优先异步请求,其次根据请求的位置,按照和AS类似的算法决定优先处理哪个请求

__cfq_group_service_tree_add(struct cfq_rb_root *st, struct cfq_group *cfqg):向service tree插入一个cfq_group,其中红黑树的key被编程为

static inline s64
cfqg_key(struct cfq_rb_root *st, struct cfq_group *cfqg)
{
return cfqg->vdisktime - st->min_vdisktime;
}

这边vdisktime和min_vdisktime是干什么用的,目前我也不清楚

cfq_link_cfqq_cfqg(struct cfq_queue *cfqq, struct cfq_group *cfqg):这个函数本身没啥可说的,但是验证了在CFQ调度器中,所有的异步请求都属于cfq_data->root_group这个cgroup,因此不受指定cgroup的任何限制


#1248 - #1267这段代码,是不需要cgroup调度支持的cfq调度器代码,可以看出简单很多,cfq_get_cfqg只是简单返回cfq_data->root_group


cfq_service_tree_add(struct cfq_data *cfqd, struct cfq_queue *cfqq,bool add_front):该函数目的是把cfq_queue加入到cfq_group对应的service_tree的红黑树中。首先根据io class, io priority来找到cfq_group对应的service_tree,类型为cfq_rb_root,其中插入的key是计算出来的一个起始时间,应该cfq_group是按照这个起始时间来依次处理挂在上面的所有cfq_queue的请求。最后调用cfq_group_notify_queue_add来通知cfq_data

cfq_prio_tree_lookup,cfq_prio_tree_add:这两个函数都是把cfq_queue加到cfq_data里的priority tree的红黑树中,cfq_data共有8个priority tree,对应不同的优先级,而红黑树中的排序基于cfq_queue中第一个请求的sector position

cfq_resort_rr_list,cfq_add_cfqq_rr,cfq_del_cfqq_rr

前者把cfq_queue加到cfq_data中的cgroup对应的service_tree数组,以及cfq_data的priority tree的红黑树中。

后者除了调用cfq_resort_rr_list以外,还递增了cfq_data->busy_queues,cfq_data->busy_sync_queues

最后把cfq_queue移除出service tree,和priority tree,并调用cfq_group_notify_queue_del通知cfq_data

cfq_del_rq_rb,cfq_add_rq_rb:这两个函数操作cfq_queue里面的request请求,把请求从cfq_queue中添加或者删除

cfq_add_rq_rb首先调用elv_rb_add把请求插到cfq_queue->sort_list这个红黑树中,基于请求的起始sector,再调用cfq_dispatch_insert真正把请求下发到底层驱动上,下面再调用cfq_add_cfqq_rr把队列挂到cfq_data代表的块设备上,下面重新选择cfq_queue->next_rq,如果和之前的cfq_queue->next_rq不同,需要改动cfq_queue对应的优先级并调整到队列所在的cfq_data下的priority tree中

cfq_del_rq_rb调用elv_rb_del把请求从cfq_queue->sort_list中删除,如果此时cfq_queue->sort_list为空了,而该队列又在cfq_data的priority tree中,则从红黑树里删除掉

cfq_remove_request(struct request *rq):调用cfq_del_rq_rb从cfq_queue中删除rq

cfq_find_rq_fmerge(struct cfq_data *cfqd, struct bio *bio):通过当前current的task_struct来找到io_context,从而调用cfq_cic_lookup来找到cfq_io_context,然后根据bio是否同步找到对应的cfq_queue;下面找到bio之后的第一个sector,然后在cfq_queue->sort_list中基于这个sector查找是否有对应的bio可以被merge

cfq_merge:检查是否可以merge,如何可以修改对应的request,把bio给merge进request

cfq_merged_request:调用cfq_reposition_rq_rb,把相应的request更新为已经merge了bio的request

__cfq_set_active_queue:似乎是初始化cfq_queue,并将其设置为active_queue

__cfq_slice_expired(struct cfq_data *cfqd, struct cfq_queue *cfqq,bool timed_out)

/*
* current cfqq expired its slice (or was too idle), select new one
*/

#1744 - #1751 (kernel.org 3.0.23 cfq-iosched.c) 这里的cfq_queue->slice_resid似乎是还没有用完的slice_time

nr_sync 计算出cfq_group同步队列的个数

cfq_group_served

charge表示已经用掉的配额,在不同模式下意义不同,如果是iops模式,用cfq_queue->slice_dispatch,也就是dispatch的请求个数作为不同的cfq_queue的配额,如果是异步请求则用cfq_queue->allocated_slice,也就是分配给该队列的时间作为配额;否则是同步非iops模式则等同于used_sl (通过cfq_cfqq_slice_usage计算得出)

#975 - #978 cfq_group->vdisktime似乎是改cgroup至今所有用掉的slice总和

如果当前的cfq_group的expire时间(cfq_data->workload_expires)在jiffies之前,那啥也不用做了,不然保存相应的cfq_group的saved_workload_slice,saved_workload,saved_serving_prio

最后调用cfq_resort_rr_list,把cfq_queue插到cfq_group,cfq_data对应的service tree, prio tree的红黑树中

所以代码看下来,我猜测所有的cfq_queue开始时都在cfq_data和其对应的cfq_group的service tree,priority tree中,当轮到这个cfq_queue被处理时,从这些红黑树中被摘下来,等到其slice expired之后,更新一系列参数后被放回原来的红黑树里;再进一步说,如果cfq_queue里已经没有请求了,则会把他们从红黑树里移除掉,如果此时cfq_queue的time slice还没用完,会留着下次再用

这应该就是CFQ大致的工作原理


cfq_dist_from_last,cfq_rq_close

前者计算出request的sector和cfq_data的last_position对应的sector之间相隔的sector数,后者拿这个值和CFQ_CLOSE_THR比较,如果在这个范围内就认为两个request是相近的请求(言下之意不会对磁头转动造成太多的overhead)

cfqq_close(struct cfq_data *cfqd,struct cfq_queue *cur_cfqq)

/*
* First, if we find a request starting at the end of the last
* request, choose it.
*/

首先调用cfq_prio_tree_lookup查找同一个priority tree下的cfq_queue中,有没有起始sector正好在cfq_data->last_position上的,如果有则返回这个离当前磁头位置最近的cfq_queue

/*
* If the exact sector wasn't found, the parent of the NULL leaf
* will contain the closest sector.
*/

如果cfq_prio_tree_lookup没有找到,则返回的parent参数包含了sector差别最小的那个cfq_queue,如果这个cfq_queue->next_rq满足了cfq_rq_close的要求,则返回这个cfq_queue

如果不满足的,开始遍历这个红黑树(只遍历一次),再次判断这个cfq_queue是否满足cfq_rq_close,如果不满足就返回NULL

cfq_choose_wl,choose_service_tree:选择最优的workload class,和选择最优的service tree

choose_service_tree选择的优先级是RT > BE > IDLE,这个决定了cfq_data->serving_prio,然后调用cfq_choose_wl来决定cfq_data->serving_type,

/*
* the workload slice is computed as a fraction of target latency
* proportional to the number of queues in that workload, over
* all the queues in the same priority class
*/

group_slice调用cfq_group_slice,根据group的权重计算出来,而slice则是这个workload里的queues占所有busy_queues_avg的比例而计算得出的

对于同步请求而言,slice在经过一系列的比对之后,会把cfq_data->workload_expires = jiffies + slice,即当前服务的cfq_queue的配额时间被放到cfq_data指定的成员中;而对于异步请求而言,由于异步的优先级比同步要低,会再经过一些处理,具体的请参考代码

/*
* Async queues are currently system wide. Just taking
* proportion of queues with-in same group will lead to higher
* async ratio system wide as generally root group is going
* to have higher weight. A more accurate thing would be to
* calculate system wide asnc/sync ratio.
*/
static struct cfq_queue *cfq_select_queue(struct cfq_data *cfqd)

/*
* Select a queue for service. If we have a current active queue,
* check whether to continue servicing it, or retrieve and set a new one.
*/

这里可以参考之前关于cfq_slice_expired函数的解析,可以看到cfq_data每个时刻只服务一个cfq_queue,就是cfq_data->active_queue

cfq_may_dispatch(struct cfq_data *cfqd, struct cfq_queue *cfqq):判断是否可以向底层驱动分发请求

#2463 如果cfq_queue允许idle一段时间,同时块设备还有异步请求on-the-fly,暂时不分发

#2469 如果要分发的这个cfq_queue是一个异步队列,同时块设备上还有同步请求on-the-fly,暂时不分发

#2472 先给max_dispatch设定个初始值,默认是cfq_quantum/2 = 4

#2473 对于idle级别的cfq_queue,max_dispatch设为1

先看#2542,如果当前cfq_queue->dispatched,即已经分发的请求数目没有超过max_dispatch,如果是同步队列则允许分发,异步队列的话,需要修改下max_dispatch的值并重新和cfq_queue->dispatched比较,具体原因请看代码注释

/*
* Async queues must wait a bit before being allowed dispatch.
* We also ramp up the dispatch depth gradually for async IO,
* based on the last sync IO we serviced

*/

再回到#2479,如果此时cfq_queue->dispatched已经超过了max_dispatch,如果这是个同步cfq_queue,同时此时块设备上只有这个cfq_queue有请求,那么不限制该队列的分发请求数,如下面注释

/*
* If there is only one sync queue
* we can ignore async queue here and give the sync
* queue no dispatch limit. The reason is a sync queue can
* preempt async queue, limiting the sync queue doesn't make
* sense. This is useful for aiostress test.
*/

否则如果只有一个cfq_queue,放大一倍max_dispatch的值,到cfq_data->cfq_quantum = 8

不管怎样,最后还是要比较一次cfq_queue->dispatched和max_dispatch的值,来决定是否给底层驱动分发下一个请求

cfq_exit_io_context:当一个task结束之后,需要对io_context关联的所有cfq_io_context,调用cfq_exit_single_io_context

cfq_exit_single_io_context,__cfq_exit_single_io_context:对cfq_io_context的异步,同步队列,分别调用cfq_exit_cfqq

changed_cgroup(struct io_context *ioc, struct cfq_io_context *cic)

首先无视掉cic对应的异步队列,由此也可以看出其实CFQ里,异步请求是不分cgroup的,下面直接把cfq_io_context里的同步队列设置为NULL,代码中的注释告诉了为什么要这么做

/*
* Drop reference to sync queue. A new sync queue will be
* assigned in new group upon arrival of a fresh request.
*/

cfq_get_queue(struct cfq_data *cfqd, bool is_sync, struct io_context *ioc,gfp_t gfp_mask):如果是is_sync表示异步,调用cfq_async_queue_prio返回块设备对应的异步队列;否则调用cfq_find_alloc_queue来创建一个新队列

cfq_get_io_context(struct cfq_data *cfqd, gfp_t gfp_mask)

/*
* Setup general io context and cfq io context. There can be several cfq
* io contexts per general io context, if this process is doing io to more
* than one device managed by cfq.
*/

首先调用get_io_context,获取current指向的task_struct对应的io_context,如果没有则创建一个io_context;下面调用cfq_cic_lookup获取cfq_io_context,如果为空则调用cfq_alloc_io_context创建一个cfq_io_context,并调用cfq_cic_link把cfq_io_context和io_context关联起来;最后调用cfq_ioc_set_ioprio,cfq_ioc_set_cgroup,实际上是对每个cfq_io_context,调用changed_ioprio,changed_cgroup,这些函数是设置个ioprio_changed, cgroup_changed之类的标签

cfq_update_idle_window(struct cfq_data *cfqd, struct cfq_queue *cfqq,struct cfq_io_context *cic)

首先,如果是异步队列或者队列级别为IDLE,不考虑idle(slice_idle, group_idle之类的磁头停留时间)

如果cfq_queue->next_rq->cmd_flags包含了REQ_NOIDLE,不考虑idle;如果slice_idle为0,也不考虑idle;如果io_context->nr_tasks为0,也不考虑idle

最后根据前后是否idle,调用cfq_mark_cfqq_idle_window/cfq_clear_cfqq_idle_window

cfq_should_preempt(struct cfq_data *cfqd, struct cfq_queue *new_cfqq,struct request *rq):判断new_cfqq是否可以抢占

/*
* Check if new_cfqq should preempt the currently active queue. Return 0 for
* no or if we aren't sure, a 1 will cause a preempt.
*/

#3317 如果new_cfqq是idle class的,其最低优先级无法抢占

#3320 如果cfqq,也就是cfq_data->active_queue是idle class的,必定可以被抢占

#3326 不允许非RT抢占RT的cfq_queue

#3333 当前cfqq不是同步队列,那么同步请求所在队列可以抢占

#3336 如果两个队列不属于同一个cgroup,不可抢占

#3339 如果当前cfqq时间片已经用完,可以抢占

#3353 如果请求是基于元数据的,可以抢占

#3359 RT请求可以抢占非RT的请求

cfq_preempt_queue(struct cfq_data *cfqd, struct cfq_queue *cfqq)

/*
* cfqq preempts the active queue. if we allowed preempt with no slice left,
* let it have half of its nominal slice.
*/

cfq_rq_enqueued(struct cfq_data *cfqd, struct cfq_queue *cfqq,struct request *rq)

/*
* Called when a new fs request (rq) is added (to cfqq). Check if there's
* something we should do about it
*/

#3419 - #3421 可以看出cfq_data->rq_queued代表了块设备调度器等待处理的request,cfq_queue->meta_pending代表了每个队列的元数据请求

#3429 - #3451 如果请求所在的队列就是当前cfq_data的active_queue

/*
* Remember that we saw a request from this process, but
* don't start queuing just yet. Otherwise we risk seeing lots
* of tiny requests, because we disrupt the normal plugging
* and merging. If the request is already larger than a single
* page, let it rip immediately. For that case we assume that
* merging is already done. Ditto for a busy system that
* has other work pending, don't risk delaying until the
* idle timer unplug to continue working.
*/

#3452 - #3460 这时意味着要抢占

无论上述哪种情况,都会调用__blk_run_queue,该函数会调用request_queue->request_fn,这个函数由底层驱动初始化,用来从调度队列里获取请求。一般这个函数会调用电梯算法的__elv_next_request,可能会再调用elevator_dispatch_fn。同时也意味着除此之外的情况不需要把请求立刻交给底层驱动


cfq_kick_queue(struct work_struct *work):该函数是一个工作队列的延迟执行函数,被赋值给cfq_data->unplug_work,该函数最后执行__blk_run_queue

void cfq_idle_slice_timer(unsigned long data)

/*
* Timer running if the active_queue is currently idling inside its time slice
*/

我猜测是队列slice_idle时间过去之后,触发的timer执行的函数

如果cfq_cfqq_must_dispatch(cfqq)为true,无脑dispatch掉;如果time slice过期,调用cfq_slice_expired;如果cfq_data还有其他的busy queue,不作为;如果cfq_queue->sort_list不为空,dispatch掉

分发的方式是调用cfq_schedule_dispatch,通过一个工作队列调用cfq_data->unplug_work,这个unplug_work可以看到调用cfq_kick_queue来让底层驱动得到请求

static struct elevator_type iosched_cfq = {
.ops = {
.elevator_merge_fn = cfq_merge,
.elevator_merged_fn =cfq_merged_request,
.elevator_merge_req_fn =cfq_merged_requests,
.elevator_allow_merge_fn =cfq_allow_merge,
.elevator_bio_merged_fn =cfq_bio_merged,
.elevator_dispatch_fn =cfq_dispatch_requests,
.elevator_add_req_fn =cfq_insert_request,
.elevator_activate_req_fn =cfq_activate_request,
.elevator_deactivate_req_fn =cfq_deactivate_request,
.elevator_completed_req_fn =cfq_completed_request,
.elevator_former_req_fn =elv_rb_former_request,
.elevator_latter_req_fn =elv_rb_latter_request,
.elevator_set_req_fn =cfq_set_request,
.elevator_put_req_fn =cfq_put_request,
.elevator_may_queue_fn =cfq_may_queue,
.elevator_init_fn =cfq_init_queue,
.elevator_exit_fn =cfq_exit_queue,
.trim = cfq_free_io_context,
},
.elevator_attrs =cfq_attrs,
.elevator_name ="cfq",
.elevator_owner =THIS_MODULE,

};

以上是CFQ调度器在电梯算法接口中的实现函数,本文对重要的函数进行解析:

cfq_dispatch_requests

该函数是电梯算法用到的最重要的一个函数,底层的设备驱动会调用电梯算法的接口elevator_dispatch_fn来获取下一个执行的请求,也就是调用cfq_dispatch_requests

#2582 可以看到cfq_data指针存在request_queue->elevator->elevator_data中

#2585 如果cfq_data->busy_queues为0,说明没有请求队列,直接返回

#2588 cfq_forced_dispatch后面再讲

#2591 cfq_select_queue前面已经提过,最终是给出一个结果:保留当前cfq_queue还是重新选一个cfq_queue。这里面涉及了诸如time slice是否耗尽,是否需要slice_idle和group_idle,当前cfq_queue里是否还有请求等一系列判断。如果选择了新的一个cfq_queue,那么把这个新的cfq_queue放到cfq_data->active_queue里,把原来的cfq_queue也加入到cfq_data,cfq_group的一系列service tree, prio tree这些红黑树中

#2598 找到了cfq_queue之后,开始调用cfq_dispatch_request,该函数的作用和代码中的注释一样

/*

* Dispatch a request from cfqq, moving them to the request queue

* dispatch list.

*/

#2551 调用cfq_may_dispatch来判断当前是否可以分发给下层驱动请求,该函数的解析请看前一篇

#2557 调用cfq_check_fifo,查看并返回cfq_queue->fifo队列里已经超时的请求。我们前面提到过,cfq_queue内部是按照部分类似deadline的调度算法来进行的。如果没有超时的请求,则返回正常的红黑树的下一个请求next_rq

这里再提下cfq_queue的请求队列,cfq_queue->sort_list是一个红黑树结构,里面是这个队列里排过序的请求。请求排序的key是基于请求的起始sector的,也就是blk_rq_pos(request),具体可以参考elv_rb_add的实现

#2564 是真正把请求下发到底层驱动的实现

/*

* Move request from internal lists to the request queue dispatch list.

*/

static void cfq_dispatch_insert(struct request_queue *q, struct request *rq)

{

struct cfq_data *cfqd = q->elevator->elevator_data;

struct cfq_queue *cfqq = RQ_CFQQ(rq);


cfq_log_cfqq(cfqd, cfqq, "dispatch_insert");


cfqq->next_rq = cfq_find_next_rq(cfqd, cfqq, rq);

cfq_remove_request(rq);

cfqq->dispatched++;

(RQ_CFQG(rq))->dispatched++;

elv_dispatch_sort(q, rq);


cfqd->rq_in_flight[cfq_cfqq_sync(cfqq)]++;

cfqq->nr_sectors += blk_rq_sectors(rq);

cfq_blkiocg_update_dispatch_stats(&cfqq->cfqg->blkg, blk_rq_bytes(rq),

rq_data_dir(rq), rq_is_sync(rq));

}

此时rq要被下发到底层驱动,所以需要找一个和rq磁盘位置最近的请求作为cfq_queue->next_rq,之后调用cfq_remove_request把请求从cfq_queue队列里删除。之后调用elv_dispatch_sort把请求加入到分发队列中并排序


#2608 如果cfq_data->busy_queues大于1,同时该cfq_queue是一个异步队列,同时cfq_queue使用的时间已经超出了cfq_prio_to_maxrq计算出来的时间或者该队列是idle class,都会调用cfq_slice_expired立刻让队列过期


static void cfq_insert_request(struct request_queue *q, struct request *rq)

#define RQ_CIC(rq) ((struct cfq_io_context *) (rq)->elevator_private[0])
#define RQ_CFQQ(rq) (struct cfq_queue *) ((rq)->elevator_private[1])
#define RQ_CFQG(rq) (struct cfq_group *) ((rq)->elevator_private[2])

从上面3个宏定义可以看出,request相关联的cfq_io_context, cfq_queue, cfq_group都是存在request->elevator_private数组中的,因此

#3466 - #3467 通过request_queue->elevator->elevator_data得到cfq_data,通过request得到相关联的cfq_queue,通过request得到相关联cfq_io_context进而得到io_context

#3470 调用cfq_init_prio_data

#2852 如果cfq_cfqq_prio_changed(cfqq)为false,直接返回,因为如果优先级没变化,没必要改变cfq_queue的优先级变量

#2855 通过io_context得到进程的io优先级ioprio_class

#2859 - #2879 ioprio_class有IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE,相应的ioprio也有不同的计算方法

#2885 - #2887 按照ioprio_class, ioprio初始化好cfq_queue

#3473 cfqq->fifo加到request->queuelist末尾

#3474 调用cfq_add_rq_rb把request添加到cfq_queue的红黑树中

#3478 调用cfq_rq_enqueue


cfq_completed_request(struct request_queue *q, struct request *rq)

开始顺序调用cfqd->rq_in_driver--,cfqd->rq_in_flight--, cfqq->dispatched--, cfq_group->dispatched--

如果cfqq是当前cfqd->active_queue

/*
* If this is the active queue, check if it needs to be expired,
* or if we want to idle in case it has no pending requests.
*/

/*
* Idling is not enabled on:
* - expired queues
* - idle-priority queues
* - async queues
* - queues with still some requests queued
* - when there is a close cooperator
*/


cfq_may_queue(struct request_queue *q, int rw):判断请求是否必须要被加入队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值