10.3 Linux中的进程

前面的几个小节是从键盘的角度来看待Linux,也就是说以用户在xterm窗口中所见的内容来看待Linux。我们给出了常用的shell命令和标准应用程序作为例子。最后,以一个对Linux系统结构的简要概括作为结尾。现在,让我们深入到系统内核,更仔细地研究Linux系统所支持的基本概念,即进程、内存、文件系统和输入/输出。这些概念非常重要,因为系统调用(到操作系统的接口)将对这些概念进行操作。举个例子来说,Linux系统中存在着用来创建进程和线程、分配内存、打开文件以及进行输入/输出操作的系统调用。

遗憾的是,由于Linux系统的版本非常之多,各个版本之间均有不同。在这一章里,我们将摒弃着眼于某一个Linux版本的方法,转而强调各个版本的共通之处。因此,在某些小节中(特别是涉及实现方法的小节),这里讨论的内容不一定同样适用于每个Linux版本。

10.3.1 基本概念

Linux系统中主要的活动实体就是进程。Linux进程与我们在第2章所学的经典顺序进程极为相似。每个进程执行一段独立的程序并且在进程初始化的时候拥有一个独立的控制线程。换句话说,每一个进程都拥有一个独立的程序计数器,用这个程序计数器可以追踪下一条将要被执行的指令。一旦进程开始运行,Linux系统将允许它创建额外的线程。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

由于Linux是一个多道程序设计系统,因此系统中可能会有多个彼此之间相互独立的进程在同时运行。而且,每一个用户可以同时开启多个进程。因此,在一个庞大的系统里,可能有成百个甚至上千个进程在同时运行。事实上,在大多数单用户的工作站里,即使用户已经退出登录,仍然会有很多后台进程,即守护进程(daemon),在运行。在系统启动的时候,这些守护进程就已经被shell脚本开启(在英语中,“daemon”是“demon”的另一种拼写,而demon是指一个恶魔)。

计划任务(cron daemon)是一个典型的守护进程。它每分钟运行一次来检查是否有工作需要它完成。如果有工作要做,它就会将之完成,然后进入休眠状态,直到下一次检查时刻来到。

在Linux系统中,你可以把在未来几分钟、几个小时、几天甚至几个月会发生的事件列成时间表,所以这个守护进程是非常必要的。举个例子来说,假定一个用户在下周二的三点钟要去看牙医,那么他就可以在计划任务的数据库里添加一条记录,让计划任务来提醒他,比如说,在两点半的时候。接下来,当相应的时间到来的时候,计划任务意识到有工作需要它来完成,就会运行起来并且开启一个新的进程来执行提醒程序。

计划任务也可以执行一些周期性的活动,比如说在每天凌晨四点的时候进行磁盘备份,或者是提醒健忘的用户每年10月31号的时候需要为万圣节储备一些好吃的糖果。当然,系统中还存在其他的守护进程,他们接收或发送电子邮件、管理打印队列、检测内存中是否有足够的空闲页等。在Linux系统中,守护进程可以直接实现,因为它不过是与其他进程无关的另一个独立的进程而已。

在Linux系统中,进程通过非常简单的方式创建。系统调用fork将会创建一个与原始进程完全相同的进程副本。调用fork函数的进程称为父进程,新的进程称为子进程。父进程和子进程都拥有自己的私有内存映像。如果在调用fork函数之后,父进程修改了属于它的一些变量,这些变化对于子进程来说是不可见的,反之亦然。

但是,父进程和子进程可以共享已经打开的文件。也就是说,如果某一个文件在父进程调用fork函数之前就已经打开了,那么在父进程调用fork函数之后,对于父进程和子进程来说,这个文件也是打开的。如果父、子进程中任何一个进程对这个文件进行了修改,那么对于另一个进程而言,这些修改都是可见的。由于这些修改对于那些打开了这个文件的其他任何无关进程来说也是可见的,所以,在父、子进程间共享已经打开的文件以及对文件的修改彼此可见的做法也是很正常的。

事实上,父、子进程的内存映像、变量、寄存器以及其他所有的东西都是相同的,这就产生了一个问题:该如何区别这两个进程,即哪一个进程该去执行父进程的代码,哪一个进程该去执行子进程的代码呢?秘密在于fork系统调用给子进程返回一个零值,而给父进程返回一个非零值。这个非零值是子进程的进程标识符(Process Identifier,PID)。两个进程检验fork函数的返回值,并且根据返回值继续执行,如图10-4所示。

阅读 ‧ 电子书库
图 10-4 Linux中的进程创建

进程以其PID来命名。如前所述,当一个进程被创建的时候,它的父进程会得到它的PID。如果子进程希望知道它自己的PID,可以调用系统调用getpid。PID有很多用处,举个例子来说,当一个子进程结束的时候,它的父进程会得到该子进程的PID。这一点非常重要,因为一个父进程可能会有多个子进程。由于子进程还可以生成子进程,那么一个原始进程可以生成一个进程树,其中包含着子进程、孙子进程以及关系更疏远的后裔进程。

Linux系统中的进程可以通过一种消息传递的方式进行通信。在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个通道中读取字节流。这些通道称为管道(pipe)。使用管道也可以实现同步,因为如果一个进程试图从一个空的管道中读取数据,这个进程就会被挂起直到管道中有可用的数据为止。

shell中的管线就是用管道技术实现的。当shell看到类似下面的一行输入时:


sort<f|head


它会创建两个进程,分别是sort和head,同时在两个进程间建立一个管道使得sort进程的标准输出作为head进程的标准输入。这样一来,sort进程产生的输出可以直接作为head进程的输入而不必写入到一个文件当中去。如果管道满了,系统会停止运行sort进程直到head进程从管道中删除一些数据。

进程还可以通过另一种方式通信:软件中断。一个进程可以给另一个进程发送信号(signal)。进程可以告诉操作系统当信号到来时它们希望发生什么事件。相关的选择有忽略这个信号、抓取这个信号或者利用这个信号杀死某个进程(大部分情况下,这是处理信号的默认方式)。如果一个进程希望获取所有发送给它的信号,它就必须指定一个信号处理函数。当信号到达时,控制立即切换到信号处理函数。当信号处理函数结束并返回之后,控制像硬件I/O中断一样返回到陷入点处。一个进程只可以给它所在进程组中的其他进程发送信号,这个进程组包括它的父进程(以及远祖进程)、兄弟进程和子进程(以及后裔进程)。同时,一个进程可以利用系统调用给它所在的进程组中所有的成员发送信号。

信号还可以用于其他用途。比如说,如果一个进程正在进行浮点运算,但是不慎除数为0,它就会得到一个SIGFPE信号(浮点运算异常信号)。POSIX系统定义的信号详见图10-5所示。很多Linux系统会有自己添加的额外信号,但是使用了这些信号的程序一般情况下将没有办法移植到Linux的其他版本或者UNIX系统上。

阅读 ‧ 电子书库
图 10-5 POSIX定义的信号