- 论坛徽章:
- 0
|
第八章〓进程控制?
8?1〓引言?
本章介绍Unix的进程控制,包括创建新进程、执行程序和进程终止。我们也说明进
程的各种
ID〖CD2〗实际、有效和保存的用户和组ID,以及它们如何受到进程控制原语的影
响。本章
也包括了解释器文件和system函数。本章以大多数Unix系统所提供的进程会计机制
结束。这
使我们从一个不同角度了解进程控制功能。?
8?2〓进程标识?
每个进程都有一个非负整型的唯一进程ID。因为进程ID标识符总是唯一的,常将其
用作为其
它标识符的一部分以保证其唯一性。在5?13节中的tmpnam函数将进程ID作为名字
的一部分
创建一个唯一的路径名。?
有某些专用的进程:进程ID0是调度进程,常常被称为交换进程(swapper)。该进程
并不执行
任何磁盘上的程序。〖CD2〗它是系统核的一部分,因此也被称为系统进程。进程
ID1通常是
init进程,在自举过程结束时由系统核调用。该进程的程序文件在Unix的较早版本
中是/etc
/init,在版新版本中是/sbin/init。此进程负责在系统核自举后起动一个Unix系
统。init
通常读与系统有关的初始化文件(/etc/rc*文件),并将系统引导到一个状态(例如
多用户)。
init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是一个在
系统核内
的系统进程),但是它以超级用户特权运行。在本章稍后部分会说明init如何成为
所有孤儿
进程的父进程。?
在某些Unix的虚存实现中,进程ID2是页精灵进程(pagedaemon)。此进程负责支持
虚存系统
的请页操作。?
除了进程ID,每个进程还有一些其它标识符。下列函数返回这些标识符。?
#include <sys/types?h>;?
#include <unistd?h>;?
pid 迹茫模*常病絫 getpid(voide;〓〓〖CD2〗调用进程的进程ID?
pid 迹茫模*常病絫 getppid(void);〓〓〖CD2〗调用进程的父进程ID?
uid 迹茫模*常病絫 getuid(void);〓〓〖CD2〗调用进程的实际用户ID?
uid 迹茫模*常病絫 geteuid(void);〓返回:〓〓〖CD2〗调用进程的有效用户I
D?
gid 迹茫模*常病絫 getgid(void);〓〓〖CD2〗调用进程的实际组ID?
gid 迹茫模*常病絫 getegid(void);〓〓〖CD2〗调用进程的有效组ID?
注意,这些函数都没有出错返回,在下一章中讨论fork函数时,将进一步讨论父进
程ID。在
4?4节中已讨论了实际和有效用户及组ID。?
8?3〓fork函数?
一个现存进程调用fork函数是Unix核创建一个新进程的唯一方法。(这并不适用于
前节提及
的交换进程、init进程和页精灵进程。这些进程是由系统核作为自举过程的一部分
以特殊方
式创建的。)?
#include <sys/types?h>;?
#include <unistd?h>;?
pid 迹茫模*常病絫 fork(void);?
Returns:0 in child,process ID of child in parent,-1 on error〓返回:子进
程中为0
,父进程中为子进程ID,出错为0?
由fork创建的新进程被称为子进程。该函数被调用一次,但返回二次。两次返回的
区别是子
进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给
父进程的
理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以
获得其所
存子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父
进程,所
以子进程总是可以调用getppid以获得其父进程的进程ID。(进程ID0总是由交换进
程使用,
所以一个子进程的进程ID不可能为0。)?
子进程和父进程继续执行fork之后的指令。子进程是父进程的复制器。例如,子进
程就得父
进程数据空间、堆和栈的复制器。注意,这是子进程所拥用的拷贝。父、子进程并
不共享这
些存储空间部分。如果正文段是只读的,则父、子进程共享正文段(7?6节)。?
很多现在的实现并不做一个父进程数据的栈和堆的完全拷贝,因为在fork之后经常
跟随着ex
ec。作为替代,使用了在写时复制(COW)的技术。这些区域由父、子进程共享,而
且系统核
将它们的存取权改变为只读的。如果有进程试图修改这些区域,则系统核为有关部
分,典型
的是虚存系统中的"页",作一个拷贝。Bach[1986]的9?2节和Lefflen等[
198
9]的5?7节对这种特征作了更详细的说明。?
实例?
程序8?1例示了Fork函数。如果执行此程序则得到:?
$ a?out?
a write to stdout?
before fork?
pid=430,glob=7,var=89〓〓子进程的变量值改变了?
pid=429,glob=6,var=88〓〓父进程的变量值没有改变?
$ a?out>;temp?out?
$ cat temp?out?
a write to stdout?
before fork?
pid=432,glob=7,var=89?
before fork?
pid=431,glob=6,var=88?
一般,在fork之后里先进程先执行,还是子进程先执行是不确定的。这取决于系统
核所使用
的调度算法。如果要求文、字进程之间相互同步,则要求某种形式的进程间通信。
在程序8
?1中,父进程体自己腔眠2秒钟,以此该子进程先执行。但并不保证2秒钟已经足
够,在8?
8市说明竟学条件时,我们还够深及这一问题及其它类型的同步方法。在10?6节口
,在fork之后我们将用信号体、父、子进程同步。?
注意,程序8?1中fork与I/O函数之间的关系。回忆第三章中所述,Wrik函数是不
带缓存的。国灰在fork之间调用Wrir后,所以具数据写到标准输出上一次。但是,
标准I/O库是带缓存的。回忆一下第5?12节,如果标准输出连到终设备,则它是
行缓冲的,否则它是冷缓冲的。当以交互方式运行该程序时,我们只课到printf
输出的行一次,具原因是标准输出缓存收新行符刷新。但是当收标准输出重新定
向到一个文件时,我们却得到printf输出行两次时,该行数据仍在缓冲中,然后
在父进程数据空间复制到子进程中时该缓存数据也被复制到子进程中。
于是那时父、子进程各自有了带该行内容的缓冲。在exit之前的第二个printf将
其数据添加到现存的缓存中。当每个进程终止时,其缓存中的内容被写到相应文件
中。??
P190??
程序8?1〓fork函数的实例?
文件共享?
对程序8?1需注意的另一点是:在重新定向父进程的标准输出时,子进程的标准输
出也被重
新定向。确定,fork的一个特性是所有由父进程打开的描述符都复制到子进程中。
父、子进
程每个相同的打开描述符共享一个文件表项。(回忆图3?4)。?
考虑下述情况,一个进程打开了三个不同文件,它们是:标准输入、标准输出和标
准出错。
在从fork返回时,我们有了如图8?1中所示的安排。??
P191??
图8?1〓fork之后父子、进程之间对打开文件的共享?
这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。考虑下述情
况:一个
进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父
、子进程
都向标准输出执行写操作。如果父进程使其标准输出重新定向(很可能是由shell实
现的),
那么子进程写到该标准输出时,它将更新与父进程共享的该文件的位移量。在我们
所考虑的
例子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止后,父进
程也写到
标准输出上,并且知道其输出会添加在子进程所写数据之后。如果父、子进程不共
享同一文
件位移量,这种形式的交互作用就很难实现。?
如果父、子进程写到同一描述符文件,但又没有任何形式的同步(例如使父进程等
待子进程)
,那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。虽然这
种情况是
可能发生的(见程序8?1),但这并不是常用的操作方式。?
有两种常见的在fork之后处理文件描述符的情况:?
1?父进程等待子进程完成。在这种情况下,父进程无需对其描述符作任何处理。
当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量也作了
相应更新。?
2?父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程
各自关闭
它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络
服务进程
常常使用的。?
除了打开文件之外,很多父进程的其它性质也由子进程继承:?
·实际用户ID、实际组ID、有效用户ID、有效组ID?
·添加组ID?
·进程组ID?
·对话期ID?
·控制终端?
·设置-用户-ID标志和设置-组-ID标志?
·当前工作目录?
·根目录?
·文件方式创建屏蔽字?
·信号屏蔽和排列?
·对任一打开文件描述符的在执行时关闭标志?
·环境?
·连接的共享存储段?
·资源限制?
父、子进程之间的区别是:?
· fork的返回值?
·进程ID?
·不同的父进程ID?
·子进程的tms 迹茫模*常病絬time,tms 迹茫模*常病絪time,tms 迹茫模*常?
〗cutime
以及tms 迹茫模*常病絬stime设置为0。?
·父进程设置的锁,子进程不继承?
·子进程的末决告警被清除?
·子进程的末决信号集设置为空集?
其中很多特性至今尚末讨论过,我们将在以后几章中对它们进行说明。?
使fork失败的两个主要原因是:(a)系统中已经有了太多的进程(通常意味着某个方
面出了
问题),或者(b)该实际用户ID的进程总数超过了系统限制。回忆图2?7,其中CHI
LD CD
*常病組AX规定了每个实际用户ID在任一时刻可具有的最大进程数。?
fork有两种用法:?
1?一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这对网络服
务进程是
常见的〖CD2〗父进程等待委托者的服务请求。当这种请求到达时,父进程调用fo
rk,使子
进程处理此请求。父进程则继续等待下一个服务请求。?
2?一个进程要执行一道不同的程序。这对shell是常见的情况。在这种情况下,子
进程在从
fork返回后立即调用exec(我们将在8?9节说明exec)。?
某些操作系统将2中的两个操作(fork之后执行exec)组合成一个,并称其为spawn。
Unix将这
两个操作分开,因为在很多场合需要单独使用fork,它后面并不跟随exec。另外,
将这两个
操作分开,使得子进程在fork和exec之间可以更改自己的属性。例如I/O重新定向
、用户ID
、信号排列等。在第十四章中有很多这方面的例子。?
8?4〓vfork函数?
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。?
vfork起源于较早的4BSD虚存版本。在Leffler et al [1989]的5?7节中指出"虽
然它是
特
别有效率的,但是vfork的语义很奇特,通常认为它具有结构上的缺陷"。尽管如此
SVR4和4
?3+BSD仍支持vfork。?
某些系统具有头文件<vfork?h>;,当调用vfork时,应当包括该头文件。?
vfork用于创建一个新进程,而该新进程的目的是exec一道新程序(为上节2中一样
)。程序1
?5中的shell基本部分就是这种类型程序的一个例子。vfork与fork一样都创建一
个子进程
,但是它并不将父进程的地址空间完全复制到子进程中,其设想是子进程会立即调
用exec(
或exit),于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它
在父进程
的空间中运行。这种工作方式在某些Unix的页式虚存实现中提高了效率(与我们上
节中提及
的,在fork之后跟随exec,并采用在写时复制技术相类似)。?
vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exi
t之后父进
程再可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步
动作,则
会导致死锁。)?
实际?
在程序8?1中使用vfork代替fork,并作其它相应修改得到程序8?2。?
?
P194??
程序8?2〓vfork函数的实例?
运行该程序得到:?
$ a?out?
befork vfork?
pid=607,glob=7,var=89?
子进程对变量glob和var作增1操作,结果改变了父进程中的变量值。因为子进程在
父进程的
地址空间中运行,所以这并不令人鹜讶。但是其作用的确与fork不同。?
注意,在程序8?2中,调用了 迹茫模*常病絜xit而不是exit。正如8?5节所述,
迹茫模?
?2〗exit并不执行标准I/O缓存的刷新操作。如果用exit而不是 迹茫模*常病?
exit,则
该程序的输出是:?
$ a?out?
before vfork?
从中可见,父进程printf的输出消失了。其原因是子进程调用了exit,它刷新开关
闭了所有
标准I/O流。这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址
空间中进
行的,所以所有受到影响的标准I/O FILE对象都是在父进程中。当父进程调用pri
ntf时,标
准输出已被关闭了,于是printf返回-1。?
Leffler et al?[1989]的5?7节中包含了fork和vfork实现方面的更多信息
。练习8
?1和8?2则继续了对vfork的讨论。?
8?5〓exit函数?
如同在7?3节中所述,进程有三种正常终止法,有两种异常终止法。?
1?正常终止?
(a)在main函数内执行return语句。如在73节中所述,这等效于调用exit。?
(b)调用exit函数。此函数由ANSIC定义,其操作包括了调用各终止处理程序(终止
处理程序
是在调用atexit函数时登录的),然后关闭所有标准I/O流等。因为ANSIC并不处理
文件描述
符、多进程(父、子进程)以及作业控制,所以这一定义对Unix系统而言是不完整的
。?
(c)调用-exit系统调用函数。此函数由exit调用,它处理Unix特定的细节。-exit
是由POSIX
?1说明的。?
2?异常终止:?
(a)调用abort。它产业SIGABRT信号,所以是下一种异常终止的一种特例。?
(b)当进程接收到某个信号时。(第十章将较详细地说明信号)。进程本身(例如调用
abort函
数)、其它进程和系统核都能产生传送到某一进程的信号。例如,进程越出其地址
空间访问
存储单元,或者除以0,系统核就会为该进程产生相应的信号。?
不管进程是如何终止的,最后都会执行系统核中的同一段代码。这段代码为相应进
程关闭所
有打开描述符,释放它所使用的存储器等等。?
对上述任何一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的
。对于ex
it和-exit,这是依靠传递给空们的退出状态参数来实现的。在异常终止情况,系
统核(不是
进程本身)产生一个指示其异常终止原因的终止状态。在任一种情况下,该终止进
程的父进
程都能用wait或waitpid函数(在下一节说明)取得其终止状态。?
注意,我们在这里使用了"退出状态"(它是传向exit或-exit的参数,或main的返回
值)和
"终止状态"两个术语,以表示有所区别。在最后调用-exit时,系统核将其退出状
态转换
成终止状态(请回忆图7?1)。图8?2说明了父进程检查子进程的终止状态的不同方
法。如果
子进程正常终止,则父进程可以获得子进程的退出状态。?
在说明fcrk函数时,一定是一个父进程生成一个子进程。上面又说明了子进程将其
终止状态
返回给父进程。但是如何父进程在子进程之前终止,则将如何呢?其回答是对于其
父进程已
经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程
领养。其
操作过程大致是:在一个进程终止时,系统核逐个检查所有活动进程,以判断它是
否是正要
终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。
这种处理
方法保证了每个进程有一个父进程。?
另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在作
相应检查
时得到子进程的终止状态呢?对此问题的回答是系统核为每个终止子进程保存了一
定量的信
息,所以当终止进程的父进程调用wait或waitpid时,可以得到有关信息。这种信
息至少包
括进程ID、该进程的终止状态、以反该进程使用的CPU时间总量。系统核可以释放
终止进程
所使用的所有存储器,关闭其所有打开文件。在Unix术语中,一个已经终止,但是
其父进程
尚末对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程
被称为僵
死进程。PS(1)命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它
fork了很
多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程就会变成
僵死进程
。?
系统V提供了一种避免僵死进程的非标准化方法,这将在10?7中介绍。?
最后一个要考虑的问题是:一个由init进程领养的进程终止时会发生什么?它会不
会变成一
个僵死进程?对此问题的回答是"否",因为init被编写成只要有一个子进程终止,
init就
会调用一个wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。
当提及"
一个init的子进程"时,这指的是init直接产生的进程(例如,将在9?2节说明的g
etty进程
),或者是其父进程已终止,由init收养的进程。?
8?6〓wait和waitpid函数?
当一个进程正常或异常终止时,系统核就向其父进程发送SIGCHLD信号。因为子进
程终止是
个异步文件(这可以在父进程运行的任何时候发生),所以这种信号也是系统核向父
进程发的
异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函
数(信号
处理程序)。对于这种信号的系统默认动作是忽略它。在第十章将说明这些选择项
。现在需
要知道的是调用wait或waitpid的进程可能会:?
·阻塞(如果其所有子进程都还在运行),或者?
·带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终
止状态),
或者?
·出错立即返回(如果它没有任何子进程)。?
如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如
果在一个
任意时刻调用wait,则进程可能会阻塞。?
#include <sys/types?h>;?
#include <sys/wait?h>;?
pid 迹茫模*常病絫 wait(int *?statloc);??
pid 迹茫模*常病絫 waitpid(pid 迹茫模*常病絫 ?pid,?int ??statloc,?
int ?op
tions);??
Both return:process ID if OK,0(see later),or -1 on error〓两个函数返回:
若成功为
进程ID,出错为-1?
这两个函数的区别是:?
·在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选择项,它可使用
调用者不
阻塞。?
·waitpid并不等待第一个终止的子进程〖CD2〗它有若干个选择项,可以控制它所
等待的进
程。?
如果一个子进程已经终止,是一个僵死进程,则wait立即返回并取得该子进程的状
态,否则
wait使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则
在其一个
子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID,所以它总能
了解是那
一个子进程终止了。?
这两个函数的参数staloc是一个整型指针。如果staloc子是一个空指针,则终止进
程的终止
状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指
针。?
依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状
态(正常
返回),其它位则指示信号编号(异常返回),有一位指示是否产生了一个core文件
等等。POS
IX?1规定终止状态用定义在<sys/wait?h>;中的各个宏来查看。有三个互斥的宏可
用来取得
进程终止的原因,它们的名字都以WIF开始。基于这三个宏中哪一个值是真,就可
选用其它
宏来取得终止状态,信号编号等。这些都示于图8?2。在8?9节中讨论作业控制时
,将说明
如何停止一个进程。??
P198??
图8?2〓检查wait和waitpid所返回的终止状态的宏?
实例?
程序8?3中的函数pr-exit使用图8?2中的宏以打印进程的终止状态。在本章的很
多程序中
都将调用此函数。注意,如果定义了WCOREDUMP,则此函数也处理该宏。?
程序8?4调用pr-exit函数,例示终止状态的不同值。运行程序8?4可得:?
$ a?out?
normal termination,exit status=7?
abnormal termination,signal number=6(core file generated)?
abnormal termination,signal number=8(core file generated)??
P199?
程序8?3〓打印exit状态的说明??
P200?
程序8?4〓例示不同的exit值?
不幸的是,没有一种可移植的方法将WTERMSIG得到的信号编号映照为说明性的名字
。(10?2
1节中说明了一种方法。)我们必须查看<signal?h>;头文件才能知道SIGABRT的值是
6,SIGFP
E的值是8。?
正如前面已提到的,如果一个进程有几个子进程,那么只要有一个子进程终止,w
ait就返回
。如果要等待一个指定的进程终止(如果知道要等待进程的ID),那么该如何做呢?
在较早的U
nix版本中,必须调用wait,然后将其返回的进程ID和所期望的进程ID相比较。如
果终止进
程不是所期望的,则将该进程ID和终止状态保存起来,然后再次调用wait。反复这
样做直到
所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若
其中已有
要等待的进程,则取有关信息,否则调用wait。其实,我们需要的是等待一个特定
进程的函
数。POSIX?1定义了waitpid函数以提供这种功能(以及其它一些功能)。?
waitpid函数是新由POSIX?1定义的。SVR4和4?3+BSD都提供此函数,但早期的系
统V和4?3
BSD并不提供此函数。??
P200??
图8?3〓waitpid的选择项常数?
对于waitpid的pid参数的解释与其值有关:?
pid==-1〓等待任一子进程。于是在这一功能方面waitpid与wait等效。?
pid>;0〓等待其进程ID与pid相等的子进程。?
pid==0〓等待其组ID等于调用进程的组ID的任一子进程。?
pid<-1〓等待其组ID等于pid的绝对值的任一子进程。?
(在9?4节说明进程组)。waitpid返回该终止子进程的进程ID,而该子进程的终止
状态则通
过statloc返回。对于wait,其唯一的出错是调用进程没有子进程。(在此函数调用
由一个信
号中断时,也可能返回另一种出错。第十章将对此进程讨论。)但是对于waitpid,
如果指定
的进程或进程组不存在,或者调用进程没有子进程都能出错。?
options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是图8?3
中常数的
按位或。?
SVR4支持两个附加的非标准的options常数。WNOWAIT使系统将其终止状态已由wai
tpid返回
的进程保持在等待状态,于是该进程就可被再次等待。对于WCONTINUED,返回由p
id指定的
某一子进程的状态,该子进程已被继续,其状态尚末报告过。?
waitpid函数提供了wait函数没有提供的三个功能:?
1? waitpid等待一个特定的进程(而wait则返回任一终止子进程的状态)。在讨论
popen函数
时会再说明这一功能。?
2? waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但
不想阻塞
。?
3? waitpiol支持作业控制(以WUNTRACED选择项)。?
实例?
回忆一下8?5节中有关僵死进程的讨论。如果一个进程要fork一个子进程,但不要
求它等待
子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的决巧
是调用fo
rk两次。程序8?5实现了这一点。?
在第二个子进程中调用sleep以保证在打印父进程ID时第一个子进程已终止。在fo
rk之后,
父、子进程都可继续执行〖CD2〗我们无法予知那一个会先执行。如果不使第二个
子进程睡
眠,则在fork之后,它可能比其父进程先执行,于是它打印的父进程ID会是它的父
进程,而
不是init进程(进程ID1)。?
?
P202??
程序8?5〓fork两次以避免僵死进程?
执行程序8?5得到:?
$ a?out?
$ second child,parent pid=1?
注意,当原先的进程(也就是exec本程序的进程)终止时,shell打印其指示符,这
在第二个
子进程打印其父进程ID之前。?
8?7〓wait3和wait4函数?
4?3+BSD提供了两个附加函数wait3和wait4。这两个函数提供的功能比POSIX?1函
数wait和
waitpid所提供的分别要多一个,它与附加参数rusage有关。该参数要求系统核返
回由终止
进程及其所有子进程使用的资源摘要。?
#include <sys/types?h>;?
#include <sys/wait?h>;?
#include <sys/time?h>;?
#include <sys/resource?h>;?
pid 迹茫模*常病絫 wait3(int ?statloc?,int ?options?,struct rusage
*?rusae)
;??
pid 迹茫模*常病絫 wait4(pid 迹茫模*常病絫 ?pid,?int *?statloc,?in
t ?optio
ns,?struct rusage *?rusage)?;?
Both return:process ID if OK,0,or -1 on error〓两个函数返回:若成功为进
程ID,出
错为-1?
SVR4在其BSD兼容库中也提供了wait3函数。?
资源信息包括用户CPU时间总量,系统CPU时间总量,缺页次数,接收到信号的次数
等。有关
细节请参阅getrusage(2)手册页。这些资源信息只包括终止子进程,并不包括处于
停止状态
的子进程。(这种资源信息与7?11节中所述的资源限制不同。)图8?4中列出了各
个wait函
数所支持的不同的参数。??
P203??
图8?4〓在不同系统上各个wait函数所支持的参数?
8?8〓竟态条件?
从本书的目的出发,当多个进程都企图对共享数据进行某种处理,而最后的结果又
取决于进
程运行的顺序时,则我们认为这发生了竟态条件。如果在fork之后的某种逻辑显式
或隐式地
依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竟态条
件活跃的
孳生地。通常,我们不能予料哪一个进程先运行。即使知道那一个进程先运行,那
么在该进
程开始运行后,所发生的事情也依赖于系统负载以及系统核的调度算法。?
在程序8?5中,当第二个子进程打印其父进程ID时,我们看到了一个潜在的竟态条
件。如果
第二个子进程在第一个子进程之前运行,则其父进程将会是第一个子进程。但是,
如果第一
个子进程先运行,并有足够的时间到达并执行exit,则第二个子进程的父进程就是
init。即
使在程序中调用sleep,这也不保证什么。如果系统负担很重,那么在第二个子进
程从sleep
返回时,可能第一个子进程还没有得到机会运行。这种形式的问题很难排除,因为
在大部分
时间,这种问题并不出现。?
如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要
等待其父
进程终止(如程序8?5中一样),则可使用下列形式的循环:?
while(getppid() !=1)?
sleep(1);?
这种形式循环(称为定期询问)的问题是它浪费了CPU时间,因为调用者每隔1秒都被
唤醒,然
后进行条件测试。?
为了避免竟态条件和定期询问,在多个进程之间需要有某种形式的信号机制。在U
nix中可以
使用信号机制,在10?16节将说明它的一种用法。各种形式的进程间通信(IPC)也
可使用,
在第十四、十五章将对此进行讨论。?
在父、子进程关系中,常常有下述景况。在fork之后,父、子进程都有一些子情要
做。例如
,父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程
创建一个
文件。在这一例子中,我们要求每个进程在执行完它的一套初始化操作后要通知对
方,并且
在继续运行之前,要等待另一方完成其初始化操作。这种景况可以描述如下:?
#include "ourhdr?h"?
TELL 迹茫模*常病絎AIT(); /* set things up for TELL 迹茫模*常病絰xx
& WAIT〖
茫模*常病絰xx */?
if((pid=fork())<0)?
err 迹茫模*常病絪ys("fork error" ;?
else if (pid==0){〓〓/* 子进程?
/? 子进程执行所需的各操作 */?
TELL 迹茫模*常病絇ARENT(getppid());〓/? tell parent we're done */通知
父进程已
执行完毕?
WAIT 迹茫模*常病絇ARENT();〓/* and wait for parent */等待父进程?
/* and the child continues on its way ··· */ 〓子进程继续运行?
}?
/* 〓父进程执行所需的各操作 */?
TELL 迹茫模*常病紺HILD(pid);〓/* tell child we're done */ 通知子进程已
执行完毕
?
WAIT 迹茫模*常病紺HILD();〓/* and wait for child */ 等待子进程?
/* and the parent continues on its way ··· */ 父进程继续运行?
exit(0);?
假定在头文件ourbdr?h中定义了各个需要使用的变量。五个例程TELL 迹茫模*?
2〗WAIT
、TELL 迹茫模*常病絇ARENT、TELL 迹茫模*常病紺HILD、WAIT 迹茫模*常病?
PAREN以及
wAIT 迹茫模*常病紺HILD可以是宏,也可以是函数。?
在后面的一些章中会说明以不同的方法实现这些TELL和WAIT例程:10?16节中说明
用信号的
一种实现,程序14?3中说明用流管道的一种实现。下面先看一个使用这五个例程
的实例。
?
实例?
程序8?6输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖
于系统核
使进程运行的顺序及每个进程运行的时间长度,所以该程序包含了一个竟态条件。
??
P205??
程序8?6〓具有竟态条件的程序?
在程序中将标准输出设置为不带缓存的,于是每个字符输出都需调用一次write。
本例的目
的是使系统核能尽可能多次地在两个进程之间进行切换,以例示竟态条件。(如果
不这样做
,可能也就决不会见到下面所示的输出。没有看到具有错误的输出并不意味着竟态
条件不存
在,这只是意味着在此特定的系统上未能见到它。)下面的实际输出说明该程序的
运行结果
是会改变的。?
$ a?out?
output from child?
output from parent?
$ a?out?
oouuttppuutt ffrroomm cphairledn?
t?
$ a?out?
oouuttppuutt ffrroomm pcahrielndt?
$ a?out?
ooutput from parent?
utput from child?
修改程序8?6,使其使用TELL和WAIT函数,于是形成了程序8?7。在行首标以'+号
的行是
新增加的行。??
P206??
程序8?7〓修改程序8?6以避免竟态条件?
运行此程序则能得到所予期的输出〖CD2〗两个进程的输出不再交叉混合。?
程序8?7是使父进程先运行。如果将fork之后的行改变成:?
else if (pid==0) {?
charatatime("output from child\n" ;?
TELL 迹茫模*常病絇ARENT(getppid());?
} else {?
WAIT 迹茫模*常病紺HILD();〓〓/* child goes first */ 子进程先运行?
charatatime("output from parent\n" ;?
}?
则子进程先运行。练习8?3继续这一实例。?
8?9〓exec函数?
在8?3节曾提及用fork函数创建子进程后,子进程往往要调用一种exec函数以执行
另一道程
序。当一个进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其
main函数
开始执行。因为调用exec并不创建新进程,所以在其前后的进程ID并末改变。exe
c只是用盘
上另一道新程序代换了当前进程的正文、数据、堆和栈段。?
有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是
Unix进程
控制原语。用fork可以创建新进程,用exec可以执行新的程序。exit函数和两个w
ait函数处
理终止和等待终止。这些是我们需要的基本的进程控制原语。在后面各节中将使用
这些原语
构造另外一些如popen和system之类的函数。?
#include <unistd?h>;?
int execl(const char *?pathname,?const char *?arg0,???? /* (char
*) 0 */)
;?
int execv(const char *?pathname,?char *const ?argv?[]);?
int execle(const char *?pathname,?const char *?arg0,?????
/* (char *)0,char *const ?envp?[] */);?
int execve(const char *?pathname,?char *const ?argv?[],char *cons
t ?envp
[]);??
int execlp(const char *?filename,?const char *?arg0,???? /* (cha
r *) 0 */
);?
int execvp(const char *?filename,?char *const ?argv[]);??
All six return:-1 on error,no return on success?
六个函数都返回:出错为-1,成功不返回?
这些函数之间的第一个区别是前四个取路径名作为参数,后两个则取文件名作为参
数。当指
定文件名作为参数时:?
·如果文件名中包含'/',则就将其视为路径名。?
·否则就按PATH环境变量,在有关目录中搜寻可执行文件。?
PATH变量包含了一张目录表(称为路径前缀),目录之间用冒号(':')分隔。例如下
列name
=value环境字符串:?
PATH=/bin:/usr/bin:/usr/local/bin:。?
指定在四个目录中进行搜索。(一个零长前缀地表示当前目录。在value的开始处可
用:表示
它,在行中间则要用::表示,在行尾以:表示它。)?
有很多出于安全性方面的考虑,要求在搜索路径中决不要包括当前目录。请参见G
arfinkel
and Spafford[1991]。?
如果execlp和execup这两个函数中的任一个使用路径前缀中的一个找到了一个可执
行文件,
但是该文件不是由连接编辑程序产生的机器可执行的代码文件,则就认为该文件是
一个shel
l脚本,于是试着调用/bin/sh,并以该文件名作为shell的输入。?
第二个区别与参数表的传递有关(l表示表(list),v表示矢量(vector))。函数exe
cl、execl
p和execle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表
以空指针
结尾。对于另外三个函数(execv,execvp和execve),则应先构造一个指向各参数的
指针数组
,然后将该数组地址作为这三个函数的参数。?
在使用ANSIC原型之前,对execl,execle和execlp三个函数表示命令行参数的一般
方法是:
char *arg0,char *arg1,???,char *argn,(char *) 0?
应当特别指出的是:在最后一个命令行参数之后跟了一个空指针。如果用常数0来
表示一个
空指针,则必须将它强制转换为一个字符指针,否则它将被解释为整型参数。如果
一个整型
数的长度与char*的长度不同,exec函数实际参数就将出错。?
最后一个区别与向新程序传递环境表相关。名字以e结尾的两个函数(execle和exe
cve)使我
们可以向其传递一个指向环境字符串的指针数组的指针。其它四个函数则使用调用
进程中的
environ变量为新程序复制现存的环境。(请回忆7?9节及图7?5中对环境字符串的
讨论。其
中曾提及如果系统支持setenv和putenv这样的函数,则可更改当前环境和后面生成
的子进程
的环境,但不能影响父进程的环境。)通常,一个进程允许将其环境传播给其子进
程,但有
时也有这种情况,一个进程想要为一个子进程指定一个确定的环境。例如,在初始
化一个新
登录的shell时,login程序创建一个只定义少数几个变量的特殊环境,而在我们登
录时,可
以通过shell起动文件,将其它变量加到环境中。在使用ANSIC原型之前,execle的
?
char *pathname,char *arg0,…,char *argn,(char *)0,char * envpl)?
从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在
ANSIC原
型中,所有命令行参数,包括空指针,envp指针都用省略号(…)表示。?
这六个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母p表示
该函数取
文件名作为参数,并且用PATH环境变量寻找可执行文件。字母l表示该函数取一个
参数表,
它与字母v互斥。v表示该函数取一个argv[]。最后,字母e表示该函数取envp[
]数组,
而不使用当前环境。图8?5显示了这六个函数之间的区别。??
P209?? |
|