8.11 更改用户ID和组ID

8.11 更改用户ID和组ID

      UNIX系统中,特权是基于用户和组ID的,当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们就需要更换自己的用户ID或组ID

      一般而言,在设计应用程序的时候,我们总是试图使用最小特权(lease privilege)模型。

可以用setuid函数设置实际用户ID和有效用户ID。与此类似,可以用s e t g i d函数设置实际组ID和有效组ID

#include <unistd.h>

int setuid(uid_t uid) ;

int setgid(gid_t g i d) ;

//两个函数返回:若成功则为0,若出错则为-1

关于谁能更改ID有若干规则。现在先考虑有关改变用户ID的规则(在这里关于用户ID所说明的一切都适用于组ID)。

l  若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置用户ID设置为uid

l  若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户– ID,则setuid只将有效用户ID设置为uid。不改变实际用户ID和保存的设置用户– ID

l  如果上面两个条件都不满足,则e r r n o设置为E P E R M,并返回出错。

关于内核所维护的三个用户ID,还要注意下列几点:

l  只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登录时,由login ( 1 )程序设置的,而且决不会改变它。因为login是一个超级用户进程,当它调用setuid时,设置所有三个用户ID

l  仅当对程序文件设置了设置用户ID位时, exec函数设置有效用户ID。如果设置用户– ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户– ID。自然,不能将有效用户ID设置为任一随机值。

l  保存的设置用户– ID是由exec从有效用户ID复制的。在exec按文件用户ID设置了有效用户ID后,即进行这种复制,并将此副本保存起来。

8.10.1 setreuidsetregid函数

4 . 3 + BSD支持setregid函数,其功能是交换实际用户ID和有效用户ID的值。

#include <unistd.h>

int setreuid(uid_t ruid, uid_t e uid) ;

int setregid(gid_t rg i d, gid_t e g i d) ;

//两个函数返回:若成功则为0,若出错则为-1

8.10.2 seteuidsetegid函数

在对P O I X . 1的建议更改中包含了两个函数seteuidsetegid。它们只更改有效用户ID和有效组ID

#include <unistd.h>

int seteuid(uid_t uid) ;

int setegid(gid_t g i d) ;

//两个函数返回:若成功则为0,若出错则为-1

8.10 exec函数

8.10 exec函数

fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其m a i n函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是U N I X进程控制原语。fork可以创建新进程,用exec可以执行新的程序exit函数和两个wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。在后面各节中将使用这些原语构造另外一些如popensystem之类的函数。

#include <unistd.h>

int execl(const char *pathname, const char * a rg 0, ... /* (char *) 0 */);

int execv(const char *pathname, char *const a rgv [] );

int execle(const char *pathname, const char * a rg 0, ...

/* (char *)0, char *const envp [] */);

int execve(const char *pathname , char *const a rgv [], char *consten vp [] );

int execlp(const char *filename, const char * a rg 0, ... /* (char *) 0 */);

int execvp(const char *filename, char *const a rgv [] );

//六个函数返回:若出错则为- 1,若成功则不返回

      可以看出这些函数的区别是前4个取路径名为参数,后2个则取文件名作为参数。

当指定filename时:

如果filename中包含/,则就将其视为路径名。

否则就按PAT H环境变量,在有关目录中搜寻可执行文件。

PAT H变量包含了一张目录表(称为路径前缀),目录之间用冒号( : )分隔。例如下列n a m e = value环境字符串:

P A T H = / bin : / u s r / bin : / u s r / l o c a l / bin :.

如果execlpexec v p中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑程序产生的机器可执行代码文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。不是可执行代码就是shell脚本。

      第二个区别与参数表的传递有关( l表示表( l i s t )v表示矢量( vector ) )。函数execlexeclpexecle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外三个函数( exec v, exec v pexec v e ),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这三个函数的参数。

在使用ANSI C原型之前,对execl , execleexeclp三个函数表示命令行参数的一般方法是:

char * arg0 , char *arg 1, …, char *arg n, (char *) 0

应当特别指出的是:在最后一个命令行参数之后跟了一个空指针。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则它将被解释为整型参数。如果一个整型数的

长度与char *的长度不同,exec函数实际参数就将出错。

