预计阅读本页时间:-
10.3.2 Linux中进程管理相关的系统调用
现在来关注一下Linux系统中与进程管理相关的系统调用。主要的系统调用如图10-6所示。为了开始我们的讨论,fork函数是一个很好的切入点。fork系统调用是Linux系统中创建一个新进程的主要方式,同时也被其他传统的UNIX系统所支持(在下一部分将讨论另一种创建进程的方法)。fork函数创建一个与原始进程完全相同的进程副本,包括相同的文件描述符、相同的寄存器内容和其他的所有东西。fork函数调用之后,原始进程和它的副本(即父进程和子进程)各循其路。虽然在fork函数刚刚结束调用的时候,父、子进程所拥有的全部变量都具有相同的变量值,但是由于父进程的全部地址空间已经被子进程完全复制,父、子进程中的任何一个对内存的后续操作所引起的变化将不会影响另外一个进程。fork函数的返回值,对于子进程来说,恒为0;对于父进程来说,是它所生成的子进程的PID。使用返回的PID,可以区分哪一个进程是父进程,哪一个进程是子进程。

在大多数情况下,调用fork函数之后,子进程需要执行不同于父进程的代码。以shell为例。它从终端读取一行命令,调用fork函数生成一个子进程,然后等待子进程来执行这个命令,子进程结束之后继续读取下一条命令。在等待子进程结束的过程中,父进程调用系统调用waitpid,一直等待直到子进程结束运行(如果该父进程不止拥有一个子进程,那么要一直等待直到所有的子进程全部结束运行)。waitpid系统调用有三个参数。设置第一个参数可以使调用者等待某一个特定的子进程。如果第一个参数为-1,任何一个子进程结束系统调用waitpid即可返回(比如说,第一个子进程)。第二个参数是一个用来存储子进程退出状态(正常退出、异常退出和退出值)的变量地址。第三个参数决定了如果没有子进程结束运行的话,调用者是阻塞还是返回。
仍然以shell为例,子进程必须执行用户键入的命令。子进程通过调用系统调用exec来执行用户命令,以exec函数的第一个参数命名的文件将会替换掉子进程原来的全部核心映像。图10-7展示了一个高度简化的shell(有助于理解系统调用fork,waitpid和exec的用法)。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
在大多数情况下,exec函数有三个参数:待执行文件的文件名,指向参数数组的指针和指向环境数组的指针。简单介绍一下其他的类似函数。很多库函数,如execl、execv、execle和execve,允许省略参数或者用不同的方式来指定参数。上述的所有库函数都会调用相同的底层系统调用。尽管系统调用是exec函数,但是函数库中却没有同名的库函数,所以只能使用上面提到的其他函数。
考虑在shell中输入如下命令:
cp file1 file2
用来建立一个名为file2的file1的副本。在shell调用fork函数之后,子进程定位并执行文件名为cp的可执行文件同时把需要复制的文件信息传递给它。
cp的主程序(还有很多其他的程序)包含一个函数声明:
main(argc,argv,envp)
在这里,参数argc表示命令行中包括程序名的项的数目。在上面所举的例子中,argc的值为3。
第二个参数argv是一个指向数组的指针。数组的第i项是一个指向命令行中第i个字符串的指针。在此例中,argv[0]指向字符串“cp”。以此类推,argv[1]指向五字节长度的字符串“file1”,argv[2]指向五字节长度的字符串“file2”。
main的第三个参数envp是一个指向环境的指针,这里的环境,是指一个包含若干个形如name=value赋值语句的字符串数组,这个数组将传递终端类型、主目录名等信息给程序。在图10-7中,没有要传给子进程的环境列表,所以在这里,execve函数的第三个参数是0。

