深入Linux内核架构-进程管理和调度(一)

现代操作系统都能够同时运行若干进程。如果系统只有一个处理器,那么在给定时刻只有一个程序可以运行。在多处理器系统上,可以真正并行运行的进程数目,取决于物理CPU的数目。

内核和处理器建立了多任务的错觉,即可以并行做几种操作,这是通过以很短的间隔在系统运行的应用程序之间不停切换而做到的。由于切换间隔如此之短,使得用户无法注意到短时间内的停滞,在感观上觉得计算机能够同时做几件事情。

这种系统管理方式引起了几个问题,内核必须解决这些问题,其中最重要的问题如下所示。

1、应用程序A的错误不能传播到应用程序B。由于Linux是一个多用户系统,它也必须确保程序不能读取或修改其他程序的内存,否则很容易访问其他用户的私有数据。

2、CPU时间必须在各种应用程序之间尽可能公平地共享,其中一些程序可能比其他程序更重要。第一个需求——存储保护。在本章中,主要讲解内核共享CPU时间的方法,以及如何在进程之间切换。这里有两个任务,其执行是相对独立的。

3、内核必须决定为各个进程分配多长时间,何时切换到下一个进程。这引出了哪个进程是下一个的问题。此类决策平台无关。

4、在内核从进程A切换到进程B时,必须确保进程B的执行环境与上一次撤销其处理器资源时完全相同。例如,处理器寄存器的内容和虚拟地址空间的结构必须与此前相同。

这里的后一项工作与处理器极度(程度极深的)相关。不能只用C语言实现,还需要汇编代码的帮助。

这两个任务是称之为调度器的内核子系统的职责。CPU时间如何分配取决于调度器策略,这与用于在各个进程之间切换的任务机制完全无关。

一、进程优先级

并不是所有进程都具有相同的重要性。除熟悉的进程优先级之外,进程还有不同的关键度类别,以满足不同需求。首先进行比较粗糙的划分,进程可分为实时进程和非实时进程。

硬实时进程有严格的限制,某些任务必须在指定的时限内完成。如果飞机的飞行控制命令通过计算机处理,则必须尽快处理发送,保证在确定的一段时间内完成。硬实时进程的关键特征是,必须在可保证的时间范围内得到处理。注意:这并不意味着所要求的时间范围特别短,而是系统必须保证决不会超过某一时间范围,即使在不大可能或条件不利的情况下也是如此。Linux不支持硬实时处理,至少在主流的内核中不支持。但有一些修改版本提供了该特性(支持硬实时)。在这些修改后的方案中,Linux内核作为独立的进程运行来处理次重要的软件,而实时的工作在内核外部完成。只有当没有实时的关键操作执行时,内核才会运行。

由于Linux是针对吞吐量优化,试图尽快处理常见情形,其实很难实现可保证的响应时间。2007年在降低内核整体延迟(指向内核发出请求到完成之间的时间间隔)方面取得进展。相关工作包括:可抢占的内核机制、实时互斥量以及完全公平的新调度器。

软实时进程是硬实时进程的一种弱化形式。尽管仍然需要快速得到结果,但稍微晚一点不会造成世界末日。软实时进程的一个例子是对CD(Compact Disc Digital Audio)的写入操作。CD写入进程接收的数据必须保持某一速率,因为数据是以连续流的形式写入介质的。如果系统负荷过高,数据流可能会暂时中断,可能导致CD不可用。不过,写入进程在需要CPU时间时应该能够得到保证。至少优先于所有其他普通进程。

大多数进程没有特定时间约束的普通进程,但仍然可根据重要性来分配优先级。

例如,冗长的编译或计算只需要极低的优先级,因为计算偶尔中断一两秒不会有什么后果。

用户不太可能注意到。相比之下,交互式应用则应该尽快响应用户命令,因为用户很容易不耐烦。

图2-1给出CPU时间分配的一个简图。进程的运行按时间片调度,分配给进程的时间片份额与其相对重要性相当。系统中时间的流动对应于圆盘的转动,CPU则由圆周旁的“扫描器”表示。最终效果是,尽管所有的进程都有机会运行,但重要的进程会比次要的得到更多的CPU时间。

