从进程调度的角度来看,Linux2.6之前的版本有如下的缺点:
针对之前的这些问题,Linux 2.6 从设计之初就将重点放在调度器算法的改善上。
从上面对Linux 2.6 之前的版本存在问题上来看,出现上述问题的原因主要在于:它们的就绪队列过于简单,只是一条双向链表,没有任何缓冲装置,从而使已耗尽时间片的进程无处可退,只能还呆在就绪进程队列;尽管本轮调度对于他们已经毫无任何意义,调度器也需对它们进行weight值的计算。
为解决这个问题,首先对处理器的就绪进程队列进行了改进。对于一个处理器来说,改进后的就绪进程队列如下图所示:
从上图可知,改进后每个处理器都有两个进程队列组,active队列组和expired队列组。每个队列组中的进程以优先级进程分类,每类进程为一个队列,组中最多可以有140个队列。
(也就是说,Linux进程优先级有140个,即从0-139,说明一下,该优先级为进程的静态优先级,还有动态优先级;其中,0-99为实时进程优先级,100-139为普通进程优先级;相同优先级的进程描述符通过链表连接起来;同样,先进先出的实时进程和时间片轮转的实时进程虽然调度策略不同,但是只要优先级相同就连接在同一个链表中,由调度算法进行辨识。)
时间片没有用完的进程队列都放在active队列组,时间片已耗完的进程队列都放在expired队列组。这样就使时间片已耗尽的进程有了缓冲之地,从而可在时间片耗尽时及时退出本轮调度,而不导致白白浪费处理器时间。
Linux 2.6 就绪进程队列工作过程大致为:当array[0]中某个进程的时间片耗尽时,在系统重新为其分配时间片和优先级之后,就马上把它从array[0]中删除,并按照其优先级插入到array[1]中的对应队列,从而它就无法再参加本轮调度。这样,当array[0]中所有进程的时间片都耗尽时,本轮调度也告结束,所有进程也就都进入了array[1],为下一轮调度做好了准备。这是,系统只需要简单地将active和expired两个指针切换一下,以array[1]为调度队列,array[0]为缓冲队列就可以直接进入下一轮调度了。
一个处理器的进程队列在一轮调度开始与结束时的情况如下图所示:
由于系统中往往有许多的就绪进程,如何快速找到待运行的就绪进程就成了关系到系统性能的一个重要因素。为此,Linux 2.6 为每个进程队列组配置了一个以优先级为序的就绪状态位图。该位图的每一位都对应着一个进程队列,如果该队列中存在就绪进程,则位图中对应位被标注为1,否则为0。这样,调度器就没有必要通过耗时且时间不恒定的遍历队列方法来查找待运行进程了,而是简单地用查表的方式即可。
为了描述上述队列结构,Linux为每一个处理器定义了一个struct runqueue结构数据:
struct runqueue
{
...
prio_array *active, *expired, array[2];
...
};
为清楚起见,只列举了结构体中最重要的部分:一个prio_array类型的数组array[]和active、expired两个指针。其中,prio_array的数据结构如下:
struct prio_array
{
unsigned int nr_active; //进程总数
struct list_head queue[MAX_PRIO]; //进程链表头指针数组
unsigned long bitmap[BITMAP_SIZE]; //进程就绪状态位图
};
其中:MAX_PRIO为140,BITMAP_SIZE为5,long为32位。
runqueue中的两个指针active和expired分别指向数组array[]的数组元素array[0]和array[1]。而数组元素array[0]和array[1]内部各自有一个以进程优先级(MAX_PRIO>i>=0)为下标的进程队列数组queue[],其中每个元素里面存放的都是进程队列的链表头,并且每条链表中的进程都具有相同的优先级。
假如一个处理器的数组array[]如下图所示:
例如,array[0]中的queue[1]是优先级1且时间片尚未耗尽的就绪进程队列,而array[1]中的queue[1]是优先级1且时间片已经耗尽的就绪进程队列。
上述的队列组织方式为Linux 2.6 实现了复杂度为O(1)的调度算法提供了有力保证,从而使调度器的开销与系统当前负载(进程数量)无关。O(1)算法中查找系统最高的优先级就转化成查找优先级位图中第一个被置1的位。与Linux 2.4 内核中依次比较每个进程的weight不同,由于进程优先级个数是定值,因此查找最佳优先级的时间恒定,它不会像以前的方法那样受可执行进程数量的影响。
也就是说:Linux2.4版本的内核调度算法理解起来简单:在每次进程切换时,内核依次扫描就绪队列上的每一个进程,计算每个进程的weight,再选择出weight最高的进程来运行;尽管这个算法理解简单,但是它花费在选择weight最高进程上的时间却不容忽视。系统中可运行的进程越多,花费的时间就越大,时间复杂度为O(n)。
而Linux 2.6内核所采用的O(1)算法则很好的解决了这个问题,该算法可以在恒定的时间内为每个进程重新分配好时间片,而且在恒定的时间内可以选取一个最高优先级的进程,重要的是这两个过程都与系统中可运行的进程数无关,这也正是该算法取名为O(1)的缘故。
参考文章:Linux如何实现进程O(1)调度。
Linux 2.6 多处理器系统的就绪进程队列如下图所示:
另外,Linux 2.6 在进程调度方面进行了两项比较大的改革:第一项是,把原来在调度器之内集中进行的进程优先级别计算,移到了调度器之外分散进行,而调度器的工作,只是直接从已按算法排序的优先级队列数组中选取待运行进程;第二项是,为了嵌入式系统的需要,Linux 2.6 允许在系统态进行调度。
在Linux 2.6 中,对于进程控制块中与计算进程优先级别有关的域,也做出了重大的调整。有的只是将原来的域换了一个名称,有的是从根本上就具有了和Linux 2.4 不同的意义。
普通进程的优先级通过一个关于静态优先级和进程交互性函数关系计算得到。随实际任务的实际运行情况得到。实时优先级和它的实时优先级成线性,不随进程的运行而改变。
也就是说:对于实时进程的优先级在创建的时候就确定了,而且一旦确定以后就不再改变,所以下面部分仅对于非实时进程而言。
参考文章:窥探 kernel。
动态优先级prio
prio相当于Linux 2.4 中goodness()的计算结果weight,它是调度器在就绪进程中选取运行进程的依据。由于它是综合各个因素计算出来的,所以也叫做进程动态优先级。
进程优先级范围为0~MAX_PRIO-1。数值越小,优先级越高。通常MAX_RT_PRIO-1=99;而MAX_PRIO-1=139。即0~99属于实时进程优先级的取值范围,而100~139属于非实时进程的优先级取值范围。
Linux 2.6 在为进程计算动态优先级prio时,以基本不变的静态优先级static_prio为基础再加上相应的bonus(奖励)。即其与static_prio和bonus两个参数有关:
prio=f(static_prio,bonus)=max (100, min (static_prio - bonus + 5, 139))
静态优先级static_prio
static_prio是进程控制块的一个成员,其取值范围为0~139。这个域主要用来确定进程初始时间片,对于非实时进程来说,static_prio还参与进程动态优先级的计算。
在Linux 2.6 中,仍然允许使用nice对进程静态优先级static_prio进行微调(取值范围为-20~19),数值越大,进程优先级越低。
static_prio与nice之间的关系如下:
static_prio=MAX_RT_PRIO+nice+20
下图为static_prio与nice之间的关系图:
需要注意的是,Linux 2.6 已经不将nice作为PCB的一个域了。
进程平均等待时间sleep_avg
凡是与用户进行交互的进程总是比较重要的。因此,调度器如何发现进程是一个交互进程,并在调度计算时赋予这种进程高一些的优先级,这是一件重要的事情。
由于交互进程大多与外设有关,所以调度器根据进程的等待时间来判断进程是否为交互进程;一旦被认为是交互进程,那么在优先级方面调度器会给它一些“奖励”bonus,而sleep_avg就是用来计算奖励额度的因子。
sleep_avg本质上是一个计数器。当进程处在等待或睡眠状态时,它为加法计数器;而当进程处于运行状态时,它为减法计数器。其实,它计算的是进程等待时间和运行时间的差值,这个差值反映进程的平均等待时间,所以可以以此判断一个进程的交互程度。sleep_avg在0~NS_MAX_SLEEP_AVG之间取值,初值为0,单位为ms。
sleep_avg是计算奖励因子bonus的重要依据,它们之间的关系如下图所示:
可见,非实时进程的优先级仅决定于静态优先级(static_prio)和进程的sleep_avg值两个因素。其中,sleep_avg反映调度系统的两个策略:交互式进程优先和分时系统的公平共享。
time_slice
time_slice为进程时间片余额,相当于Linux 2.4 的counter,但由于static_prio和sleep_avg的存在,它不再直接影响进程的动态优先级,仅仅是一个供查询的参数。
Linux 2.6 不再是在调度器选择进程时集中进行优先级的计算,只要进程状态发生改变,就计算并设置进程的动态优先级。计算优先级的时机主要有以下几个: