CPU调度

目录

1.进程调度

1.1 工作负载简化假设

1.2 调度指标:周转时间

1.3 先进先出(FIFO)

1.4 最短任务优先(SJF)

1.5 最短完成时间优先(STCF)

1.6 新度量指标:响应时间

1.7 轮转

1.8 结合I/O

2. 多级反馈队列

2.1 MLFQ:基本规则

2.2 如何改变优先级

2.2.1 单个长工作

2.2.2 来了一个短工作

2.2.3 如果有I/O

2.2.4 当前 MLFQ 的一些问题

2.3 更好的计时方式

2.4 提升优先级

2.5 不同队列可变的时间片长度

3. 比例份额

3.1 彩票调度

3.1.1 彩票数表示份额

3.1.2 实现

3.3 步长调度

3.4 比例份额调度使用场景

4. 多处理器调度

4.1 多处理器架构

4.1.1 缓存

4.1.2 缓存一致性问题

4.2 共享数据访问加锁

4.3 缓存亲和度

4.4 单队列调度

4.5 多队列调度

4.6 Linux 多处理器调度


Operating Systems: Three Easy Pieces 笔记
第7章 进程调度:介绍
第8章 调度:多级反馈队列
第9章 调度:比例份额
第10章 多处理器调度(高级)

1.进程调度

调度策略(sheduling policy)

1.1 工作负载简化假设

我们对操作系统中运行的进程(有时也叫工作任务)做出如下的假设:
1.每一个工作运行相同的时间。
2.所有的工作同时到达。
3.一旦开始,每个工作保持运行直到完成。
4.所有的工作只是用 CPU(即它们不执行 IO 操作)。
5.每个工作的运行时间是已知的。

1.2 调度指标:周转时间

暂时仅关心一个指标 -> 周转时间(turnaround time):任务完成时间减去任务到达系统的时间。

1.3 先进先出(FIFO)

先进先出(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

CPU调度_第1张图片

现在放宽 1.1 的假设 1,假设 3 个任务(A、B 和 C), A 运行 100s,而 B 和 C 运行 10s =>

系统的瓶颈周转时间是 (100s + 110s + 120s)/ 3 = 110s

这个问题通常被称为护航效应(convoy effect),一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。这个调度方案就像在杂货店只有一个排队队伍的时候,前面的人装满 3 辆购物车食品并且掏出了支票本。

1.4 最短任务优先(SJF)

最短任务优先(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。

CPU调度_第2张图片

1.5 最短完成时间优先(STCF)

现在放宽放宽1.1 的假设 3,工作不必保持运行直到完成

SJF 添加抢占  ==> 最短完成时间优先(Shortest Time-to-Completion First,STCF)或抢占式最短作业优先(Preemptive Shortest Job First ,PSJF)调度程序

每当新工作进入系统时,它就会确定剩余工作和新工作中, 谁的剩余时间最少,然后调度该工作。因此,STCF 将抢占 A 并运行 B 和 C ,只有在它们完成后,才调度 A 的剩余时间。

CPU调度_第3张图片

平均周转时间 50s(120 + (20-10)+(30-10))/3 = 50)

1.6 新度量指标:响应时间

用户会和系统交互 --> 需要一个新的度量标准 -->响应时间(response time)

 

例如,如果我们有上面的调度(A 在时间 0 到达,B 和 C 在时间 10 达到),每个作业 的响应时间如下:作业 A 为 0,B 为 0,C 为 10(平均:3.33)。

STCF 和相关方法在响应时间上并不是很好。例如,如果 3 个工作同时到 达,第三个工作必须等待前两个工作全部运行后才能运行。如何构建对响应时间敏感的调度程序? --> 轮转

1.7 轮转

轮转(Round-Robin, RR)调度:在一个时间片(time slice)内运行一个工作,然后切换到运行队列中的下一个任务,如此反复执行,直到所有任务完成。

注意:时间片长度必须是时钟中断周期的倍数。

系统设计者需要权衡时间片的长度,使其足够长,以便摊销上下文切换成本,而又不会使系统不及时响应

我们开发了两种调度程序。第一种类型(SJF、STCF)优化周转时间,但对响应时间不 利。第二种类型(RR)优化响应时间,但对周转时间不利。

1.8 结合I/O

现在放宽放宽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)。

