操作系统 - 7 调度算法:介绍

到目前为止,进程运行的低级机制(比如,上下文切换)应该是清晰的,然而我们有必要理解操作系统调度程序采用的策略。我们现在展示多种调度策略。

事实上,调度的起源早于计算机操作系统,早期的一些方法取自业务管理领域并应用于计算机。装配线和许多其他生产活动也需要调度,其中存在许多相同的问题,包括对效率的思考。

工作量假设

我们先对操作系统中运行的进程做一些简单的假设,确定工作量建立调度策略中一个重要的部分。

这里我们做的工作量假设大多不切实际,但那对于现在来说已经足够了,因为我们会逐渐放宽条件,最后开发出我们希望的一套全运作调度规则。

我我们有时将操作系统中运行的进程称为作业,关于作业我们做以下假设:

  • 每一个作业都运行相同的时间。

  • 所有的作业都同时到达。

  • 一旦开始,每一个作业都要运行到结束。

  • 所有作业都仅用 CPU(例如它们没用到 I/O)。

  • 每一个作业的运行时间都是已知的。

每个作业的运行时间已知,这可能会使你感到困扰,因为这条假设会使得调度程序无所不知,尽管这很棒,但不太可能发生。

调度指标

除了做一些工作量假设,我们也需要做更多的事情来使得我们能够比较不同的调度策略:调度指标。通过指标,我们衡量一些事情,关于调度有多种指标来衡量。

到目前为止,先让我们认识一个简单的指标:周转时间。一个作业的周转时间被定义为作业完成的时间点减去作业到达系统的时间点,更正式的描述是:

Tturnaround=TcompletionTarrival

由于我们假设所有作业同时到达系统,即现在 Tarrival=0,因此 Tturnaround=Tcompletion。随着我们放宽上述假设条件,这一事实会改变。

我们应该注意到周转时间是一个性能指标,这一指标是这一章节里基本的关注点。另一个有趣的指标是公平性,比如由 Jain 公平指数衡量的公平性。性能和公平性经常在调度过程中是矛盾的,比如,一个调度器可能会优化性能但是以阻止一些工作作为代价(减少了公平性),这个难题向我们展示了生活并不总是完美的。

先进先出(FIFO)

我们能完成的最基本的算法是先进先出(FIFO)调度策略,有时也称为先来先服务(FCFS)。FIFO 有许多好性质:它简单因此容易实现。而且按照我们的假设,它能工作得很好。

举一个例子,假设有三个作业到达系统,分别是 A,B 和 C。这三个作业几乎同时到达。因为 FIFO 必须以某一作业开始,所以我们假设尽管它们都同时到达,但 A 比 B 早一点到达,而 B 比 C 早一点达到,还假设每个作业运行 10 秒,则这些作业的平均周转时间是多少?

显然,A 在 10 结束,B 在 20 结束,C 在 30 结束。因此,这三个作业的平均周转时间是 (10+20+30)/3=20。现在我们放宽其中一个假设,特别地,我们放宽第一个假设,因此不再假设每一个作业运行相同的时间。让我们做一个例子来展示不同作业的时间怎样导致 FIFO 调度策略有一个不好的表现。特别地,让我们又一次假设三个作业(A,B 和 C),但这次 A 运行 100 秒,而 B 和 C 运行 10 秒。

若我们用 FIFO 来调度,可以计算平均周转时间:(100+110+120)/3=110

这个问题通常被称为护航效应(convoy effect)。

最短作业优先(SJF)

事实表明,一个非常简单方法可以解决这个问题,这个新的思想是从运筹学中借鉴而来,并应用到计算机系统的作业调度。这个新的调度策略称为最短作业优先(SJF),这个名称就完整地描述了这个策略:先运行最短作业,然后第二短的作业,依次执行。

让我们以 SJF 作为调度策略再计算一次此时作业的平均周转时间,显然此时我们先运行作业 B 和 C,再运行作业 A,计算周转时间 (10+20+120)/3=50,超过两倍的改进。

