今天重读了cfs调度器,使我忍不住再写一篇关于cfs的文章,cfs调度器的运行时间是0(logN),而以前的调度器的运行时间是O(1),这是不是就是说cfs的效率比O(1)的更差呢?并不是那样,我们知道cfs调度器下的运行队列是基于红黑树组织的,找出下一个进程就是截下左下角的节点,固定时间完成,所谓的O(logN)指的是插入时间,可是红黑树的统计性能是不错的,没有多大概率真的用得了那么多时间,因为红节点和黑节点的特殊排列方式既保证了树的一定程度的平衡,又不至于花太多的时间来维持这种平衡,插入操作大多数情况下都可以很快的完成,特别是对于组织得相当好的数据。实际上cfs之所以能进入内核并不是因为它能提供多好的性能,因为在一个事物的一种架构下,性能的提升肯定有个极限,不能一味的朝一个方向走,比如cpu的流水线在设计之初被认为是好的,可是intel的NetBurst架构一味的加长流水线,以为流水线越长性能越高,吞吐量越大,可是工程学中永远没有一直单调递增的函数,总会有一个拐点的。
cfs的精髓在于它改变了内核看待进程调度的方式,时间片越细越好吗?抢占点越多越好吗?如果是的话,那么很抱歉,到了2.6.17左右的内核,时间片已经很精细了,而且抢占点也不能再多了,如果这个推理正确的话,那岂不是说调度器不能继续发展了吗,linux绝对不是这样的性格,O(1)调度器的设计者有破有立,提供了另外一种截然不同的调度方式,这就是cfs,抛弃了时间片,抛弃了复杂的算法,从一个新的起点开始了调度器的新时代,最开始的2.6.23版本内核的cfs是很简单的,思想十分明确。cfs的本质思想就是提供一个虚拟的时钟,所有进程复用这个虚拟时钟的时间,cfs将时钟的概念从底层体系相关的硬件中抽象出来,进程调度模块直接和这个虚拟的时钟接口而不必再为硬件时钟操作而操心,如此一来,整个进程调度模块就完整了,从时钟到调度算法,到不同进程的不同策略,全部都由虚拟系统提供,也正是在这个新的内核,引入了调度类。因此新的调度器就是不同特性的进程在统一的虚拟时钟下按照不同的策略被调度。我们看一下这样做有什么好处,cfs抛弃了时间片,采用了平滑的机制,用互相追赶的方式促使所有的进程一起向前走,那么怎么区分不同优先级的进程呢?总不能所有进程以相同的速度往前走吧,显然不能!我们都相信,用互相追赶的方式是最好的方式,就好像竞争一样有益,我中中学的时候,老师就教导我们要互相追赶,虽然那个时候老师的意思就是大家都追赶我!既然相信互相追赶时最好的方式,那么下一步就是怎么实现互相追赶,如果在传统的调度体系下,用硬时钟,那么很难实现不同进程的不同优先级的管理,于是提出虚拟时钟的概念,每个进程都有一个虚拟时钟,然后整个系统有一个虚拟时钟,系统虚拟时钟以固定的步调前进,而各个进程的虚拟时钟的前进步调却因为其优先级不同而不同,比如优先级低的进程其虚拟时钟的步调比较快,而优先级高的步调比较慢,这一点用硬件时钟是很难做到的,如果用硬件时钟,我们就要维护每一个进程的状态,然后将这个每个进程的状态设置为一个全局的变量,可想而知,这个变量应该是个链表或者数组,每当硬件时钟中断的时候都要查找这个链表进行查找,删除,选举等操作,十分繁琐,不说时间,空间上占用的内存都无法接受。cfs提出的虚拟时钟概念使得互相追赶更为容易实现,基于时间片的调度机制中,进程调度是被动的,调度模块不得不想办法探知每个进程的特点并且根据不同的特性设置给它们不同的策略,然后依据这些不同的策略分配给它们不同的时间片,这样必然会出现变态级的预测算法,很复杂,让学究们看来可以为我们卖弄一把,可是这个世界不喜欢复杂,因此学究们必须退让;现在看看cfs调度器,进程有自己的优先级,并且映射到权值,每个权值的进程的虚拟时钟步调不同,大家都按照自己的虚拟时钟往前走就是了,调度模块会选择最落后于系统虚拟时钟的进程来执行,这就是公平的含义,那么在O(1)调度器中对交互进程的预测算法在cfs中怎么体现呢?其实完全没有必要考虑这个问题,因为在O(1)调度器中,之所以要区分交互进程是因为O(1)调度器完全是基于优先级来调度的,其它所有的调度依据都需要动态预测,而动态预测就必然需要复杂的算法,在cfs调度器中,不单单有优先级作为调度依据了,其实优先级映射到了进程权值,然后各个进程互相追赶,这样可以保证任何进程都不会被耽搁太久,交互进程的问题就解决了,O(1)调度器不能这样的原因就是因为进程们不能自己互相追赶。举个例子,就是在O(1)中,一个交互进程和一个非交互进程的优先级都是p,调度器仅仅根据它们的优先级来进行调度,别的什么也不知道,只有靠探测它们的睡眠时间来进行进程类别的判断,通过判断如果一个进程是交互进程,那么就暂且不把它放到过期队列而是继续运行用来补偿它过多的睡眠,而在cfs调度器中就不会这样,即使一个交互进程和一个非交互进程都被映射到了一个权值,交互进程随便睡眠,只要它醒来,它就可以以不太大的(也不会太小,按照它自己的权值和当前的系统虚拟时钟决定)key被加到红黑树,随着系统继续运行,很公平的它就会运行。之所以O(1)中如果不预测交互进程会导致交互进程的延迟是有原因的,另起一段吧,这一段太长了。
O(1)中导致交互进程延迟响应的原因就是两个数组存在,一个运行队列一个过期队列,按照一般道理,系统依据进程优先级来分配时间片,只要时间片完毕就会被放到过期队列,问题就出在这个过期队列,按照道理,只有所有优先级的所有进程都放到了过期队列,也就是所有优先级的所有进程都运行完毕了,才会交换过期队列和运行队列,如果不进行区分的话,系统内运行的进程越多,交互进程看起来响应越迟钝,因此O(1)中用睡眠时间区分了交互进程和非交互进程,可见在O(1)中,交互进程的延迟主要在所有进程的数量,其实这对于服务器是无所谓的,但是对于交互进程就不行,因为服务器进程强调优先级下的公平性,而交互进程强调响应性,对于公平性而言,要等只要大家一起等就没有问题,对于交互进程就根本不允许等待太久,不管怎样,O(1)调度器都不是很令人满意,在运行队列的进程可以看到公平和高效,可是对于过期队列的进程,面临却是无期的等待...在爽完了以后,虽然很多人都喜欢休息一会,但是我相信更多的人喜欢在不久的将来再爽一次。这么说的话,O(1)的调度周期的粒度就是所有的进程的运行时间之和,这个算法虽然可以保证运行队列进程的公平性却无法保证全局的公平性,任何一个进程在运行了不管多久之后,都必须等待很长一段时间,在一个进程的整个生命周期看来是断断续续的,在cfs中,任何一个进程的执行都将是连续的,这就是cfs的进步,以前的调度器必须以硬件时钟为准,因此不能太灵活,cfs以虚拟时钟为准,灵活性很随意。
在2.6.23内核中,刚刚实现的cfs调度器显得很淳朴,每次的时钟滴答中都会将当前进程先出队,推进其虚拟时钟和系统虚拟时钟后再入队,然后判断红黑树的左下角的进程是否还是当前进程而抉择是否要调度,这种调度器的key的计算是用当前的虚拟时钟减去待计算进程的等待时间,如果该计算进程在运行,那么其等待时间就是负值,这样,等待越长的进程key越小,从而越容易被选中投入运行;在2.6.25内核以后实现了一种更为简单的方式,就是设置一个运行队列的虚拟时钟,它单调增长并且跟踪该队列的最小虚拟时钟的进程,key值由进程的vruntime和队列的虚拟时钟的差值计算,这种方式就是真正的追赶,比2.6.23实现的简单,但是很巧妙,不必在每次时钟滴答中都将当前进程出队,入队,而是根据当前进程实际运行的时间和理想应该运行的时间判断是否应该调度。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1274147