CPU调度_第4张图片

以上介绍了两类调度方法,

第一类是运行最短的工作,从而优化周转时间。

第二类是交替运行所有工作,从而优化响应时间。

我们还有一个假设需要放宽:假设 5(每个作业的运行时间是已知的)。稍后,我们将看到如何通过构建一个调度程序,利用最近的历史预测未来,从而解决这个问题。

2. 多级反馈队列

多级反馈队列(Multi-level Feedback Queue, MLFQ)

多级反馈队列需要解决两方面的问题。

首先,它要优化周转时间。这通过先执行短工作来实现,然而,操作系统通常不知道工作要运行多久,而这又是 SJF(或 STCF)等算法所必需的。

其次,MLFQ 希望给交互用户很好的交互体验,因此需要降低响应时间。

如何做出更好的调度决策? --> 从历史中学习

2.1 MLFQ:基本规则

MLFQ 中有许多独立的队列(queue),每个队列有不同的优先级(priority level)。任何时刻,一个工作只能存在于一个队列中。MLFQ 总是优先执行较高优先级的工作(即在较高级队列中的工作)。 当然,每个队列中可能会有多个工作,因此具有同样的优先级。在这种情况下,我们就对这些工作采用轮转调度。

规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。

规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A 和 B。

MLFQ 调度策略的关键在于如何设置优先级 --> 根据观察到的行为调整它的优先级。

例如,如果一个工作不断放弃 CPU 去等待键盘输入,这是交互型进程的可能行为,MLFQ 因此会让它保持高优先级。相反,如果一个工作长时间地占用 CPU,MLFQ 会降低其优先级。通过这种方式,MLFQ 在 进程运行过程中学习其行为,从而利用工作的历史来预测它未来的行为。

2.2 如何改变优先级

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

规则 4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。

规则 4b:如果工作在其时间片以内主动释放 CPU, 则优先级不变。

2.2.1 单个长工作

CPU调度_第5张图片

如果系统中有一个需要长时间运行的工作,图 8.2 展示了在一个有 3 个队列的调度程序中,该工作首先进入最高优先级 (Q2)。执行一个 10ms 的时间片后,调度程序将工作的优先级减 1 ,进入 Q1。在 Q1 执行一个时间片后,最终降低优先级进入系统的最低优先级 (Q0),一直留在那里。

2.2.2 来了一个短工作

现在有两个工作:A 是一个长时间运行的 CPU 密集型工作,B 是一个运行时间很短的交互型工作。假设 A 执行 一段时间后 B 到达。

图 8.3 中 A(用黑色表示)在最低优先级队列执行,B(用灰色表示)在时间 T=100 时到达,并被加入最高优先级 队列。由于它的运行时间很短(只有 20ms),经过两个时间片,在被移入最低优先级队列之 前,B 执行完毕。然后 A 继续运行(在低优先级)。

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

CPU调度_第6张图片

2.2.3 如果有I/O

根据上述规则 4b,如果进程在时间片用完之前主动放弃 CPU, 则保持它的优先级不变。这条规则的意图很简单:假设交互型工作中有大量的 I/O 操作(比 如等待用户的键盘或鼠标输入),它会在时间片用完之前放弃 CPU。在这种情况下,只是保持它的优先级不变。

图 8.4 展示了这个运行过程,交互型工作 B(用灰色表示)每执行 1ms 便需要进行 I/O 操作,它与长时间运行的工作 A(用黑色表示)竞争 CPU。MLFQ 算法保持 B 在最高优先级,因为 B 总是让出 CPU。

2.2.4 当前 MLFQ 的一些问题

首先,会有饥饿(starvation)问题。如果系统有“太多”交互型工作,就会不断占用 CPU,导致长工作永远无法得到 CPU。

其次,聪明的用户会重写程序,愚弄调度程序(game the scheduler)。例如,进程在时间片用完之前,调用一个 I/O 操作(比如访问一个无关的文件),从而主动释放 CPU。如此便可以保持在高优先级,占用更多的 CPU 时间。做得好时(比如,每运行 99%的时间片时间就主动放弃一次 CPU),工作可以几乎独占 CPU。