最后一个区别与向新程序传递环境表相关。e结尾的两个函数( execleexec v e

可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。(回忆7 . 9节及表7 – 2中对环境字符串的讨论。其中曾提及如果系统支持s e t e n vp u t e n v这样的函数,则可更改当前环境和后面生成的子进程的环境,但不能影响父进程的环境。)通常,一个进程允许将其环境传播给其子进程,但有时也有这种情况,进程想要为子进程指定一个确定的环境。例如,在初始化一个新登录的shell时, login程序创建一个只定义少数几个变量的特殊环境,而在我们登录时,可以通过shell起动文件,将其他变量加到环境中。在使用ANSI C 原型之前, execle 的参数是:

char * pathname, char *arg 0, , char *arg n, (char *)0, char *envp[ ]

从中可见,最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在ANSI C原型中,所有命令行参数,包括空指针, envp指针都用省略号()表示。

这六个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母p表示该函数取filename作为参数,并且用PAT H环境变量寻找可执行文件。字母l表示该函数取一个参数表,它与字母v互斥。v表示该函数取一个argv[ ]。最后,字母e表示该函数取envp[ ] 数组,而不使用当前环境。表8 – 4显示了这六个函数之间的区别。

函数

pathname

filename

参数表

argv[]

environ

envp[]

execl

·

 

·

 

·

 

execlp

 

·

·

 

·

 

execle

·

 

·

 

 

·

execv

·

 

 

·

·

 

execvp

 

·

 

·

·

 

execve

·

 

 

·

 

·

字母表示

 

p

 

v

 

e

每个系统对参数表和环境表的总长度都有一个限制。在表2 – 7中,这种限制是A R G _ M A X。在P O S I X . 1系统中,此值至少是4 0 9 6字节。当使用shell的文件名扩充功能产生一个文件名表时,可能会受到此值的限制。例如,命令

grep _POSIX_SOURCE /usr/include/*/*.h

在某些系统上可能产生下列形式的shell错误:

arg list too long

前面曾提及在执行exec后,进程ID没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征:

进程ID和父进程ID

实际用户ID和实际组ID

添加组ID

进程组ID

对话期ID

控制终端。

闹钟尚余留的时间。

当前工作目录。

根目录。

文件方式创建屏蔽字。

文件锁。

进程信号屏蔽。

未决信号。

资源限制。

• tms_utime, tms_stime, tms_cutime以及t m s _ u s time值。

对打开文件的处理与每个描述符的exec关闭标志值有关。见图3 – 1以及3 . 1 3 节中对F D _ C L O EXEC的说明,进程中每个打开描述符都有一个exec关闭标志。若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用f c n t l设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开。

P O S I X . 1明确要求在exec时关闭打开目录流(见4 . 2 1节中所述的o p e n d i r函数)。这通常是由o p e n d i r函数实现的,它调用f c n t l函数为对应于打开目录流的描述符设置exec关闭标志。

注意,在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序的文件的设置用户– ID位和设置– ID位是否设置。如果新程序的设置用户– ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对组ID的处理方式与此相同。在很多U N I X实现中,这六个函数中只有一个exec v e是内核的系统调用。另外五个只是库函数,它们最终都要调用系统调用。这六个函数之间的关系示于图8 – 2中。在这种安排中,库函数execlp execvp 使用PAT H环境变量查找第一个包含名为filename的可执行文件的路径名前缀。

clip_image001

6exec函数之间的关系

程序8 – 8例示了exec函数。

#include <unistd.h>

#include <stdlib.h>

#include <sys/wait.h>



char *env_init[] = { "USER=unknown", "PATH=/tmp", NULL };



int main(void)

{

pid_t pid;



if ((pid = fork()) < 0) {

perror("fork error");

} else if (pid == 0) { /* specify pathname, specify environment */

if (execle("/usr/bin/lscpu", "lscpu", "myarg1",

"MY ARG2", (char *) 0, env_init) < 0)

perror("execle error");

}



if (waitpid(pid, NULL, 0) < 0)

perror("wait error");



if ((pid = fork()) < 0) {

perror("fork error");

} else if (pid == 0) { /* specify filename, inherit environment */

if (execlp("ls", "ls", ".", (char *) 0) < 0)

perror("execlp error");

}



exit(0);

}

在该程序中先调用execle,它要求一个路径名和一个特定的环境。下一个调用的是execlp,它用一个文件名,并将调用者的环境传送给新程序。execlp在这里能够工作的原因是因为目录/ bin是当前路径前缀之一。注意,我们将第一个参数(新程序中的a rgv [0])设置为路径名的文件名分量。某些shell将此参数设置为完全的路径名。

