关键词: wait()、waitpid()、WIFEXITED/WIFSIGNALED/WIFSTOPPED/WIFCONTINUED、waitid()、wait3()/wait4()、SIGCHLD 等等。

1. 等待子进程

对于需要创建子进程的应用来说,父进程能够检测子进程的终止时间和过程是很有必要的。

1.1 系统调用wait()

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

#include <sys/wait.h>
pid_t wait(int *status);
  Returns process ID of terminated child, or –1 on error
  • 如果调用进程并无 之前未被等待的子进程 终止,调用将一直阻塞,直至某个子进程终止。如果调用时已有子进程终止,wait()则立即返回。
  • 如果status非空,那么关于子进程如何终止的信息则会通过status指向的整型变量返回。
  • 内核将会为父进程下所有紫禁城的运行总量追加进程CPU时间以及资源使用数据。
  • 将终止子进程的ID作为wait()的结果返回。
  • 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 ) usageErr( " %s sleep-time...\n " , argv[ 0 ]); setbuf(stdout, NULL); /* Disable buffering of stdout */ for (j = 1 ; j < argc; j++) { /* Create one child for each argument */ switch (fork()) { case - 1 : errExit( " fork " ); 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(getInt(argv[j], GN_NONNEG, " sleep-time " )); _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 ) {------------------------------------------------------------ 出错时,wait()返回-1。可能原因是调用进程并无未被等待的子进程,此时errno置为ECHILD。 if (errno == ECHILD) { printf( " No more children - bye!\n " ); exit(EXIT_SUCCESS); } else { /* Some other (unexpected) error */ errExit( " wait " ); numDead ++ ; printf( " [%s] wait() returned child PID %ld (numDead=%d)\n " , currTime( " %T " ), ( long ) childPid, numDead);

    运行结果如下:

    al@al-B250-HD3:~/tlpi/procexec$ ./multi_wait 6 5 4 3 2 1 0
    [11:22:26] child 2 started with PID 25439, sleeping 5 seconds------------------启动7个子进程。
    [11:22:26] child 4 started with PID 25441, sleeping 3 seconds
    [11:22:26] child 3 started with PID 25440, sleeping 4 seconds
    [11:22:26] child 5 started with PID 25442, sleeping 2 seconds
    [11:22:26] child 6 started with PID 25443, sleeping 1 seconds
    [11:22:26] child 1 started with PID 25438, sleeping 6 seconds
    [11:22:26] child 7 started with PID 25444, sleeping 0 seconds
    [11:22:26] wait() returned child PID 25444 (numDead=1)-------------------------子进程依次退出,主进程捕捉到。
    [11:22:27] wait() returned child PID 25443 (numDead=2)
    [11:22:28] wait() returned child PID 25442 (numDead=3)
    [11:22:29] wait() returned child PID 25441 (numDead=4)
    [11:22:30] wait() returned child PID 25440 (numDead=5)
    [11:22:31] wait() returned child PID 25439 (numDead=6)
    [11:22:32] wait() returned child PID 25438 (numDead=7)
    No more children - bye!--------------------------------------------------------所有子进程退出后,wait()返回-1。

    1.2 系统调用waitpid()

    waitpid()相对于wait()有如下优势:

    如果父进程已经创建了多个子进程,使用wait()将无法等待等待某个特定子进程的完成,只能按顺序等待下一个子进程的终止。

    如果没有子进程退出,wait()总是保持阻塞。有时候会希望执行非阻塞的等待。

    使用wait()只能发现那些已经终止的子进程。对于子进程因某个信号而停止,或是已停止子进程收到SIGCONT信号后恢复执行的情况就无能为力了。

    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *status, int options);
        Returns process ID of child, 0 (see text), or –1 on error

    status意义和wait()相同。

    pid用来表示需要等待的具体子进程:

    如果pid大于0,表示等待进程ID为pid的子进程。

    如果pid等于0,则等待与调用进程同一个进程组的所有子进程。

    如果pid等于-1,则会等待任意子进程。wait(&status)与waitpid(-1, &status, 0)等价。

    如果pid小于-1,则会等待进程组标识符与pid绝对值相等的所有子进程。

    参数options是一个位掩码,可以包含0个或多个如下标志:

    WUNTRACED:除了返回终止子进程的信息外,还返回因信号而停止的子进程信息。

    WCONTINUED:返回那些因收到SIGCONT信号而恢复执行的已停止子进程的状态信息。

    WNOHANG:如果参数pid所指定的子进程并未发生状态改变,则立即返回,而不会阻塞,亦即poll;waitpid()返回0,如果调用进程并无与pid匹配的子进程,则waitpid()报错,将错误号置为ECHILD。

    1. 3 等待状态值

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

    子进程调用_exit()或者exit()而终止,并制定一个整型值作为退出装填。

    子进程收到未处理信号而终止。

    子进程因为信号而停止,并以WUNTRACED标志调用waitpid()。

    子进程因收到信号SIGCONT而恢复,并以WCONTINUED标志调用waitpid()。

    status为整型变量,但是实际只使用了其最低的2个字节。

    头文件<sys/wait.h>中定义了用于解析等待状态值的一组标准宏。对自wait()或waitpid()返回的status值进行处理。

    WIFEXITED(staus) :若子进程正常结束则返回true。此时,可以通过 WEXITSTATUS(status) 返回子进程的退出状态。

    WIFSIGNALED(status) :若子进程是被信号杀掉则返回真。此时,可以通过 WTERMSIG(status) 返回导致子进程终止的信号编号。若子进程产生内核转储文件,则宏 WCOREDUMP(status) 返回真。

    WIFSTOPPED(status) :若子进程因信号而停止,则返回真。此时, WSTOPSIG(status) 返回导致子进程停止的信号编号。

    WIFCONTINUED(status) :若子进程收到SIGCONT而恢复执行,则返回真。

    #include <sys/wait.h>
    #include "print_wait_status.h"          /* Declares printWaitStatus() */
    #include "tlpi_hdr.h"
    main(int argc, char *argv[])
        int status;
        pid_t childPid;
        if (argc > 1 && strcmp(argv[1], "--help") == 0)
            usageErr("%s [exit-status]\n", argv[0]);
        switch (fork()) {
        case -1: errExit("fork");
        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(getInt(argv[1], 0, "exit-status"));-----------------------------直接以退出码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
                        );-----------------------------------------------------------等待进程组标识符与当前进程pid绝对值相等的所有子进程。
                if (childPid == -1)
                    errExit("waitpid");----------------------------------------------返回-1表示错误。
                /* 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);---------------------------------------打印status返回信息。
                if (WIFEXITED(status) || WIFSIGNALED(status))
                    exit(EXIT_SUCCESS);
    

    printWaitStatus()解析status,并输出可读性文本。

    void                    /* Examine a wait() status using the W* macros */
    printWaitStatus(const char *msg, int status)
        if (msg != NULL)
            printf("%s", msg);
        if (WIFEXITED(status)) {---------------------------------------status低8位为0,表示正常终止。退出状态在高8位。
            printf("child exited, status=%d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {------------------------------低7位为终止信号,高8位未用。
            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))-------------------------------------bit7表示是否core dumped。
                printf(" (core dumped)");
    #endif
            printf("\n");
        } else if (WIFSTOPPED(status)) {-------------------------------低8位为0x7f表示被信号所终止,高8位。
            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)) {-----------------------------status为0xffff表示通过信号恢复执行。
            printf("child continued\n");
    #endif
        } else {            /* Should never happen */
            printf("what happened to this child? (status=%x)\n",
                    (unsigned int) status);
    

    正常退出:

    ./child_status 128
    Child started with PID = 22743
    waitpid() returned: PID=22743; status=0x8000 (128,0)---------返回值status为0x8000,低8位为0;高8位为0x80,返回值为128。
    child exited, status=128

    为信号所杀:

    ./child_status &
    [1] 22748
    al@al-B250-HD3:~/tlpi/procexec$ Child started with PID = 22749al@al-B250-HD3:~/tlpi/procexec$ kill -SIGILL 22749
    al@al-B250-HD3:~/tlpi/procexec$ waitpid() returned: PID=22749; status=0x0084 (0,132)--------status为0x0084,低8位为0x84,bit7位1表示core dumped,信号为4表示SIGILL;高8位为0x00。
    child killed by signal 4 (Illegal instruction) (core dumped)

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

     默认情况下某些信号会终止进程,如果希望在进程终止之前执行一些清理步骤。可以设置一个处理程序来捕获这些信号,清理完之后再终止进程。

    void handler(int sig)
      /* Perform cleanup steps */
      signal(sig, SIG_DFL); /* Disestablish handler */
      raise(sig); /* Raise signal again */
    

    1.5 系统调用waitid()

    waitid()返回子进程的状态,相对于waitpid()提供了扩展功能。

    #include <sys/wait.h>
    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
        Returns 0 on success or if WNOHANG was specified and there were no children to wait for, or –1 on error

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

  • 如果idtype为P_ALL,则等待任何子进程,同时忽略id值。
  • 如果idtype为P_PID,则等待进程ID为id进程的子进程。
  • 如果idtype为P_PGID,则等待进程组ID为id给进程的所有子进程。
  • waitid()通过options提供更为精确控制:

    WEXITED:等待已终止的子进程,而无论其是否正常返回。

    WSTOPPED:等待已通过信号而停止的子进程。

    WCONTINUED:等待经由信号SIGCONT而恢复的子进程。

     WNOHANG:如果参数pid所指定的子进程并未发生状态改变,则立即返回,而不会阻塞,亦即poll;waitpid()返回0,如果调用进程并无与pid匹配的子进程,则waitpid()报错,将错误号置为ECHILD。

    WNOWAIT:如果指定了WNOWAIT,则会返回子进程状态,但子进程依然处于可等待的状态,稍后可在此等待并获取相同信息。

    执行成功waitid()返回0,且会更新指针infop所指向的siginfo_t结构,已包含子进程的相关信息。

    结构siginfo_t字段如下:

    1.6 系统调用wait3()和wait4()

    系统调用wait3()和wait4()执行与waitpid()类似的工作,主要差别在于参数rusage所指向的结构中返回终止子进程的资源使用情况。

    #define _BSD_SOURCE /* Or #define _XOPEN_SOURCE 500 for wait3() */
    #include <sys/resource.h>
    #include <sys/wait.h>
    pid_t wait3(int *status, int options, struct rusage *rusage);
    pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
      Both return process ID of child, or –1 on error

    struct rusage结构如下:

    struct    rusage {
        struct timeval ru_utime;    /* user time used */
        struct timeval ru_stime;    /* system time used */
        __kernel_long_t    ru_maxrss;    /* maximum resident set size */
        __kernel_long_t    ru_ixrss;    /* integral shared memory size */
        __kernel_long_t    ru_idrss;    /* integral unshared data size */
        __kernel_long_t    ru_isrss;    /* integral unshared stack size */
        __kernel_long_t    ru_minflt;    /* page reclaims */
        __kernel_long_t    ru_majflt;    /* page faults */
        __kernel_long_t    ru_nswap;    /* swaps */
        __kernel_long_t    ru_inblock;    /* block input operations */
        __kernel_long_t    ru_oublock;    /* block output operations */
        __kernel_long_t    ru_msgsnd;    /* messages sent */
        __kernel_long_t    ru_msgrcv;    /* messages received */
        __kernel_long_t    ru_nsignals;    /* signals received */
        __kernel_long_t    ru_nvcsw;    /* voluntary context switches */
        __kernel_long_t    ru_nivcsw;    /* involuntary " */
    

    wait3()和wait4()的区别在于,wait3()等待任意子进程,wait4()可以用于等待选定的一个或多个子进程。

    除了获取rusage信息外,wait3()等同于waitpid(-1, &status, options);wait4()等同于wiatpid(pid, &status, options)。

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

    PS:这两系统调用可移植性较差

    2. 孤儿进程与僵尸进程

    谁是孤儿子进程的父进程?

    进程ID为1的init进程是众进程之祖,init会接管孤儿进程。

    父进程执行wait()之前,其子进程就已经终止,这将会发生什么?

    即使子进程已经结束,系统任然允许其父进程在之后的某一时刻执行wait()。该进程所唯一保留的是内核进程表中的一条记录,其中包含子进程ID、终止状态、资源使用情况等。

    父进程执行wait()之后,由于不再需要子进程所剩余的最后信息,内核将删除僵尸进程。如果父进程未执行wait()随即退出,那么init进程将接管子进程并自动调用wait(),从系统中移除僵尸进程。

    如果父进程创建了某一子进程,但并未执行wait(),那么内核进程表中降为盖子进程永久保留一条记录。如果存在大量僵尸进程,势必将填满内核进程表,从而阻碍新进程的创建。

    既然无法用新号杀死僵尸进程,那么从系统中移除的唯一方法就是杀掉它的父进程,此时init进程将接管和等待这些僵尸进程,从而从系统中将他们清理掉。

    #include <signal.h>
    #include <libgen.h>             /* For basename() declaration */
    #include "tlpi_hdr.h"
    #define CMD_SIZE 200
    main(int argc, char *argv[])
        char cmd[CMD_SIZE];
        pid_t childPid;
        setbuf(stdout, NULL);       /* Disable buffering of stdout */
        printf("Parent PID=%ld\n", (long) getpid());
        switch (childPid = fork()) {
        case -1:
            errExit("fork");
        case 0:     /* Child: immediately exits to become zombie */
            printf("Child (PID=%ld) exiting\n", (long) getpid());
            _exit(EXIT_SUCCESS);----------------------------------------子进程打印一条消息后,立即退出。此时变成了僵尸进程。
        default:    /* Parent */
            sleep(3);               /* Give child a chance to start and exit */
            snprintf(cmd, CMD_SIZE, "ps | grep %s", basename(argv[0]));
            system(cmd);            /* View zombie child */-------------等待子进程变成僵尸进程,ps查看进程情况。
            /* Now send the "sure kill" signal to the zombie */
            if (kill(childPid, SIGKILL) == -1)--------------------------发送SIGKILL信号杀死僵尸子进程。
                errMsg("kill");
            sleep(3);               /* Give child a chance to react to signal */
            printf("After sending SIGKILL to zombie (PID=%ld):\n", (long) childPid);
            system(cmd);            /* View zombie child again */-------等待SIGKILL信号发送到僵尸子进程,并作出反应。
            exit(EXIT_SUCCESS);
    

    使用如下命令执行结果如下:

    ./make_zombie && ps -a | grep make_zombile
    Parent PID=14727
    Child (PID=14728) exiting
    14727 pts/22   00:00:00 make_zombie
    14728 pts/22   00:00:00 make_zombie <defunct>
    After sending SIGKILL to zombie (PID=14728):
    14727 pts/22   00:00:00 make_zombie
    14728 pts/22   00:00:00 make_zombie <defunct>

    说明SIGKILL对僵尸进程不起作用,僵尸进程在其父进程退出后也同样被回收。

    3. SIGHLD信号

    子进程的终止属于异步事件。即使父进程向子进程发送SIGKILL信号,子进程终止的确切时间还依赖与系统的调度:子进程下一次在何时使用CPU。

    父进程使用wait()来防止僵尸子进程的累积,以及如下两种方法来避免这一问题:

  • 父进程调用不带WNOHANG标志的wait()或waitpid(),此时如果尚无已经终止的子进程,那么调用将会阻塞。
  • 父进程周期性地调用带有WNOHANG标志的waitpid(),执行针对已终止子进程的非阻塞式检查。
  • 第一种方法会造成父进程阻塞;第二种造成CPU资源浪费,增加应用复杂度。

    为了规避这些问题,可以采用这对SIGHLD信号的处理程序。

    3.1 为SIGCHLD建立信号处理程序

    无论一个子进程何时终止,系统都会向其父进程发送SIGHLD信号。

    系统对SIGCHLD信号默认处理是将其忽略,如果通过安装处理程序来捕获它。

    会面临如下问题:当调用信号处理程序时,会暂时将引发调用的信号阻塞起来,且不会带SIGCHLD之类的标准信号进行排队处理。当SIGHLD信号处理程序正在为一个终止的子程序运行时,如果相继有两个子进程终止,即使产生了两次SIGHLD信号,父进程也只能捕获到一个。如果父进程的SIGHLD信号处理程序每次只调用一次wait(),那么一些僵尸子进程可能会成为漏网之鱼。

    下面的示例程序演示了如何写SIGCHLD处理函数,并且保证不会遗漏。而且说明了SIGCHLD信号不会排队,阻塞期间多次触发,在解除阻塞之后,只会执行一次。

    #include <signal.h>
    #include <sys/wait.h>
    #include "print_wait_status.h"
    #include "curr_time.h"
    #include "tlpi_hdr.h"
    static volatile int numLiveChildren = 0;
                    /* Number of children started but not yet waited on */
    static void
    sigchldHandler(int sig)
        int status, savedErrno;
        pid_t childPid;
        /* UNSAFE: This handler uses non-async-signal-safe functions
           (printf(), printWaitStatus(), currTime(); see Section 21.1.2) */
        savedErrno = errno;         /* In case we modify 'errno' */
        printf("%s handler: Caught SIGCHLD\n", currTime("%T"));
        /* Do nonblocking waits until no more dead children are found */
        while ((childPid = waitpid(-1, &status, WNOHANG)) > 0) {
            printf("%s handler: Reaped child %ld - ", currTime("%T"),
                    (long) childPid);
            printWaitStatus(NULL, status);
            numLiveChildren--;
        if (childPid == -1 && errno != ECHILD)
            errMsg("waitpid");
        sleep(5);           /* Artificially lengthen execution of handler */
        printf("%s handler: returning\n", currTime("%T"));
        errno = savedErrno;
    main(int argc, char *argv[])
        int j, sigCnt;
        sigset_t blockMask, emptyMask;
        struct sigaction sa;
        if (argc < 2 || strcmp(argv[1], "--help") == 0)
            usageErr("%s child-sleep-time...\n", argv[0]);
        setbuf(stdout, NULL);       /* Disable buffering of stdout */
        sigCnt = 0;
        numLiveChildren = argc - 1;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        sa.sa_handler = sigchldHandler;
        if (sigaction(SIGCHLD, &sa, NULL) == -1)-------------------------------------------------设置SIGCHLD信号处理函数。
            errExit("sigaction");
        /* Block SIGCHLD to prevent its delivery if a child terminates
           before the parent commences the sigsuspend() loop below */
        sigemptyset(&blockMask);
        sigaddset(&blockMask, SIGCHLD);
        if (sigprocmask(SIG_SETMASK, &blockMask, NULL) == -1)
            errExit("sigprocmask");
        /* Create one child process for each command-line argument */
        for (j = 1; j < argc; j++) {
            switch (fork()) {
            case -1:
                errExit("fork");
            case 0:         /* Child - sleeps and then exits */
                sleep(getInt(argv[j], GN_NONNEG, "child-sleep-time"));-----------------------------依次创建3个子进程,分别睡眠1、2、 4秒之后退出。
                printf("%s Child %d (PID=%ld) exiting\n", currTime("%T"),
                        j, (long) getpid());
                _exit(EXIT_SUCCESS);
            default:        /* Parent - loops to create next child */
                break;
        /* Parent comes here: wait for SIGCHLD until all children are dead */
        sigemptyset(&emptyMask);
        while (numLiveChildren > 0) {
            if (sigsuspend(&emptyMask) == -1 && errno != EINTR)------------------------------------在此等待SIGCHLD信号。
                errExit("sigsuspend");
            sigCnt++;
        printf("%s All %d children have terminated; SIGCHLD was caught "
                "%d times\n", currTime("%T"), argc - 1, sigCnt);
        exit(EXIT_SUCCESS);
    

    分别创建3个子进程,执行结果如下:

    ./multi_SIGCHLD 1 2 4
    10:27:10 Child 1 (PID=14225) exiting------------------------------睡眠1秒的进程退出。
    10:27:10 handler: Caught SIGCHLD----------------------------------父进程收到SIGHLD信号,并进入信号处理函数。
    10:27:10 handler: Reaped child 14225 - child exited, status=0-----waitpid()回收子进程资源,然后开始睡眠5秒。此时SIGCHLD信号处理是被阻塞的。
    10:27:11 Child 2 (PID=14226) exiting
    10:27:13 Child 3 (PID=14227) exiting------------------------------2、4秒睡眠的子进程依次退出,但是由于此时SIGCHLD信号处于阻塞状态,所以不会立即被处理。
    10:27:15 handler: returning---------------------------------------直到SIGCHLD处理函数退出,对SIGCHLD的阻塞解除。
    10:27:15 handler: Caught SIGCHLD----------------------------------再次触发SIGCHLD信号处理函数,但是仅触发一次。
    10:27:15 handler: Reaped child 14226 - child exited, status=0
    10:27:15 handler: Reaped child 14227 - child exited, status=0
    10:27:20 handler: returning---------------------------------------第二次SIGCHLD信号处理函数退出。
    10:27:20 All 3 children have terminated; SIGCHLD was caught 2 times---整个流程结束。

    3.2 向已停止的子进程发送SIGCHLD信号

    当信号导致子进程停止时,父进程也就有可能收到SIGCHLD信号。调用sigaction()设置SIGCHLD信号处理程序时,如传入SA_NOCLDSTOP标志即可控制主义行为。如未使用,系统会在子进程停止时向父进程发送SIGCHLD信号。反之,就不会因子进程的停止而发出SIGCHLD信号。

    3.3 忽略终止的子进程

    将对SIGCHLD的处置显式置为SIG_IGN,系统从而会将其后终止的子进程立即删除,毋庸转为僵尸进程。将子进程的状态弃之不问,故而后续的wait()调用不会返回子进程的任何信息。

    4. 小结

    联系方式:arnoldlu@qq.com