11.2 进程的结构

我们来看看操作系统是如何管理多个进程的。如果有两个用户neil和rick,他们同时运行grep程序在不同的文件中查找不同的字符串。他们使用的进程如图11-1所示。

11.2 进程的结构 - 图1

图 11-1

如果在搜索结束之前运行ps命令,则该命令输出类似下面这样的内容:

11.2 进程的结构 - 图2

每个进程都会被分配一个唯一的数字编号,我们称之为进程标识符或PID。它通常是一个取值范围从2到32768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当数字已经回绕一圈时,新的PID重新从2开始。数字1一般是为特殊进程init保留的,init进程负责管理其他进程,我们很快就会再次谈到它。这里我们可以看到由用户neil和rick启动的两个进程被分配的PID分别是101和102。

将要被grep命令执行的程序代码被保存在一个磁盘文件中。正常情况下,Linux进程不能对用来存放程序代码的内存区域进行写操作,即程序代码是以只读方式加载到内存中的。我们从图11-1中可以看到,虽然不能对这个区域执行写操作,但它可以被多个进程安全地共享。

系统函数库也可以被共享。例如,不管有多少个正在运行的程序要调用printf函数,内存中只要有它的一份副本即可。这种做法与微软Windows操作系统中使用的动态链接库(DLL)机制类似,但更加复杂。

从上图中还可以看出,共享函数库带来的另一个优点是,包含可执行程序grep的磁盘文件容量比较小,因为它不包含共享函数库代码。这对一个单独的程序来说,算不上大优点,但对整个操作系统来说,把常用例程提取出来放入(比如说)C语言的标准函数库中将节省大量的磁盘空间。

当然,并不是程序在运行时所需要的所有东西都可以被共享。例如,进程使用的变量就与其他进程所使用的截然不同。在本例中,传递给grep程序的搜索字符串以变量s的形式出现在每个进程的数据区中。它们之间是分离的,通常不能被其他进程读取。这两个grep命令所使用的文件也各不相同,进程通过各自的文件描述符来访问文件。

除此之外,进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回。进程还有自己的环境空间,包含专门为这个进程建立的环境变量,我们在第4章介绍putenv和getenv函数时已用过这些环境变量。进程还必须维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在执行线程中的位置。我们将在下一章看到,在使用线程时,进程可以有不止一个执行线程。

在许多Linux系统(也包括一些UNIX系统)上,在目录/proc中有一组特殊的文件,这些文件的特殊之处在于它们允许你“窥视”正在运行的进程的内部情况,就好像这些进程是目录中的文件一样。我们在第3章已简单介绍过/proc文件系统了。

最后,因为Linux和UNIX一样,有一个虚拟内存系统,能够把程序代码和数据以内存页面的形式放到硬盘的一个区域中,所以Linux可以管理的进程比物理内存所能容纳的要多得多。

11.2.1 进程表

Linux进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过进程的PID对它们进行管理,这些PID是进程表的索引。进程表的长度是有限制的,所以系统能够支持的同时运行的进程数也是有限制的。早期的UNIX系统只能同时运行256个进程。最新的实现版本已大幅度放宽这一限制,可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数字限制了。

11.2.2 查看进程

ps命令可以显示我们正在运行的进程、其他用户正在运行的进程或者目前在系统上运行的所有进程。下面是ps命令的输出样本:

11.2 进程的结构 - 图3

这个命令显示了许多进程的相关信息,包括在X视窗系统中运行的Emacs编辑器。例如,TTY一列显示了进程是从哪一个终端启动的,TIME一列是进程目前为止所占用的CPU时间,CMD一列显示启动进程所使用的命令。下面我们来仔细查看其中的一些进程信息。

11.2 进程的结构 - 图4

用户的初始登录是在第4个虚拟终端完成的。该终端是这台机器的一个主控台。运行的shell程序是Linux系统的默认shell: bash。

11.2 进程的结构 - 图5

X视窗系统是由命令startx启动的。该命令是一个shell脚本,它启动X服务器并运行一些初始化X视窗系统的程序。

11.2 进程的结构 - 图6

这个进程代表着X视窗系统中一个运行着Emacs编辑器的窗口。它是由窗口管理器响应一个创建新窗口的请求而启动的。系统还分配给shell一个新的伪终端pts/0,shell可以通过该终端进行读写操作。

11.2 进程的结构 - 图7

这是由窗口管理器启动的GNOME帮助信息浏览器。

默认情况下,ps程序只显示与终端、主控台、串行口或伪终端保持连接的进程的信息。其他进程在运行时不需要通过终端与用户进行通信,它们通常都是一些系统进程,Linux用它们来管理共享的资源。我们可以用ps命令的-a选项查看所有的进程,用-f选项显示进程完整的信息。

ps命令的精确语法及其输出内容的格式随系统的不同而稍有变化。Linux使用的GNU版本的ps命令支持来自以前几个ps命令实现版本中的选项(包括来自UNIX变体BSD和AT&T中ps命令的选项),并且它还新增了一些选项。有关ps命令可使用的选项和输出格式的更多细节请参考其手册。

11.2.3 系统进程

