经典的操作系统调度算法-多级反馈队列(MLFQ)

       这里介绍一种著名的调度算法,多级反馈队列(multi-level feedback queue,MLFQ),这种调度策略不但应用在Solaris和FreeBSD和Linux Schedule_RR Policy等Unix家族操作系统中,几乎所有的 RTOS操作系统使用的基于优先级的时间片轮转调度算法,也是MLFQ的一个精简化的变种.

多级反馈队列要解决两个方面的问题,首先,它要优化周转时间,这通过先执行短工作来实现,然而,操作系统通常不知道工作要运行多久,而这又是SJF(或者STCF)等算法所必须的,其次,MLFQ希望给交互用户(例如用户坐在屏幕前,等着进程响应)很好的交互体验,因此需要降低响应时间,然而,像轮转这样的算法虽然降低了响应时间,周转时间却很差,所以这里的问题是,通常我们对进程一无所知,应该如何构建调度程序来实现这些目标?调度程序如何在运行过程中学习进程的特征,从而作出更好的调度决策?

关键问题是:没有完备的知识如何调度?没有工作长度的先验知识,如何实际一个能够同时减少响应时间和周转时间的调度程序?这个问题的答案是,从历史中学习,多级反馈队列是用历史经验预测未来的一个典型例子,操作系统中有很多地方采用了这种技术(同样的问题存在于计算机科学领域的很多地方,比如硬件的分支预测以及缓存算法),如果工作有明显的阶段性行为,因此可以预测,这种方式会很有效.

MLFQ:基本规则

MLFQ中有许多独立的队列,每个队列有不同的优先级,任何时刻,一个工作只能存在与一个队列中.MLFQ总是优先执行较高优先级的工作,也就是在较高级队列中的工作.

当然,每个队列中可能有多个工作,因此具有同样的优先级,在这种情况下,就对这些工作采用轮转(round robin)调度策略.

因此,MLFQ调度策略的关键在于如何设置优先级.MLFQ没有为每个工作指定不变的优先级顺序而已,而是根据观察到的行为调整它的优先级.例如,如果一个工作不断放弃CPU去等待键盘输入,只是交互型进程的可能行为.MLFQ因此会让它保持高优先级.相反,如果一个工作长时间占用CPU,MLFQ会降低它的优先级,通过这种方式,MLFQ在进程运行过程中学习其行为,从而利用工作的历史来预测它未来的行为.

因此,我们的到了MLFQ的两条基本规则:

1.如果A的优先级大于B的优先级,运行A,不运行B

2.如果A的优先级等于B的优先级,论转运行A和B.

如下图所示,它可以看成是MLFQ调度策略某一个时刻的快照.

最高优先级有两个工作A和B,工作C,E,F位于中等有限级,而D的优先级最低,按照上面的介绍的基本规则,由于A和B有最高优先级,调度程序将交替的调度A和B,可怜的C,D,E,F将永远都没有机会运行,这有点气人.

在RTOS系统中,优先级一般是固定的,为了避免出现低优先级的任务饥饿的情况,要求每个任务都要主动的调用休眠接口,最常见的是sleep系统调用,它会将当前任务挂入系统休眠队列一段时间,让低优先级的任务有执行的机会,如下图,注意系统就绪对类和系统定时器队列之间任务的流转交互.

MLFQ:改变优先级

在一个工作的生命周期中,MLQF改变任务的优先级,也就是确定任务在那个队列中,要注意的是,工作负载中,既有运行时间很短,频繁放弃CPU的交互型工作,也有需要很多CPU时间,响应时间却不重要的长时间计算密集型给你哦工作,下面是一种优先级条这个你策略.

1.工作进入系统时,放在最高优先级,也就是最上层队列.

2.工作用完整个时间片后,降低其优先级(移如下一级队列)

3.如果共最在其时间片以内主动释放CPU,则优先级不变.

按照这种策略,对于一个计算密集型的长工作来讲,它的优先级调整过程会是这个样子的:

从这个例子可以看出,该工作首先进入最高优先级,执行一个时间片后,调度程序将工作的优先级减1,进入下级优先级,在执行一个时间片后,优先级继续降低,最终任务会进入系统的的最低优先级,并一直留在那里.每次优先级降低,将会影响下一个调度点的选择策略,从而影响到处理器在不同任务之间的分配.

如果来了一个短工作,看看MLFQ如何近似与SJF,在这个例子中,有两个工作,A是一个长时间运行的CPU密集型工作,B是一个晕习惯你时间很短的交互型工作,假设A执行一段时间后B达到,会发生什么呢?对B来说,MLFQ会近似等于SJF吗?

下图展示了这种场景的结果,A在最低优先级队列执行(长时间运行的CPU密集型工作都这样),B在中间某个时刻到达,并被加入最高优先级队列由于它的运行时间很短,经过两个时间片,在被移入最低优先级队列之前,B执行完毕,然后A继续执行.

通过这个例子可以看出,这个算法的一个主要目标,如果不知道工作是短工作还是长工作,那么就在开始的时候假设其是短工作,并赋予最高优先级,如果确实是短工作,则很快会执行完毕,否则将被慢慢移入低优先级队列,而这时该工作也被认为是长工作了,通过这种方式,MLFQ近似于SJF.

第三个例子,如果有IO的情况,根据上述规则,如果一个进程在时间片用完之前主动放弃CPU,则保持它的优先级不变.这条规则的意图很简单,假设交互工作中有大量的IO操作,它会在时间片用完之前放弃CPU,在这种情况下,我们不想处罚它,只是保持它的优先级不变.

上图展示了整个过程,交互型工作每执行1ms便会通过IO操作主动放弃处理器,它与长时间运行的工作A竞争CPU,MLFQ算法保持B在最高优先级,因为B总是让出CPU,如果B是交互型工作,MLFQ就进一步实现了它的目标,让交互型工作快速运行,有少的响应时间.

问题

至此,有了一个基本的MLFQ,长工作之间可以公平的分享CPU,又能给短工作或者交互型工作很好的响应时间,然而,这种算法还有一个非常严重的缺点.首先,会有饥饿问题,如果系统有太多的交互型工作,就会不断占用CPU,导致长工作永远无法得到CPU,,即使在这种情况下,我们希望这些长工作也能有所进展.

其次,聪明的用户会重写程序,愚弄调度程序,让它给你远超公平的资源.上述算法对如下的攻击束手无策:进程在时间片用完之前,调用一个IO操作,从而主动放弃CPU,如此便可以保持在高优先级.占用更多的CPU时间,做的好的时候,工作可以几乎独占CPU.

最后的问题是,一个程序可能在不同的时间表现不同,一个计算密集型的进程可能在某段时间表现为一个交互型进程,用我们目前的方法,它不会享受系统中其它交互型工作的待遇,优先级会逐步递减,直到最低.

提升优先级

让我们尝试着改变之前的规则,看能否避免饥饿问题.要让CPU密集型的工作也能取得一些进展,即使不多.

一个简单的思路是周期性的提升所有工作的优先级,可有有很多方法做到,但我们就用最简单的,将所有工作扔到最高优先级队列,于是,就有了如下的新规则.

1.经过一段时间S,就将系统中所有工作重新加入最高优先级队列.

新的规则一下解决了两个问题,首先,进程不会饿死,在最高优先级队列中它会以轮转的方式,与其它高优先级的工作分享CPU,从而最终获得执行,其次,如果一个CPU密集型工作变成了交互型当它优先级提升时,调度程序会正确对待它.

上面的例子说明,在这种场景下,展示了长工作与两个交互型短工作竞争CPU时的行为,左边的没有使用优先级提设个你策略,长工作在两个短工作到达后被饿死.右边表示每隔一段时间就会有一次优先级的提升,因此至少保证长工作有一些进展.每隔一段时间,长工作就会被提升到最高优先级,从而定期获得执行.添加周期优先级提升导致了一个明显的问题,那就是周期值大小该如何设置,这需要一点魔法,因为没有人知道确切的值是什么.如果设置的太高,长工作仍然会饥饿,如果设置得太低,交互型工作又得不到合适的CPU时间比例.