在程序8 – 8中要执行两次的程序e c h o a l l示于程序8 – 9中。这是一个普通程序,它回送其所有命令行参数及其全部环境表。

#include <stdio.h>

#include <stdlib.h>



int main(int argc, char *argv[])

{

int i;

char **ptr;

extern char **environ;



for (i = 0; i < argc; i++) /* echo all command-line args */

printf("argv[%d]: %sn", i, argv[i]);



for (ptr = environ; *ptr != 0; ptr++) /* and all env strings */

printf("%sn", *ptr);



exit(0);

}

8.9 竞态条件

8.9 竞态条件

从本书的目的出发,当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞态条件( race condition。如果在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞态条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使知道哪一个进程先运行,那么在该进程开始运行后,所发生的事情也依赖于系统负载以及内核的调度算法。

在程序8 – 5中,当第二个子进程打印其父进程ID时,我们看到了一个潜在的竞态条件。如果第二个子进程在第一个子进程之前运行,则其父进程将会是第一个子进程。但是,如果第一个子进程先运行,并有足够的时间到达并执行exit,则第二个子进程的父进程就是init。即使在程序中调用sleep,这也不保证什么。如果系统负担很重,那么在第二个子进程从sleep返回时,可能第一个子进程还没有得到机会运行。这种形式的问题很难排除,因为在大部分时间,这种问题并不出现。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要等待其父进程终止(如程序8 – 5中一样),则可使用下列形式的循环:

while(getppid() !=1)

sleep ( 1 ) ;

这种形式的循环(称为轮询polling))的问题是它浪费了CPU时间,因为调用者每隔1秒都被唤醒,然后进行条件测试。

为了避免竞态条件和轮询,在多个进程之间需要有某种形式的信号机制。在UNIX中可以使用信号机制,在1 0 . 1 6节将说明它的一种用法。各种形式的进程间通信( IPC )也可使用,在第151 7章将对此进行讨论。

在父、子进程的关系中,常常出现下述情况。在fork之后,父、子进程都有一些事情要做。例如,父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方,并且在继续运行之前,要等待另一方完成其初始化操作。这种情况可以描述如下:

#include  "***"

TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx */

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */

/* child does whatever is necessary ... */

TELL_PARENT(getppid()); /* tell parent we're done */
WAIT_PARENT(); /* and wait for parent */

/* and the child continues on its way ... */

exit(0);
}

/* parent does whatever is necessary ... */

TELL_CHILD(pid); /* tell child we're done */
WAIT_CHILD(); /* and wait for child */

/* and the parent continues on its way ... */

exit(0);

程序8 – 6输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖于内核使进程运行的顺序及每个进程运行的时间长度,所以该程序包含了一个竞态条件。程序具有竞争条件的程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charactatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); //set unbuffered
for (ptr = str; (c = *ptr++) != 0;)
putc(c, stdout);
}

int main(void)
{
pid_t pid;

if ((pid = fork()) < 0)
perror("fork error");
else if (pid == 0) //child
charactatime("output from childn");
else
charactatime("output from parentn");

exit(0);
}

由于setbuf(stdout, NULL);     //set unbuffered所以每个字符输出都需要调用一次write函数。这样就会导致输出是不可控的。

如果加入我们刚开始讨论的子进程要等父进程操作完成后或者相反的代码,就能得到预期的输出,两个进程的输出不再交叉混合。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charactatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); //set unbuffered,so every character will call write
for (ptr = str; (c = *ptr++) != 0;)
putc(c, stdout);
}

int main(void)
{
pid_t pid;

TELL_WAIT();

if ((pid = fork()) < 0)
perror("fork error");
else if (pid == 0) //child
{
WAIT_PARENT();
charactatime("output from childn");

} else {
charactatime("output from parentn");
TELL_CHILD();
}
exit(0);
}

8.7 waitid函数-进程控制

8.7 waitid函数进程控制

waitid函数类似于waitpid,但提供了更多的灵活性。

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t *infop. int options);

//返回值:成功返回0,出错返回-1

waitpid相似,waitid允许一个进程指定要等待的子进程。但它使用单独的参数表示要等待的子进程的类型,而不是将此与进程ID或进程组ID组合成一个参数。id参数的作用与idtype的值相关。

 常量

说明

P_PID

等待一个特定的进程:id包含要等待子进程的进程ID

P_PGID

等待一个特定进程组中的任一个子进程:id包含要等待子进程的进程组ID

P_ALL

等待任一个子进程:忽略id

8.3 waitididtype常量

