CFS调度器的思想的新理解

本文通过详细分析老的调度器来说明cfs的优势。总是新理解新理解的,我怎么这么没完没了啊,其实每隔一段时间我都会在工作之余再读一读linux内核源代码的关键部分,每次读都有新的理解,然后就第一时间将心得记录下来,今天又读了cfs调度器,越来越发现其美妙了。这次配合了sched-nice-design.txt文档阅读,很受启发,万恶的调度程序终止了,新的时代开始了,O(1)调度器和CFS的作者都是Ingo Molnar,他真的是一个有破有立的家伙,太猛了!

O(1)调度器是很不错的,因为它的效率,正如它的名称一样,可是我们考虑的周全一点,操作系统对任务的调度不仅仅是用最快的速度选择最值得运行的进程,而也要兼顾最不值得运行的进程,所谓“效率优先,兼顾公平”。O(1)对于优先级调度的本意是很有效的,因为它可以在最快的时间内选择出优先级最高的进程,可是低优先级的进程可能因此饥饿而得不到执行的机会正如那篇sched-nice-design文档中讲的那样,O(1)调度器以及以前的更土的调度器的根本麻烦在于时间片的计算,因为那些调度器没有抽象到真正的的层次,更多的是利用底层的硬件中断来进行的,这样的话,根本无法用统一的方式来计算时间片,必须受累于底层的硬件,也就是说没有一个万能的时间片计算公式对每一台机器每一个系统都适用,比如一个系统的HZ定义为1000,另一个是100,那么这两个系统的调度行为就是不一样的;第二,老的时间片分时调度中的时间片更多的是一种绝对的参考值,一会会说出原因,现在要说的就是时间片计算公式计算出来的时间片和进程的优先级不是一个和谐的关系,比如线性关系,比如在nice值为+19到+15这之间,nice值每减少一个点其时间片平均增加delta1,而nice在0到4,时间片平均增加delta2,而这两者是不同的,这是不应该的,因为比如我想让时间片大大增加些,我必须知道我当前的nice值。

以上说的是总体上的,下面我来说说我的理解,到O(1)调度器加上更土的调度器,linux的调度器的时间片的计算大致分了三个阶段,更土的调度器里面也即是O(n),计算出来的时间片几乎分辨不出谁的优先级更高,优先级更高的进程的时间片也不会更具有优势,因为那时的调度器想把时间片限制在50毫秒的左右不超出多少的范围内,计算规则很简单:

#elif HZ < 1600

#define TICK_SCALE(x) ((x) << 1)

#define NICE_TO_TICKS(nice) (TICK_SCALE(20-(nice))+1)

这个简单的公式很线性,很和谐,但是计算出来的时间片几乎差不离,高优先级的进程没有什么优势,后来发展到了O(1)调度器以后,当然进行了改进:

#define BASE_TIMESLICE(p) (MIN_TIMESLICE + /

((MAX_TIMESLICE - MIN_TIMESLICE) * /

(MAX_PRIO-1 - (p)->static_prio) / (MAX_USER_PRIO-1)))

static inline unsigned int task_timeslice(task_t *p)

{

return BASE_TIMESLICE(p);

}

这个公式将最高优先级的时间片延长了,而把最低优先级的时间片缩短了,不管怎样增加了优先级时间片的动态范围,这个在效果上的表现就是,优先级高的进程表现出来的时间片更加长了,为了将优先级最高的进程的时间片和优先级最低的进程的时间片有效分离,增加时间片基数是一种方式,比如,最低优先级的时间片是10,然后每增加1个优先级则增加时间片n倍,取n为2,那么从低到高的时间片依次为10,20,40...,或者按照等差数列增加,比如差为10,那么就是10,20,30,40...但是会遇到一个问题,nice值或者优先级值的增加会和时间片的增加同步吗?如果我们强调同步,那么最高优先级的进程的时间片就会过于长,要么就是基数过小,这样的话就会引起频繁调度而刷新cache。后来在2.6.9内核往后的实现里,又有了新的策略:

static unsigned int task_timeslice(task_t *p)

{

if (p->static_prio < NICE_TO_PRIO(0))

return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);

else

return SCALE_PRIO(DEF_TIMESLICE, p->static_prio);

}

新的实现采用了以nice 0为分界点,正值采用缓线性,负值采用陡线性,这可以保证在nice为正值的时候,nice值的减少率和时间片增加率同步,其实就是两个点的同步,就是+19的nice值和0的nice值,并且为了防止HZ为1000的系统中频繁调度,采用了约定最小时间片为5ms的方式硬性规定,可是为何选择+19的nice值考虑呢?因为它是一个极端值,两点确定一条直线,一个是0,一个是+19,可是为何采用两类斜率的线段呢?因为如果统一采用缓斜率的线段,那么会造成和2.4内核以及2.6.9以前一样的问题,动态范围小,高优先级不突出,但是如果统一采用陡斜率,那么就会造成高优先级的进程时间片过于长,这会引起系统响应性差,注意这在unix不是问题,可是对于linux就是问题了,因此为了既要实现突出高优先级进程,又要削弱低优先级进程又不至于削的更弱而引起频繁调度而刷新cache,那么就很“艺术”的采用了双斜率线性结构,这里有一个哲学,就是如果你既要抬高优势又要削减劣者,那么你要看看它们是否对称,如果不是,比如劣者有个显然的极端,那么在线性结构中,这个显然的极端值一定是一个定线段的端点,正如这里的5ms,另外一个端点就是缓陡分界点,还有一个就是最高的极值,这后面两个极值就需要经验值和别的约束条件了,为何使用线性结构呢?不为别的,正是因为它简单!我一直在想世界不是对称的,其实我们的世界就不是一个对称的,比如温度,我们知道-273°C,而高温已经到达很高的了,有没有高温极限呢?现在还没有找到,如果说我们人类是和谐,是美的化身,那么我们为何不生活在(-273+1000000000000000000000)/2°C呢?...要么我们否认-273是最低的温度,要么我们人类就不要再得瑟。闲话少说,又跑偏了...正是由于旧的调度器的时间片的双斜率导致了nice 0两边的不对称性,导致了你在当前nice为正值和负值两种情况下进行的nice系统调用的行为不同,一个+1时间片减少的快一个减少的慢。在那篇文档中,作者特意说了,最重要的就是linux想用统一的方式进行时间片计算去没有想到不可能,因为时间片的计算和HZ的定义密切相关,这就引入了一个变量,想完全线性的实现是不可能的。