更好的记时方式:

还有一个问题没有得到解决,如何阻止调度程序被愚弄?可以看出,问题出在时间片以内释放cpu,就保留它的优先级,应该怎么做呢?

解决方案是,为MLFQ的每层队列提供更完善的CPU记时方式,调度程序应该记录一个进程在某一层中消耗的总时间,而不是在调度时重新记时,只要进程用完了自己的配额,就将它降到低一级的队列中去,不论它是一次用完的,还是拆成很多次用完的,因此,重写规则如下:

1.一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列).

看一个例子:

上图左边表示按照之前的规则策略下,试图愚弄调度程序的进程表现,可以看到,它得逞了.通过在每个时间片结束之前,发起一次IO操作,从而垄断了CPU时间.右边表示采用新的策略后,不论进程的IO行为如何,都会慢慢的降低优先级.因而无法垄断CPU执行.

其他问题

关于MLFQ调度算法还有一些问题,其中一个大问题是如何配置一个调度程序,例如,配置多少优先级,没一层队列的时间片配置多大,为了避免饥饿问题以及进程行为改变,应该多久提升一次进程的优先级.这些问题都没有显而易见的答案,因此只有利用对工作负载的经验,以及后续对调度程序的调优,才会导致使人满意的平衡.

例如,大多数的MLFQ变体都支持不同队列可变的时间片长度,高优先级队列通常只有较短的时间片,因而这一层的交互工作可以更快的切换,相反,低优先级队列中更多的是CPU密集型工作,配置更长的时间片会取得更好的效果.

总结:

经过上面的总结,重新整理规则如下:

1.如果A的优先级大于B的优先级,运行A,不运行B.

2.如果A的优先级等于B的优先级,论转运行A和B.

3.工作进入系统时,放在最高优先级(最上层队列)

4.一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(入低一级队列).

5.经过一段时间,就将系统中所有工作重新加入最高优先级队列.

MLFQ被称为多级反馈队列,因为它有多优先级的队列,并能够利用反馈信息决定某个工作的优先级,以史为鉴,关注进程的一贯表现,然后区别对待.MLFQ不需要对工作的运行方式有先验的知识,而是通过观察工作的运行来给出对应的优先级.通过这种方式,MLFQ可以同时满足各种工作的需求,对于短时间运行的交互型工作,获得类似与SJF/STCF的很好的全局性能.同时对长时间运行的CPU密集型负载也可以公平对地,不断的稳步向前.因此,许多系统使用某种类型的MLFQ作为自己的基础调度程序,包括类BSD UNIX系统,solaris以及windows NT和之后的windows系列操作系统.

拓展:

Linux的实现中,无论RT进程还是普通CFS进程,调度的关键都在于调度的时机、下一个进程的选取。RT线程通过优先级队列来管理处于就绪任务。而CFS调度器则通过红黑树维护就绪任务列表。

为什么在实时调度中,容易出现饿死?

对于优先级高的实时进程,它一直占有cpu,如果不主动让出,即使时间片耗尽,也只是移动到优先级队列尾部,如果有更高优先级的进程,则执行更高优先级,如果没有,还会运行本优先级的进程,总之,其他低优先级的程还是不能运行,那么这种情况下,低优先级的进程是会被饿死的。这也是RTOS中会出现的情况。

Linux CFS调度器,之所有能够很大程度上避免这个问题,在于在Linux CFS上,优先级有了深层含义,就是权重,优先级即便非常高,也会按照权重给每个任务分摊一定的CPU时间,所有其它线程总有执行的机会。另外,Linux支持调度组调度,可以给RT线程设置最大的执行比例,方法是是设置如下两个文件:


结束!

猜你喜欢

转载自blog.csdn.net/tugouxp/article/details/116403591