news 2026/7/17 6:52:40

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TLPI 第26 章 读书笔记:Monitoring Child Processes

笔记和练习博客总目录见:开始读TLPI。

在许多应用程序设计中,父进程需要知道其某个子进程何时状态发生变化——无论是子进程终止还是被信号停止。本章介绍了两种用于监控子进程的技术:wait() 系统调用(及其变体)和使用 SIGCHLD 信号。

26.1 Waiting on a Child Process

在许多应用中,一个父进程创建子进程时,父进程能够监控子进程以了解它们何时以及如何终止是很有用的。这个功能由 wait() 及若干相关的系统调用提供。

26.1.1 The wait() System Call

wait() 系统调用等待调用进程的一个子进程终止,并将该子进程的终止状态返回到 status 指向的缓冲区中。

#include<sys/wait.h>pid_twait(int*status);

wait() 系统调用执行以下操作:

  1. 如果调用进程的任何(之前未等待的)子进程尚未终止,则该调用会阻塞,直到其中一个子进程终止。如果在调用时某个子进程已经终止,wait() 会立即返回。
  2. 如果 status 不为 NULL,有关子进程如何终止的信息会返回到 status 指向的整数中。我们在第 26.1.3 节中描述了返回在 status 中的信息。
  3. 内核将进程的 CPU 时间(第 10.7 节)和资源使用统计(第 36.1 节)添加到该父进程所有子进程的运行总计中。
  4. 作为其函数结果,wait() 返回已终止子进程的进程 ID

在出错时,wait() 返回 –1。一个可能的错误是调用进程没有(之前未等待的)子进程,这由 errno 值 ECHILD 表示。这意味着我们可以使用以下循环来等待调用进程的所有子进程终止:

