目录
Operating Systems: Three Easy Pieces 笔记
第7章 进程调度:介绍
第8章 调度:多级反馈队列
第9章 调度:比例份额
第10章 多处理器调度(高级)
调度策略(sheduling policy)
我们对操作系统中运行的进程(有时也叫工作任务)做出如下的假设:
1.每一个工作运行相同的时间。
2.所有的工作同时到达。
3.一旦开始,每个工作保持运行直到完成。
4.所有的工作只是用 CPU(即它们不执行 IO 操作)。
5.每个工作的运行时间是已知的。
暂时仅关心一个指标 -> 周转时间(turnaround time):任务完成时间减去任务到达系统的时间。
先进先出(First In First Out, FIFO),也称为先到先服务(First Come First Served, FCFS)。
现有3个工作 A、B 和 C 在大致相同的时间到达系统。假设当它们都同时到达时,A 比 B 早一点点,然后 B 比 C 早到达一点点(图7.1);假设每个工作运行 10s ==>
A 在 10s 时完成,B 在 20s 时完成,C 在 30s 时完成 ==>
这 3个任务的平均周转时间为(10s + 20s + 30s)/ 3 = 20s
现在放宽 1.1 的假设 1,假设 3 个任务(A、B 和 C), A 运行 100s,而 B 和 C 运行 10s =>
系统的瓶颈周转时间是 (100s + 110s + 120s)/ 3 = 110s
这个问题通常被称为护航效应(convoy effect),一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。这个调度方案就像在杂货店只有一个排队队伍的时候,前面的人装满 3 辆购物车食品并且掏出了支票本。
最短任务优先(Shortest Job First,SJF): 先运行最短的任务,然后是次短的任务,如此下去。
在 A 之前运行 B 和 C,SJF 将平均周转时间从 110s 降低到 50s((10 + 20 + 120)/3 = 50)。
SJF 是一种非抢占式 (non-preemptive)调度程序
非抢占式调度程序 -> 系统会将每项 工作做完,再考虑是否运行新工作。
现在放宽 1.1 的假设 2,工作可以随时到达,而不是同时到达。
假设 A 在 t = 0 时到达,且需要运 行 100s。而 B 和 C 在 t = 10 到达,且各需要运行 10s。这 3 项工作的平均周转时间为 103.33s,即(100+(110−10)+(120−10))/3。
现在放宽放宽1.1 的假设 3,工作不必保持运行直到完成
SJF 添加抢占 ==> 最短完成时间优先(Shortest Time-to-Completion First,STCF)或抢占式最短作业优先(Preemptive Shortest Job First ,PSJF)调度程序
每当新工作进入系统时,它就会确定剩余工作和新工作中, 谁的剩余时间最少,然后调度该工作。因此,STCF 将抢占 A 并运行 B 和 C ,只有在它们完成后,才调度 A 的剩余时间。
平均周转时间 50s(120 + (20-10)+(30-10))/3 = 50)
用户会和系统交互 --> 需要一个新的度量标准 -->响应时间(response time)
例如,如果我们有上面的调度(A 在时间 0 到达,B 和 C 在时间 10 达到),每个作业 的响应时间如下:作业 A 为 0,B 为 0,C 为 10(平均:3.33)。
STCF 和相关方法在响应时间上并不是很好。例如,如果 3 个工作同时到 达,第三个工作必须等待前两个工作全部运行后才能运行。如何构建对响应时间敏感的调度程序? --> 轮转
轮转(Round-Robin, RR)调度:在一个时间片(time slice)内运行一个工作,然后切换到运行队列中的下一个任务,如此反复执行,直到所有任务完成。
注意:时间片长度必须是时钟中断周期的倍数。
系统设计者需要权衡时间片的长度,使其足够长,以便摊销上下文切换成本,而又不会使系统不及时响应。
我们开发了两种调度程序。第一种类型(SJF、STCF)优化周转时间,但对响应时间不 利。第二种类型(RR)优化响应时间,但对周转时间不利。
现在放宽放宽1.1 的假设 4,考虑I/O
假设有两项工作 A 和 B,每项工作需要 50ms 的 CPU 时间。但是,A 运行 10ms,然后发出 I/O 请求(假设 I/O 每个都需要 10ms),而 B 只是使用 CPU 50ms,不执行 I/O。调度程序先运行 A,然后运行 B(见图 7.8)。
假设我们正在尝试构建 STCF 调度程序。将 A 的每个 10ms 的子工作视为一项独立的工作。当系统启动时,对于STCF选择较短的 一个,即 A。然后,A 的工作已完成,只剩下 B,并开始运行。然后提交 A 的一个新子工作,它抢占 B 并运行 10ms。这样做可以实现重叠(overlap),一个进程在等待另一个进程的 I/O 完成时使用 CPU,系统因此得到更好的利用(见图 7.9)。
以上介绍了两类调度方法,
第一类是运行最短的工作,从而优化周转时间。
第二类是交替运行所有工作,从而优化响应时间。
我们还有一个假设需要放宽:假设 5(每个作业的运行时间是已知的)。稍后,我们将看到如何通过构建一个调度程序,利用最近的历史预测未来,从而解决这个问题。
多级反馈队列(Multi-level Feedback Queue, MLFQ)
多级反馈队列需要解决两方面的问题。
首先,它要优化周转时间。这通过先执行短工作来实现,然而,操作系统通常不知道工作要运行多久,而这又是 SJF(或 STCF)等算法所必需的。
其次,MLFQ 希望给交互用户很好的交互体验,因此需要降低响应时间。
如何做出更好的调度决策? --> 从历史中学习
MLFQ 中有许多独立的队列(queue),每个队列有不同的优先级(priority level)。任何时刻,一个工作只能存在于一个队列中。MLFQ 总是优先执行较高优先级的工作(即在较高级队列中的工作)。 当然,每个队列中可能会有多个工作,因此具有同样的优先级。在这种情况下,我们就对这些工作采用轮转调度。
规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。
规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A 和 B。
MLFQ 调度策略的关键在于如何设置优先级 --> 根据观察到的行为调整它的优先级。
例如,如果一个工作不断放弃 CPU 去等待键盘输入,这是交互型进程的可能行为,MLFQ 因此会让它保持高优先级。相反,如果一个工作长时间地占用 CPU,MLFQ 会降低其优先级。通过这种方式,MLFQ 在 进程运行过程中学习其行为,从而利用工作的历史来预测它未来的行为。
规则 3:工作进入系统时,放在最高优先级(最上层队列)。
规则 4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。
规则 4b:如果工作在其时间片以内主动释放 CPU, 则优先级不变。
如果系统中有一个需要长时间运行的工作,图 8.2 展示了在一个有 3 个队列的调度程序中,该工作首先进入最高优先级 (Q2)。执行一个 10ms 的时间片后,调度程序将工作的优先级减 1 ,进入 Q1。在 Q1 执行一个时间片后,最终降低优先级进入系统的最低优先级 (Q0),一直留在那里。
现在有两个工作:A 是一个长时间运行的 CPU 密集型工作,B 是一个运行时间很短的交互型工作。假设 A 执行 一段时间后 B 到达。
图 8.3 中 A(用黑色表示)在最低优先级队列执行,B(用灰色表示)在时间 T=100 时到达,并被加入最高优先级 队列。由于它的运行时间很短(只有 20ms),经过两个时间片,在被移入最低优先级队列之 前,B 执行完毕。然后 A 继续运行(在低优先级)。
如果不知道工作是短工作还是长工作,那么就在开始的时候假设其是短工作,并赋予最高优先级。如果确实是短工作,则很快会执行完毕,否则将被慢慢移入低优先级队列,而这时该工作也被认为是长 工作了。通过这种方式,MLFQ 近似于 SJF。
根据上述规则 4b,如果进程在时间片用完之前主动放弃 CPU, 则保持它的优先级不变。这条规则的意图很简单:假设交互型工作中有大量的 I/O 操作(比 如等待用户的键盘或鼠标输入),它会在时间片用完之前放弃 CPU。在这种情况下,只是保持它的优先级不变。
图 8.4 展示了这个运行过程,交互型工作 B(用灰色表示)每执行 1ms 便需要进行 I/O 操作,它与长时间运行的工作 A(用黑色表示)竞争 CPU。MLFQ 算法保持 B 在最高优先级,因为 B 总是让出 CPU。
首先,会有饥饿(starvation)问题。如果系统有“太多”交互型工作,就会不断占用 CPU,导致长工作永远无法得到 CPU。
其次,聪明的用户会重写程序,愚弄调度程序(game the scheduler)。例如,进程在时间片用完之前,调用一个 I/O 操作(比如访问一个无关的文件),从而主动释放 CPU。如此便可以保持在高优先级,占用更多的 CPU 时间。做得好时(比如,每运行 99%的时间片时间就主动放弃一次 CPU),工作可以几乎独占 CPU。
最后,一个程序可能在不同时间表现不同。一个计算密集的进程可能在某段时间表现为一个交互型的进程。用我们目前的方法,它不会享受系统中其他交互型工作的待遇。
规则 4:工作一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次 CPU),就降低其优先级(移入低一级队列)。
规则 5:经过一段时间 S,就将系统中所有工作重新加入最高优先级队列。
解决了两个问题 --> 首先,进程不会饿死
其次,如果一个 CPU 密集 型工作变成了交互型,当它优先级提升时,调度程序会正确对待它
大多数的 MLFQ 变体都支持不同队列可变的时间片长度。
高优先级队列通常只 有较短的时间片(比如 10ms 或者更少),因而这一层的交互工作可以更快地切换。低优先级队列中更多的是 CPU 密集型工作,配置更长的时间片会取得更好的效果。
比例份额(proportional-share)调度程序,有时也称为公平份额(fair-share)调度程序。 -->
调度程序的最终目标,是确保每个工作获得一定比例的 CPU 时间。
例子:彩 票调度(lottery scheduling)
假设有两个进程 A 和 B,我们希望 A 占用 75%的 CPU 时间,而 B 占用 25%。通过不断定时地抽取彩票,彩票调度从概率上获得这种份额比例。抽取彩票的过程 -->
调度程序知道总共的彩票数,例如100 张。调度程序抽取中奖彩票,这是从 0到 99之间的一个数,拥有这个数对应的彩票的进程中奖。假设进程 A 拥有 0 到 74 共 75 张彩票,进程 B 拥有 75 到 99 的 25 张, 中奖的彩票就决定了运行 A 或 B。
随机方法的优势:随机方法很轻量,几乎不需要记录任何状态;随机方法很快。
注意:彩票调度中利用了随机性,这导致了从概率上满足期望的比例, 但并不能确保。
随机数生成器来选择中奖彩票、记录系统中所有进程的数据结构(一个列表)、所有彩票的总数
假定我们用列表记录进程。下面的例子中有 A、B、C 这 3 个进程,在做出调度决策之前,首先要从彩票总数 400 中选择一个随机数(中奖号码)。假设 选择了 300。然后,遍历链表,用一个简单的计数器帮助我们找到中奖者。
从前向后遍历进程列表,将每张票的值加到 counter 上,直到值超过 winner。 这时,当前的列表元素所对应的进程就是中奖者。例如,中奖彩票是 300。首先,计 A 的票后,counter 增加到 100。因为 100 小于 300,继续遍历。然后 counter 会增加到 150 (B 的彩票),仍然小于 300,继续遍历。最后,counter 增加到 400(大于 300),退出遍历,当前指针指向 C(中奖者)。
注:要让这个过程更有效率,建议将列表项按照彩票数递减排序。
虽然随机方式 可以使得调度程序的实现简单,但并不能产生正确的比例,尤其在工作运行时间很短的情况下。而步长调度(stride scheduling)是一个确定性的公平分配算法。
系统中的每个工作都有自己的步长,这个值与票数值成反比。
在上面的例子中,A、B、C 这 3 个工作的票数分别是 100、50 和 250,我们通过用一个大数分别除以他们的票数来获得每个进程的步长。
比如用 10000 除以这些票数值,得到了 3 个进程的步长分别为 100、200 和 40。我们称这个值为每个进程的步长(stride)。每次进程运行后,我们会让它的计数器 ,称为行程(pass)值],增加它的步长,记录它的总体进展。
调度时,选择目前拥有最小行程值的进程,并且在运行之后将该进程的行程值增加一个步长。
在我们的例子中,3 个进程(A、B、C)的步长值分别为 100、200 和 40,初始行程值都为 0。因此,最初所有进程都可能被选择执行。假设选择 A,运行记录如下,
可以看出,C 运行了 5 次、A 运行了 2 次,B 运行了一次,正好是票数的比例——200、100 和 50。彩票调度算法只能一段时间后,在概率上实现比例,而步长调度算法可以在每个调度周期后做到完全正确。
但是,彩票调度有一个步长调度没有的优势——不需要全局状态。假如一个新的进 程在上面的步长调度执行过程中加入系统,应该怎么设置它的行程值呢?如果设置成 0 ,它就独占 CPU 了。
彩票调度和步长调度,并没有作为 CPU 调度程序被广泛使用。一个原因是这两种方式都不能很好地适合 I/O;另一个原因是其中最难的票数分配 问题并没有确定的解决方式,例如,如何知道浏览器进程应该拥有多少票数?
比例份额调度程序只有在这些问题可以相对容易解决的领域更有用。例如在虚拟(virtualized)数据中心中,你可能会希望分配 1/4 的 CPU 周期给 Windows 虚拟机,剩余的给 Linux 系统,比例分配的方式可以更简单高效。
多处理器调度(multiprocessor scheduling),建议认真学习并发相关的内容后再读
在单 CPU 系统中,存在多级的硬件缓存(hardware cache),一般来说会让处理器更快地执行程序。缓存是很小但很快的存储设备,通常拥有内存中最热的数据的备份。相比之 下,内存很大且拥有所有的数据,但访问速度较慢。
缓存是基于局部性(locality)的概念。时间局部性是指当一个数据被访问后,它很有可能会在不久的将来被再次访问,比如循环代码中的数据或指令本身。空间局部性指的是,当程序访问地址为 x 的数据时,很有可能会紧接着访问 x 周围的数据,比如遍历数组或指令的顺序执行。由于这两种局部性存在于大多数的程序中,硬件系统可以很好地预测哪些数据可以放入缓存,从而运行得很好。
如果系统有多个处理器,并共享同一个内存,如图 10.2 所示,会怎样呢?
例如,假设一个运行在 CPU 1 上的程序 从内存地址 A 读取数据。由于不在 CPU 1 的缓存中,所以系统直接访问内存,得到值 D。 程序然后修改了地址 A 处的值,只是将它的缓存更新为新值 D'。将数据写回内存比较慢, 因此系统(通常)会稍后再做。假设这时操作系统中断了该程序的运行,并将其交给 CPU 2, 重新读取地址 A 的数据,由于 CPU 2 的缓存中并没有该数据,所以会直接从内存中读取, 得到了旧值 D,而不是正确的值 D'。
通过监控内存访问,硬件可以保证获得正确的数据,并保证共享内存的唯一性。在基于总线的系统中,一种方式是使用总线窥探(bus snooping)。每个缓存都通过监听链接所有缓存和内存的总线,来发现内存访问。如果 CPU 发现对它放在缓存中的数据的更新,会作废本地副本(从缓存中移除), 或更新它(修改为新值)。
跨 CPU 访问(尤其是写入)共享数据或数据结构时,需要使用互斥原语(比如锁),才能保证正确性。
一个进程在某个 CPU 上运行时,会在该 CPU 的缓存中维护许多状态。下次 该进程在相同 CPU 上运行时,由于缓存中的数据而执行得更快。相反,在不同的 CPU 上执 行,会由于需要重新加载数据而很慢(好在硬件保证的缓存一致性可以保证正确执行)。
单队列多处理器调度(Single Queue Multiprocessor Scheduling,SQMS):简单地复用单处理器调度的基本架构,将所有需要调度的工作放入一个单独的队列中。
存在的问题:
第一个是缺乏可扩展性(scalability)。为了保证在多 CPU 上正常运行,调度程序的开发者需要在代码中通过加锁(locking)来保证原子性。
第二个是缓存亲和性。假设我们有 5 个工作(A、B、C、D、 E)和 4 个处理器。调度队列如下:
假设每个工作依次执行一个时间片,然后选择另一个工作,下面是每个 CPU 可能的调度序列:
为了解决这个问题,大多数 SQMS 调度程序都引入了一些亲和度机制,尽可能让进程在同一个 CPU 上运行。保持一些工作的亲和度的同时,可能需要牺牲其他工作的亲和度来 实现负载均衡。
多队列多处理器调度(Multi-Queue Multiprocessor Scheduling,MQMS):基本调度框架包含多个调度队列(比如每个 CPU 一个队列),每个队列可以使用不同的调度规则, 比如轮转或其他任何可能的算法。当一个工作进入系统后,系统会依照一些启发性规则(如随机或选择较空的队列)将其放入某个调度队列。从而,每个 CPU 调度之间相互独立, 就避免了单队列方式中由于数据共享及同步带来的问题。
例如,假设系统中有两个 CPU(CPU 0 和 CPU 1)。这时一些工作进入系统:A、B、C 和 D。每个 CPU 都有自己的调度队列,操作系统会决定每个工作放入哪个队列。可能像下面这样做:
根据不同队列的调度策略,每个 CPU 从两个工作中选择,决定谁将运行。例如,利用轮转:
MQMS 比 SQMS 有明显的优势,它天生更具有可扩展性。队列的数量会随着 CPU 的增加而增加,因此锁和缓存争用的开销不是大问题。此外,MQMS 天生具有良好的缓存亲和度。所有工作都保持在固定的 CPU 上,因而可以很好地利用缓存数据。
存在的问题:
负载不均(load imbalance)。假定和上面设定一样(4 个工作,2 个 CPU),但假设一个工作(如 C)这时执行完毕。现在调度队列如下:
A 会获得 B 和 D 两倍的 CPU 时间, 假设 A 和 C 都执行完毕,CPU 0 就闲置了。
解决方式 --> 让工作移动,这种技术我们称为迁移(migration)
系统如何决定发起这样的迁移?
工作窃取(work stealing)--> 工作量较少的(源)队列不定期地“偷看”其他(目标)队列是不是比自己的工作多。如果目标队列比源队列(显著地)更满,就从目标队列“窃取”一个或多个工作,实现负载均衡。 需要权衡的是,如果太频繁地检查其他队列,就会带来较高的开销;如果检查间隔太长,又可能会带来严重的负载不均。
SQMS 和 MQMS比较
单队列的方式(SQMS)比较容易构建,负载均衡较好,但在扩展性和缓存亲和度方面有着固有的缺陷。多队列的方式(MQMS)有很好的扩展性和缓存亲和度,但实现负载均衡却很困难。
Linux 的 3 种调度程序:O(1)调度程序、完全公平调度程序(CFS)以及 BF 调度程序(BFS)
O(1) | 多队列 | 基于优先级调度(类似于之前介绍的 MLFQ) |
CFS | 多队列 | 比例调度方法(类似之前介绍的步长调度) |
BFS | 用单队列 | 基于比例调度 |