进程调度算法

1.简介

进程调度是为了能够更合理的安排进程的执行顺序,使得各个进程能够高效、及时、公平的使用CPU资源。
事实上调度算法的起源要早于计算机系统,比如装配线以及很多人类活动都需要调度算法来进行协助。
调度算法通常都是要求在一定的工作负载下,使得调度算法的各种指标表现良好。
再次,我们先对操作系统的工作负载提出以下假设:
1.每一个任务的运行时间相同
2.所有的任务达到时间相同
3.任务一旦开始,将保持运行直到完成
4.所有的任务都只占用CPU而不占用IO资源,即所有任务都是计算密集型而非IO密集型。
5.每个任务的运行时间已知

我们首先考虑的性能指标是:周转时间和平均周转时间

T周转时间 = T完成时间 - T到达时间

T平均周转时间 = sum(T完成时间 - T到达时间)/ 任务数

接下来我们将介绍一系列调度算法,并逐步放松对工作负载的假设(因为目前我们的工作负载是不切实际的)。此外,对于不同的调度算法,我们还会比较一些额外的指标。

2.先到先服务(First Come First Served)

先到先服务算法也叫先进先出算法,类似队列这种结构,简单易于实现。
来看一个例子:
假设有3个任务A、B、C在基本的相同时间到达系统,因为必须要安排一个任务占有CPU,因此我们假设A比B稍微提前到达一点点,B比C也稍微提前到达一点点,每个任务运行10s。
T周转时间 = 10 + 20 + 30 = 60s
T平均周转时间 = 60 / 3 = 20s

接下来我们放松工作负载的假设1,即每个任务运行时间并不相同,你能构建什么样的任务序列使得FCFS性能表现不好呢?
没错,很简单。假设A需要运行100s,B和C运行时间不变。
T周转时间 = 100 + 110 + 120 = 330s
T平均周转时间 = 330 / 3 = 110s

这个问题通常被称为护航效应(convoy effect):一些耗时较少的潜在资源消费者被安排的重量级的资源消费者之后。这个问题在HTTP1.0中也被称为队头阻塞。试想一下这样的场景:你在超市排队付款时,前面有几个人买了一整个购物车的东西,而你只买了一包饼干,这时候你的心情是什么样?
因此当我们放松工作负载1之后,FCFS性能表现不是很好,下面一个算法将改善这个问题。

3.短作业优先(Shortest Job First)

通过这个算法的名字很容易理解这个算法的执行流程,首先执行最短的任务,然后执行次短的任务,依次进行下去直到没有任务。
同样是上述任务序列:

T周转时间 = 10 + 20 + 120 = 150s
T平均周转时间 = 150 / 3 = 50s

事实上如果所有的任务同时到达,我们可以证明SJF算法是一个最优的调度算法。
但是,同样的这时候我们要放松工作负载假设2,即每个任务不是同时到达。类似的,你能构建什么样的任务序列使得SJF性能表现不好呢?
此时,我们假设B和C比A晚到10s。

T周转时间 = 100 + (110 - 10) + (120 - 10)= 310s
T平均周转时间 = 310 / 3 = 103.33s

为什么会出现这种情况呢? 原因在于我们的工作负载假设3,即任务一旦开始必须运行直到完成。
接下来我们会放松这个假设,并提出一种新的调度算法。

4.最短完成时间优先

最短完成时间优先(Shortest Time-to-Completion First)STCF也被称为抢占式短作业优先调度算法。
每当新任务进入系统时,它会比较新任务和当前正在执行的任务谁的剩余时间少,然后调度该任务。
此时对于上述任务序列:

T周转时间 = 10 + 20 120 = 150s
T平均周转时间 = 150 / 3 = 50s

事实上如果考虑上述工作负载假设(我们此时已经放松了1 2 3),我们可以证明STCF算法是一个最优的调度算法。
一切看起来都是美好的,但是考虑这样的场景,你此时正坐在计算机面前输入信息,你可能需要计算机及时的给予你响应,即交互性要好。这样我们得到了一个新的度量指标:

T响应时间 = T首次运行 - T到达时间

此时,如果对于A、B、C三个任务同时到达,按照STCF那么必然有一个任务要等待前两个任务执行结束后才能运行,那么对于这个用户而言交互性就很差。试想如果你此时是该用户,你不得不等待20s甚至更长的时间才能得到系统的回应,你的感受可能和在超市购物时一样?
一般而言,SJF和STCF算法会存在饿死现象,即长作业任务可能会因为源源不断的短作业而迟迟得不到执行。
现在我们的问题是如何构建响应速度快的程序,接下来介绍一种算法完成这个需求。