事实上,按照我们对作业同时到达的假设,我们能证明 SJF 实际上是一个最优调度算法。在这门课上不给证明,详细证明可参看运筹学的相关内容。

因此我们找到了一种用 SJF 来调度的好方法,但我们的假设依旧不现实,让我们再放宽条件,特别地,我们考虑将第二个条件放宽,现在假设作业可能在任何时间到达。

举一个例子,这次我们假设 A 在 t=0 时达到且需要运行 100 秒,B 和 C 在 t=10 到达且需要运行 10 秒。此时即使 B 和 C 在 A 之后不久就达到系统,它们依旧被强迫等待直到 A 作业完成,因此存在同样的问题,即护航效应。这三个作业的平均周转时间是 (100+(11010)+(12010))/3103.33

最短完成时间优先(STCF)

为了解决这个问题,我们需要放宽第三个假设(作业必须运行到结束),按照我们之前对时钟中断和上下文切换的讨论,调度器在 B 和 C 到达时能做一些事情:抢占作业 A 且决定运行另一个作业,也许稍后继续运行作业 A。按我们的定义,SJF 是一个非抢占式调度器,因此产生了上述说的一些问题。

幸运地是,存在一个调度器,它给 SJF 抢占机制,著名的像最短完成时间优先(STCF)或者抢占式最短作业优先调度器。任何一个新作业进入系统,STCF 调度器会决定哪个剩下的作业有最早的离开时间,然后调度这个作业,因此,在我们的例子中,STCF 会抢占 A 运行 B 和 C 直至结束。

结果显示,平均周转时间提升了很多,(120+(2010)+(3010))/3=50。与之前的类似,按我们的假设,STCF 能被证明是最优的。

新的指标:响应时间

因此,如果我们知道作业的运行时间,且作业仅使用 CPU,我们唯一的衡量指标是周转时间,则 STCF 是很好的调度策略,事实上,对于许多早期的批处理计算机系统,这些类型的调度算法有一定意义。然而,分时计算机的引入完全改变了这一切,一个新的衡量指标诞生:响应时间。

我们定义响应时间,作业到达系统的时间至该作业第一次被调度的时间之间,更正式地说:

Tresponse=TfirstrunTarrival

例如,假设 A 在 0 时达到,而 B 和 C 在 10 时达到,每个作业的响应时间分别如下:作业 A 为 0,作业 B 为 0,作业 C 为 10,平均为 3.33

如同你可能想到的,STCF 和类似的策略对响应时间这一指标表现不是很好。如果三个作业同时达到,则在调度第三个作业前,不得不等待前两个作业完整地运行,该方法尽管对周转时间表现得很好,但它在响应时间和交互性上表现得很差。事实上,想象面对一个终端,输入命令,然后不得不等待 10 秒才能看到系统对此的响应(由于其他作业被事先调度了),那不是一件愉快的事。

因此,我们有了另一个问题,怎样构建一个调度器使得它对响应时间敏感?

时间片轮转(RR)

为了解决这个问题,我们介绍一种经典的新调度算法,被称为时间片轮转(RR)调度。其基本的想法是:我们不会运行一个作业直至结束,相反,RR 只在一个时间片(有时也称为调度量子)内运行一个作业,然后切换到运行队列中的下一个作业。它就这样重复下去,直至所有作业被完成。注意到一个时间片的长度一定得是时钟中断周期的整数倍。因此,如果时钟每 10 毫秒中断一次,则时间片一定得是 1020 或其他 10 的整数倍毫秒。

为了理解 RR 中的更多细节,让我们看一个例子。假设有三个工作 A,B 和 C 同时达到系统,且它们每一个都希望运行 5 秒。按照 SJF 调度运行每个作业,会在运行下一个之前完整运行本次作业。作为对比,以 1 秒作为时间片的 RR 调度会循环运行各个作业。

RR 的平均响应时间是:(0+1+2)/3=1SJF 的平均响应时间是:(0+5+10)/3=5