这种方案称之为抢占式多任务处理,每个进程都分配到一定的时间段可以执行。时间段到期后,内核从进程收回控制权,让一个不同的进程运行,不考虑前一个进程执行的上一个任务。被抢占进程的运行时环境,即所有CPU寄存器的内容和页表,会保存起来,因此其执行结果不会丢失。在该进程恢复执行时,其进程环境可以完全恢复。时间片的长度会根据进程重要性的不同而变化。

这种简化模型没有考虑几个重要问题。例如,进程在某些时间可能因为无事可做而无法立即执行。为使CPU时间的利益汇报尽可能最大化,这样的进程决不能执行。这种情况在图2-1中看不出来,因为其中假定所有的进程都可以立即运行的。另外一个忽略的事实是Linux支持不同的调度类别(在进程之间完全公平的调度和实时调度),调度时必须考虑到这一点。此外,在有重要的进程变为就绪态可以运行时,有一种选项是抢占当前的进程,图中没有反映出这一点。

注意:进程调度在内核开发者之间引起了讨论,尤其是提到挑选最合适的算法时。为调度器的质量确立一种定量标准,即使可能,也非常困难。另外调度器要满足Linux系统上许多不同工作负荷所提出的需求,这具有挑战性。自动化控制所需的小型嵌入式系统和大型计算机的需求非常不同,而多媒体系统的需求与前两者也不同。实际上,调度器的代码近年来已经重写了两次。

(1)在2.5系列内核开发期间,O(1)调度器代替了前一个调度器。该调度器一个特别的性质是,可以在常数时间内完成其工作,不依赖于系统上运行的进程数目。该设计从根本上打破了先前使用的调度体系结构。

(2)完全公平调度器(completely fair scheduler)在内核版本2.6.23开发期间合并进来。新的代码再一次完全放弃原有的设计原则,例如,前一个调度器中为确保用户交互任务响应快速,需要许多启发式原则。该调度器的关键特性是,它试图尽可能地模仿理想情况下的公平调度。此外,它不仅可以调度单个进程,还能够处理更一般性的调度实体。例如,该调度器分配可用时间时,可以首先在不同用户之间分配,接下来在各个用户的进程之间分配。

讨论该调度器的实现细节。

在关注内核如何实现调度之前,首先来讨论进程可能拥有的状态。

二、进程生命周期

进程并不总是可以立即运行。有时它必须等待来自外部信号源、不受其控制的事件,例如在文本编辑器中等待键盘输入。在事件发生之前,进程无法运行。

调度器在进程之间切换时,必须知道系统中每个进程的状态。将CPU时间分配到无事可做的进程,显然没有意义。进程在各个状态之间的转换也同样重要。例如,如果一个进程在等待来自外设的数据,那么调度器的职责是一旦数据已经到达,则需要将进程的状态由等待改为可运行。

进程可能有以下几种状态。

就绪:

运行:该进程此刻正在执行。

等待:进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次任务切换时选择该进程。

睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。

终止:

系统将所有进程保存在一个进程表中,无论其状态是运行、睡眠或等待。但睡眠进程会特别标记出来,调度器会知道它们无法立即运行。睡眠进程会分类到若干队列中,因此它们可在适当的时间唤醒,例如在进程等待的外部事件已经发生时。

图2-2描述进程的几种状态及其转换。

对于一个排队中的可运行进程,来考察其各种可能的状态转换。该进程已经就绪,但没有运行,因为CPU分配给其他进程(因此该进程的状态是“等待”)。在调度器授予CPU时间之前,进程会一直保持该状态(等待)。在分配CPU时间之后,其状态改变为“运行”。

在调度器决定从该进程收回CPU资源时,过程状态从“运行”改变为“等待”,循环重新开始。实际上根据是否可以被信号中断,有两种“睡眠”状态。

如果进程必须等待事件,则其状态从“运行”改变为“睡眠”。但进程状态无法从“睡眠”直接改变为“运行”。在所等待的事件发生后,进程先变回到“等待”状态,然后重新回到正常循环。

在程序执行终止(例如,用户关闭应用程序)后,过程状态由“运行”变为“终止”。