5.轮转调度

轮转(Round-Robin)RR调度算法:在一个时间片内运行一个工作,然后切换到就绪队列的下一个任务,而不是运行一个任务直到结束。这个过程反复执行,知道所有任务运行结束。因此RR调度算法有时也被称为时间片调度算法。
注意:时间片长度必须是时钟周期的整数倍
举一个例子:假设3个任务A、B、C同时到达,并且都运行5s,时间片取1s。
则STCF的平均响应时间是 (0 + 5 + 10)/ 3 = 5s
RR的平均响应时间是:(0 + 1 + 2)/ 3 = 1s

此外,时间片的选取是RR算法的一个重要问题!时间片越短RR的响应时间越小,然而事物都是矛盾统一的,时间片太短意味着需要大量的上下文切换,上下文切换也需要很多时间,进而影响了整体性能。时间片过长,则会向着STCF进行退化。因此一个关键点就是系统设计者要权衡时间片的长度,使其足够长足以摊销上下文切换的成本,但又不至于发生退化。

进程上下文切换的成本不仅仅来自于保存和回复寄存器的代价,还包括了程序运行时建立在CPU高速缓存、TLB、分支预测器和其它硬件上的大量状态。切换进程时会导致这些状态被刷新,因此切换频率过高会导致性能急速下降。这里如果对于线程有一定了解的话,可以思考以下,线程上下文切换和进程上下文切换代价一样吗?答案肯定是不一样的,线程上下文切换不需要改变TLB、高速缓存等内容,只需要保存每个线程的少量寄存器和数据信息。因此从这个角度来看线程的并发性更好。

此外我们继续考虑之前的指标:周转时间
RR的平均周转时间为:(13 + 14 + 15)/ 3 = 14s
STCF的平均周转时间为 :(5 + 10 + 15)/ 3 = 10s
这个结果并不是很难理解:RR所做的是延伸每个任务,只运行每个任务一小段时间,就转向下一个任务。而周转时间只关心作业何时完成,不关注交互性,因此RR在周转时间上表现的很差,但它保证了公平性。
因此我们有了两种调度程序:第一种关注周转时间,第二种关注响应时间。

接下来我们思考工作负载假设4、5,他们如何与调度算法结合。
对于工作负载假设4,我们现在允许IO请求。一个很简单的想法就是,在一个进程执行IO操作的同时另一个进程占有CPU的使用权。
假设我们现在有两个任务A、B,每个任务都需要50ms的CPU时间,但是A每运行10ms就发出IO请求,而B只使用CPU50ms,不执行IO。此时我们可以将A看出5个10ms的小任务组成,对于STCF算法:首先选择A的第一个子任务,然后A请求IO,B占有CPU,接着A的第二个子任务到达,抢占CPU运行10ms,继续请求IO,B占有CPU…这样就使得一个进程在等待IO时,另一个进程占有CPU使用权,重叠使用CPU提高了效率。

而对于工作负载5,我们一般情况下不可能知道每个任务的具体运行时间,我们也不可能信任用户告诉调度程序自己需要多久的运行时间(试想这样的话用户完全可以欺骗调度程序,进而长久的占据CPU),基于这一事实单纯的STCF也并不能直接应用。接下来我们将介绍另外一个算法来解决这个工作负载假设。

6.多级反馈队列

在分时操作系统中一个重要的目标就是降低响应时间,前面我们了解RR算法可以很好的完成这个任务,但是RR算法对于周转时间太不友好。我们能不能提出一种算法结合RR算法和前面描述的一些对周转时间比较友好的算法来获得较好的周转时间和响应时间呢?此外还要适应工作负载5?事实证明多级反馈队列(Multi-level Feedback Queue)MLFQ就是一种很好的算法。

MLFQ的基本规则:MLFQ中有许多独立的队列,每个队列都有不同的优先级。任何时刻,一个工作只能存在于一个队列中。MLFQ总是先执行优先级较高的工作。每个队列中可能存在多个任务,这多个任务的优先级相同,此时采用轮转调度算法。
总结一下,此时MLFQ有两条基本规则:
1.如果A的优先级 > B的优先级,运行A
2.如果A的优先级 = B的优先级,轮转运行A、B

