TLPI 第26 章 读书笔记:Monitoring Child Processes

TLPI 第26 章 读书笔记:Monitoring Child Processes
笔记和练习博客总目录见开始读TLPI。在许多应用程序设计中父进程需要知道其某个子进程何时状态发生变化——无论是子进程终止还是被信号停止。本章介绍了两种用于监控子进程的技术wait() 系统调用及其变体和使用 SIGCHLD 信号。26.1 Waiting on a Child Process在许多应用中一个父进程创建子进程时父进程能够监控子进程以了解它们何时以及如何终止是很有用的。这个功能由 wait() 及若干相关的系统调用提供。26.1.1 The wait() System Callwait() 系统调用等待调用进程的一个子进程终止并将该子进程的终止状态返回到 status 指向的缓冲区中。#includesys/wait.hpid_twait(int*status);wait() 系统调用执行以下操作如果调用进程的任何之前未等待的子进程尚未终止则该调用会阻塞直到其中一个子进程终止。如果在调用时某个子进程已经终止wait() 会立即返回。如果 status 不为 NULL有关子进程如何终止的信息会返回到 status 指向的整数中。我们在第 26.1.3 节中描述了返回在 status 中的信息。内核将进程的 CPU 时间第 10.7 节和资源使用统计第 36.1 节添加到该父进程所有子进程的运行总计中。作为其函数结果wait() 返回已终止子进程的进程 ID。在出错时wait() 返回 –1。一个可能的错误是调用进程没有之前未等待的子进程这由 errno 值 ECHILD 表示。这意味着我们可以使用以下循环来等待调用进程的所有子进程终止while((childPidwait(NULL))!-1)continue;if(errno!ECHILD)/* An unexpected error... */errExit(wait);示例 26-1 展示了 wait() 的使用。该程序创建多个子进程每个子进程对应一个整数命令行参数。每个子进程会睡眠命令行参数中指定的秒数然后退出。与此同时在所有子进程创建完成后父进程会重复调用 wait() 来监控子进程的终止。这个循环会持续进行直到 wait() 返回 –1。这并不是唯一的方法我们也可以在已终止的子进程数量 numDead 与创建的子进程数量相匹配时退出循环。下面的 shell 会话日志显示了使用该程序创建三个子进程时的情况$ ./multi_wait714[00:37:51]child1started with PID15243, sleeping7seconds[00:37:51]child3started with PID15245, sleeping4seconds[00:37:51]child2started with PID15244, sleeping1seconds[00:37:53]wait()returned child PID15244(numDead1)[00:37:55]wait()returned child PID15245(numDead2)[00:37:59]wait()returned child PID15243(numDead3)Nomorechildren - bye!如果在某一时刻有多个已终止的子进程SUSv3 并未指定这些子进程将按照什么顺序被一系列 wait() 调用收回也就是说顺序取决于实现。即使在不同版本的 Linux 内核中行为也会有所不同。Listing 26-1: Creating and waiting for multiple children// procexec/multi_wait.c// 代码略26.1.2 The waitpid() System Callwait() 系统调用有一些限制而 waitpid() 的设计就是为了解决这些问题如果父进程创建了多个子进程无法使用 wait()等待特定子进程的完成我们只能等待下一个终止的子进程。如果没有子进程已终止wait() 总是会阻塞。有时更希望执行非阻塞等待这样如果没有子进程已终止我们可以立即得到这一信息。使用 wait()我们只能了解已经终止的子进程。无法在子进程被信号如 SIGSTOP 或 SIGTTIN停止时收到通知也不能在子进程被 SIGCONT 信号恢复时收到通知。 以上3个特性等待特定子进程通过pid参数实现非阻塞和收到通知通过options参数实现。#includesys/wait.hpid_twaitpid(pid_tpid,int*status,intoptions);waitpid() 的返回值和状态参数与 wait() 相同。有关 status 返回值的解释请参见第 26.1.3 节。pid 参数用于选择要等待的子进程如下所示如果 pid 大于 0则等待进程 ID 等于 pid 的子进程。如果 pid 等于 0则等待与调用者父进程属于同一进程组的任意子进程。我们在第 34.2 节中描述了进程组。如果 pid 小于 -1则等待进程组标识符等于 pid 绝对值的任意子进程。如果 pid 等于 -1则等待任意子进程。调用 wait(status) 等同于调用 waitpid(-1, status, 0)。options 参数是一个位掩码可以包含通过 OR零个或多个以下标志所有标志均在 SUSv3 中指定WUNTRACED除了返回已终止子进程的信息外还在子进程被信号停止时返回信息。WCONTINUED自 Linux 2.6.10 起还返回那些已通过 SIGCONT 信号恢复的停止子进程的状态信息。WNOHANG如果由 pid 指定的任何子进程尚未改变状态则立即返回而不是阻塞即执行“轮询”。在这种情况下waitpid() 的返回值为 0。如果调用进程没有与 pid 中指定匹配的子进程waitpid() 将失败并返回错误 ECHILD。我们在清单 26-3 中演示了 waitpid() 的用法。在其对 waitpid() 的原理说明中SUSv3 指出名称 WUNTRACED 是该标志在 BSD 中起源的历史遗留产物在 BSD 中进程可以通过两种方式之一被停止作为 ptrace() 系统调用跟踪的结果或者被信号停止即未被跟踪。当子进程被 ptrace() 跟踪时任何信号的传递除了 SIGKILL都会导致子进程停止并相应地向父进程发送 SIGCHLD 信号。即使子进程忽略该信号这种行为仍然会发生。然而如果子进程阻塞该信号则它不会被停止除非信号是 SIGSTOP因为 SIGSTOP 无法被阻塞。26.1.3 The Wait Status Valuewait() 和 waitpid() 返回的状态值允许我们区分子进程的以下事件子进程通过调用 _exit()或 exit()终止并指定一个整数退出状态。子进程因未处理的信号而被终止。子进程因信号而停止并且 waitpid() 使用 WUNTRACED 标志调用。子进程因 SIGCONT 信号而恢复并且 waitpid() 使用 WCONTINUED 标志调用。我们使用“等待状态”这个术语来涵盖上述所有情况。“终止状态”这个称呼用来指前两种情况。在 shell 中我们可以通过检查变量$?的内容来获取上一个执行命令的终止状态。虽然定义为 int 类型但实际上只使用 status 指向的值的最低 2 个字节。这 2 个字节的填充方式取决于子进程发生了上述哪种事件如图 26-1 所示。图 26-1 显示了 Linux/x86-32 的等待状态值的布局。具体细节在不同实现中有所不同。SUSv3 并未指定该信息的特定布局甚至不要求其包含在 status 指向的值的底部 2 个字节中。可移植的应用程序应始终使用本节中描述的宏来检查该值而不是直接检查其位掩码组件。Figure 26-1: Value returned in the status argument of wait() and waitpid() 上图是x86架构下的也就是little-endian。sys/wait.h 头文件定义了一组标准宏可用于解析 wait 状态值。当应用于 wait() 或 waitpid() 返回的状态值时下面列表中的宏中只有一个会返回 true。附加宏提供了进一步解析状态值的方法如列表中所示。WIFEXITED(status)如果子进程正常退出则该宏返回 true。在这种情况下宏 WEXITSTATUS(status) 返回子进程的退出状态。如第 25.1 节所述父进程只能获得子进程退出状态的最低有效字节。WIFSIGNALED(status)如果子进程被信号终止则该宏返回 true。在这种情况下宏 WTERMSIG(status) 返回导致进程终止的信号编号而宏 WCOREDUMP(status) 返回 true 如果子进程生成了核心转储文件。WCOREDUMP() 宏未被 SUSv3 指定但在大多数 UNIX 实现中可用。WIFSTOPPED(status)如果子进程被信号暂停则该宏返回 true。在这种情况下宏 WSTOPSIG(status) 返回导致进程暂停的信号编号。WIFCONTINUED(status)这个宏如果子进程因接收到 SIGCONT 信号而继续执行则返回 true。自 Linux 2.6.10 起可用。注意虽然名称 status 也用于上述宏的参数但它们需要一个普通整数而不是像 wait() 和 waitpid() 那样的整数指针。Example program清单26-2中的printWaitStatus()函数使用了上述所有宏。该函数解析并打印等待状态值的内容。Listing 26-2: Displaying the status value returned by wait() and related calls// procexec/print_wait_status.c// 代码略printWaitStatus() 函数在清单 26-3 中使用。该程序创建一个子进程该子进程要么持续循环调用 pause()此期间可以向子进程发送信号要么如果提供了一个整数命令行参数则使用该整数作为退出状态立即退出。同时父进程通过 waitpid() 监控子进程打印返回的状态值并将该值传递给 printWaitStatus()。当父进程检测到子进程要么正常退出要么被信号终止时它将退出。下面的 shell 会话显示了清单 26-3 中程序的几个示例运行。我们首先创建一个立即以状态 23 退出的子进程$ ./child_status23Child started with PID15271waitpid()returned:PID15271;status0x1700(23,0)child exited,status23在下一次运行中我们在后台启动程序然后向子进程发送 SIGSTOP 和 SIGCONT 信号$ ./child_status[1]15272$ Child started with PID15273$kill-STOP15273$ waitpid()returned:PID15273;status0x137f(19,127)child stopped by signal19(Stopped(signal))$kill-CONT15273waitpid()returned:PID15273;status0xffff(255,255)child continued输出的最后两行只会出现在 Linux 2.6.10 及更高版本上因为早期的内核不支持 waitpid() 的 WCONTINUED 选项。这个 shell 会话略显难以阅读因为在某些情况下后台程序的输出会与 shell 产生的提示符交错。我们通过发送 SIGABRT 信号来终止子进程继续 shell 会话$kill-ABRT15273$ waitpid()returned:PID15273;status0x0086(0,134)child killed by signal6(Aborted)(core dumped)[1] Done ./child_status $ulimit-cunlimited $ coredumpctl list TIME PIDUIDGID SIG COREFILE EXE SIThu2026-03-26 00:59:42 UTC1527310001000SIGABRT present /home/vagrant/tlpi-book/procexec/child_status19.$ $ coredumpctl info15273PID:15273(child_status)UID:1000(vagrant)GID:1000(vagrant)Signal:6(ABRT)Timestamp: Thu2026-03-26 00:59:42 UTC(1min 47s ago)Command Line: ./child_status Executable: /home/vagrant/tlpi-book/procexec/child_status Control Group: /user.slice/user-1000.slice/session-3.scope Unit: session-3.scope Slice: user-1000.slice Session:3OwnerUID:1000(vagrant)Boot ID: fe63da9472cc4d49957840e55228ad82 Machine ID: 822b376fd7ec4c7095595a8fa73a3b75 Hostname: ol9-vagrant Storage: /var/lib/systemd/coredump/core.child_status.1000.fe63da9472cc4d49957840e55228ad82.15273.177448Size on Disk:19.7K Message: Process15273(child_status)of user1000dumped core. Stack trace of thread15273:#0 0x00007f1545ad3247 pause (libc.so.6 0xda247)#1 0x0000000000401337 main (child_status 0x1337)#2 0x00007f1545a23610 __libc_start_call_main (libc.so.6 0x2a610)#3 0x00007f1545a236c0 __libc_start_mainGLIBC_2.34 (libc.so.6 0x2a6c0)#4 0x00000000004011c5 _start (child_status 0x11c5)ELF object binary architecture: AMD x86-64虽然 SIGABRT 的默认操作是生成核心转储文件并终止进程但没有生成核心文件。这是因为核心转储被禁用了——RLIMIT_CORE 软资源限制第 36.3 节用于指定核心文件的最大大小被设置为 0如上面的 ulimit 命令所示。我的实验环境中核心文件大小没有限制Listing 26-3: Using waitpid() to retrieve the status of a child process// procexec/child_status.c// 代码略26.1.4 Process Termination from a Signal Handler如表20-1第396页所示某些信号默认会终止一个进程。在某些情况下我们可能希望在进程终止之前执行特定的清理步骤。为此我们可以安排一个处理程序来捕捉这些信号执行清理步骤然后终止进程。如果我们这样做就应当记住进程的终止状态可以通过 wait() 或 waitpid() 被其父进程获取。例如从信号处理程序中调用 _exit(EXIT_SUCCESS) 将使父进程认为子进程成功终止。如果子进程需要通知父进程它因为一个信号而终止那么子进程的信号处理程序应首先取消自身的注册然后再次发送相同的信号这一次将终止进程。信号处理程序将包含如下代码voidhandler(intsig){/* Perform cleanup steps */signal(sig,SIG_DFL);/* Disestablish handler */raise(sig);/* Raise signal again */}26.1.5 The waitid() System Call像 waitpid() 一样waitid() 返回子进程的状态。然而waitid() 提供了 waitpid() 所不具备的额外功能。这个系统调用源自 System V但现在已在 SUSv3 中规定。它在 Linux 2.6.9 内核中被添加。在 Linux 2.6.9 之前glibc 提供了 waitid() 的实现版本。然而由于该接口的完整实现需要内核支持glibc 的实现没有提供超过使用 waitpid() 可用的功能。#includesys/wait.hintwaitid(idtype_tidtype,id_tid,siginfo_t*infop,intoptions);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 中按位或OR下列一个或多个标志来控制WEXITED等待已终止正常或异常的子进程。WSTOPPED等待因信号而停止的子进程。WCONTINUED等待因 SIGCONT 信号而恢复的子进程。以下附加标志可以在选项中使用按位或操作OR:WNOHANG此标志的含义与 waitpid() 中相同。如果没有任何符合 id 规范的子进程有状态信息可返回则立即返回轮询。在这种情况下waitid() 的返回值为 0。如果调用进程没有任何符合 id 规范的子进程则 waitid() 将返回错误 ECHILD。WNOWAIT通常一旦使用 waitid() 等待了子进程那么该“状态事件”就会被消耗。然而如果指定了 WNOWAIT则会返回子进程状态但子进程仍然保持可等待状态我们可以稍后再次等待它以检索相同的信息。成功时waitid() 返回 0并且由 infop 指向的 siginfo_t 结构见第 21.4 节会被更新以包含有关子进程的信息。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 结构清零。这提供了一种区分这两种可能性的方式我们可以检查 si_pid 的值是 0 还是非零值。不幸的是这种行为并非 SUSv3 的要求一些 UNIX 实现会在这种情况下保持 siginfo_t 结构不变。预计 SUSv4 的未来勘误可能会增加要求在这种情况下将 si_pid 和 si_signo 清零。区分这两种情况的唯一可移植方法是在调用 waitid() 之前先将 siginfo_t 结构清零如以下代码所示siginfo_tinfo;...memset(info,0,sizeof(siginfo_t));if(waitid(idtype,id,info,options|WNOHANG)-1)errExit(waitid);if(info.si_pid0){/* No children changed state */}else{/* A child changed state; details are provided in info */}26.1.6 The wait3() and wait4() System Callswait3() 和 wait4() 系统调用执行的任务与 waitpid() 类似。主要的语义差异在于wait3() 和 wait4() 会在 rusage 参数指向的结构中返回已终止子进程的资源使用信息。这些信息包括进程使用的 CPU 时间和内存管理统计信息。我们将在第 36.1 节中详细讨论 rusage 结构在那里我们将描述 getrusage() 系统调用。#define_BSD_SOURCE/* Or #define _XOPEN_SOURCE 500 for wait3() */#includesys/resource.h#includesys/wait.hpid_twait3(int*status,intoptions,structrusage*rusage);pid_twait4(pid_tpid,int*status,intoptions,structrusage*rusage);除去 rusage 参数的使用调用 wait3() 相当于以下 waitpid() 调用waitpid(-1,status,options);同样wait4() 相当于以下调用waitpid(pid,status,options);换句话说wait3() 等待任何子进程而 wait4() 可以用于选择特定的一个或多个子进程来等待。在一些 UNIX 实现中wait3() 和 wait4() 仅为已终止的子进程返回资源使用信息。在 Linux 上如果在选项中指定 WUNTRACED 标志也可以检索已停止子进程的资源使用信息。这两个系统调用的名称是指它们各自接受的参数数量。这两个系统调用起源于 BSD但现在在大多数 UNIX 实现中都可用。它们都没有在 SUSv3 中标准化。SUSv2 确实指定了 wait3()但标记为 LEGACY。在本书中我们通常避免使用 wait3() 和 wait4()。通常我们不需要这些调用返回的额外信息。此外缺乏标准化也限制了它们的可移植性。26.2 Orphans and Zombies父进程和子进程的生命周期通常不相同——要么父进程比子进程活得久要么相反。这引出了两个问题谁会成为孤儿子进程的父进程孤儿子进程会被 init 进程收养init 是所有进程的祖先进程其进程 ID 为 1。换句话说在子进程的父进程终止后调用 getppid() 将返回值 1。这可以用来判断子进程的真实父进程是否还存活这里假设子进程是由除 init 以外的进程创建的。通过使用 Linux 特有的 prctl() 系统调用的 PR_SET_PDEATHSIG 操作可以安排在进程成为孤儿时接收指定的信号。在父进程有机会执行 wait() 之前先终止的子进程会发生什么关键在于尽管子进程已经完成了它的工作但父进程仍然应该被允许在稍后的时间执行 wait() 来确定子进程是如何终止的。内核通过将子进程变为僵尸进程来处理这种情况。这意味着子进程所持有的大部分资源会释放回系统以供其他进程重用。唯一保留的部分是在内核的进程表中的一个条目用于记录子进程的一些信息包括子进程的进程 ID、终止状态和资源使用统计信息第 36.1 节。关于僵尸进程UNIX 系统像电影一样——僵尸进程无法通过信号终止甚至无法通过银弹SIGKILL 终止。这确保了父进程最终总能执行 wait()。当父进程执行 wait() 时内核会移除僵尸进程因为对该子进程的最后信息不再需要。另一方面如果父进程在没有执行 wait() 的情况下终止那么 init 进程会收养该子进程并自动执行 wait()从而将僵尸进程从系统中移除。**如果父进程创建了子进程但未能执行 wait()那么僵尸子进程的条目将在内核的进程表中无限期保留。**如果创建了大量这样的僵尸子进程它们最终会填满内核进程表阻止新进程的创建。由于僵尸进程不能被信号终止唯一将它们从系统中移除的方法是终止它们的父进程或等待其退出此时僵尸进程会被 init 收养并等待从而被从系统中移除。这些语义对设计长寿命父进程具有重要意义例如创建大量子进程的网络服务器和 shell。换句话说在此类应用中父进程应执行 wait() 调用以确保死去的子进程总是从系统中移除而不会成为长期存在的僵尸。父进程可以同步执行这些 wait() 调用也可以异步执行以响应 SIGCHLD 信号的传递如第 26.3.1 节所述。清单 26-4 演示了僵尸的创建以及僵尸无法被 SIGKILL 终止。当我们运行此程序时会看到以下输出$ ./make_zombie ParentPID15367Child(PID15368)exiting15367pts/0 00:00:00 make_zombie15368pts/0 00:00:00 make_zombiedefunctAfter sending SIGKILL to zombie(PID15368):15367pts/0 00:00:00 make_zombie15368pts/0 00:00:00 make_zombiedefunct在上述输出中我们看到 ps(1) 显示字符串defunct来表示处于僵尸状态的进程。 defunct意为“不复存在”。清单 26-4 中的程序使用 system() 函数来执行其字符字符串参数中给出的 shell 命令。我们在第 27.6 节中详细描述了 system()。Listing 26-4: Creating a zombie child process// procexec/make_zombie.c// 代码略26.3 The SIGCHLD Signal子进程的终止是一个异步发生的事件。父进程无法预测其子进程何时会终止。即使父进程向子进程发送 SIGKILL 信号终止的确切时间仍然取决于子进程下一次何时被调度使用 CPU。我们已经看到父进程应该使用 wait()或类似函数来防止僵尸子进程的积累并且已经考察了两种实现方法父进程可以调用 wait() 或 waitpid() 而不指定 WNOHANG 标志这种情况下如果子进程尚未终止调用将会阻塞。父进程可以定期通过调用 waitpid() 并指定 WNOHANG 标志进行非阻塞检查轮询以发现死亡子进程。这两种方法都有不便之处。一方面我们可能不希望父进程被阻塞等待子进程终止。另一方面重复进行非阻塞 waitpid() 调用会浪费 CPU 时间并增加应用设计的复杂性。为了避免这些问题我们可以为 SIGCHLD 信号设置一个处理程序。26.3.1 Establishing a Handler for SIGCHLD每当一个子进程终止时SIGCHLD 信号会发送给父进程。默认情况下这个信号会被忽略但我们可以通过安装一个信号处理程序来捕捉它。在信号处理程序中我们可以使用 wait()或类似函数来回收僵尸子进程。然而这种方法有一个需要注意的微妙之处。在第 20.10 节和第 20.12 节中我们观察到当信号处理程序被调用时导致其调用的信号会被暂时阻塞除非指定了 sigaction() 的 SA_NODEFER 标志并且标准信号其中 SIGCHLD 就是一个不会排队。因此如果第二个和第三个子进程在一个 SIGCHLD 处理程序正在执行以处理已经终止的子进程时迅速终止那么尽管会生成两次 SIGCHLD 信号但它只会被排队一次传递给父进程。结果是如果父进程的 SIGCHLD 处理程序每次调用时只执行一次 wait()处理程序可能未能回收某些僵尸子进程。解决方法是在 SIGCHLD 处理程序中循环重复调用带有 WNOHANG 标志的 waitpid()直到没有更多需要回收的已终止子进程。通常SIGCHLD 处理程序的主体仅由以下代码组成它会在不检查子进程状态的情况下回收任何已终止的子进程while(waitpid(-1,NULL,WNOHANG)0)continue;上述循环会一直继续直到 waitpid() 返回 0表示没有更多僵尸子进程或者返回 -1表示发生错误可能是 ECHILD意味着没有更多子进程。Design issues for SIGCHLD handlers假设在我们为 SIGCHLD 设置处理程序的时候该进程已经有一个终止的子进程。那么内核会立即为父进程生成 SIGCHLD 信号吗SUSv3 对这一点没有明确规定。一些源自 System V 的实现确实在这种情况下生成 SIGCHLD其他实现包括 Linux则不会。可移植的应用程序可以通过在创建任何子进程之前设置 SIGCHLD 处理程序来屏蔽这种差异。当然这通常也是自然的做法。需要进一步考虑的一个问题是重入性。在第 21.1.2 节中我们指出在信号处理程序中使用系统调用例如 waitpid()可能会改变全局变量 errno 的值。这种变化可能会干扰主程序显式设置 errno例如参见第 35.1 节关于 getpriority() 的讨论或在系统调用失败后检查其值。因此有时有必要在 SIGCHLD 处理程序中将 errno 在进入处理程序时保存到局部变量中然后在返回之前恢复 errno 值。示例请参见清单 26-5。Example program清单 26-5 提供了一个更复杂的 SIGCHLD 处理程序的示例。该处理程序显示每个被收割子进程的进程 ID 和等待状态。为了观察在处理程序已经被调用时多个 SIGCHLD 信号不会排队处理程序的执行通过调用 sleep() 被人为延长。主程序为每个整数型命令行参数创建一个子进程。这些子进程中的每一个都会睡眠其对应命令行参数指定的秒数然后退出。在以下对该程序执行的示例中我们看到即使三个子进程终止SIGCHLD 仍然只排队到父进程两次$ ./multi_SIGCHLD12402:49:46 Child1(PID15525)exiting 02:49:46 handler: Caught SIGCHLD 02:49:46 handler: Reaped child15525- child exited,status002:49:47 Child2(PID15526)exiting 02:49:49 Child3(PID15527)exiting 02:49:51 handler: returning 02:49:51 handler: Caught SIGCHLD 02:49:51 handler: Reaped child15526- child exited,status002:49:51 handler: Reaped child15527- child exited,status002:49:56 handler: returning 02:49:56 All3children have terminated;SIGCHLD was caught2timesListing 26-5: Reaping dead children via a handler for SIGCHLD// procexec/multi_SIGCHLD.c// 代码略26.3.2 Delivery of SIGCHLD for Stopped Children正如可以使用 waitpid() 来监视停止的子进程一样当子进程被信号停止时父进程也可以接收到 SIGCHLD 信号。这个行为由使用 sigaction() 为 SIGCHLD 信号建立处理程序时的 SA_NOCLDSTOP 标志控制。如果省略此标志当一个子进程停止时SIGCHLD 信号会发送给父进程如果该标志存在则停止的子进程不会发送 SIGCHLD 信号。第 22.7 节给出的 signal() 实现未指定 SA_NOCLDSTOP。由于 SIGCHLD 默认被忽略SA_NOCLDSTOP 标志只有在建立 SIGCHLD 处理程序时才有意义。此外SIGCHLD 是唯一一个 SA_NOCLDSTOP 标志有效的信号。SUSv3 还允许当一个停止的子进程通过接收 SIGCONT 信号而恢复运行时发送 SIGCHLD 信号给父进程。这对应于 waitpid() 的 WCONTINUED 标志。此功能自 Linux 内核 2.6.9 起已实现。26.3.3 Ignoring Dead Child Processes处理已终止子进程还有另一种可能的方法。**显式将 SIGCHLD 的处理方式设置为 SIG_IGN 会导致任何随后终止的子进程立即从系统中移除而不是转化为僵尸进程。**在这种情况下由于子进程的状态会被直接丢弃因此随后调用 wait()或类似函数将无法返回已终止子进程的任何信息。请注意尽管 SIGCHLD 的默认处理方式是忽略但显式将其处理方式设置为 SIG_IGN 会导致此处描述的不同行为。在这方面SIGCHLD 在信号中是独特的。在 Linux 上与许多 UNIX 实现一样将 SIGCHLD 的处理方式设置为 SIG_IGN 不会影响任何已存在的僵尸子进程这些进程仍需以通常方式进行等待。在其他一些 UNIX 实现中例如 Solaris 8将 SIGCHLD 的处理方式设置为 SIG_IGN 确实会移除已存在的僵尸子进程。SIG_IGN 对 SIGCHLD 的语义有着悠久的历史起源于 System V。SUSv3 指定了这里描述的行为但在最初的 POSIX.1 标准中这些语义是未定义的。因此在一些较老的 UNIX 实现中忽略 SIGCHLD 对僵尸进程的产生没有影响。唯一具备完全可移植性、避免生成僵尸进程的方式是调用wait()或waitpid()可将其放在 SIGCHLD 信号处理函数内部执行。Deviations from SUSv3 in older Linux kernelsSUSv3 指出如果 SIGCHLD 的处置被设置为 SIG_IGN那么子进程的资源使用信息应被丢弃并且在父进程调用 getrusage() 并指定 RUSAGE_CHILDREN 标志时不应包括在返回的总数中第36.1节。然而在 Linux 2.6.9 内核之前的版本中子进程使用的 CPU 时间和资源会被记录并且可以在调用 getrusage() 时看到。这种不符合情况在 Linux 2.6.9 及之后版本中已修复。将 SIGCHLD 的处置设置为 SIG_IGN 还应防止子进程的 CPU 时间被包括在 times() 返回的结构中第10.7节。然而在 2.6.9 内核之前的 Linux 系统中对于 times() 返回的信息也存在类似的不符合情况。SUSv3 指出如果 SIGCHLD 的处置被设置为 SIG_IGN并且父进程没有已终止且已转化为僵尸但尚未被等待的子进程那么对 wait()或 waitpid()的调用应阻塞直到所有父进程的子进程都已终止此时调用应以错误 ECHILD 结束。Linux 2.6 符合这一要求。然而在 Linux 2.4 及更早版本中wait() 仅会阻塞直到下一个子进程终止然后返回该子进程的进程 ID 和状态即行为与 SIGCHLD 的处置未设置为 SIG_IGN 时相同。The sigaction() SA_NOCLDWAIT flagSUSv3 指定了 SA_NOCLDWAIT 标志该标志可在使用 sigaction() 设置 SIGCHLD 信号的处置时使用。**该标志产生的行为类似于将 SIGCHLD 的处置设置为 SIG_IGN 时的行为。**该标志在 Linux 2.4 及更早版本中未实现但在 Linux 2.6 中已实现。将 SIGCHLD 的处置设置为 SIG_IGN 与使用 SA_NOCLDWAIT 的主要区别在于当使用 SA_NOCLDWAIT 建立处理程序时SUSv3 未规定子进程终止时是否向父进程发送 SIGCHLD 信号。换句话说当指定 SA_NOCLDWAIT 时实施可以发送 SIGCHLD而应用程序可以捕获该信号尽管 SIGCHLD 处理程序将无法使用 wait() 获取子进程状态因为内核已丢弃僵尸进程。在某些 UNIX 实现中包括 Linux内核会为父进程生成 SIGCHLD 信号。在其他 UNIX 实现中不会生成 SIGCHLD。为 SIGCHLD 信号设置 SA_NOCLDWAIT 标志时旧的 Linux 内核表现出与上述将 SIGCHLD 处置设置为 SIG_IGN 时相同的不符合 SUSv3 的细节。The System V SIGCLD signal在 Linux 上名称 SIGCLD 被提供作为 SIGCHLD 信号的同义词。两者存在的原因是历史性的。SIGCHLD 信号起源于 BSD这个名称被 POSIX 采用而 POSIX 在很大程度上以 BSD 信号模型为标准。System V 提供了相应的 SIGCLD 信号其语义略有不同。BSD SIGCHLD 与 System V SIGCLD 的关键区别在于当信号的处理方式被设置为 SIG_IGN 时会发生什么在历史上的以及一些现代BSD 实现中即使忽略 SIGCHLD系统仍然会为未被等待的子进程生成僵尸进程。在 System V 中使用 signal()但不是 sigaction()忽略 SIGCLD将导致在子进程死亡时不会生成僵尸进程。如前所述原始的 POSIX.1 标准对忽略 SIGCHLD 的结果没有规定因此允许 System V 的行为。如今这种 System V 行为被规定为 SUSv3 的一部分尽管仍然使用 SIGCHLD 这个名称。现代的 System V 衍生系统使用标准名称 SIGCHLD 来表示该信号但仍继续提供同义词 SIGCLD。有关 SIGCLD 的更多细节可以参考 [Stevens Rago, 2005]。26.4 Summary使用 wait() 和 waitpid()以及其他相关函数父进程可以获取其已终止和已停止子进程的状态。该状态指示子进程是正常终止退出状态表示成功或失败、异常终止、被信号停止还是被 SIGCONT 信号恢复。如果子进程的父进程终止子进程将成为孤儿并被进程 ID 为 1 的 init 进程收养。当子进程终止时它会变为僵尸进程只有当其父进程调用 wait()或类似函数以获取子进程状态时它才会从系统中移除。像 shell 和守护进程这样运行时间较长的程序应设计成总是收集其创建的子进程的状态因为处于僵尸状态的进程无法被杀死未被收集的僵尸进程最终会阻塞内核进程表。收集已死子进程的一种常用方法是为 SIGCHLD 信号建立一个处理函数。每当其中一个子进程终止时这个信号会发送给父进程子进程被信号停止时也可以选择性地发送。或者虽然便携性稍差进程也可以选择将 SIGCHLD 的处理方式设置为 SIG_IGN这样已终止子进程的状态会被立即丢弃因此父进程无法再获取同时子进程不会变为僵尸进程。Further information参考第24.6节列出的进一步信息来源。