最后,一个程序可能在不同时间表现不同。一个计算密集的进程可能在某段时间表现为一个交互型的进程。用我们目前的方法,它不会享受系统中其他交互型工作的待遇。

2.3 更好的计时方式

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

CPU调度_第7张图片

2.4 提升优先级

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

解决了两个问题 --> 首先,进程不会饿死

其次,如果一个 CPU 密集 型工作变成了交互型,当它优先级提升时,调度程序会正确对待它CPU调度_第8张图片

2.5 不同队列可变的时间片长度

大多数的 MLFQ 变体都支持不同队列可变的时间片长度。

高优先级队列通常只 有较短的时间片(比如 10ms 或者更少),因而这一层的交互工作可以更快地切换。低优先级队列中更多的是 CPU 密集型工作,配置更长的时间片会取得更好的效果。

3. 比例份额

比例份额(proportional-share)调度程序,有时也称为公平份额(fair-share)调度程序。 -->

调度程序的最终目标,是确保每个工作获得一定比例的 CPU 时间。

例子:彩 票调度(lottery scheduling)

3.1 彩票调度

3.1.1 彩票数表示份额

假设有两个进程 A 和 B,我们希望 A 占用 75%的 CPU 时间,而 B 占用 25%。通过不断定时地抽取彩票,彩票调度从概率上获得这种份额比例。抽取彩票的过程 -->

调度程序知道总共的彩票数,例如100 张。调度程序抽取中奖彩票,这是从 0到 99之间的一个数,拥有这个数对应的彩票的进程中奖。假设进程 A 拥有 0 到 74 共 75 张彩票,进程 B 拥有 75 到 99 的 25 张, 中奖的彩票就决定了运行 A 或 B。

随机方法的优势:随机方法很轻量,几乎不需要记录任何状态;随机方法很快。

注意:彩票调度中利用了随机性,这导致了从概率上满足期望的比例, 但并不能确保。

3.1.2 实现

随机数生成器来选择中奖彩票、记录系统中所有进程的数据结构(一个列表)、所有彩票的总数

假定我们用列表记录进程。下面的例子中有 A、B、C 这 3 个进程,在做出调度决策之前,首先要从彩票总数 400 中选择一个随机数(中奖号码)。假设 选择了 300。然后,遍历链表,用一个简单的计数器帮助我们找到中奖者。

从前向后遍历进程列表,将每张票的值加到 counter 上,直到值超过 winner。 这时,当前的列表元素所对应的进程就是中奖者。例如,中奖彩票是 300。首先,计 A 的票后,counter 增加到 100。因为 100 小于 300,继续遍历。然后 counter 会增加到 150 (B 的彩票),仍然小于 300,继续遍历。最后,counter 增加到 400(大于 300),退出遍历,当前指针指向 C(中奖者)。

注:要让这个过程更有效率,建议将列表项按照彩票数递减排序。

CPU调度_第9张图片

3.3 步长调度