从上面的规则可以看出,MLFQ的核心在于优先级如何设置?
MLFQ并不是为每个任务指定固定的优先级,而是会根据具体的行为调整它的优先级。
这个具体的行为如何理解?比如此时一个工作不断的放弃CPU而去请求键盘IO,这是一个交互性极强的工作,需要一个很好的响应时间,因此可以适当的提高优先级。相反地,如果一个工作长时间占据CPU,这是一个计算密集型工作,不关注交互性,因此可以适当的降低优先级。
因此对于上面简单的两条规则并不能保证MLFQ很好的应用于操作系统。我们的目标是设计一个能够动态调整优先级的算法。

新加入两条规则:
3.工作刚进入系统时,放在最高优先级队列(照顾每一个任务)
4a.工作用完一个时间片后,降低其优先级,向下移动一个队列(关注计算密集型任务)
4b.如果工作在其时间片以内主动放弃CPU,则优先级不变(关注交互型任务)

加入这两条规则后,对于长任务,则会不断的降低其优先级,直到处于最低优先级并一直留在那里。
对于短任务,在最初的几个时间片内就完成。这里可以看出MLFQ是有一些SJF的思想的,首先我们将所有新进入的工作放在最高优先级这是相当于假设每个任务都是短任务,如果它确实是短任务,那么它很快就运行结束,如果它是长任务,则会被慢慢移入低优先级队列。
此外对于有IO的任务,在任务放弃CPU执行IO时并不降低其优先级,从而保证了有较高的优先级,提高了交互性。

至此,一切看起来很完美了!但是你是否还记得前面提到过的饿死现象,这里同样存在这样的问题。
1.如果系统中交互型任务太多,就会不断地轮流占用CPU,进而导致低优先级的任务无法得到CPU。
2.此外,如果别有用心的用户可以故意添加一些操作来愚弄操作系统,比如在时间片快要用完时加入一次无关紧要的IO操作,从而让任务一直处于高优先级,进而一直占据CPU。(回想一下,这是不是和我们新人用户会告诉我们程序的运行时间时遇到的问题类似。)
3.最后还有一点,一个工作从整体上看和局部上看是不同的,比如一个计算密集型的任务,在某一时刻可能需要用户输入数据,这个局部来看它是一个交互性强的任务。而我们现在的算法不会照顾到这一点。

这些问题概括一下,其核心在于如何让低优先级的任务在优先级高的任务聚集的状态下有机会得到执行。

因此我们这里引入第五条规则:
5.经过一段时间S,就将系统中的所有任务重新加入到最高优先级队列
当引入规则5之后,只要S设置的合理,长作业任务和某一局部的交互性任务也会被重新放回最高优先级,从而得到执行。但是很明显,要设置合理的S值并不是很容易,S设置的太高,长工作会饥饿,S设置的太低,交互性作业得不到合适的CPU时间。S也被John Ousterhout称为 voo-doo constant。
此外对于用户程序可能会愚弄调度程序这个问题的根源在于4a,4b两条规则,任务在时间片内释放CPU就保留优先级。因此我们可以改变计时规则:调度程序记录一个进程在某一个队列中消耗的总时间,而不是调度时重新开始计时,只要在某一队列中的总时间消耗完毕就把它降到下一个队列去。重写后的4a和4b如下:
4:一旦工作用完了其在某一层中的时间额度(不管是一次用完还是多次用完),就降低它的优先级。

上面我们介绍了MLFQ的工作原理,但是这里还存在一些常见问题:

  1. 配置多少队列?
  2. 每一个队列的时间额度设置多大?
  3. S的值设置为多大?
    关于这些问题,通常并没有一个固定的方法来解决,一些有用的方法我们可以借鉴。
    比如大多数MLFQ队列支持不同队列拥有可变的时间片长度,高优先级队列的时间额度较少,低优先级队列更可能是计算密集型任务因此时间额度较大。而对于这些具体的数值,我们可以采用配置文件的方式暴露给系统管理员,管理员可以根据负载情况调整数值。
    此外,有些MLFQ的实现并没有完全按照上述规则,比如FreeBSD(4.3)调度程序,会基于当前进程使用了多少CPU,通过公式计算某个工作的当前优先级,此外CPU使用量会随着时间衰减,这就使得某些任务可以提升优先级。还有一些MLFQ算法,将较高的队列留给操作系统,允许用户设置进程的建议优先级(比如nice指令),从而增加或降低进程在某个时刻运行的机会。

许多操作系统都是用MLFQ作为其调度程序的基础(会做出一定的修改),包括类UNIX系统,以及Windows NT及其以后的Window系统。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

可怕的竹鼠商

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值