options参数是图8.3中各标志的按位。这些标志指示调用者关注那些状态变化。

常量

说明

WCONTINUED

等待一个进程,它以前曾被暂停,此后又已继续,但其状态尚未报告

WEXITED

等待已退出的进程

WNOHANG

如无可用的子进程退出状态,立即返回而非阻塞

WNOWAIT

不破坏子进程退出状态。该子进程退出状态可由后续的waitwaitidwaitpid调用取得

WSTOPPED

等待一个进程,它已经暂停,但其状态尚未报告

8.4 waitidoptions常量

infop参数是指向siginfo结构的指针。该结构包含了有关引起子进程状态改变的生成信号的详细信息。

8.6 wait和waitpid函数-进程控制

8.6 waitwaitpid函数进程控制

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。第1 0章将说明这些选择项。现在需要知道的是调用waitwaitpid的进程可能会:

阻塞(如果其所有子进程都还在运行)

带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)

出错立即返回(如果它没有任何子进程)

如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在一个任一时刻调用wait,则进程可能会阻塞。

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *statloc) ;

pid_t waitpid(pid_t pid, int *statloc, int options) ;

//两个函数返回:若成功则为进程I D,若出错则为-1

这两个函数的区别是:

在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞

• waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。

如果一个子进程已经终止,是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回终止子进程的进程I D,所以它总能了解是哪一个子进程终止了。

这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了一个core文件等等。POSIX.1规定终止状态用定义在< sys / wait . h >中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,它们的名字都以W I F开始。基于这三个宏中哪一个值是真,就可选用其他宏来取得终止状态、信号编号等。这些都在表8 – 1中给出。在8 . 9节中讨论作业控制时,将说明如何停止一个进程。

8-1 检查waitwaitpid所返回的终止状态的宏

说明

WIFEXITED(status)

若为正常终止子进程返回的状态,则为真。对于这种情况可执行W EXIT STATUS(status) 取子进程传送给exit_ exit参数的低8

WIFSIGNALED(status)

若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行W T E R M S I G(status)取使子进程终止的信号编号。另外,S V R 44 . 3 + B S D(但是,非POSIX.1)定义宏:W CORE D U M P(status)若已产生终止进程的core文件,则它返回真

W I F S T O P P E D(status)

若为当前暂停子进程的返回的状态,则为真。对于这种情况,可执行WSTOPSIG(status)取使子进程暂停的信号编号

WIFCONTINUEDstatus

若在作业控制暂停后已经继续的子进程返回了状态,则为真。

实例

程序8 – 3中的函数pr_exit使用表8 – 1中的宏以打印进程的终止状态。本章的很多程序都将调用此函数。注意,如果定义了W CORE D U M P,则此函数也处理该宏。

#include <stdio.h>

#include <sys/wait.h>



void pr_exit(int status)

{

if (WIFEXITED(status))

printf("normal termination, exit status = %dn",

WEXITSTATUS(status));

else if (WIFSIGNALED(status))

printf("abnormal termination, signal number = %d%sn",

WTERMSIG(status),

#ifdef WCOREDUMP

WCOREDUMP(status) ? " (core file generated)" : "");

#else

"");

#endif

else if (WIFSTOPPED(status))

printf("child stopped, signal number = %dn",

WSTOPSIG(status));

}

程序8 – 4调用pr_exit函数,例示终止状态的不同值。运行程序8 – 4可得:

#include <stdio.h>

#include <stdlib.h>

#include <sys/wait.h>



void pr_exit(int status)

{

if (WIFEXITED(status))

printf("normal termination, exit status = %dn",

WEXITSTATUS(status));

else if (WIFSIGNALED(status))

printf("abnormal termination, signal number = %d%sn",

WTERMSIG(status),

#ifdef WCOREDUMP

WCOREDUMP(status) ? " (core file generated)" : "");

#else

"");

#endif

else if (WIFSTOPPED(status))

printf("child stopped, signal number = %dn",

WSTOPSIG(status));

}



int main(void)

{

pid_t pid;

int status;



if ((pid = fork()) < 0)

perror("fork error");

else if (pid == 0) /* child */

exit(7);



if (wait(&status) != pid) /* wait for child */

perror("wait error");

pr_exit(status); /* and print its status */



if ((pid = fork()) < 0)

perror("fork error");

else if (pid == 0) /* child */

abort(); /* generates SIGABRT */



if (wait(&status) != pid) /* wait for child */

perror("wait error");

pr_exit(status); /* and print its status */



if ((pid = fork()) < 0)

perror("fork error");

else if (pid == 0) /* child */

status /= 0; /* divide by 0 generates SIGFPE */



if (wait(&status) != pid) /* wait for child */

perror("wait error");

pr_exit(status); /* and print its status */



exit(0);

}

