深入 Linux 内核架构
作者:解琛
时间:2020 年 8 月 28 日
二、进程管理和调度
在多处理器系统中,可以真正并行运行的进程数目,取决于物理 CPU 的数目。
内核和处理器建立多任务的错觉,是通过以很短时间间隔在系统运行的应用程序之间不停切换而做到的。
这种系统系统管理方式会引起几个问题,内核必须去解决。
- 除非明确要求,否则应用程序之间不能相互干扰;
- CPU 时间必须在各个应用程序之间尽可能公平地共享;
- 内核必须决定为各个进程分配多长时间,何时切换到下一个进程;
- 在进程切换时,必须保证切换的目标程序的执行环境和上一次撤销其处理器资源时完全相同。
两个进程切换时的现场保留是 调度器 的内核子系统的职责。
CPU 时间如何分配取决于 调度器策略,这与用于在各个进程之间切换的任务切换机制完全无关。
2.1 进程优先级
并非所有的进程都具有相同的重要性。除了进程优先级这个概念之外,进程还有不同 关键度 类别。
进程可分为实时进程和非实时进程。
- 硬实时进程有严格的时间限制,任务必须在定的时间内完成;
- 软实时进程是硬实时进程的一种弱化形式;
- 大多数进程是没有特定时间约束的普通进程,可根据重要性来分配优先级。
主流的 Linux 内核不支持硬实时处理,RTLinux、Xenomai、RATI 这些修改版本提供了该特性。在这些修改方案中,Linux 内核作为独立的进程运行,来处理次重要的软件,实时的工作在内核外部完成,当没有实时的关键操作执行时,内核才会运行。
进程的运行按时间片来调度,分配给进程的时间片份额与其相对重要性相当。
这种分配方案称为 抢占式多任务处理(preemptive multitasking),各个进程都可以分配到一定的时间段可以执行。
这种简化模型没有考虑三个重要的问题。
- 进程在某些时间可能因为无事可做而无法立即执行,为了使 CPU 时间的利益回报尽可能最大化,这样的进程决不能执行;
- Linux 支持不同的调度类别,即进程之间完全公平的调度和实时调度;
- 在有重要的进程变为就绪状态可以运行时,可以抢占当前的进程。
2.2 进程的生命周期
在关注内核如何实现调度之前,先来讨论进程可能拥有的一些状态。
进程有以下几种状态。
- 运行:该进程正在执行;
- 等待:进程能够运行,但 CPU 分配给另一个进程,该进程没有得到许可。调度器可以在下一次任务切换时选择该进程;
- 睡眠:进程正在睡眠,无法运行它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。
系统将所有进程保存在一个进程表中,睡觉进程会特别标记出来,调度器会知道它们无法立即运行。睡眠进程会分类到若干队列中,可在适当的时间唤醒。
对于一个排队中的可运行进程,其可能的状态转换为:
- 该进程已经就绪,但没有运行,因为 CPU 分配给了其他进程,该进程状态为等待,在调度器授予 CPU 时间之前,进程一直保持该状态,分配时间之后,其状态改为运行(路径 4);
- 在调度器决定从该进程收回 CPU 资源时,过程状态从运行改为等待(路径 2);
- 如果进程必须等待时间,则其状态从运行改变为睡眠(路径 1),进程无法直接从睡眠改变为运行;
- 在所等待的时间发生后,进程先变回到等待状态(路径 3),然后重新回到正常循环;
- 在程序执行终止(用户关闭应用程序)后,过程状态由运行变为终止(路径 5)。
- 一个特殊的进程状态是所谓的 僵尸 状态,即该进程已经死亡,但仍然以某种方式活着,其资源已经释放,但是进程表中仍然有对应的表项。
僵尸进程产生的原因,在于 UNIX 系统中进程创建和销毁的方式。
在两种事件发生时,程序将终止运行。
- 程序必须由另一个进程或用户杀死(发送 SIGTERM 或 SIGKILL 信号来正常地终止进程);
- 进程的父进程在子进程终止时,必须调用或已经调用 wait4(wait for)系统调用,相当于向内核证实父进程已经确认子进程的终结,来使内核释放子进程保留的资源。
只有在第 1 个条件发生(程序终止)而第 2 个条件不成立时会出现僵尸状态。
在进程终止之后,其数据尚未从数据表删除之前,进程总是暂时处于僵尸状态。
如果父进程编程极其糟糕,没有发出 wait 调用,僵尸进程则稳定地存在于进程表中,直至下一次系统重启,进程表被初始化。
由于僵尸进程残存的数据在内核中占据的空间极少,所以几乎不是一个问题。
2.2.1 抢占式多任务处理
Linux 进程管理的结构中还需要另外两种进程状态选项:用户态 和 内核态。
内核态拥有无限的权利,而用户态则受到各种限制。这种区别是建立封闭隔离罩的一个重要前提,维持着系统中现存的各个进程,防止它们与系统其他部分相互干扰。
进程通常处于用户态,只能访问自身的数据,无法干扰系统中其他的用户程序,甚至不会注意到自身以外其他程序的存在。
如果进程想要访问系统数据或功能,必须切换到核心态。
从用户态切换到核心态的另一种方法是通过中断,这种切换是自动触发的。系统调用是由用户应用程序有意调用的,而中断的发生是不可预测的。
Linux 在执行中断时,当前的进程不会察觉。
内核的抢占调度模型建立了一个层次结构,用于判断哪些进程状态可以由其他状态抢占。
- 普通进程总是可能被抢占,甚至是由其他进程。对于实现良好的交互行为和低系统延迟,这种抢占起到了重要作用;
- 如果系统处于核心态并正在处理系统调用,那么系统中的其他进程无法夺取其 CPU 时间。调度器必须等待系统调用执行结束,才能选择另一个进程执行,但是中断可以终止系统调用;
- 中断可以暂停处于用户状态和核心态的进程。中断具有最高优先级。
在内核版本 2.5 开发期间,一个称为 内核抢占(kernel preemption) 的选项添加到内核。
该选项支持在紧急情况下切换到另一个进程,甚至是当前在核心态处理系统调用时也是可以的,但是中断处理期间不行。
该特性可以减少依赖恒定数据流的应用程序系统调用的等待时间,其代价是增加了内核的复杂度。