8.8 wait3和wait4函数-进程控制

8.8 wait3wait4函数进程控制

wait3wait4提供的功能比waitwaitpidwaitid所提供的功能要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资源汇总。

#include <sys/types.h>

#include <sys/wait.h>

#include <sys/time.h>

#include <sys/resource.h>

pid_t wait3(int *statloc, int options, struct rusage *rusage);

pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

//两个函数返回值:若成功返回进程ID,若出错则返回-1

资源统计信息包括用户CPU时间总量、系统CPU时间总量、页面出错次数、接收到信号的次数等。有关细节参阅getrusage(2)手册页。

8 – 5中列出了各个wait函数所支持的不同的参数。

函数

pid

options

ru s a g e

POSIX.1

Free BSD 5.2.1

Linux 2.4.22

Mac OSX 10.3

Solaris 9

wait

 

 

 

·

·

·

·

·

waited

·

·

 

 

 

 

 

·

waitpid

·

·

 

·

·

·

·

·

wait 3

 

·

·

 

·

·

·

·

wait 4

·

·

·

 

·

·

·

·

 

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之前。

8.5 exit函数-进程控制

8.5 exit函数进程控制

如同7 . 3节所述,进程有三种正常终止法及两种异常终止法。

(1) 正常终止:

a)     main函数内执行return语句。如在7 . 3节中所述,这等效于调用exit

b)     调用exit函数。此函数由ANSI C定义,其操作包括调用各终止处理程序(终止处理程序在调用a t exit函数时登录),然后关闭所有标准I / O流等。因为ANSI C并不处理文件描述符、多进程(父、子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。

c)     调用_ exit_Exit系统调用函数。此函数由exit调用,它处理UNIX特定的细节。_ exit是由POSIX.1说明的。其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准IO流是否进行冲洗,这取决于实现。在UNIX系统中,两个函数是同义的,并不清洗IO流。

d)     进程的最后一个线程在其启动例程中执行返回语句。但是,该线程的返回值不会用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回;

e)     进程的最后一个线程调用pthread_exit函数。如同前面一样,在这要那个情况中,进程终止状态总是0,这与传送给pthread_exit的参数无关。

(2) 异常终止:

a)     调用abort。它产生S I G A B RT信号,所以是下一种异常终止的一种特例。

b)     当进程接收到某个信号时。(第1 0章将较详细地说明信号。)进程本身(例如调用abort函数)、其他进程和内核都能产生传送到某一进程的信号。例如,进程越出其地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。

c)     最后一个线程对取消cancellation请求做出响应。按系统默认,取消以延迟方式发生:一个线程要求取消另一个线程,一段时间以后,目标线程终止。

 

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等等。

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于exit_ exit,这是依靠传递给它们的退出状态( exit status)参数来实现的。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态( termination status)。在任意一种情况下,该终止进程的父进程都能用waitwaitpid函数(在下一节说明)取得其终止状态。

注意,这里使用了“退出状态”(它是传向exit_ exit的参数,或main的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_ exit时内核将其退出状态转换成终止状态(回忆图7 – 1)。下一节中的表8 – 1说明了父进程检查子进程的终止状态的不同方法。如果子进程正常终止,则父进程可以获得子进程的退出状态。

在说明fork函数时,一定是一个父进程生成一个子进程。上面又说明了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,则将如何呢?其回答是对于其父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程I D就更改为1 ( init进程的I D )。这种处理方法保证了每个进程有一个父进程

另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢?对此问题的回答是内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用waitwaitpid 时,可以得到有关信息。这种信息至少包括进程I D、该进程的终止状态、以反该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储器,关闭其所有打开文件。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程( zombiep s ( 1 )命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程就会变成僵死进程。

最后一个要考虑的问题是:一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?对此问题的回答是“”,因为init被编写成只要有一个子进程终止, init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。当提及“一个init的子进程”时,这指的是init直接产生的进程(例如,将在9 . 2节说明的g e t t y进程),或者是其父进程已终止,由init 领养的进程

8.4 vfork函数-进程控制

8.4 vfork函数进程控制

vfork函数的调用序列和返回值与fork相同,但两者的语义不同。

vfork用于创建一个新进程,而该新进程的目的是exec一个新程序(如上节(2) 中一样)。程序1 – 5中的shell基本部分就是这种类型程序的一个例子。vforkfork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec (exit ),于是也就不会存访该地址空间。不过在子进程调用execexit之前,它在父进程的空间中运行。这种工作方式在某些UNIX的页式虚存实现中提高了效率(与上节中提及的,在fork之后跟随exec,并采用在写时复制技术相类似)。

vforkfork之间的另一个区别是: vfork保证子进程先运行,在它调用execexit之后父进程才可能被调度运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)

实例

在程序8 – 1中使用vfork代替fork,并做其他相应修改得到程序8 – 2。删除了对于标准输出的write调用,另外,我们也不再需要让父进程调用sleepvfork已保证在子进程调用execexit之前,内核会使得父进程处于休眠状态。

 

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>



int glob = 6; /* external variable in initialized data */



int main(void)

{

int var; /* automatic variable on the stack */

pid_t pid;



var = 88;

printf("before vforkn"); /* we don't flush stdio */

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

perror("vfork error");

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

glob++; /* modify parent's variables */

var++;

_exit(0); /* child terminates */

}

/*

* Parent continues here.

*/

printf("pid = %d, glob = %d, var = %dn", getpid(), glob, var);

exit(0);

}

运行该程序得到:

$ a.out

before vfork

pid = 2777, glob = 7, var = 89

子进程对变量globvar做增1操作,结果改变了父进程中的变量值因为子进程在父进程的地址空间中运行,所以这并不令人惊讶。但是其作用的确与fork不同。

注意,在程序8 – 2中,调用了_ exit而不是exit。正如8 . 5节所述,_ exit并不执行标准I / O缓存的刷新操作。如果用exit而不是_ exit,则该程序的输出是不确定的。它依赖于标准IO库的实现。可能什么也不输出:

$ a.out

before vfork

从中可见,父进程printf的输出消失了。其原因是子进程调用了exit,它刷新开关闭了所有标准I / O流,这包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准I/O FILE对象都是在父进程中的。当父进程调用printf时,标准输出已被关闭了,于是printf返回– 1