如果exec函数看起来太复杂了,不要泄气,这已经是最复杂的系统调用了,剩下的要简单很多。作为一个简单的例子,我们来考虑exit函数,当进程结束运行时会调用这个函数。它有一个参数,即退出状态(从0到255),这个参数的值最后会传递给父进程调用waitpid函数的第二个参数——状态参数。状态参数的低字节部分包含着结束状态,0意味着正常结束,其他的值代表各种不同的错误。状态参数的高字节部分包含着子进程的退出状态(从0到255),其值由子进程调用的exit系统调用指定。例如,如果父进程执行如下语句:
n=waitpid(-1,&status,0);
它将一直处于挂起状态,直到有子进程结束运行。如果子进程退出时以4作为exit函数的参数,父进程将会被唤醒,同时将变量n设置为子进程的PID,变量status设置为0x0400(在C语言中,以0x作为前缀表示十六进制)。变量status的低字节与信号有关,高字节是子进程返回时调用exit函数的参数值。
如果一个进程退出但是它的父进程并没有在等待它,这个进程进入僵死状态(zombie state)。最后当父进程等待它时,这个进程才会结束。
一些与信号相关的系统调用以各种各样的方式被运用。比方说,如果一个用户偶然间命令文字编辑器显示一篇超长文档的全部内容,然后意识到这是一个误操作,这就需要采用某些方法来打断文字编辑器的工作。对于用户来说,最常用的选择是敲击某些特定的键(如DEL或者CTRL-C等),从而给文字编辑器发送一个信号。文字编辑器捕捉到这个信号,然后停止显示。
为了表明所关心的信号有哪些,进程可以调用系统调用sigaction。这个函数的第一个参数是希望捕捉的信号(如图10-5所示)。第二个参数是一个指向结构的指针,在这个结构中包括一个指向信号处理函数的指针以及一些其他的位和标志。第三个参数也是一个指向结构的指针,这个结构接收系统返回的当前正在进行的信号处理的相关信息,有可能以后这些信息需要恢复。
信号处理函数可以运行任意长的时间。尽管如此,在实践当中,通常情况下信号处理函数都非常短小精悍。当信号处理完毕之后,控制返回到断点处继续执行。
sigaction系统调用也可以用来忽略一个信号,或者恢复为一个杀死进程的缺省操作。
敲击DEL键并不是发送信号的惟一方式。系统调用kill允许一个进程给它相关的进程发送信号。选择“kill”作为这个系统调用的名字其实并不是十分贴切,因为大多数进程发送信号给别的进程只是为了信号能够被捕捉到。
对于很多实时应用程序,在一段特定的时间间隔之后,一个进程必须被打断,系统会转去做一些其他的事情,比如说在一个不可信的信道上重新发送一个可能丢失的数据包。为了处理这种情况,系统提供了alarm系统调用。这个系统调用的参数规定了一个以秒为单位的时间间隔,这个时间间隔过后,一个名为SIGALRM的信号会被发送给进程。一个进程在某一个特定的时刻只能有惟一一个未处理的警报。如果alarm系统调用首先以10秒为参数被调用,3秒钟之后,又以20秒为参数被调用,那么只会生成一个SIGALRM信号,这个信号生成在第二次调用alarm系统调用的20秒之后。第一次alarm系统调用设置的信号被第二次alarm系统调用取消了。如果alarm系统调用的参数为0,任何即将发生的警报信号都会被取消。如果没有捕捉到警报信号,将会采取默认的处理方式,收取信号的进程将会被杀死。从技术角度来讲,警报信号是可以忽略的,但是这样做毫无意义。
有些时候会发生这样的情况,在信号到来之前,进程无事可做。比如说,考虑一个用来测试阅读速度和理解能力的计算机辅助教学程序。它在屏幕上显示一些文本然后调用alarm函数于30秒后生成一个警报信号。当学生读课文的时候,程序就无事可做。它可以进入空循环而不做任何事情,但是这样一来就会浪费其他后台程序或用户急需的CPU时间。一个更好的解决办法就是使用pause系统调用,它会通知Linux系统将本进程挂起直到下一个信号到来。