多级反馈队列(Multi-level Feedback Queue,MLFQ)需要解决两方面的问题。

  • 首先它要优化周转时间,这可以通过优先执行较短的工作来实现。然而,操作系统常常不知道工作要运行多久,而这又是SJF等算法所必需的。
  • 其次,MLFQ希望给用户提供较好的交互体验,因此需要降低响应时间。然而,轮转调度虽然降低了响应时间,周转时间却很差。

所以这里的问题是:通常我们对进程一无所知,应该如何构建调度程序来实现这些目标?调度程序如何在运行过程中学习进程的特征,从而做出更好的调度决策?

MLFQ:基本规则

MLFQ 中有许多独立的队列(queue),每个队列有不同的优先级(priority level)。任何时刻,一个工作只能存在于一个队列中。MLFQ 总是优先执行较高优先级的工作(即在较高级队列中的工作)。对于同一个队列中的任务(具有相同优先级),采用轮转调度

MLFQ中工作优先级并不是固定的,而是会根据进程的行为动态调整优先级。例如,如果一个工作不断放弃 CPU 去等待键盘输入,这是交互型进程的可能行为,MLFQ 因此会让它保持高优先级。相反,如果一个工作长时间地占用 CPU,MLFQ 会降低其优先级。

MLFQ 的两条基本规则:

  • 规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。
  • 规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A 和 B 。

java多级反馈队列进程调度 多级反馈队列调度例题_linux


上图中,最高优先级有两个工作(A和B),工作C位于中等优先级,而D的优先级最低。按刚才介绍的基本规则,由于A和B有最高优先级,调度程序将交替的调度他们,而C和D永远都没有机会运行,除非A和B已经完成。

尝试 1:如何改变优先级

我们必须决定,在一个工作的生命周期中,MLFQ 如何改变其优先级(在哪个队列中)。要做到这一点,我们必须记得工作负载:既有运行时间很短频繁放弃 CPU 的交互型工作,也有需要很多 CPU 时间响应时间却不重要长时间计算密集型工作。下面是我们第一次尝试优先级调整算法。

  • 规则 3 :工作进入系统时,放在最高优先级(最上层队列)。
  • 规则 4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。
  • 规则 4b:如果工作在其时间片以内主动释放 CPU,则优先级不变。

实例 1:单个长工作

java多级反馈队列进程调度 多级反馈队列调度例题_优先级_02

从这个例子可以看出,该工作首先进入最高优先级(Q2)。执行一个 10ms 的时间片后,调度程序将工作的优先级减 1,因此进入 Q1。在 Q1 执行一个时间片后,最终降低优先级进入系统的最低优先级(Q0),并一直留在那里。

实例 2:加入一个短工作

java多级反馈队列进程调度 多级反馈队列调度例题_java多级反馈队列进程调度_03

工作B 在 T=100 时到达

如果不知道工作是短工作还是长工作,那么就在开始的时候假设其是短工作,并赋予最高优先级。如果确实是短工作,则很快会执行完毕,否则将被慢慢移入低优先级队列,而这时该工作也被认为是长工作了。通过这种方式,MLFQ 近似于 SJF(最短任务优先)。

实例 3:如果有 I/O 呢

java多级反馈队列进程调度 多级反馈队列调度例题_linux_04


交互型工作 B(用灰色表示)每执行 1ms 便需要进行 I/O 操作,它与长时间运行的工作 A(用黑色表示)竞争 CPU。MLFQ 算法保持 B 在最高优先级,因为 B 总是让出 CPU。如果 B 是交互型工作,MLFQ 就进一步实现了它的目标,让交互型工作快速运行

当前MLQF的一些问题

