《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()调用不会返回子进程的任何信息。