不仅如此,就是因为老的调度器(呵呵,现在O(1)都成了老的调度器了)只选择最高优先级的进程而不管低优先级的进程,因此它是“少了一环”的调度,不能称为是完全的调度,正是它的不完全,才引入了很多复杂的外围算法,比如基于平均睡眠时间的交互检测算法,比如优先级的静态和动态分类算法,前面的一篇文章说过,O(1)调度器的延时减少了,减少的是寻找最高优先级进程的时间,别的时间并没有有效减少也不可能减少,因为一个进程需要占用确定的时间片,那么所有的进程完全运行完也将是确定的时间片,而系统的进程数量将使整个调度周期延长,这可能造成更大面积的饥饿现象存在,为了避免这种现象,不管是linux还是windows,都有很多动态算法,比如饥饿检测以及优先级临时提升等等,这些算法本身就造成了不多但是有的延时。新的CFS改变了这一切。

cfs很有创意,它囊括了关于调度的一切,包括优先级,包括寻找最值得运行的进程等等一切,在cfs中,以前O(1)的创意如今简单的成了一棵红黑树,红黑树本身就有优先级的性质,因为它是一棵排序树,而且插入和删除的效率很高,由于从运行队列删除进程很多都是发生了该进程运行时期,出队运行更是增加了效率,到此为此O(1)的所有工作,一棵红黑树就全部搞定了,就是简单的找出最高“优先级”的进程,在cfs中,没有了时间片,优先级对应成了weight,叫做权值,系统的运行时间完全在所有进程之间按照权值公平分配,公平性体现在每个进程都有一个虚拟时钟,各个虚拟时钟相互追赶,调度器总是选择虚拟时钟最慢的进程运行,前面文章说过,权值小的进程虚拟时钟走的快,比如它在1个tick就走了1个字,而权值大的进程虚拟时钟走的慢,比如10个tick走了一个字,cfs的创举是,它可保证一个虚拟时钟的速度比是1:10的进程对,那么它的权值比是10:1,怎么保证呢?在linux的cfs调度器中,有一个静态数组,叫做prio_to_weight,它有40个元素,其中对应0到39这40个优先级,每增加一个优先级值,那么它的权值减少十分之一,注意,如果开始一个进程的权值是10,优先级是10,那么它的优先级变成9以后其权值会是9吗?不!因为整个系统的运行时间不变,你减少了1/10的时间,那么其它的所有进程将会增加这些时间,因为一个进程减少1/10,别的进程增加1/10,那么这个进程将减少25%的时间,这就是这个数组的妙处,每一个元素都是后一个元素乘以1.25%。这样的话,整个系统的权值将是一个按比例缩减的,相应的,其虚拟时钟的速度将按照这个比例增加,虚拟时钟向前推进你完全可以理解成以前的时间片,只不过这个时间片不再是固定的了,而是动态的,比如一个进程运行了10个tick其虚拟时钟走了一个字,这时系统中又来了一个进程,那么这个进程就不再运行10个tick了,可能会变成9个,如果这个系统就剩下它一个进程了,那么它将一直运行,不再进行无用的--p->task_timeslice的操作。记住,一切都是按照比例进行的,这个比例就是那个数组的缩减比例。

我们看看cfs怎么解决之前调度器遇到的那些问题,第一,不再需要双斜率的线段,因为可以保证高权值的进程的“时间片”就是低权值的N倍,这是这个数组决定的,调度器会计算这个进程的权值占用系统当前所有进程的权值和的几分之几,然后就分配整个调度周期几分之几的“时间片”,这个机制之所以可以如此完美又有效的进行就是因为每个进程虚拟时钟的向前推进方式--互相追赶,调度器选择最慢的运行(推进的速度不同)。cfs中的所谓动态运行时间可以保证cpu永远不会浪费去做没有意义的事情,如果系统只剩下一个进程,那么调度器会把整个cpu全部给了这个进程,本质上,正如作者的话:"CFS百分之八十的工作可以用一句话概括:CFS在真实的硬件上模拟了完全理想的多任务处理器"。在“完全理想的多任务处理器“下,每个进程都能同时获得CPU的执行时间。当系统中有两个进程时,CPU的计算时间被分成两份,每个进程获得50%。然而在实际的硬件上,当一个进程占用CPU时,其它进程就必须等待。这就产生了不公平。cfs的调度器只知道把cpu分给当前的进程们,这种分配是按照进程的权值平均分配的,虚拟时钟只是实现这种公平的手段而已。cfs中的nice调用可以“放心”的进行,因为nice影响的进程运行的“时间片”只和相对值有关和绝对值无关。cfs中将优先级巧妙的转换成了权值这个概念,而权值又是那么巧妙的组织,好,非常好!

cfs如此统一的解决了那么多问题,统一就是美,简单就是美!cfs是一个完全的调度方案,而不仅仅是挑选第一个值得运行的进程的方案,这个意义上O(1)仅仅是一个挑选第一个进程的方案罢了。

发布了1551 篇原创文章 · 获赞 4789 · 访问量 1066万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览