如同你所看到的,时间片的长度对于 RR 调度是至关重要的,如果它更短,RR 就会在响应时间这一指标上有更好的表现。然而,时间片太短也会导致问题:此时上下文切换的开销会主导整体的性能。因此,时间片的长度展现一个系统设计者的权衡,使时间片足够长以摊销上下文切换的开销,同时不会使得它过长以至于系统不灵敏。

注意到上下文切换的成本不仅仅来自保存和恢复一些寄存器的操作系统行为。当程序运行时,它们会在 CPU 缓存,TLBs,分支预测器和其他片上的硬件。切换到另一个工作会导致该状态被刷新并于当前运行的作业相关的新状态被引入,这可能导致显著的性能成本。

但我们会发现 RR 在平均周转时间指标上表现得非常糟糕。事实上,如果我们将平均周转时间作为我们唯一的指标,RR 是最差的调度策略中的一个。直观上我们可以理解,RR 做的事情会让每一个作业的完成时间尽可能地延长,而平均周转时间仅关心一个作业什么时候结束。

更一般地说,任何一个公平策略(比如 RR),这里指在小时间范围内将 CPU 平均分配给活动进程,都在周转时间等指标上表现不佳。事实上,这是一个内在的权衡,如果你想不公平,你可以运行更短的时间达到完成时刻,但代价却是响应时间;如果你重视公平性,则响应时间会降低,但却会以周转时间为代价。这种类型的权衡在系统中是常见的。

我们现在已经有两类调度器。第一类(SJFSTCF)优化周转时间,但却在响应时间指标上表现不好。第二类是(RR)优化响应时间,但却在周转时间上表现不好。此外我们依旧还有两个假设需要放宽,让我们接下来处理这些假设。

包含 I/O

我们先放宽假设 4。显然我们的程序很可能使用 I/O。想象一个程序没有任何输入,它每次产生相同的输出。想象另一个程序没有输出。

一个调度器当一个作业请求 I/O 时显然要做出决策,因为当前的作业在 I/O 期间不再使用 CPU;为了等待 I/O 完成它被阻塞。如果该 I/O 被送到一个硬盘驱动,该进程可能会阻塞几毫秒或更久,这取决于该驱动器的当前 I/O 负载。因此,该调度器在那时可能会调度另一个作业到 CPU

调度器在 I/O 完成时也不得不做出决策。当那发生时,会抛出一个中断,操作系统运行,然后将发出 I/O 请求的进程从阻塞态移动至就绪态,当然在那时,操作系统可以决定运行该作业。操作系统怎样对待每一个作业的?

为了更好地理解这个问题,让我们假设有两个作业,A 和 B,这两个都需要 50 毫秒的 CPU 时间。然而,它们存在明显的差异:A 先运行 10 毫秒,然后发出 I/O 请求(假设这里每次 I/O 都花费 10 毫秒)。然而 B 仅用 50 毫秒的 CPU 且没有 I/O。调度器先运行 A,之后运行 B。

假设我们正尝试建立一个 STCF 调度器。我们怎样说明调度程序将 A 分成了 510 毫秒的子任务,然而 B 就是单个的 50 毫秒的子任务?显然,仅运行一个作业,然后其他作业都没有考虑 I/O 是无意义的。

一个通常的方法是将 A 的每 10 毫秒的子任务视为一个独立的任务。因此,当这个系统开始,它的选择要么调度 10 毫秒的 A,要么调度 50 毫秒的 B。由 STCF 可确定我们的选择:选择更短的一个,即 A。然后,当 A 的第一个子任务完成的时候,仅仅剩下 B 任务,然后它开始运行。然后当 A 的一个新的子任务被提交,它会抢占 B 且运行 10 毫秒。这样做允许重叠,即在等待另一个进程的 I/O 完成时,CPU 也同时被一个进程所使用。由此这个系统被更充分的利用。

因此我们看见一个调度器是怎样包含 I/O 操作的。通过将每一次 CPU 突发视为一个作业,调度器确保“交互式”进程频繁地进行。当这些交互式作业执行 I/O 时,其他 CPU 密集型作业运行,因此更好地利用了处理器。

posted on   Black_x  阅读(264)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
历史上的今天:
2020-04-10 Python - 函数的五大参数
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示