Unix/Linux编程:系统调用 wait()(及其变体)

等待子进程

对于很多需要创建子进程的应用来说,父进程能够检测子进程的终止时间和过程是很有必要的。wait()以及相关系统调用提供了这一功能

系统调用 wait()

系统调用 wait()等待调用进程的任一子进程终止,同时在参数status所指向的缓冲区中返回该子进程的终止状态

#include <sys/wait.h>
/*
* 参数: wstatus表示终止进程的终止状态。如果为null表示不关心终止状态
*/
pid_t wait(int *wstatus);

wait()执行如下动作:

  • 如果调用进程并无之前未被等待的子进程终止,调用将一直阻塞,直到某个子进程终止。如果调用时已有子进程终止,wait()将立即返回
  • 如果status非空,那么关于子进程如何终止的信息则会通过status指向对的整形变量返回
  • 内核将会为父进程下所有子进程的运行总量追加进程CPU时间以及资源使用数据
  • 将终止子进程的ID作为wait()的结果返回

出错时,wait()返回-1。可能的错误原因之一是调用进程并无之前未被等待的子进程,此时会将 errno 置为 ECHILD。换言之,可使用如下代码中的循环来等待调用进程的所有子进程退出:

while((childpid = wait(NULL)) != -1) 
	continue;
if(errno != ECHILD){  // an unexpected error
	perror("wait");
	exit(EXIT_FAILURE);
}

如果于同一时点存在多个子进程退出,SUSv3 并未对 wait()处理这些子进程的顺序加以规定,换言之,该顺序取决于具体实现。即使不同的 Linux 内核版本之间,行为也有所不同

也就是说,对于wait:

  • 如果一个进程有几个子进程,那么只要有一个子进程终止,wait就返回
  • 如果想要指定某一个进程终止,我们可以这样做: 调用wait,然后将其返回的进程ID与期望ID比较,如果相同则终止

看个例子:下面程序创建多个子进程,每个子进程对应于一个命令行参数。每个子进程休眠若干秒之后退出,休眠时间分别由个各命名行参数指定。与此同时,在创建所有的子进程之后,父进程循环调用wait()来监控这些子进程的终止。而直到wait()返回-1时才会退出循环(这并非唯一的手段,另一种退出循环的方法是当记录终止
子进程数量的变量 numDead 与创建的子进程数目相同时,也会退出循环。)

//创建并等待多个子进程
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <zconf.h>
#include <sys/wait.h>
#include <errno.h>
#include <time.h>
#define BUF_SIZE 1000
char * currTime(const char *format)
{
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}
int main(int argc, char *argv[])
{
    int numDead;       /* Number of children so far waited for */
    pid_t childPid;    /* PID of waited for child */
    int j;

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        printf("%s sleep-time...\n", argv[0]);
        exit(EXIT_FAILURE);
    }


    setbuf(stdout, NULL);           /* Disable buffering of stdout */

    for (j = 1; j < argc; j++) {    /* Create one child for each argument */
        switch (fork()) {
            case -1:{
                perror("fork");
                exit(EXIT_FAILURE);
            }

            case 0:                     /* Child sleeps for a while then exits */
                printf("[%s] child %d started with PID %ld, sleeping %s "
                       "seconds\n", currTime("%T"), j, (long) getpid(),
                       argv[j]);
                sleep(atoi(argv[j]));
                _exit(EXIT_SUCCESS);

            default:                    /* Parent just continues around loop */
                break;
        }
    }

    numDead = 0;
    for (;;) {                      /* Parent waits for each child to exit */
        childPid = wait(NULL);
        if (childPid == -1) {
            if (errno == ECHILD) {
                printf("No more children - bye!\n");
                exit(EXIT_SUCCESS);
            } else {                /* Some other (unexpected) error */
                perror("wait");
                exit(EXIT_FAILURE);
            }
        }

        numDead++;
        printf("[%s] wait() returned child PID %ld (numDead=%d)\n",
               currTime("%T"), (long) childPid, numDead);
    }
}

在这里插入图片描述

系统调用 waitpid()

系统调用 wait()存在很多限制,而waitpid()就是为了突破这些限制:

  • 如果父进程已经创建了多个子进程,使用wait()将无法等待某个特定子进程的完成,只能按照顺序等待下一个子进程的终止
  • 如果没有子进程退出,wait()总是保持阻塞。有时候希望执行非阻塞的等待:是否有子进程退出,立判可知
  • 使用wait()只能发现那些已经终止的子进程。对于子进程因某个信号(如 SIGSTOP 或SIG TTIN)而停止,或是已停止子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