下面显示的是运行在另一台Linux系统上的一些进程。为清楚起见,我们对输出结果进行了简化。在下面的例子中,你将看到如何查看进程的状态。ps命令输出中的STAT一列用来表明进程的当前状态。常见的STAT代码见表11-1。其中一些代码的含义将随着本章后面的介绍变得更加清晰,而另一些代码则超出了本书介绍的范围,你可以安全地忽略它们。

表 11-1

11.2 进程的结构 - 图8

11.2 进程的结构 - 图9

我们在这里看到了一个非常重要的进程。

11.2 进程的结构 - 图10

一般而言,每个进程都是由另一个我们称之为父进程的进程启动的,被父进程启动的进程叫做子进程。Linux系统启动时,它将运行一个名为init的进程,该进程是系统运行的第一个进程,它的进程号为1。你可以把init进程看作为操作系统的进程管理器,它是其他所有进程的祖先进程。我们将要看到的其他系统进程要么是由init进程启动的,要么是由被init进程启动的其他进程启动的。

用户登录的处理过程就是一个这样的例子。init进程为每个用户用来登录的串行终端或拨号调制解调器启动一次getty程序。对应的ps命令输出如下所示:

11.2 进程的结构 - 图11

getty进程等待来自终端的操作,向用户显示熟悉的登录提示符,然后把控制移交给登录程序,登录程序设置用户环境,最后启动一个shell。用户退出系统时,init进程将再次启动另一个getty进程。

启动新进程并等待它们结束的能力是整个系统的基础。我们将在本章的后面看到如何从自己的程序中用系统调用fork、exec和wait来完成同样的任务。

11.2.4 进程调度

ps命令的输出结果中还有一条对应ps命令本身的记录:

11.2 进程的结构 - 图12

这行表明进程21475处于运行状态(R),正在执行的命令是ps ax。也就是说,这个进程出现在自己的输出结果中了。这个状态指示符只表示程序已准备好运行,并不意味着它正在运行。在一台单处理器计算机上,同一时间只能有一个进程可以运行,其他进程处于等待运行状态。每个进程轮到的运行时间(我们称之为时间片)是相当短暂的,这就给人一种多个程序在同时运行的假象。状态R+只表示这个程序是一个前台任务,它不是在等待其他进程结束或等待输入输出操作完成。这就是为什么你可能会在ps命令的输出结果中看到两个这样的进程的原因(另一个常见的标记为正在运行的进程是X显示服务器)。

Linux内核用进程调度器来决定下一个时间片应该分配给哪个进程。它的判断依据是进程的优先级(我们在第4章已讨论过优先级的概念)。优先级高的进程运行得更为频繁。而其他进程,如低优先级的后台任务运行的就不是非常频繁。在Linux中,进程的运行时间不可能超过分配给它们的时间片,它们采用的是抢先式多任务处理,所以进程的挂起和继续运行无需彼此之间的协作。但早一些的系统,如微软的Windows 3.x,通常需要进程明确地退出时间片,然后其他进程才能继续运行。

在一个如Linux这样的多任务系统中,多个程序可能会竞争使用同一个资源。在这种情况下,执行短期的突发性工作并暂停运行来等待输入的程序,要比持续占用处理器来进行计算或不断轮询系统来查看是否有新的输入到达的程序要更好。我们称表现良好的程序为nice程序,而且在某种意义上,这个nice是可以被计算出来的。操作系统根据进程的nice值来决定它的优先级,一个进程的nice值默认为0并将根据这个程序的表现而不断变化。长期不间断运行的程序的优先级一般会比较低。而(例如)暂停来等待输入的程序会得到奖励。这可以帮助与用户进行交互的程序保持及时的响应性。在程序等待用户的输入时,系统会增加它的优先级,这样,当它准备继续运行时,它就会有比较高的优先级而能优先执行。我们可以用nice命令设置进程的nice值,使用renice命令调整它的值。nice命令是将进程的nice值增加10,从而降低该进程的优先级。我们可以用ps命令的-l或-f(长格式输出)选项查看正在运行的进程的nice值。我们感兴趣的值列在NI(nice)一栏,如下所示:

11.2 进程的结构 - 图13

我们看到oclock程序(进程号为1362)正在以默认的nice值运行。如果我们用下面的命令来启动它:

$ nice oclock &

它将分配到一个+10的nice值。如果用下面的命令调整这个值:

$ renice 10 1362

1362: old priority 0, new priority 10

这个时钟程序运行得就会不那么频繁了。我们可以再用ps命令查看修改过的nice值,如下所示:

11.2 进程的结构 - 图14

状态栏STAT中包含字符N表明这个进程的nice值已被修改过,已经不是默认值了。

11.2 进程的结构 - 图15

ps命令输出中的PPID栏给出的是父进程的进程ID,它是启动这个进程的进程的PID。如果原来的父进程已经不存在,该栏显示的就是init进程的进程ID(PID为1)。

Linux调度器根据进程的优先级来决定运行哪个进程。每个系统的具体实现各有不同,但高优先级的进程总是运行得更频繁。某些情况下,只要还有高优先级的进程可以运行,低优先级的进程就根本不能运行。