至此,我们有了基本的MLFQ。它看起来似乎相当不错,长工作之间可以公平地分享CPU,又能给短工作或交互型工作很好的响应时间。然而,这种算法有一些非常严重的缺点。

  • 饥饿问题。如果系统有“太多”交互型工作,就会不断占用CPU,导致长工作永远无法得到CPU。即使在这种情况下,我们也希望这些长工作也能有所进展。
  • 愚弄调度程序(game the scheduler)。其次,某些用户会用一些手段欺骗调度程序,让它给予进程远超公平的资源。例如,上述算法对如下的攻击束手无策:进程在时间片用完之前,调用一个I/O操作(比如访问一个无关的文件),从而主动释放CPU。如此便可以保持在高优先级,占用更多的CPU时间。做得好时(比如,每运行99%的时间片时间就主动放弃一次CPU),工作可以几乎独占CPU。
  • 一个程序可能在不同时间表现不同。一个计算密集的进程可能在某段时间需要作为一个交互型的进程。用我们目前的方法,它不会享受系统中其他交互型工作的待遇。因为优先级一旦下降就无法提升

尝试 2:提升优先级

我们首先来尝试避免饥饿问题。要让CPU密集型工作也能局的一些进展,一个简单的思路是周期性地提升所有工作地优先级,最简单的实现就是将所有工作一股脑儿地扔到最高优先级队列。于是,我们有了以下规则。

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

新规则一下解决了两个问题。

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

java多级反馈队列进程调度 多级反馈队列调度例题_时间片_05

  • 左边没有优先级提升,长工作在两个短工作到达后被饿死。
  • 右边每 50ms 就有一次优先级提升(这里只是举例,这个值可能过小),因此至少保证长工作会有一些进展,每过 50ms 就被提升到最高优先级,从而定期获得执行。

添加时间段 S 导致了明显的问题:S 的值应该如何设置?德高望重的系统研究员 John Ousterhout 曾将这种值称为“巫毒常量(voo-doo constant)”,因为似乎需要一些黑魔法才能正确设置。如果 S 设置得太高,长工作会饥饿;如果设置得太低,交互型工作又得不到合适的 CPU 时间比例。

尝试 3:更好的计时方式

为了防止用户欺骗调度程序,让它给予进程远超公平的资源,MLQF为每层队列提供更为完善的CPU计时方式。调度程序记录一个进程在某一层中消耗的总时间,而不是在调度时重新计时。只要进程用完了自己的配额,就将它降到低一级队列中去。不论它是一次用完的,还是拆成很多次用完。

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

java多级反馈队列进程调度 多级反馈队列调度例题_java多级反馈队列进程调度_06


上图对比了在规则4a、4b的策略下(左),以及在新的规则4(右)的策略下,同样试图欺骗调度程序的进程的表现。没有规则4的保护时,进程可以在每个时间片结束前发起一次I/O操作,从而垄断CPU时间。有了这样的保护后,不论进程的I/O行为如何,都会慢慢地降低优先级,因而无法获得超过公平的CPU时间比例。

MLFQ 调优及其他问题

关于MLFQ调度算法还有一些问题。

  1. 其中一个大问题是如何配置一个调度程序,例如,配置多少队列?
  2. 每一层队列的时间片配置多大?
  3. 为了避免饥饿问题以及进程行为改变,应该多久提升一次进程的优先级?

这些问题都没有显而易见的答案,因此只有利用对工作负载的经验,以及后续对调度程序的调优,才会导致令人满意的平衡。例如,大多数的MLFQ变体都支持不同队列可变的时间片长度。高优先级队列通常只有较短的时间片(比如10ms或者更少),因而这一层的交互工作可以更快地切换。相反,低优先级队列中更多的是CPU密集型工作,配置更长的时间片会取得更好的效果。

小结

本章介绍了一种调度方式,名为多级反馈队列(MLFQ)。

本章包含了一组优化的 MLFQ 规则。为了方便查阅,我们重新列在这里。(建议熟记)

  • 规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。
  • 规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A和 B。
  • 规则 3:工作进入系统时,放在最高优先级(最上层队列)。
  • 规则4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次 CPU),就降低其优先级(移入低一级队列)。
  • 规则 5:经过一段时间 S,就将系统中所有工作重新加入最高优先级队列。