【Linux】Linux 2.6 对调度器的改进

从进程调度的角度来看,Linux2.6之前的版本有如下的缺点:

  • 由于只设置了一个进程就绪队列,于是在一轮调度中先耗尽时间片的进程虽然已经无法取得处理器控制权,但是还要参与weight值的计算,导致白白浪费了处理器的时间;
  • 调度算法与系统负荷的关系较大。也就是说,调度器耗时与当时系统内进程数量有关:数量大,耗时长;数量小,耗时短。不适合应用在硬实时系统;
  • 在多处理器系统中,由于只有一个就绪队列,经常因某种原因而导致系统各个处理机之间的等待,使得就绪队列成为一个提高系统效率的瓶颈;
  • 内核态不能抢占。某一个进程一旦进入内核态,那么其他具有更高优先级的进程,也只有等待该进程返回用户态才有机会占用处理器。缺乏对实时进程的支持。

针对之前的这些问题,Linux 2.6 从设计之初就将重点放在调度器算法的改善上。

 

就绪进程队列runqueue

从上面对Linux 2.6 之前的版本存在问题上来看,出现上述问题的原因主要在于:它们的就绪队列过于简单,只是一条双向链表,没有任何缓冲装置,从而使已耗尽时间片的进程无处可退,只能还呆在就绪进程队列;尽管本轮调度对于他们已经毫无任何意义,调度器也需对它们进行weight值的计算。

为解决这个问题,首先对处理器的就绪进程队列进行了改进。对于一个处理器来说,改进后的就绪进程队列如下图所示:

【Linux】Linux 2.6 对调度器的改进_第1张图片

从上图可知,改进后每个处理器都有两个进程队列组,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】Linux 2.6 对调度器的改进_第2张图片

由于系统中往往有许多的就绪进程,如何快速找到待运行的就绪进程就成了关系到系统性能的一个重要因素。为此,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[]如下图所示:

【Linux】Linux 2.6 对调度器的改进_第3张图片

例如,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】Linux 2.6 对调度器的改进_第4张图片

另外,Linux 2.6 在进程调度方面进行了两项比较大的改革:第一项是,把原来在调度器之内集中进行的进程优先级别计算,移到了调度器之外分散进行,而调度器的工作,只是直接从已按算法排序的优先级队列数组中选取待运行进程;第二项是,为了嵌入式系统的需要,Linux 2.6 允许在系统态进行调度。

 

优先级的计算方法

PCB中与进程优先级计算有关的域

在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】Linux 2.6 对调度器的改进_第5张图片

需要注意的是,Linux 2.6 已经不将nice作为PCB的一个域了。

进程平均等待时间sleep_avg

凡是与用户进行交互的进程总是比较重要的。因此,调度器如何发现进程是一个交互进程,并在调度计算时赋予这种进程高一些的优先级,这是一件重要的事情。

由于交互进程大多与外设有关,所以调度器根据进程的等待时间来判断进程是否为交互进程;一旦被认为是交互进程,那么在优先级方面调度器会给它一些“奖励”bonus,而sleep_avg就是用来计算奖励额度的因子。

sleep_avg本质上是一个计数器。当进程处在等待或睡眠状态时,它为加法计数器;而当进程处于运行状态时,它为减法计数器。其实,它计算的是进程等待时间和运行时间的差值,这个差值反映进程的平均等待时间,所以可以以此判断一个进程的交互程度。sleep_avg在0~NS_MAX_SLEEP_AVG之间取值,初值为0,单位为ms。

sleep_avg是计算奖励因子bonus的重要依据,它们之间的关系如下图所示:

【Linux】Linux 2.6 对调度器的改进_第6张图片

可见,非实时进程的优先级仅决定于静态优先级(static_prio)和进程的sleep_avg值两个因素。其中,sleep_avg反映调度系统的两个策略:交互式进程优先和分时系统的公平共享。

time_slice

time_slice为进程时间片余额,相当于Linux 2.4 的counter,但由于static_prio和sleep_avg的存在,它不再直接影响进程的动态优先级,仅仅是一个供查询的参数。

进程优先级的计算时机

Linux 2.6 不再是在调度器选择进程时集中进行优先级的计算,只要进程状态发生改变,就计算并设置进程的动态优先级。计算优先级的时机主要有以下几个:

  • 创建进程时:在创建一个子进程时,系统会调用内核函数wake_up_forked_process(),使子进程继承父进程的动态优先级,并将其插入父进程所在的就绪队列。如果父进程不属于任何就绪进程队列(例如它是idle进程),那么就调用effect_prio()计算子进程的优先级;
  • 唤醒休眠进程时:当进程从休眠状态醒来时,系统调用函数recale_task_prio()来设置进程的动态优先级;
  • 处于TASK_INTERRUPTABLE状态的进程被唤醒时:实际上,此时进程已经选定了候选进程,但考虑到这一类进程很可能为交互式进程,仍调用recale_task_prio()对该进程进行修正;
  • 其他时机:idle进程初始化(init_idle())、负载平衡(move_task_away())以及修改nice值(set_user_nice())、修改调度策略(setscheduler())等主动要求改变优先级的情况。

 

你可能感兴趣的:(《操作系统》Linux掠影笔记)