/*
* 参数: pid
*  * pid == -1    等待任一子进程。此时相当于wait
*  * pid > 0      等待有pid相等的子进程
*  * pid == 0     等待组ID与调用进程组ID的任一子进程
*  * pid < -1      等待组ID等于pid绝对值的任一子进程
* 	
* 返回值:返回终止子进程的进程ID,并将该子进程的终止状态存放在staloc指向的存储单元中
*/
pid_t waitpid(pid_t pid, int *wstatus, int options);

waitpid()与 wait()的返回值以及参数 status 的意义相同

参数 pid 用来表示需要等待的具体子进程,意义如下

  • 如果 pid 大于 0,表示等待进程 ID 为 pid 的子进程。
  • 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组(process group)的所有子进程
  • 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程。
  • 如果 pid 等于-1,则等待任意子进程。wait(&status)的调用与 waitpid(-1, &status, 0)等价。

参数options提供了一些额外的选项来控制waitpid,它 是一个位掩码(bit mask),取值如下。

  • WUNTRACED :
    • 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。
    • 除了返回终止子进程的信息外,还返回因信号而停止的子进程信息。
  • WCONTINUED (自 Linux2.6.10 以来) :返回那些因收到 SIGCONT 信号而恢复执行的已停止子进程的状态信息。
  • WNOHANG :
    • WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
    • 如果调用进程并无与 pid 匹配的子进程,则 waitpid()报错,将错误号置为 ECHILD。

可以为 0 或可以用"|"运算符把它们连接起来使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:

ret=waitpid(-1,NULL,0);

SUSv3在其对waitpid()的原理阐述中特别指出,WUNTRACED 的名称是源于 BSD的历史产物。BSD 有两种停止进程的方法:作为系统调用ptrace()追踪的结果,或者因为收到一个信号而停止。当通过ptrace()追踪一个子进程时,那么(除 SIGKILL 之外的)任何信号都会造成子进程停止,接着会将信号SIGCHLD发给父进程。即使子进程忽略和谐信号,这一行为仍会发生。不过,如果子进程阻塞了这些信号(除非是无法阻塞的 SIGSTOP信号),子进程就不会停止

wait和waitpid的区别如下:

  • 在一个子进程终止前,wait使其调用者阻塞,waitpid可以通过参数__options选择是不是阻塞调用者
  • waitpid并不等待在其调用之后的第一个终止子进程,可以通过选项控制它所等待的进程

等待状态值

由wait()和waitpid()返回的status的值,可以用来区分以下子进程事件:

  • 子进程调用_exit()(或 exit())而终止,并指定一个整型值作为退出状态
  • 子进程收到未处理信号而终止
  • 子进程因为信号而停止,并以 WUNTRACED 标志调用 waitpid()
  • 子进程因收到信号 SIGCONT 而恢复,并以 WCONTINUED 标志调用 waitpid()。

