文章

《Operating Systems: Three Easy Pieces》学习笔记(六) 调度:多级反馈队列

本章将介绍一种著名的调度方法–多级反馈队列(Multi-level Feedback Queue,MLFQ)。1962 年,Corbato 首次提出多级反馈队列,应用于兼容时分共享系统(CTSS)。Corbato 因在 CTSS 中的贡献和后来在 Multics 中的贡献,获得了 ACM 颁发的图灵奖(Turing Award)。该调度程序经过多年的一系列优化,出现在许多现代操作系统中。

提示:从历史中学习

多级反馈队列是用历史经验预测未来的一个典型的例子,操作系统中有很多地方采用了这种技术(同样存在于计算机科学领域的很多其他地方,比如硬件的分支预测及缓存算法)。如果工作有明显的阶段性行为,因此可以预测,那么这种方式会很有效。当然,必须十分小心地使用这种技术,因为它可能出错,让系统做出比一无所知的时候更糟的决定。

MLFQ:基本规则

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

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

MLFQ 的两条基本规则:

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

mlfq-1

尝试 1:如何改变优先级

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

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

实例 1:单个长工作

mlfq-2

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

实例 2:加入一个短工作

mlfq-3

B 在 T=100 时到达

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

实例 3:如果有 I/O 呢

mlfq-4

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

当前 MLFQ 的一些问题

  1. 饥饿(starvation)问题。如果系统有“太多”交互型工作,就会不断占用 CPU,导致长工作永远无法得到 CPU(它们饿死了)。
  2. 愚弄调度程序(game the scheduler)。愚弄调度程序指的是用一些卑鄙的手段欺骗调度程序,让它给你远超公平的资源。上述算法对如下的攻击束手无策:进程在时间片用完之前,调用一个 I/O 操作(比如访问一个无关的文件),从而主动释放 CPU。如此便可以保持在高优先级,占用更多的 CPU 时间。
  3. 一个程序可能在不同时间表现不同。一个计算密集的进程可能在某段时间需要作为一个交互型的进程。用我们目前的方法,它不会享受系统中其他交互型工作的待遇。因为优先级一旦下降就无法提升

尝试 2:提升优先级

为解决[1]饥饿问题

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

mlfq-5

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

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

尝试 3:更好的计时方式

为解决[2]愚弄调度程序问题

起因是规则 4a 和 4b 不合理,调度程序应该记录一个进程在某一层中消耗的总时间,而不是在调度时重新计时。

重写规则 4:

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

mlfq-6

没有规则 4的保护时,进程可以在每个时间片结束前发起一次 I/O 操作,从而垄断 CPU 时间。有了这样的保护后,不论进程的 I/O 行为如何,都会慢慢地降低优先级,因而无法获得超过公平的 CPU 时间比例。同时由于规则 5的存在,原来的交互性进程还是可以在之后提升优先级。

MLFQ 调优及其他问题

关于 MLFQ 调度算法还有一些问题。其中一个大问题是如何配置一个调度程序,例如:

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

这些问题都没有显而易见的答案,因此只有利用对工作负载的经验,以及后续对调度程序的调优,才会导致令人满意的平衡。

mlfq-7

例如,大多数的 MLFQ 变体都支持不同队列可变的时间片长度。高优先级队列通常只有较短的时间片(比如 10ms 或者更少),因而这一层的交互工作可以更快地切换。相反,低优先级队列中更多的是 CPU 密集型工作,配置更长的时间片会取得更好的效果。图 8.7 展示了一个例子,两个长工作在高优先级队列执行 10ms,中间队列执行 20ms,最后在最低优先级队列执行 40ms。

Solaris 的 MLFQ 实现(时分调度类 TS)很容易配置。它提供了一组表来决定进程在其生命周期中如何调整优先级,每层的时间片多大,以及多久提升一个工作的优先级。管理员可以通过这些表,让调度程序的行为方式不同。该表默认有 60 层队列,时间片长度从 20ms(最高优先级),到几百 ms(最低优先级),每一秒左右提升一次进程的优先级。

其他一些 MLFQ 调度程序没用表,甚至没用本章中讲到的规则,有些采用数学公式来调整优先级。例如,FreeBSD 调度程序(4.3 版本),会基于当前进程使用了多少 CPU,通过公式计算某个工作的当前优先级。另外,使用量会随时间衰减,这提供了期望的优先级提升,但与这里描述方式不同。阅读 Epema 的论文,他漂亮地概括了这种使用量衰减(decay-usage)算法及其特征

最后,许多调度程序有一些我们没有提到的特征。例如,有些调度程序将最高优先级队列留给操作系统使用,因此通常的用户工作是无法得到系统的最高优先级的。有些系统允许用户给出优先级设置的建议(advice),比如通过命令行工具 nice,可以增加或降低工作的优先级(稍微),从而增加或降低它在某个时刻运行的机会。更多信息请查看 man 手册。

提示:尽可能多地使用建议

操作系统很少知道什么策略对系统中的单个进程和每个进程算是好的,因此提供接口并允许用户或管理员给操作系统一些提示(hint)常常很有用。我们通常称之为建议(advice),因为操作系统不一定要关注它,但是可能会将建议考虑在内,以便做出更好的决定。这种用户建议的方式在操作系统中的各个领域经常十分有用,包括调度程序(通过 nice)、内存管理(madvise),以及文件系统(通知预取和缓存[P+95])

MLFQ:小结

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

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

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

MLFQ 有趣的原因是:它不需要对工作的运行方式有先验知识,而是通过观察工作的运行来给出对应的优先级。通过这种方式,MLFQ 可以同时满足各种工作的需求:对于短时间运行的交互型工作,获得类似于 SJF/STCF 的很好的全局性能,同时对长时间运行的 CPU 密集型负载也可以公平地、不断地稳步向前。因此,许多系统使用某种类型的 MLFQ 作为自己的基础调度程序,包括类 BSD UNIX 系统Solaris以及 Windows NT 和其后的 Window 系列操作系统

作业

程序 mlfq.py 允许你查看本章介绍的 MLFQ 调度程序的行为。详情请参阅 README 文件。

  1. 只用两个工作和两个队列运行几个随机生成的问题。针对每个工作计算 MLFQ 的执行记录。限制每项作业的长度并关闭 I/O,让你的生活更轻松。
  2. 如何运行调度程序来重现本章中的每个实例?
  3. 将如何配置调度程序参数,像轮转调度程序那样工作?
  4. 设计两个工作的负载和调度程序参数,以便一个工作利用较早的规则 4a 和 4b(用-S 标志打开)来“愚弄”调度程序,在特定的时间间隔内获得 99%的 CPU。
  5. 给定一个系统,其最高队列中的时间片长度为 10ms,你需要如何频繁地将工作推回到最高优先级级别(带有-B 标志),以保证一个长时间运行(并可能饥饿)的工作得到至少 5%的 CPU?
  6. 调度中有一个问题,即刚完成 I/O 的作业添加在队列的哪一端。-I 标志改变了这个调度模拟器的这方面行为。尝试一些工作负载,看看你是否能看到这个标志的效果。

参考

本文由作者按照 CC BY 4.0 进行授权

© Kai. 保留部分权利。

浙ICP备20006745号-2,本站由 Jekyll 生成,采用 Chirpy 主题。