CFQ调度器算法

 

在相对比较老的Linux中,CFQ机制的实现还比较简单,仅仅是针对不同的thread进行磁盘带宽的公平调度。但是,自从新Kernel引入Cgroup机制之后,CFQ的机制就显得比较复杂了,因为,此时的CFQ不仅可以对IO带宽进行公平调度,更重要的是对磁盘IO进行QoS控制。

IOQoS控制是非常必要的,在同一系统中,不同进程/线程所享有的IO带宽可以是不同的,为了达到此目的,LinuxCgroup框架下引入了blkio_cgroup对不同线程的带宽资源进行调度控制。由于cgroup的引入,CFQ算法中引入了cfq_group的概念,所以使得整个IO资源调度的算法看起来复杂多了。

为了更好的理解CFQ算法的整个框架,需要理清几个关键数据结构之间的关系,下图是cfq_groupcfq_datacfq_queueioccfq_iocblkio_cgroup以及blkio_group数据结构之间的关系图。

wKiom1NFSO_DlI8LAAHZj40ZJ5M283.jpg

通过上图,我们可以知道ioc是每个线程所拥有的IO上下文(context),由于一个线程可以对多个磁盘设备进行操作,因此,每个线程会对每个正在操作的磁盘对象创建一个cfq_ioc。在cfq_ioc中会保存与这个磁盘设备相关的策略对象cfq_group以及IO调度对象cfq_queue。因此,当一个线程往一个磁盘设备进行写操作时,可以通过IO上下文获取这个request所应该附属的策略对象cfq_group和调度对象cfq_queue

IO上下文是线程相关的,多个线程可以定义相同的策略行为,因此,在Linux Cgroup机制中为每个IO策略集定义了一个blkio_cgroup对象,该对象负责管理IO的控制策略。例如,控制IO的策略主要有带宽、延迟。由于一个线程需要对多个磁盘设备进行IO操作,因此,每个blkio_cgroup对象管理了针对每个磁盘设备的策略集,该对象为blkio_group。假设系统中有两个线程ThreadAThreadB,他们拥有相同的策略并且都在对同一个设备Sda进行操作,那么在blkio_cgroup中只存在一个和sda相关的blkio_group。如果ThreadAThreadB分别对sdasdb进行操作,那么在blkio_cgroup中将会存在两个blkio_group对象。在CFQ算法中,对blkio_group对象进行了封装,形成了cfq_group对象。因此,可以认为cfq_groupblkio_group对象是一一对应的。也就是说cfq_group对象是针对一个磁盘设备的一种策略,当然同一个磁盘设备会存在多种IO策略,他们分别归属不同的线程。因此,一个磁盘的cfq对象中会管理多个cfq_group对象。

CFQ算法是针对一个磁盘设备的,即每个磁盘设备都会管理一个cfq对象。在磁盘设备的request queue对象中会保存这个cfq对象,这点和其他的调度算法是相同的。Cfq_data就是那个cfq对象。前面提到,由于一个磁盘设备会被多个线程进行访问,因此可以存在多种IO策略,所以,一个cfq对象需要管理多个cfq_group,每个cfq_group管理了一种IO策略。在cfq算法中,每个按时间片调度的基本单元是线程,由于多个线程可以共享一种策略,即共享cfq_group,因此,在cfq_group对象中为每个线程独立管理了一个request队列,这个队列对象是cfq_queueCfq_queue是与线程相关的调度对象,cfq算法在调度的过程中会给被调度的cfq_queue分配时间片,然后在这个时间片内处理cfq_queue中的request

调度cfq_queue中的request方法和deadline算法是基本一致的。因此,在cfq_queue中会维护一棵红黑树以及一条FIFO队列,FIFO队列中的request按照时间先后次序排列,红黑树上的request采用LBA访问地址进行索引。并且在处理的过程中,也会考虑临近IO优先处理的策略,使得磁盘临近IO批量处理,提高了IO性能。

从上面的数据结构关系图,我们可以发现,和老版本的cfq算法相比,新版本的CFQ引入了blkio_cgroup以及blkio_group对象,在cfq中的体现为cfq_group。引入cfq_group之后,IO的调度处理将变的更加灵活。例如,如果ThreadA被定义成PolicyA,其访问sda将有80%的带宽,ThreadB被定义成PolicyB,其访问sda将有20%的带宽。对于上述情况,将会创建两个blkio_cgroup,并且拥有两个cfq_group对象,这两个cfq_group对象都会被加入到sda设备的cfq_data对象中进行管理。Cfq_groupA对应着policyA,管理threadAIO请求,cfq_groupB对应着policyB,管理threadBIO请求。由于cfq_groupA策略占有80%带宽,那么在调度过程中,cfq算法会把80%的时间片分给cfq_groupA,而cfq_groupB只能分到20%的时间片,因此达到了对ThreadABIO QoS控制目的。

理清楚cfq算法中关键数据结构之间的关系之后,最重要的问题就是如何实现request的调度处理。下图展示了cfq中各层对象的调度处理方法。

wKiom1NFSQyAkA_6AAIPK_FKgGU193.jpg

在调度一个request时,首先需要选择一个一个合适的cfq_groupCfq调度器会为每个cfq_group分配一个时间片,当这个时间片耗尽之后,会选择下一个cfq_group。每个cfq_group都会分配一个vdisktime,并且通过该值采用红黑树对cfq_group进行排序。在调度的过程中,每次都会选择一个vdisktime最小的cfq_group进行处理。