此处用术语“等待状态”(wait status)来涵盖上述所有情况,而使用“终止状态”(termination status)的称谓来指代前两种情况。(在 shell 中,可通过读取$?变量值来获取上次执行命令的终止状态。

虽然将变量 status 定义为整型(int),但实际上仅使用了其最低的 2 个字节。对这 2 个字节的填充方式取决于子进程所发生的具体事件,如下图所示:
在这里插入图片描述
头文件<sys/wait.h>定义了用于解析等待状态值的一组标准宏。对自 wait()或 waitpid()返回的status 值进行处理时,以下列表中各宏只有一个会返回真(true)值。如列表所示,另有其他宏可对 status 值做进一步分析

  • WIFEXITED (status):
    • 若子进程正常结束则返回真(true)。
    • 此时,宏 WEXITSTATUS(status)返回子进程的退出状态。
  • WIFSIGNALED (status):
    • 若通过信号杀掉子进程则返回真(true)。
    • 此时,宏 WTERMSIG(status)返回导致子进程终止
      的信号编号。若子进程产生内核转储文件,则宏WCOREDUMP(status)返回真值(true)。
    • SUSv3并未规范宏 WCOREDUMP(),不过大部分 UNIX 实现均支持该宏
  • WIFSTOPPED (status)
    • 若子进程因信号而停止,则此宏返回为真(true)。
    • 此时,宏 WSTOPSIG(status)返回导致子进程停止的信号编号
  • WIFCONTINUED (status)
    • 若子进程收到 SIGCONT 而恢复执行,则此宏返回真值(true)。
    • 自 Linux 2.6.10 之后开始支持该宏

看个例子:使用 waitpid()获取子进程状态

  • 该程序创建了一个子进程,该子进程会循环调用 pause()(在此期间可以向子进程发送信号),但如果在命令行中指定了整型参数,则子进程会立即退出,并以该整型值作为退出状态
  • 同时,父进程通过 waitpid()监控子进程,打印子进程返回的状态值并将其作为参数传递给printWaitStatus()。一旦发现子进程已正常退出,亦或因某一信号而终止,父进程会随即退出。
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <zconf.h>
#include <sys/wait.h>
#include <errno.h>
#include <time.h>
#define BUF_SIZE 1000


void                    /* Examine a wait() status using the W* macros */
printWaitStatus(const char *msg, int status)
{
    if (msg != NULL)
        printf("%s", msg);

    if (WIFEXITED(status)) {
        printf("child exited, status=%d\n", WEXITSTATUS(status));

    } else if (WIFSIGNALED(status)) {
        printf("child killed by signal %d (%s)",
               WTERMSIG(status), strsignal(WTERMSIG(status)));
#ifdef WCOREDUMP        /* Not in SUSv3, may be absent on some systems */
        if (WCOREDUMP(status))
            printf(" (core dumped)");
#endif
        printf("\n");

    } else if (WIFSTOPPED(status)) {
        printf("child stopped by signal %d (%s)\n",
               WSTOPSIG(status), strsignal(WSTOPSIG(status)));

#ifdef WIFCONTINUED     /* SUSv3 has this, but older Linux versions and
                           some other UNIX implementations don't */
    } else if (WIFCONTINUED(status)) {
        printf("child continued\n");
#endif

    } else {            /* Should never happen */
        printf("what happened to this child? (status=%x)\n",
               (unsigned int) status);
    }
}
int
main(int argc, char *argv[])
{
    int status;
    pid_t childPid;

    if (argc > 1 && strcmp(argv[1], "--help") == 0){
        printf("%s [exit-status]\n", argv[0]);
        exit(EXIT_FAILURE);
    }


    switch (fork()) {
        case -1:
            perror("fork");
            exit(EXIT_FAILURE);
        case 0:             /* Child: either exits immediately with given
                           status or loops waiting for signals */
            printf("Child started with PID = %ld\n", (long) getpid());
            if (argc > 1)                   /* Status supplied on command line? */
                exit(atoi(argv[1]));
            else                            /* Otherwise, wait for signals */
                for (;;)
                    pause();
            exit(EXIT_FAILURE);             /* Not reached, but good practice */

        default:            /* Parent: repeatedly wait on child until it
                           either exits or is terminated by a signal */
            for (;;) {
                childPid = waitpid(-1, &status, WUNTRACED
                                                #ifdef WCONTINUED       /* Not present on older versions of Linux */
                                                | WCONTINUED
#endif
                );
                if (childPid == -1){
                    perror("waitpid");
                    exit(EXIT_FAILURE);
                }


                /* Print status in hex, and as separate decimal bytes */

                printf("waitpid() returned: PID=%ld; status=0x%04x (%d,%d)\n",
                       (long) childPid,
                       (unsigned int) status, status >> 8, status & 0xff);
                printWaitStatus(NULL, status);

                if (WIFEXITED(status) || WIFSIGNALED(status))
                    exit(EXIT_SUCCESS);
            }
    }
}
  • 创建一子进程并立即退出,且其状态值为 23
    在这里插入图片描述

  • 接下来,在后台运行该程序,并向子进程发送SIGSTOP 和 SIGCONT 信号( 可以看到被杀死的程序自己起来了)
    在这里插入图片描述

  • 接着,再发送 SIGABRT 信号来终止子进程:
    在这里插入图片描述
    虽然 SIGABRT 的默认行为是产生一个内核转储文件并终止进程,但这里并未产生转储文件。这是由于屏蔽内核转储所致,如以上命令ulimit的输出所示,将RLIMIT_CORE资源软限制置为0,该限制规定了转储文件大小的最大值。

  • 再次重复同一实验,不过这次在发送信号 SIGABRT 给子进程之前,放开了对转储文件大小的限制。
    在这里插入图片描述

再看个例子:


#include <sys/wait.h>
#include <zconf.h>
#include <stdio.h>
#include <stdlib.h>

void pr_exit(int status);
int main(void)
{
    pid_t	pid;
    int		status;

    if ((pid = fork()) < 0){
        printf("fork error");
        exit(0);
    }else if (pid == 0)	{ //child
        //sleep(10l);
        exit(7);
    }

    if (wait(&status) != pid)	{ // wait for child
        printf("wait error");
        exit(0);
    }
    pr_exit(status);				// and print its status

    if ((pid = fork()) < 0){
        printf("fork error");
        exit(0);
    }else if (pid == 0)	{//child
        abort();					// generates SIGABRT
    }


    if (wait(&status) != pid)	{ // wait for child
        printf("wait error");
        exit(0);
    }
    pr_exit(status);				//and print its status

    if ((pid = fork()) < 0){
        printf("fork error");
        exit(0);
    }else if (pid == 0)		{ //child
        status /= 0;		      //divide by 0 generates SIGFPE
    }


    if (wait(&status) != pid)	{ // wait for child
        printf("wait error");
        exit(0);
    }
    pr_exit(status);				/* and print its status */

    exit(0);
}



void pr_exit(int status)
{
    if(WIFEXITED(status))  {  //正常终止子进程返回的状态
        printf("normal termination, exit status = %d\n", WEXITSTATUS(status)); // WEXITSTATUS获取子进程传送给exit或_exit参数的低8位
    }else if(WIFSIGNALED(status)){ // 若为异常终止子进程返回的状态(接到一个不捕捉的信号)
        printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),//WTERMSIG 获取子进程终止的信号编号
#ifdef WCOREDUMP
                       WCOREDUMP(status) ? " (core file generated)" : "");  // 若已产生终止进程的core文件,则返回真
#else
                      "");
