第二章:多任务处理

@高效码农  September 1, 2023

假设您正在构建一个操作系统,并且希望用户能够同时运行多个程序。不过,您没有精美的多核处理器,因此您的 CPU 一次只能运行一条指令!

幸运的是,您是一位非常聪明的操作系统开发人员。您发现可以通过让进程轮流使用 CPU 来伪造并行性。如果您循环遍历这些进程并运行每个进程中的几条指令,那么它们都可以响应,而不会出现任何单个进程占用 CPU 的情况。

但是如何从程序代码中夺回控制权来切换进程呢?经过一番研究后,您发现大多数计算机都带有定时器芯片。您可以对定时器芯片进行编程,以在经过一定时间后触发切换到操作系统中断处理程序。

硬件中断

之前,我们讨论了如何使用软件中断将控制权从用户层程序交给操作系统。这些被称为“软件”中断,因为它们是由程序自愿触发的——处理器在正常的获取-执行周期中执行的机器代码告诉它将控制权切换到内核。
2023-08-31T07:56:39.png
该图说明了硬件中断如何破坏正常执行。 顶部:一张键盘图,其中突出显示了一个按键,右侧的 CPU 上画有一个闪电。 底部:一些标记为“程序代码”的二进制文件,一个类似的闪电,还有一些标记为“内核代码”的二进制文件。 闪电标记为“中断触发上下文切换”。

操作系统调度程序使用PIT定时器芯片来触发多任务处理的硬件中断:[](https://en.wikipedia.org/wiki/Programmable_interval_timer)

  1. 在跳转到程序代码之前,操作系统设置定时器芯片在一段时间后触发中断。
  2. 操作系统切换到用户模式并跳转到程序的下一条指令。
  3. 当定时器到时,它会触发硬件中断以切换到内核模式并跳转到操作系统代码。
  4. 操作系统现在可以保存程序停止的位置,加载不同的程序,然后重复该过程。

这称为抢占式多任务处理;进程的中断称为抢占)。比如说,如果您在浏览器上阅读本文并在同一台计算机上听音乐,那么您自己的计算机可能每秒会重复这个精确的循环数千次。

时间片计算

时间是操作系统调度程序允许进程在抢占之前运行的持续时间。选择时间片的最简单方法是为每个进程提供相同的时间片(可能在 10 毫秒范围内),并按顺序循环执行任务。这称为固定时间片循环调度。

旁白:有趣的行话事实!

您知道时间片通常被称为“量子”吗?现在您做到了,您可以给所有技术朋友留下深刻的印象。我认为我应该受到很多赞扬,因为我在本文的其他句子中都没有提到量子。

说到时间片术语,Linux 内核开发人员使用jiffy时间单位来计算固定频率计时器的滴答数。除此之外,jiffies 用于测量时间片的长度。Linux 的 jiffy 频率通常为 1000 Hz,但可以在编译内核时进行配置。

固定时间片调度的一个微小改进是选择目标延迟——进程响应的理想最长时间。目标延迟是进程被抢占后恢复执行所需的时间(假设进程数量合理)。这很难想象!别担心,图表很快就会出现。

时间片的计算方法是目标延迟除以任务总数;这比固定时间片调度更好,因为它消除了用更少的进程进行浪费的任务切换。如果目标延迟为 15 毫秒并有 10 个进程,则每个进程将获得 15/10 或 1.5 毫秒的运行时间。由于只有 3 个进程,每个进程获得更长的 5 毫秒时间片,同时仍达到目标延迟。

进程切换的计算成本很高,因为它需要保存当前程序的整个状态并恢复不同的状态。过了某个点,时间片太小可能会导致进程切换太快而出现性能问题。通常给时间片持续时间一个下限(最小粒度)。这确实意味着当有足够的进程使最小粒度生效时,就会超出目标延迟。

在撰写本文时,Linux 的调度程序使用 6 毫秒的目标延迟和 0.75 毫秒的最小粒度。
2023-08-31T07:57:16.png
标题为“朴素动态时间片循环调度”的图表。 它描述了 3 个不同进程的时间序列,它们有时间在重复循环中执行。 在每个进程的执行块之间有一个短得多的块,标记为“内核调度程序”。 每个程序执行块的长度被标记为“时间片(2ms)”。 从进程 1 开始执行到进程 1 下一次开始执行的距离(包含进程 2 和 3 的执行时间)被标记为“目标延迟 (6ms)”。

使用这种基本时间片计算的循环调度接近于当今大多数计算机的做法。这还是有点天真;大多数操作系统往往有更复杂的调度程序,其中考虑了进程优先级和截止日期。从 2007 年开始,Linux 使用了一种名为Completely Fair Scheduler 的调度器。CFS 做了很多非常奇特的计算机科学事情来确定任务的优先级并分配 CPU 时间。

每次操作系统抢占一个进程时,它都需要加载新程序保存的执行上下文,包括其内存环境。这是通过告诉 CPU 使用不同的页表(从“虚拟”地址到物理地址的映射)来实现的。这也是防止程序互相访问内存的系统;我们将在本文的第 5章和第 6章中深入探讨这个兔子洞。

注意#1:内核可抢占性

到目前为止,我们只讨论了用户态进程的抢占和调度。如果内核代码处理系统调用或执行驱动程序代码花费的时间过长,则可能会使程序感觉滞后。

现代内核,包括 Linux,都是抢占式内核。这意味着它们的编程方式允许内核代码本身像用户态进程一样被中断和调度。

除非您正在编写内核或其他东西,否则了解这一点并不是很重要,但基本上我读过的每一篇文章都提到了它,所以我想我也会!额外的知识很少是坏事。

注#2:历史课

古代操作系统,包括经典的 Mac OS 和早于 NT 的 Windows 版本,都使用前身来抢占式多任务处理。程序本身会选择屈服于操作系统,而不是操作系统决定何时抢占程序。他们会触发一个软件中断说:“嘿,你现在可以让另一个程序运行了。” 这些显式的让出是操作系统重新获得控制并切换到下一个计划进程的唯一方法。

这称为协作多任务处理。它有几个主要缺陷:恶意或设计不当的程序可以轻松冻结整个操作系统,并且几乎不可能确保实时/时间敏感任务的时间一致性。由于这些原因,科技界很早以前就转向了抢占式多任务处理,并且从未回头。



评论已关闭