上文没有列出的一个特殊的进程状态是“僵尸”状态。这样的进程已经死亡,但仍然以某种方式活着。实际上,说这些进程死了,是因为其资源(内存、与外设的连接,等等)已经释放,因此它们无法也决不会再次运行。说它们仍然活着,是因为进程表中仍然有对应的表项。

僵尸是如何产生的?其原因在于UNIX操作系统下进程创建和销毁的方式。在两种事件发生时,程序将终止运行。第一,程序必须由另一个进程或一个用户杀死(通常是通过发送SIGTERM或SIGKILL信号来完成,这等价于正常地终止进程);进程的父进程在子进程终止时必须调用或已经调用wait4系统调用。这相当于向内核证实父进程已经确认子进程的终结。该系统调用使得内核可以释放为子进程保留的资源。

只有在第一个条件发生(程序终止)而第二个条件不成立的情况下(wait4),才会出现“僵尸”状态。在进程终止之后,其数据尚未从进程表删除之前,进程总是处于“僵尸”状态。有时,僵尸进程可能稳定地寄身于进程表中,直至下一次系统重启。从进程工具(如ps或top)的输出,可看到僵尸进程。因为残余的数据在内核中占据的空间极少,所以这几乎不是问题。

三、抢占式多任务处理

Linux进程管理的结构中还需要另外两种进程状态选项:用户状态和核心态。这反映了所有现代CPU都有(至少)两种不同执行状态的事实,其中一种具有无限的权利,而另一种则受到各种限制。例如,可能禁止访问某些内存区域。这种区别是建立封闭“隔离罩”的一个重要前提,它维持着系统中现存的各个进程,防止它们与系统其他部分相互干扰。

进程通常都处于用户状态,只能访问自身的数据,无法干扰系统中的其他应用程序,甚至也不会注意到自身之外其他程序的存在。

如果进程想要访问系统数据或功能(后者管理着所有进程之间共享的资源,例如文件系统空间),则必须切换到核心态。显然这只能在受控情况下完成,否则所有建立的保护机制是多余的,而且这种访问必须经由明确定义的路径。“系统调用”是在状态之间切换的一种方法。

从用户状态切换到核心态(内核态)的第二种方法是通过中断,此时切换是自动触发的。系统调用是由用户应用程序有意调用的,中断则不同,其发生或多或少是不可预测的。处理中断的操作,通常与中断发生时执行的进程无关。例如,外部块设备向内存传输数据完毕会引发一个中断,但相关数据用于系统中运行的任何进程都是有可能的。类似地,进入系统的网络数据包也是通过中断通知的。显然,该数据包也未必是用于当前运行的进程。因此,在Linux执行中断操作时,当前运行的进程不会察觉。

内核的抢占调度模型建立了一个层次结构,用于判断哪些进程状态可以由其他状态抢占。

(1)普通进程总是可能被抢占,甚至是由其他进程抢占。在一个重要进程变为可运行时,例如编辑器接收到了等待已久的键盘输入,调度器可以决定是否立即执行该进程,即使当前进程仍然在正常运行。对于实现良好的交互行为和低系统延迟,这种抢占起到了重要作用。

(2)如果系统处于核心态并正在处理系统调用,那么系统中的其他进程是无法夺取其CPU时间的。调度器必须等到系统调用执行结束,才能选择另一个进程执行,但中断可以中止系统调用。

(3)中断可以暂停处于用户状态和核心态的进程。中断具有最高优先级,因为在中断触发后需要尽快处理。

在内核2.5开发期间,一个称之为内核抢占(kernel preemption)的选项添加到内核。该选项支持在紧急情况下切换到另一个进程,甚至当前是处于核心态执行系统调用(中断处理期间是不行的)。尽管内核会试图尽快执行系统调用,但对于依赖恒定数据流的应用程序来说,系统调用所需的时间仍然太长了。内核抢占可以减少这样的等待时间,因而保证“更平滑的”程序执行。但该特性的代价是增加内核的复杂度,因为接下来有许多数据结构需要针对并发访问进行保护,即使在单处理器系统上也是如此。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值