一个cfq_group管理了7service tree,每棵service tree管理了需要调度处理的对象cfq_queue。因此,一旦cfq_group被选定之后,需要选择一棵service tree进行处理。这7service tree被分成了三大类,分别为RTBEIDLE。这三大类service tree的调度是按照优先级展开的,即RT的优先级高于BEBE的高于IDLE。通过优先级可以很容易的选定一类Service tree。当一类service tree被选定之后,采用service time的方式选定一个合适的cfq_queue。每个Service tree是一棵红黑树,这些红黑树是按照service time进行检索的,每个cfq_queue都会维护自己的service time。分析到这里,我们知道,cfq算法通过每个cfq_groupvdisktime值来选定一个cfq_group进行服务,在处理cfq_group的过程通过优先级选择一个最需要服务的service tree。通过该Service tree得到最需要服务的cfq_queue。该过程在cfq_select_queue函数中实现。

一个cfq_queue被选定之后,后面的过程和deadline算法有点类似。在选择request的时候需要考虑每个request的延迟等待时间,选择那种等待时间最长的request进行处理。但是,考虑到磁盘抖动的问题,cfq在处理的时候也会进行顺序批量处理,即将那些在磁盘上连续的request批量处理掉。

cfq调度的过程中,选择cfq_queue的重要函数实现如下:

static struct cfq_queue *cfq_select_queue(struct cfq_data *cfqd)
{
struct cfq_queue *cfqq, *new_cfqq = NULL;
/* 如果cfq调度器当前没有服务的对象,那么直接跳转去选择一个新的cfq_queue */
cfqq = cfqd->active_queue;
if (!cfqq)
goto new_queue;
/* 如果在cfq中没有request,直接返回 */
if (!cfqd->rq_queued)
return NULL;
/*
* We were waiting for group to get backlogged. Expire the queue
*/
if (cfq_cfqq_wait_busy(cfqq) && !RB_EMPTY_ROOT(&cfqq->sort_list))
goto expire;
/*
* The active queue has run out of time, expire it and select new.
当前的cfq_queue已经服务到限,重新选择一个新的cfq_queue
*/
if (cfq_slice_used(cfqq) && !cfq_cfqq_must_dispatch(cfqq)) {
/*
* If slice had not expired at the completion of last request
* we might not have turned on wait_busy flag. Don't expire
* the queue yet. Allow the group to get backlogged.
*
* The very fact that we have used the slice, that means we
* have been idling all along on this queue and it should be
* ok to wait for this request to complete.
*/
if (cfqq->cfqg->nr_cfqq == 1 && RB_EMPTY_ROOT(&cfqq->sort_list)
&& cfqq->dispatched && cfq_should_idle(cfqd, cfqq)) {
cfqq = NULL;
goto keep_queue;
} else
goto check_group_idle;
}
/*
* The active queue has requests and isn't expired, allow it to
* dispatch.
Cfq_queue还存在很多需要处理的request,继续处理这个cfq_queue
*/
if (!RB_EMPTY_ROOT(&cfqq->sort_list))
goto keep_queue;
/*
* If another queue has a request waiting within our mean seek
* distance, let it run.  The expire code will check for close
* cooperators and put the close queue at the front of the service
* tree.  If possible, merge the expiring queue with the new cfqq.
当前处理的cfq_queue已经超时,需要选择一个在磁盘上与当前处理的request临近的cfq_queue
*/
new_cfqq = cfq_close_cooperator(cfqd, cfqq);
if (new_cfqq) {
if (!cfqq->new_cfqq)
cfq_setup_merge(cfqq, new_cfqq);
goto expire;
}
/*
* No requests pending. If the active queue still has requests in
* flight or is idling for a new request, allow either of these
* conditions to happen (or time out) before selecting a new queue.
*/
if (timer_pending(&cfqd->idle_slice_timer)) {
cfqq = NULL;
goto keep_queue;
}
/*
* This is a deep seek queue, but the device is much faster than
* the queue can deliver, don't idle
**/
if (CFQQ_SEEKY(cfqq) && cfq_cfqq_idle_window(cfqq) &&
(cfq_cfqq_slice_new(cfqq) ||
(cfqq->slice_end - jiffies > jiffies - cfqq->slice_start))) {
cfq_clear_cfqq_deep(cfqq);
cfq_clear_cfqq_idle_window(cfqq);
}
if (cfqq->dispatched && cfq_should_idle(cfqd, cfqq)) {
cfqq = NULL;
goto keep_queue;
}
/*
* If group idle is enabled and there are requests dispatched from
* this group, wait for requests to complete.
*/
check_group_idle:
if (cfqd->cfq_group_idle && cfqq->cfqg->nr_cfqq == 1 &&
cfqq->cfqg->dispatched &&
!cfq_io_thinktime_big(cfqd, &cfqq->cfqg->ttime, true)) {
cfqq = NULL;
goto keep_queue;
}
expire:
/* 将当前cfq_queue设置成超时 */
cfq_slice_expired(cfqd, 0);
new_queue:
/*
* Current queue expired. Check if we have to switch to a new
* service tree
如果没有找到一个临近的cfq_queue,那么选择一个新的service tree进行处理
*/
if (!new_cfqq)
cfq_choose_cfqg(cfqd);
/* 初始化一个调度处理的cfq_queue */
cfqq = cfq_set_active_queue(cfqd, new_cfqq);
keep_queue:
return cfqq;
}