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


62

给定一个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
该外壳程序可以忽略不是从键盘或终端发送的SIGINT信号。
konsolebox 2014年

Answers:


86

如何CTRL+ C作品

首先是要了解CTRL+的C工作原理。

当您按CTRL+时C,终端仿真器将发送一个ETX字符(文本结尾/ 0x03)。
对TTY进行配置,以使其在接收到此字符时将SIGINT发送到终端的前台进程组。该结构通过这样做来查看stty和寻找intr = ^C;。的POSIX说明书说,当接收时INTR,它应发送一个SIGINT给该终端的前台进程组。

什么是前台进程组?

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

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

ps ax -O tpgid

第二列将是进程组ID。

如何向流程组发送信号?

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

可以kill通过-在组ID前面加上a 来完成此操作。
例如,如果您的进程组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


6
值得指出的是,直接来自终端的信号会绕过权限检查,因此,除非您在终端属性中将其关闭,否则Ctrl + C总是会成功传递信号,除非kill命令可能会失败。
布赖恩·毕

4
+1,代表sends a SIGINT to the foreground process group of the terminal.
andy

值得一提的是,子进程组与之后的父进程组相同fork。可运行的C最小示例,网址
Ciro Santilli新疆改造中心法轮功六四事件

15

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

但是,这样的命令

kill -SIGINT -12345

将信号发送到进程组 12345中的所有进程;参见kill(1)kill(2)。外壳程序的子级通常位于外壳程序的进程组中(至少,如果它们不是异步的),因此将信号发送到外壳程序的PID的负数可能会满足您的要求。


哎呀

正如vinc17指出的那样,这不适用于交互式shell。下面是另一种可能的工作:

kill -SIGINT-$(回显$(ps -p PID_of_shell o tpgid =))

ps -pPID_of_shell在外壳上获取进程信息。  o tpgid=告诉ps仅输出没有标题的终端进程组ID。如果小于10000,ps则显示前导空格。这$(echo …)是删除前导(和尾随)空格的快速技巧。

我的确可以在Debian机器上进行粗略的测试。


1
在交互式外壳程序(OP正在使用的外壳程序)中启动过程时,此方法不起作用。不过,我没有这种行为的参考。
vinc17 2014年

12

该问题包含其自己的答案。发送SIGINTcat与流程kill是当你按下会发生什么完美模拟^C

更准确地说,中断字符(^C默认情况下)发送SIGINT到终端的前台进程组中的每个进程。如果不是cat运行一个涉及多个进程的更复杂的命令,而是必须杀死该进程组以达到与相同的效果^C

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

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

在后cat工序模具,外壳将被通知,因为它的父cat进程。然后,外壳变为活动状态,并再次将其置于前台。

这是增加您的理解力的练习。

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

exec cat

exec关键字会导致执行shell cat,而无需创建子进程。外壳被替换cat。以前属于外壳的PID现在是的PID catps在其他终端中进行验证。键入一些随机行,并观察到cat它们会重复出现,这证明尽管没有将shell进程作为父进程,但它仍然正常运行。现在按一下会发生什么^C

回答:

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


炮弹已经挡开了。+1
Piotr Dobrogost

我不明白为什么在exec cat压榨后^C不只是^C掉进猫里。为什么它将终止cat已经替换了外壳的?由于替换了外壳程序,因此外壳程序实现了在接收到时将SIGINT发送给其子级的逻辑^C
史蒂文·卢

关键是外壳程序不会将SIGINT发送给其子级。SIGINT来自终端驱动程序,并发送到所有前台进程。

3

没有理由将其传播SIGINT给孩子。而且system()POSIX规范说:“在等待命令终止时,system()函数将忽略SIGINT和SIGQUIT信号,并阻塞SIGCHLD信号。”

如果外壳程序SIGINT(例如,在执行真实的Ctrl-C之后)传播了接收到的消息,则这意味着子进程将SIGINT两次接收信号,这可能具有不良行为。


Shell不必使用来实现system()。但是您是对的,如果它捕获到信号(显然是的),则没有理由向下传播信号。
goldilocks 2014年

@goldilocks我已经完成了回答,也许给出了更好的理由。请注意,外壳程序无法知道孩子是否已经接收到信号,因此出现了问题。
vinc17 2014年

1

setpgid POSIX C流程组最小示例

使用基础API的最小可运行示例可能更容易理解。

这说明了如果孩子未使用更改其过程组,则如何将信号发送给孩子setpgid

main.c

#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会向整个进程组发送kill

通过向两个进程发送不同的信号来退出程序,例如使用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上测试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.