LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

《Linux/UNIX系统编程手册》第26章 监控子进程

关键词: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()的结果返回。
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)
        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"

int
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

int
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;
}

int
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. 小结

posted on 2020-09-05 10:53  ArnoldLu  阅读(926)  评论(0编辑  收藏  举报

导航