14

我有一个分叉的 Perl 脚本。

每个 fork 运行一个外部程序,解析输出,并将输出转换为 Storable 文件。

然后父级读取可存储文件,并分析来自每个子级的总数据,然后再继续重复前一个分叉,否则父级停止。

当我发出 ^C 而一些孩子仍在运行外部程序时,究竟会发生什么?父 perl 脚本在前台被调用,我想,尽管有分叉,它仍然在前台。

SIGINT是否传递给所有的孩子,即父母,父母的孩子,以及孩子们调用的外部程序?

更新

我应该补充一点,当我发出 SIGINIT 时,我的脚本子级调用的外部程序似乎确认了信号并终止。但是孩子们,或者也许是家长计划,继续进行。这一切我都不清楚。

更新 2

关于 tchrist 的注释,外部程序是用 Perl 的system()命令调用的。

事实上,tchrist 的评论似乎也包含我正在寻找的解释。经过更多调试后,根据我的程序的行为,似乎确实 SIGINT 正在从父级传递给所有子级,并从所有子级传递给所有子级(外部程序)。

因此,根据 tchrist 的评论,似乎正在发生的事情是 CTRL-C 正在杀死导致孩子们离开system()命令的外部程序 - 仅此而已。

虽然我让我的孩子检查了调用的退出状态system(),但我假设 CTRL-C 会杀死父级以下的所有内容,而不是导致创建更多轮处理,这就是正在发生的事情! !

解决方案(我的问题)

我只需要在父级中为 SIGINT 创建一个信号处理程序。然后,信号处理程序会将 SIGTERM 发送给每个孩子(我想这也会向孩子的孩子发送 SIGTERM),然后让父母优雅地退出。尽管这个有点明显的解决方案可能会解决问题,但我想了解我对 SIGINT 与 Perl 中的分叉相关的行为的误解。

4

3 回答 3

13

就信号而言, Perl 的内置函数与标准 C 库中system的 C系统(3) 函数一样工作。如果您使用 Perl 版本的system()或管道打开或反引号,那么父级(调用system者而不是被它调用的那个)将在子级运行时忽略任何 SIGINT 和 SIGQUIT。如果您已经使用fork?wait:exec三重奏的某些变体推出了自己的版本,那么您必须自己考虑这些问题。

system("vi somefile")考虑一下在长时间搜索中使用并点击 ^C 时会发生什么vi:只vi需要一个(非致命的)SIGINT;父母忽略它。这是正确的行为。这就是 C 以这种方式工作的原因,也是 Perl 以这种方式工作的原因。

您必须记住的是,仅仅因为 ^C 向前台进程组中的所有进程(即使是不同有效 UID 或 GID 的进程)发送 SIGINT,并不意味着它会导致所有这些进程退出。^C 只是一个 SIGINT,意在中断进程,而不是 SIGKILL,意在不问任何问题就终止。

有很多程序在没有警告的情况下直接终止是错误的;编辑器就是这样一个例子。邮件程序可能是另一个。对此要格外小心。

许多种类的程序选择性地忽略、捕获或阻止(意味着延迟传递)各种信号。只有 SIGINT 的默认行为是导致进程退出。您可以使用传统操作系统上的此类代码了解这是否发生,以及实际上是哪个信号导致它发生(除其他外):

if ($wait_status = system("whatever")) {
    $sig_killed   = $wait_status & 127;
    $did_coredump = $wait_status & 128;
    $exit_status  = $wait_status >>  8;
    # now do something based on that...
}

vi请注意,例如^C'd不会有一个等待状态字表明它死于未捕获的 SIGINT,因为没有一个:它抓住了它。

有时你的孩子会在你背后生下他们自己的孩子。凌乱但真实。因此,我有时会以这种方式种族灭绝所有已知和未知的后代:

# scope to temporize (save+restore) any previous value of $SIG{HUP}
{
    local $SIG{HUP} = "IGNORE";
    kill HUP => -$$;   # the killpg(getpid(), SIGHUP) syscall
}

这当然不适用于 SIGKILL 或 SIGSTOP,它们不能像那样被忽略。

您可能需要注意的另一件事是,在 5.8 版本之前,Perl 中的信号处理在历史上并不是一个可靠安全的操作。现在是但这是一个版本相关的问题。如果您还没有这样做,那么您绝对应该阅读 perlipc手册页中的延迟信号,也许还应该阅读perlrun 手册PERL_SIGNALS中的变量。

于 2011-01-18T00:50:34.840 回答
9

当您在终端窗口(或任何终端)中点击 ^C 时,它将向该终端中的前台进程 GROUP 发送一个 SIGINT。现在当你从命令行启动一个程序时,它通常在它自己的进程组中,这成为前台进程组。默认情况下,当你 fork 一个子进程时,它将与父进程在同一个进程组中,因此默认情况下,父进程(从命令行调用的顶级程序)、它的所有子进程、子进程的子进程等,以及由其中任何一个调用的任何外部程序(它们都只是子程序)都将位于同一个进程组中,因此都将收到 SIGINT 信号。

但是,如果这些子进程或程序中的任何一个调用 setpgrp 或 setpgid 或 setsid,或任何其他导致进程成为新进度组中的调用,则这些进程(以及它们在离开前台进程组后启动的任何子进程)将不会接收 SIGINT。

此外,当一个进程接收到一个 SIGINT 时,它可能不会终止——它可能会忽略信号,或者它可能会捕获它并做一些完全不同的事情。SIGINT 的默认信号处理程序终止进程,但这只是可以覆盖的默认值。

编辑

从您的更新中,听起来一切都保留在同一个进程组中,因此信号正在传递给每个人,因为孙子(外部程序)正在退出,但孩子正在捕捉并忽略 SIGINT。根据 tchrist 的评论,这听起来像是 perl 的默认行为。

于 2011-01-17T20:13:48.243 回答
2

如果您终止父进程,子进程(和正在运行的外部程序)仍将运行,直到它们以一种或另一种方式终止。如果父进程有一个捕获 SIGINT 的信号处理程序,然后终止进程组(通常是父进程的 pid),则可以避免这种情况。这样,一旦父母获得 SIGINT,它将杀死所有的孩子。

因此,要回答您的问题,这完全取决于信号处理程序的实现。根据您的更新,似乎父母确实杀死了它的孩子,然后它没有终止自己,而是回到做其他事情(将其视为重置而不是完整的关闭/启动组合)。

于 2011-01-17T19:48:09.690 回答