为什么 SIGINT 发送到其父进程时没有传播到子进程?

为什么 SIGINT 发送到其父进程时没有传播到子进程?

给定一个 shell 进程(例如sh)及其子进程(例如cat),如何使用 shell 的进程 ID模拟Ctrl+的行为?C


这是我尝试过的:

运行sh然后cat

[user@host ~]$ sh
sh-4.3$ cat
test
test

SIGINTcat另一个终端发送:

[user@host ~]$ kill -SIGINT $PID_OF_CAT

cat收到信号并终止(如预期)。

向父进程发送信号似乎不起作用。为什么信号cat发送到其父进程时没有传播到sh

这不起作用:

[user@host ~]$ kill -SIGINT $PID_OF_SH

答案1

CTRL+如何C运作

首先要了解CTRL+是如何C工作的。

当您按CTRL+时C,终端模拟器会发送一个 ETX 字符(文本结束/0x03)。
TTY 被配置为当它收到此字符时,它会向终端的前台进程组发送 SIGINT。可以通过执行stty -a和查看来查看此配置intr = ^C;。这POSIX规范表示当收到 INTR 时,它应该向该终端的前台进程组发送一个 SIGINT。

什么是前台进程组?

那么,现在的问题是,如何确定前台进程组是什么?前台进程组只是将接收键盘生成的任何信号(SIGTSTP、SIGINT 等)的进程组。

确定进程组 ID 的最简单方法是使用ps

ps ax -O tpgid

第二列是进程组 ID。

如何向进程组发送信号?

现在我们知道了进程组 ID 是什么,我们需要模拟向整个组发送信号的 POSIX 行为。

这可以通过在组 ID 前面kill添加 来完成。 例如,如果您的进程组 ID 是 1234,您将使用:-

kill -INT -1234

使用终端号模拟CTRL+ 。C

所以上面介绍了如何将CTRL+模拟C为手动过程。但是,如果您知道 TTY 号码,并且想要为该终端模拟CTRL+该怎么办?C

这变得非常容易。

让我们假设$tty是您想要定位的终端(您可以通过tty | sed 's#^/dev/##'在终端中运行来获取它)。

kill -INT -$(ps h -t $tty -o tpgid | uniq)

这将向前台进程组发送一个 SIGINT $tty。  

答案2

作为vinc17 说,没有理由发生这种情况。当您键入信号生成键序列(例如Ctrl+ C)时,信号将发送到连接到(关联)终端的所有进程。生成的信号没有这样的机制kill

然而,像这样的命令

kill -SIGINT -12345

将向所有进程发送信号进程组12345;看杀死(1)杀死(2)。 shell 的子进程通常位于 shell 的进程组中(至少,如果它们不是异步的),因此将信号发送到 shell PID 的负数可能会执行您想要的操作。


哎呀

作为vinc17 指出,这不适用于交互式 shell。这是一个替代方案可能工作:

杀死 -SIGINT -$(回显 $(ps -pshell的PIDtpgid=))

ps -pPID_of_shell获取 shell 上的进程信息。  o tpgid=指示ps仅输出终端进程组 ID,不带标头。如果小于 10000,ps则显示前导空格;这$(echo …)是去除前导(和尾随)空格的快速技巧。

我确实让它在 Debian 机器上进行了粗略测试。

答案3

这个问题包含它自己的答案。将 发送SIGINTcat进程是对按+kill时发生的情况的完美模拟。CtrlC

更准确地说,中断字符(^C默认情况下)发送SIGINT到终端前台进程组中的每个进程。如果cat您运行的是涉及多个进程的更复杂的命令,则必须终止进程组才能达到与^C.

当您在没有后台操作符的情况下运行任何外部命令时&,shell 会为该命令创建一个新的进程组,并通知终端该进程组现在位于前台。 shell 仍位于其自己的进程组中,不再位于前台。然后 shell 等待命令退出。

这就是您似乎成为一个常见误解的受害者的地方:认为 shell 正在做一些事情来促进其子进程和终端之间的交互。那不是真的。一旦完成设置工作(进程创建、终端模式设置、管道创建和其他文件描述符重定向以及执行目标程序),shell只是等待。您输入的内容cat不会通过 shell,无论是普通输入还是生成信号的特殊字符(如 )^C。进程cat可以通过自己的文件描述符直接访问终端,并且终端能够直接向进程发送信号,cat因为它是前台进程组。外壳已经脱离了。

进程死亡后cat,shell 将收到通知,因为它是进程的父进程cat。然后 shell 变得活跃并再次将其自身置于前台。

这是一个增强你的理解的练习。

在新终端的 shell 提示符下,运行以下命令:

exec cat

exec关键字使 shell 执行cat而不创建子进程。外壳被替换为cat.以前属于 shell 的 PID 现在是 的 PID catps在不同的终端中验证这一点。输入一些随机行,然后看到cat它们会重复给您,证明尽管没有 shell 进程作为父进程,它仍然表现正常。现在按Ctrl+会发生什么C

回答:

SIGINT 被传递到 cat 进程,该进程终止。因为它是终端上的唯一进程,所以会话结束,就像您在 shell 提示符下说“退出”一样。实际上猫曾是你的外壳一段时间了。

答案4

setpgidPOSIX C 进程组最小示例

通过底层 API 的最小可运行示例可能会更容易理解。

这说明了如果子进程没有使用 更改其进程组,信号如何发送到子进程setpgid

主程序

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        is_child = 1;
        if (argc > 1) {
            /* Change the pgid.
             * The new one is guaranteed to be different than the previous, which was equal to the parent's,
             * because `man setpgid` says:
             * > the child has its own unique process ID, and this PID does not match
             * > the ID of any existing process group (setpgid(2)) or session.
             */
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}

GitHub 上游

编译:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c

运行时不带setpgid

如果没有任何 CLI 参数,setpgid则不会完成:

./setpgid

可能的结果:

child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child

并且程序挂起。

正如我们所看到的,两个进程的 pgid 是相同的,因为它是跨fork.

然后每当你点击:

Ctrl + C

它再次输出:

sigint parent
sigint child

这显示了如何:

  • 向整个进程组发送信号kill(-pgid, SIGINT)
  • 终端上的 Ctrl + C 默认向整个进程组发送终止命令

通过向两个进程发送不同的信号来退出程序,例如带有 的 SIGQUIT Ctrl + \

运行与setpgid

如果你带着参数运行,例如:

./setpgid 1

然后子进程更改其 pgid,现在每次仅从父进程打印一个 sigint:

child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent

现在,每当你点击:

Ctrl + C

只有父母也收到信号:

sigint parent

您仍然可以像以前一样使用 SIGQUIT 杀死父级:

Ctrl + \

然而,孩子现在有一个不同的 PGID,并且不会收到该信号!这可以从以下方面看出:

ps aux | grep setpgid

你必须明确地杀死它:

kill -9 16470

这清楚地表明了信号组存在的原因:否则我们会留下一堆需要手动清理的进程。

在 Ubuntu 18.04 上测试。

相关内容