4 进程
在本章中,我们将讨论操作系统为用户提供的最基本的抽象之一:进程(process)。 非正式地说,进程的定义很简单:一个运行的程序[V + 65,BH70]。 程序本身是一个没有生命的东西:它只是位于在磁盘上,包括一堆指令(以及一些静态数据),等待开始行动。 操作系统读取这些字节并使其运行,将程序转换为有用的程序。 事实证明,人们经常想要同时运行多个程序;例如,考虑您可能喜欢在桌面或笔记本电脑上运行Web浏览器,邮件程序,游戏,音乐播放器等的。 事实上,典型的系统似乎可能同时运行数十甚至数百个进程。 这样做可以使系统易于使用,人们不需要关心CPU是否可用,只需要运行程序。 因此我们的挑战:
问题关键:如何提供多个CPU的错觉?
尽管这里只有很少的物理CPU可以使用,OS如何提供多个的CPU的错觉?
操作系统通过虚拟化CPU来创建这种错觉。 通过运行一个进程,然后停止它并运行另一个进程,等等,操作系统可以提供许多虚拟CPU存在的错觉,而实际上只有一个物理CPU(或几个)。 这种基本技术称为CPU的分时(time sharing),允许用户根据需要运行尽可能多的并发进程;潜在的成本是性能,因为如果共享CPU,每个进程都会运行得更慢。
为了很好的实现CPU虚拟化,OS同时需要底层的机制(mechanisms)和高层的策略(policies)。
机制是实现所需功能的低级方法或协议。 例如,我们稍后将学习如何实现一个上下文转换(context switch),它使操作系统能够停止运行一个程序并在给定的CPU上开始运行另一个程序; 所有现代操作系统都采用这种分时机制。
策略是OS做出某种决策的算法。 例如,考虑到在CPU上运行的许多可能的程序,操作系统应该运行哪个程序? 操作系统中的调度策略将做出此决定,可能使用历史信息(例如,哪个程序在最后一分钟运行得更多?),工作负载知识(例如,运行的程序类型)以及性能指标 (例如,系统是否优化交互性能或吞吐量?)来做出决定。
提示:使用分时(和空间共享(space sharing))
分时是操作系统用来共享资源的基本技术。 通过允许资源由一个实体使用一段时间,然后由另一个实体使用一段时间,资源(例如,CPU或网络链路)可以由许多人共享。 分时的对应物是空间共享,其中资源在希望使用它的人之间被(空间)划分。 例如,磁盘空间自然是空间共享资源,一旦将块分配给文件,在用户删除原始文件之前,通常不会将其分配给另一个文件。
4.1 进程
OS对运行的程序提供的抽象我们称之为进程。如上所述,一个进程就是一个正在运行的程序。在任何时刻,我们都可以通过对其在执行过程中访问或影响的系统的不同部分进行盘点,来总结一个进程。
为了理解进程的构成,我们必须了解其机器状态(machine state):程序在运行时可以读取或更新的内容。在任何给定时间,机器的哪些部分对执行该程序很重要?
包含进程的机器状态的一个明显组成部分是其内存。指令存于内存中,运行程序读写的数据也在内存中。因此,进程可以寻址的内存(称为其地址空间)是进程的一部分。
进程的机器状态的一部分是寄存器。许多指令明确地读取或更新寄存器,因此它们对于执行过程很重要。
请注意,有一些特殊的寄存器构成了这种机器状态的一部分。 例如,程序计数器(PC)(有时称为指令指针( instruction pointer,IP))告诉我们当前正在执行哪个程序指令。类似地,堆栈指针和相关的帧指针用于管理函数参数,局部变量和返回地址的堆栈。
最后,程序通常也访问持久存储设备。 此类I/O信息可能包括进程当前打开的文件列表。
4.2 进程API
虽然我们将讨论真实的进程API推迟到后续章节,但在这里我们首先要了解操作系统的任何接口中必须包含的内容。这些API以某种形式可用于任何现代操作系统。
- 创建:操作系统必须包含一些创建新流程的方法。在shell中键入命令或双击应用程序图标时,将调用操作系统创建一个新进程来运行您指定的程序。
- 销毁:由于存在用于创建进程的接口,因此系统还可以提供强制摧毁进程的接口。当然,许多进程都会在完成之后自行完成;然而,当他们不这样做时,用户可能希望杀死他们,因此停止失控进程的接口非常有用。、
- 等待:有时等待进程停止运行是有用的;因此经常提供某种等待接口。
- 杂项控制:除了杀死或等待进程之外,有时可能还有其他控制措施。例如,大多数操作系统提供某种方法来暂停进程(阻止它运行一段时间)然后恢复它(继续运行)。
- 状态:通常还有接口来获取有关进程的某些状态信息,例如运行的时间或状态。
4.3 进程的创建
程序如何转化为进程,具体来说,操作系统如何启动并运行程序?进程创建实际上如何运作?
操作系统为了运行程序必须做的第一件事是将其代码和任何静态数据(例如,初始化变量)加载(load)到进程的地址空间中。程序最初以某种可执行格式驻留在磁盘上(或者,在某些现代系统中,基于闪存的SSD);将程序和静态数据加载到内存中的过程要求操作系统从磁盘读取这些字节并将它们放在内存中(如图4.1所示)。
在早期(或简单)操作系统中,加载过程是在运行程序之前完成的,即一次完成,现代操作系统懒惰(lazily)地执行该过程,即仅在程序执行期间根据需要加载代码或数据。要真正了解代码和数据的延迟加载是如何工作的,您必须更多地了解分页((paging)和交换((swapping)机制,我们将在讨论内存虚拟化时讨论这些主题。现在只需要记住,在运行任何东西之前,操作系统显然必须做一些工作才能将程序的重要字节从磁盘放入内存。
将代码和静态数据加载到内存后,操作系统在运行该进程之前还需要执行一些其他操作。必须为程序的运行时堆栈(run-time stack)(或仅堆栈(stack))分配一些内存。正如您可能已经知道的那样,C程序将堆栈用于局部变量,函数参数和返回地址。操作系统分配此内存并将其提供给进程。操作系统也可能会使用参数初始化堆栈,具体来说,它将填充main())函数的参数,即argc和argv数组。
操作系统还可以为程序的堆(heap)分配一些内存。在C程序中,堆用于显式请求的动态分配数据,程序通过调用malloc()来请求这样的空间,并通过调用free()来明确地释放它。数据结构需要堆,例如链表,哈希表,树和其他有趣的数据结构。堆最初会很小,当程序运行并通过malloc()库API请求更多内存时,操作系统可能会参与并为进程分配更多内存以帮助满足此类调用。
操作系统还将执行一些其他初始化任务,尤其是与输入/输出(I/O)相关的任务。例如,在UNIX系统中,默认情况下每个进程都有三个打开的文件描述符,用于标准输入,输出和错误。这些描述符使程序可以轻松地从终端读取输入以及将输出打印到屏幕。我们将在本书的第三部分中详细了解有关持久性的I/O,文件描述符等。
通过将代码和静态数据加载到内存中,通过创建和初始化堆栈,以及通过执行与I/O设置相关的其他工作,操作系统现在(最终)为程序执行设置了阶段。因此它有一个最后的任务:启动在入口点运行的程序,即main()。通过跳转到main()方法(通过我们将在下一章讨论的专用机制),OS将CPU的控制权转移到新创建的进程,从而程序开始执行。
4.4 进程状态
既然我们已经知道一个进程是什么(虽然我们将继续改进这个概念),并且(大致)如何创建它,让我们谈谈一个进程在给定时间可以处于的不同状态。过程可以处于这些状态之一的概念出现在早期的计算机系统中[DV66,V + 65]。 在简化视图中,进程可以处于以下三种状态之一:
- Running:在运行状态下,进程正在处理器上运行。 这意味着它正在执行指令。
- Ready:处于就绪状态,进程已准备好运行但由于某种原因,操作系统已选择不在此时刻运行它。
- Blocked:在阻塞状态下,进程已执行某种操作,使其在其他事件发生之前无法运行。 一个常见示例:当进程向磁盘发起I / O请求时,它会被阻塞,因此其他一些进程可以使用该处理器。
如果我们将这些状态映射到图表,我们将得到图4.2中的图表。 正如您在图中看到的那样,根据操作系统的判定,进程可以在准备状态和运行状态之间移动。 从准备运行到运行意味着该过程已被调度(scheduled);从运行转移到准备就绪意味着该过程已被取消调度(descheduled)。 一旦进程被阻塞(例如,通过启动I/O操作),OS将保持这样直到某些事件发生(例如,I/O完成),此时,进程再次进入就绪状态(如果操作系统决定,可能会立即再次运行)。
让我们通过其中一些状态了解进程。 首先,假设有两个进程在运行,每个进程只使用CPU(它们没有I/O)。 在这种情况下,每个进程的状态跟踪可能如下所示(图4.3)。
在下一个示例中,第一个进程在运行一段时间后发出I/O。此时,该进程被阻止,使另一个进程有机会运行。图4.4显示了这种情况的痕迹。更具体地说,进程0启动I/O并被阻塞等待它完成;例如,从磁盘读取或等待来自网络的数据包时,进程会被阻止。操作系统识别进程0未使用CPU并开始运行进程1。在进程1运行时,I/O完成,将进程0恢复为就绪状态。最后,进程1完成,进程0运行然后完成。
请注意,即使在这个简单的示例中,操作系统也必须做出许多决定。首先,当进程0发出I/O,系统必须决定运行进程1,这样做可以通过保持CPU忙碌来提高资源利用率。其次,系统决定在其I/O完成时不切换回进程0,目前尚不清楚这是否是一个好的决定。你怎么看?这些类型的决策是由OS调度程序做出的,我们将在未来讨论几个章节。
4.5 数据结构
操作系统是一个程序,与任何程序一样,它有一些跟踪各种相关信息的关键数据结构。例如,为了跟踪每个进程的状态,操作系统可能会为所有就绪的进程保留某种进程列表,以及跟踪当前正在运行的进程的一些其他信息。操作系统还必须以某种方式跟踪被阻止的进程;当I/O事件完成时,操作系统应确保唤醒正确的进程并准备再次运行。
图4.5显示了操作系统需要跟踪xv6内核[CK + 08]中每个进程的信息类型。类似的进程结构存在于“真实”操作系统中,例如Linux,Mac OS X或Windows。查看它们,看看它们有多复杂。从图中,您可以看到操作系统跟踪过程的几个重要信息。对于已停止的进程,寄存器上下文将保持其寄存器的内容。当进程停止时,其寄存器将保存到该存储器位置。通过恢复这些寄存器(即,将它们的值放回实际的物理寄存器中),OS可以完成整个过程。在接下来的章节中,我们将更多地了解这种被称为上下文切换的技术。
/*
* the registers xv6 will save and restore
* to stop and subsequently restart a process
* 当停止和重启进程时,xv6会保存和恢复的寄存器
*/
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
/* 进程可能的状态*/
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
/*
* xv6跟踪每个进程的信息,包括寄存器上下文和状态
*/
struct proc {
char * mem; /* Start of process memory */
uint sz; /* Size of process memory */
char * kstack; /* Bottom of kernel stack */
/* for this process */
enum proc_state state; /* Process state */
int pid; /* Process ID */
struct proc * parent; /* Parent process */
void * chan; /* If non-zero, sleeping on chan */
int killed; /* If non-zero, have been killed */
struct file * ofile[NOFILE]; /* Open files */
struct inode * cwd; /* Current directory */
struct context context; /* Switch here to run process */
struct trapframe * tf; /* Trap frame for the */
/* current interrupt */
};
Figure 4.5: The xv6 Proc Structure
您还可以从图中看到,除了运行,准备和阻止之外,还有一些其他状态可以进入。 有些系统为刚创建的进程提供初始状态。 此外,可以将进程置于已退出但尚未清除的最终状态(在基于UNIX的系统中,这称为僵尸(zombie)状态)。 这个最终状态非常有用,因为它允许其他进程(通常是创建进程的父进程)检查进程的返回值并查看刚刚完成的进程是否成功执行(通常,基于UNIX的系统中,程序成功执行返回0,否则为非0)。 完成后,父进程将进行最后一次调用(例如,wait())以等待子进程完成,并向操作系统表明它可以清理任何涉及现已死去进程的相关数据结构。
旁注:数据结构 - 进程列表
操作系统充满了各种重要的数据结构,我们将在这些注释中讨论。 进程列表是第一个这样的结构。 它是一个比较简单的结构,任何能够同时运行多个程序的操作系统都会有类似于这种结构的东西,以便跟踪系统中所有正在运行的程序。 有时人们会将存储过程信息的个体结构称为进程控制块(Process Control Block,PCB)。
4.6 总结
我们已经介绍了操作系统的最基本的抽象:进程。 它被简单地视为一个正在运行的程序。 考虑到这种概念性观点,我们现在将继续讨论实质性过程:实现进程所需的低层机制,以及以智能方式调度进程所需的高层政策。 通过结合机制和策略,我们将建立对操作系统如何虚拟化CPU的理解。
原文地址:https://www.cnblogs.com/redreampt/p/9410850.html