#endif
    }else if(WIFSTOPPED(status))  // 若为当前暂停子进程的返回状态,则为真
        printf("child stopped, signal number = %d\n", WSTOPSIG(status)); // 获取使子进程暂停的信号编号。
}

在这里插入图片描述

从信号处理程序中终止进程。

默认情况下某些信号会终止进程。有时,可能希望在进程终止前执行一些清理步骤。为此,可以设置一个处理程序来捕获这些信号,随即执行清理步骤,再终止进程。

如果这么做,需要牢记的是:通过wait()和waitpid()调用,父进程依然可以获取子进程的终止状态。例如,如果在信号处理程序中调用_exit(EXIT_SUCCESS),父进程会认为子进程是正常终止

修改序号通知父进程自己因为某个进程而种植,那么子进程的信号处理程序应该首先将自己废除,然后再次发出相同的信号,该信号这次将终止这一子进程:

void handler(int sig){
	// 执行清理步骤

	signal(sig, SIG_DEL);  //disestablish handler
	
	raise(sig);  //raise signal again
}

系统调用waitid()

与 waitpid()类似,waitid()返回子进程的状态。不过,waitid()提供了 waitpid()所没有的扩展功能。该系统调用源于系统 V(System V),不过现在已获 SUSv3 采用,并从版本 2.6.9 开始,将其加入 Linux 内核。

NAME
       waitid - wait for process to change state

SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

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

参数idtype和id指定需要等待哪些子进程:

  • 如果idtype为P_ALL,则等待任何子进程,同时忽略id值
  • 如果idtype为P_PID,则等待进程ID为id进程的子进程
  • 如果idtype为P_PGID,则等待进程组ID为id各进程的所有子进程

请注意,与 waitpid()不同,不能靠指定 id 为 0 来表示与调用者属于同一进程组的所有进程。相反,必须以 getpgrp()的返回值来显式指定调用者的进程组 ID。

waitpid()与 waitid()最显著的区别在于,对于应该等待的子进程事件,waitid()可以更为精确地控制。可通过在 options 中指定一个或多个如下标识(按位或运算)来实现这种控制。

  • WEXITED :等待已终止的子进程,而无论其是否正常返回。
  • WSTOPPED :等待已通过信号而停止的子进程
  • WCONTINUE:等待经由信号 SIGCONT 而恢复的子进程。

以下附加标识也可以通过按位或运算加入 options 中。

  • WNOHANG:与其在 waitpid()中的意义相同。
    • 如果匹配id值的子进程中并无状态信号需要返回,则立即返回(一个轮询)。此时,waitid()返回 0。
    • 如果调用进程并无子进程与 id 的值相匹配,则waitid 调用失败,且错误号为 ECHILD
  • WNOWAIT :
    • 通常,一旦通过waitid()来等待子进程,那么必然会去处理所谓"状态事件"。
    • 不过,如果制定了WNOWAIT,则会返回子进程状态,但子进程依然处理可等待的(waitable)状态,稍后可再次等待并获取相同信息