虽然随机方式 可以使得调度程序的实现简单,但并不能产生正确的比例,尤其在工作运行时间很短的情况下。而步长调度(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,运行记录如下,

CPU调度_第10张图片

可以看出,C 运行了 5 次、A 运行了 2 次,B 运行了一次,正好是票数的比例——200、100 和 50。彩票调度算法只能一段时间后,在概率上实现比例,而步长调度算法可以在每个调度周期后做到完全正确。

但是,彩票调度有一个步长调度没有的优势——不需要全局状态。假如一个新的进 程在上面的步长调度执行过程中加入系统,应该怎么设置它的行程值呢?如果设置成 0 ,它就独占 CPU 了。

3.4 比例份额调度使用场景

彩票调度和步长调度,并没有作为 CPU 调度程序被广泛使用。一个原因是这两种方式都不能很好地适合 I/O;另一个原因是其中最难的票数分配 问题并没有确定的解决方式,例如,如何知道浏览器进程应该拥有多少票数?

比例份额调度程序只有在这些问题可以相对容易解决的领域更有用。例如在虚拟(virtualized)数据中心中,你可能会希望分配 1/4 的 CPU 周期给 Windows 虚拟机,剩余的给 Linux 系统,比例分配的方式可以更简单高效。

4. 多处理器调度

多处理器调度(multiprocessor scheduling),建议认真学习并发相关的内容后再读

4.1 多处理器架构

4.1.1 缓存

在单 CPU 系统中,存在多级的硬件缓存(hardware cache),一般来说会让处理器更快地执行程序。缓存是很小但很快的存储设备,通常拥有内存中最热的数据的备份。相比之 下,内存很大且拥有所有的数据,但访问速度较慢。

缓存是基于局部性(locality)的概念。时间局部性是指当一个数据被访问后,它很有可能会在不久的将来被再次访问,比如循环代码中的数据或指令本身。空间局部性指的是,当程序访问地址为 x 的数据时,很有可能会紧接着访问 x 周围的数据,比如遍历数组或指令的顺序执行。由于这两种局部性存在于大多数的程序中,硬件系统可以很好地预测哪些数据可以放入缓存,从而运行得很好。

如果系统有多个处理器,并共享同一个内存,如图 10.2 所示,会怎样呢?

CPU调度_第11张图片

4.1.2 缓存一致性问题

例如,假设一个运行在 CPU 1 上的程序 从内存地址 A 读取数据。由于不在 CPU 1 的缓存中,所以系统直接访问内存,得到值 D。 程序然后修改了地址 A 处的值,只是将它的缓存更新为新值 D'。将数据写回内存比较慢, 因此系统(通常)会稍后再做。假设这时操作系统中断了该程序的运行,并将其交给 CPU 2, 重新读取地址 A 的数据,由于 CPU 2 的缓存中并没有该数据,所以会直接从内存中读取, 得到了旧值 D,而不是正确的值 D'。

通过监控内存访问,硬件可以保证获得正确的数据,并保证共享内存的唯一性。在基于总线的系统中,一种方式是使用总线窥探(bus snooping)。每个缓存都通过监听链接所有缓存和内存的总线,来发现内存访问。如果 CPU 发现对它放在缓存中的数据的更新,会作废本地副本(从缓存中移除), 或更新它(修改为新值)。

4.2 共享数据访问加锁

跨 CPU 访问(尤其是写入)共享数据或数据结构时,需要使用互斥原语(比如锁),才能保证正确性。

4.3 缓存亲和度

一个进程在某个 CPU 上运行时,会在该 CPU 的缓存中维护许多状态。下次 该进程在相同 CPU 上运行时,由于缓存中的数据而执行得更快。相反,在不同的 CPU 上执 行,会由于需要重新加载数据而很慢(好在硬件保证的缓存一致性可以保证正确执行)。

4.4 单队列调度

单队列多处理器调度(Single Queue Multiprocessor Scheduling,SQMS):简单地复用单处理器调度的基本架构,将所有需要调度的工作放入一个单独的队列中。

存在的问题:

第一个是缺乏可扩展性(scalability)。为了保证在多 CPU 上正常运行,调度程序的开发者需要在代码中通过加锁(locking)来保证原子性。

第二个是缓存亲和性。假设我们有 5 个工作(A、B、C、D、 E)和 4 个处理器。调度队列如下:

假设每个工作依次执行一个时间片,然后选择另一个工作,下面是每个 CPU 可能的调度序列:

CPU调度_第12张图片

为了解决这个问题,大多数 SQMS 调度程序都引入了一些亲和度机制,尽可能让进程在同一个 CPU 上运行。保持一些工作的亲和度的同时,可能需要牺牲其他工作的亲和度来 实现负载均衡。

4.5 多队列调度

多队列多处理器调度(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)有很好的扩展性和缓存亲和度,但实现负载均衡却很困难。

4.6 Linux 多处理器调度

Linux  的 3 种调度程序:O(1)调度程序、完全公平调度程序(CFS)以及 BF 调度程序(BFS)

O(1) 多队列 基于优先级调度(类似于之前介绍的 MLFQ)
CFS 多队列 比例调度方法(类似之前介绍的步长调度)
BFS 用单队列 基于比例调度

你可能感兴趣的:(操作系统)