$ a.out

normal termination, exit status = 7

abnormal termination, signal number = 6 (core file generated)

abnormal termination, signal number = 8 (core file generated)

不幸的是,没有一种可移植的方法将W T E R M S I G得到的信号编号映射为说明性的名字。( 1 0 . 2 1节中说明了一种方法。)我们必须查看< s i g n a l . h >头文件才能知道S I G A B RT的值是6S I G F P E的值是8

正如前面所述,如果一个进程有几个子进程,那么只要有一个子进程终止, wait就返回。如果要等待一个指定的进程终止(如果知道要等待进程的I D ),那么该如何做呢?在早期的UNIX版本中,必须调用wait,然后将其返回的进程I D和所期望的进程I D相比较。如果终止进程不是所期望的,则将该进程I D和终止状态保存起来,然后再次调用wait。反复这样做直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若其中已有要等待的进程,则取有关信息,否则调用wait。其实,我们需要的是等待一个特定进程的函数。P O S I X .1定义了waitpid函数以提供这种功能(以及其他一些功能)

对于waitpidpid参数的解释与其值有关:

pid == 1 等待任一子进程。于是在这一功能方面waitpidwait等效。

pid > 0 等待其进程I Dpid相等的子进程。

pid == 0 等待其组I D等于调用进程的组I D的任一子进程。

pid < 1 等待其组I D等于pid的绝对值的任一子进程。( 9 . 4节将说明进程组。)

waitpid返回终止子进程的进程I D,而该子进程的终止状态则通过statloc返回。对于wait,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时,也可能返回另一种出错。第1 0章将对此进行讨论)。但是对于waitpid,如果指定的进程或进程组不存在,或者调用进程没有子进程都能出错。

options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是表8 – 2中常数的逐位或运算。

8-2 waitpid的选择项常数

常数

说明

W N O H A N G

若由pid指定的子进程并不立即可用,则waitpid不阻塞,此时其返回值为0

W U N T R A C E D

若某实现支持作业控制,则由pid指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。W I F S TO P P E D宏确定返回值是否对应于一个暂停子进程

WCONTINUED

若实现支持作业控制,那么由pid指定的任一个子进程在暂停后已经继续,但其状态尚未报告,则返回其状态。

waitpid函数提供了wait函数没有提供的三个功能:

a)     waitpid等待一个特定的进程(wait则返回任一终止子进程的状态)。在讨论p o p e n函数时会再说明这一功能。

b)     waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。

c)     waitpid支持作业控制(以W U N T R A C E D选择项)。

实例

回忆一下8 . 5节中有关僵死进程的讨论。如果一个进程要fork一个子进程,但不要求它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的诀窍是调用fork两次。程序8 – 5实现了这一点。

在第二个子进程中调用sleep以保证在打印父进程I D时第一个子进程已终止。在fork之后,父、子进程都可继续执行——我们无法预知哪一个会先执行。如果不使第二个子进程睡眠,则在fork之后,它可能比其父进程先执行,于是它打印的父进程I D将是创建它的父进程,而不是init进程(进程ID 1)。

#include <stdio.h>

#include <stdlib.h>

#include <sys/wait.h>



int main(void)

{

pid_t pid;



if ((pid = fork()) < 0) {

perror("fork error");

} else if (pid == 0) { /* first child */

if ((pid = fork()) < 0)

perror("fork error");

else if (pid > 0)

exit(0); /* parent from second fork == first child */

/*

* We're the second child; our parent becomes init as soon

* as our real parent calls exit() in the statement above.

* Here's where we'd continue executing, knowing that when

* we're done, init will reap our status.

*/

sleep(2);

printf("second child, parent pid = %dn", getppid());

exit(0);

}



if (waitpid(pid, NULL, 0) != pid) /* wait for first child */

perror("waitpid error");



/*

* We're the parent (the original process); we continue executing,

* knowing that we're not the parent of the second child.

*/

exit(0);

}

执行程序8 – 5得到:

$ a.out

$ second child, parent pid = 1

注意,当原先的进程(也就是exec本程序的进程)终止时, shell打印其指示符,这在第二个子进程打印其父进程I D之前。