执行成功,wait()返回0,而且会更新指针infop所指向的siginfo_t结构,以包含子进程的相关信息。以下是结构 siginfo_t 的字段情况。

  • si_code :该字段包含以下值之一:
    • CLD_EXITED,表示子进程已通过调用_exit()而终止;
    • CLD_KILLED,表示子进程为某个信号所杀;
    • CLD_STOPPED,表示子进程因某个信号而停止
    • CLD_CONTINUED,表示(之前停止的)子进程因接收到(SIGCONT)信号而恢复执行
  • si_pid :该字段包含状态发生变化子进程的进程 ID。
  • si_signo :总是将该字段置为 SIGCHLD。
  • si_status :
    • 该字段要么包含传递给_exit()的子进程退出状态,要么包含导致子进程停止、继续或终止的信号值。
    • 可以通过读取 si_code 值来判定具体包含的是哪一种类型的信息。
  • si_uid :
    • 该字段包含子进程的真正用户 ID。
    • 大部分其他 UNIX 实现不会设置该字段

在 Solaris 系统中,此结构还包含两个附加字段:si_stime 和 si_utime,分别包含子进程使用的系统和用户 CPU 时间。SUSv3 并不要求 waitid()处理这两个字段

waitid()操作的一处细节需要进一步澄清。如果options中指定了WNOHANG,那么 waitid()返回 0 意味着以下两种情况之一:

  • 在调用时子进程的状态已经改变(关于子进程的相关信息保存在 infop 指针所指向的结构 siginfo_t 中)
  • 没有任何子进程的状态有所改变。

对于没有任何子进程改变状态的情况,一些 UNIX 实现(包括 Linux)会将 siginfo_t 结构内容清 0。这也是区分两种情况的方法之一:检查 si_pid 的值是否为 0。不幸的是,SUSv3 并未规范这一行为,一些 UNIX 实现此时会保持结构 siginfo_t 原封不动。(未来针对 SUSv4 的勘误表可能会增加在这
种情况下将 si_pid 和 si_signo 置 0 的要求。)区分这两种情况唯一可移植的方法是:在调用 waitid()之前就将结构 siginfo_t 的内容置为 0,正如以下代码所示:

siginfo_t info;

memset(&info, 0, sizeof(siginfo_t));
if(waitid(idtype, id, &info, option| WNOHANG) == -1){
	perror("waitid");
	exit(EXIT_FAILURE);
}
if(info.si_pid == 0){
	// no children changed state
}else{
	// a child changed state; detailes ar provided in 'info'
}

系统调用 wait3()和 wait4()

系统调用 wait3()和 wait4()执行与 waitpid()类似的工作。主要的语义差别在于,wait3()和 wait4()在参数 rusage 所指向的结构中返回终止子进程的资源使用情况。其中包括进程使用的 CPU 时间总量以及内存管理的统计数据。

NAME
       wait3, wait4 - wait for process to change state, BSD style

SYNOPSIS
       #include <sys/types.h>
       #include <sys/time.h>
       #include <sys/resource.h>
       #include <sys/wait.h>

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

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


除了对参数 rusage 的使用之外,调用 wait3()等同于以如下方式调用waitpid():

waitpid(-1, &status, options);

与之相类似,对 wait4()的调用等同于对 waitpid()的如下调用:

waitpid(pid, &status, options);

换言之,wait3()等待的是任意子进程,而 wait4()则可以用于等待选定的一个或多个子进程。

在一些 UNIX 实现中,wait3()和 wait4()仅返回已终止子进程的资源使用情况。而对于 Linux 系统,如果在 options 中指定了 WUNTRACED 选项,则还可以获取到停止子进程的资源使用信息。

这两个系统调用的名称来自于它们所使用参数的个数。虽然源自 BSD 系统,不过现在大部分的 UNIX 实现都支持它们。这两个系统调用均未获得 SUSv3 标准的接纳。(SUSv2 标准纳入了 wait3(),但将其标记为“已过时”。)

请不要使用wait3()和 wait4()。通常情况下,此类调用所返回的额外信息没有什么价值。此外,未获业界标准的接纳也会限制其可移植性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值