while((childPid=wait(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(numDead=1)[00:37:55]wait()returned child PID15245(numDead=2)[00:37:59]wait()returned child PID15243(numDead=3)Nomorechildren - bye!

如果在某一时刻有多个已终止的子进程,SUSv3 并未指定这些子进程将按照什么顺序被一系列 wait() 调用收回;也就是说,顺序取决于实现。即使在不同版本的 Linux 内核中,行为也会有所不同。

Listing 26-1: Creating and waiting for multiple children

// procexec/multi_wait.c// 代码略

26.1.2 The waitpid() System Call

wait() 系统调用有一些限制,而 waitpid() 的设计就是为了解决这些问题:

  • 如果父进程创建了多个子进程,无法使用 wait()等待特定子进程的完成;我们只能等待下一个终止的子进程。
  • 如果没有子进程已终止,wait() 总是会阻塞。有时,更希望执行非阻塞等待,这样如果没有子进程已终止,我们可以立即得到这一信息。
  • 使用 wait(),我们只能了解已经终止的子进程。无法在子进程被信号(如 SIGSTOP 或 SIGTTIN)停止时收到通知,也不能在子进程被 SIGCONT 信号恢复时收到通知。

💡 以上3个特性,等待特定子进程通过pid参数实现,非阻塞和收到通知通过options参数实现。

#include<sys/wait.h>pid_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 Value

wait() 和 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 PID=15271waitpid()returned:PID=15271;status=0x1700(23,0)child exited,status=23

在下一次运行中,我们在后台启动程序,然后向子进程发送 SIGSTOP 和 SIGCONT 信号:

$ ./child_status&[1]15272$ Child started with PID=15273$kill-STOP15273$ waitpid()returned:PID=15273;status=0x137f(19,127)child stopped by signal19(Stopped(signal))$kill-CONT15273waitpid()returned:PID=15273;status=0xffff(255,255)child continued

输出的最后两行只会出现在 Linux 2.6.10 及更高版本上,因为早期的内核不支持 waitpid() 的 WCONTINUED 选项。(这个 shell 会话略显难以阅读,因为在某些情况下后台程序的输出会与 shell 产生的提示符交错。)

我们通过发送 SIGABRT 信号来终止子进程,继续 shell 会话:

$kill-ABRT15273$ waitpid()returned:PID=15273;status=0x0086(0,134)child killed by signal6(Aborted)(core dumped)[1]+ Done ./child_status $ulimit-cunlimited $ coredumpctl list TIME PIDUIDGID SIG COREFILE EXE SI>Thu2026-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.177448>Size 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_main@@GLIBC_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() 可用的功能。

#include<sys/wait.h>intwaitid(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_pid==0){/* No children changed state */}else{/* A child changed state; details are provided in 'info' */}

26.1.6 The wait3() and wait4() System Calls

wait3() 和 wait4() 系统调用执行的任务与 waitpid() 类似。主要的语义差异在于,wait3() 和 wait4() 会在 rusage 参数指向的结构中返回已终止子进程的资源使用信息。这些信息包括进程使用的 CPU 时间和内存管理统计信息。我们将在第 36.1 节中详细讨论 rusage 结构,在那里我们将描述 getrusage() 系统调用。

#define_BSD_SOURCE/* Or #define _XOPEN_SOURCE 500 for wait3() */#include<sys/resource.h>#include<sys/wait.h>pid_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 ParentPID=15367Child(PID=15368)exiting15367pts/0 00:00:00 make_zombie15368pts/0 00:00:00 make_zombie<defunct>After sending SIGKILL to zombie(PID=15368):15367pts/0 00:00:00 make_zombie15368pts/0 00:00:00 make_zombie<defunct>

在上述输出中,我们看到 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(PID=15525)exiting 02:49:46 handler: Caught SIGCHLD 02:49:46 handler: Reaped child15525- child exited,status=002:49:47 Child2(PID=15526)exiting 02:49:49 Child3(PID=15527)exiting 02:49:51 handler: returning 02:49:51 handler: Caught SIGCHLD 02:49:51 handler: Reaped child15526- child exited,status=002:49:51 handler: Reaped child15527- child exited,status=002:49:56 handler: returning 02:49:56 All3children have terminated;SIGCHLD was caught2times

Listing 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 kernels
SUSv3 指出,如果 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 flag
SUSv3 指定了 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节列出的进一步信息来源。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/17 6:51:35

WSL2中解决systemd启动错误与daemonize安装指南

1. 问题背景与错误分析在Windows Subsystem for Linux 2&#xff08;WSL2&#xff09;环境中启动systemd时&#xff0c;很多开发者会遇到"Cannot execute daemonize to start systemd"的错误提示。这个问题的根源在于WSL2的特殊架构与传统Linux系统的差异。WSL2本质上…

作者头像 李华
网站建设 2026/7/17 6:48:06

TransNeXt-Tiny:高效混合注意力模型在ImageNet上的突破

1. 项目概述&#xff1a;TransNeXt-Tiny的突破性表现昨天还在刷榜的模型今天可能就过时了——这就是计算机视觉领域残酷的技术迭代速度。TransNeXt-Tiny这个听起来像"下一代Transformer变体"的模型&#xff0c;最近在ImageNet上跑出了84.0%的top-1准确率&#xff0c;…

作者头像 李华
网站建设 2026/7/17 6:47:36

14.5使用「阿里云函数计算 FC」时浏览器显示‘访问出错’

这个「网页出错」一般是函数实例启动失败 / 代码报错导致的一、先查「实时日志」&#xff0c;定位具体错误&#xff08;最关键&#xff01;&#xff09;回到函数计算 audio-web 详情页 → 「代码」标签页点击左上角 「实时日志」 按钮&#xff08;在「测试函数」旁边&#xff0…

作者头像 李华
网站建设 2026/7/17 6:47:24

14.7使用「阿里云函数计算 FC,改 URL 后缀,彻底骗过浏览器

因为浏览器是根据「URL 后缀」「响应头」双重判断的&#xff0c;现在 URL 是 /we.html&#xff0c;浏览器直接判定是文件。我们把后缀改成 /we&#xff0c;浏览器就不会触发下载了。方案一1. 直接修改函数代码&#xff08;10 秒改完&#xff09;把 app.py 里的路由从 /we.html …

作者头像 李华
网站建设 2026/7/17 6:44:07

AI视频理解技术:原理、应用与TwelveLabs创新

1. TwelveLabs融资事件与AI视频理解技术崛起2024年7月&#xff0c;AI视频理解领域迎来重要里程碑——TwelveLabs宣布完成1亿美元B轮融资。这家成立于2022年的初创公司&#xff0c;凭借其创新的视频内容理解技术&#xff0c;在短短两年内就获得了包括NVIDIA、Intel Capital在内的…

作者头像 李华
网站建设 2026/7/17 6:44:01

ShellGPT:AI驱动的命令行生产力工具实战指南

你是否曾经在终端前绞尽脑汁回忆某个复杂的shell命令语法&#xff1f;或者为了一个简单的代码片段不得不中断思路去搜索文档&#xff1f;如果你经常在命令行环境下工作&#xff0c;那么今天介绍的ShellGPT可能会成为你的生产力倍增器。ShellGPT不是一个简单的AI聊天工具&#x…

作者头像 李华