Ctrl-C如何终止子进程?


69

我试图了解CTRL+如何C终止子进程而不终止父进程。我在某些脚本外壳中看到了这种行为,例如bash可以在其中启动一些长时间运行的进程,然后通过输入CTRL-终止它,然后C控件返回到外壳。

您能解释一下它是如何工作的,尤其是为什么父(shell)进程不会终止?

外壳程序是否必须对CTRL+C事件做一些特殊处理,如果是,它到底是做什么的?


1
为什么终止孩子会终止父母?不,不需要任何特殊处理。

3
@Neil Butterworth:我不是在问为什么终止孩子不终止父母。问题是为什么子进程为什么会得到Ctrl-C事件而不是父进程?
vitaut 2011年

Answers:


68

默认情况下,信号由内核处理。旧的Unix系统有15个信号。现在他们有更多。您可以检查</usr/include/signal.h>(或杀死-l)。CTRL+C是带有名称的信号SIGINT

内核中也定义了处理每个信号的默认操作,通常它会终止接收信号的进程。

所有信号(但SIGKILL)都可以由程序处理。

这就是shell的作用:

  • 当Shell在交互模式下运行时,此模式具有特殊的信号处理功能。
  • 例如find,运行程序时,shell:
    • fork本身
    • 并为孩子设置默认信号处理
    • 用给定的命令替换孩子(例如,用find)
    • 当您按CTRL+时C,父级外壳将处理此信号,但子级将收到此信号(采用默认操作),终止。(孩子也可以实现信号处理)

您也可以trap在shell脚本中发出信号...

您也可以为交互式外壳程序设置信号处理,请尝试在您的顶部输入此内容~/.profile。(确保您已经登录并在另一个终端上进行测试-您可以锁定自己)

trap 'echo "Dont do this"' 2

现在,每当您在外壳中按CTRL+C时,它将打印一条消息。不要忘记删除行!

如果有兴趣,可以/bin/sh此处的源代码中检查普通的旧信号处理。

在上面的注释中有一些错误信息(现已删除),因此,如果对此感兴趣的人是一个很好的链接-信号处理的工作原理


1
感谢您的解释和链接。“ TTY神秘化”是我见过的最好的介绍之一。
vitaut 2011年

8
SIGSTOP也是不可捕获的。
William Pursell 2011年

2
@PiotrDobrogost见男人。纠缠于评论毫无意义。如果您知道进程组是如何工作的,那么,除了设置进程组之外,您还知道外壳的工作。JeBP的答案(大部分)是正确的。多说一句,因为只有具有作业控制的POSIX shell才是这样。基本原理仍然存在:外壳为信号处理“做某事”。它可以通过将信号处理程序设置为忽略来进行管理,或者(如JdeBP所说)可以处理进程组。
jm666'2

2
@PiotrDobrogost请记住,这里有许多shell和许多系统-并不是所有的东西都关心并与流程组一起玩。这是一个相当复杂的主题,不可能(普遍)说:“一切都基于流程组”-因为那不是真的 在现实中-这两个答案都是正确的。大多数现代外壳(如bash)的答案都是正确的,而其他外壳(例如ash,无作业控制的普通V系统外壳)则是我的答案-它们​​通过信号处理来管理它。并且,信号处理方法是通用的。检查一下自己的一些shell源代码。就这样。
jm666'2

2
@PiotrDobrogost,最后,问自己如何处理SIGHUP。(例如,当您将某些东西发送到后台并断开控制终端的连接时(例如生成SIGHUP信号的线路
规程

26

首先,请一直阅读POSIX终端接口上的Wikipedia文章

SIGINT信号是由终端线纪律产生,并且广播到终端的中的所有进程前台进程组。您的外壳程序已经为您运行的命令(或命令管道)创建了一个新的进程组,并告诉终端该进程组是其(终端的)前台进程组。每个并发命令管道都有其自己的进程组,而前台命令管道是外壳程序已将其编程到终端中的进程组作为终端的前台进程组的那个。在前台和后台之间切换“工作”(除了一些细节之外)仅是shell告诉终端哪个进程组现在是前台。

Shell进程本身完全位于另一个进程组中,因此当这些进程组之一位于前台时不会接收信号。就这么简单。


7

终端将INT(中断)信号发送到当前连接到终端的进程。程序随后将其接收,并可以选择忽略它或退出。

不必强制关闭任何进程(尽管默认情况下,如果您不处理sigint,我相信行为是致电abort(),但我需要查一下)。

当然,正在运行的进程与启动它的外壳是隔离的。

如果您希望使用父外壳,请使用以下命令启动程序exec

exec ./myprogram

这样,父shell被子进程替换


我是否正确理解,当我从外壳启动某个进程时,它将附加到终端,这就是为什么它会收到INT信号的原因?
vitaut 2011年

1
通常是的。对于流行外壳中的前台作业,可以。这将取决于外壳,以及是否还有终端。
sehe 2011年

2
@kobame:您是说信号首先发送到shell,然后再传递给子进程吗?为什么然后将SIGINT手动发送到shell不会杀死子进程,例如'kill -2 <shell-pid>'对子进程却没有任何作用,而Ctrl-C会杀死它?
vitaut 2011年

3

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)
  • CtrlC默认情况下,终端上的+发送一个kill给整个进程组

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

与运行 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上测试。


1
在信号处理程序中,您有write(STDOUT_FILENO, sigint_str, sizeof(sigint_str));—但这也将一个空字节写入输出。最好sizeof(sigint_str) - 1避免写入空字节。在POSIX 2018年,很多str*mem*()功能从<string.h>最终标出“信号安全”,所以现在有可能使用strlen(),但由于该字符串是一个固定的大小,sizeof(sigint_str) - 1是非常合情合理的太(和良好的编译器可能会优化strlen()掉无论如何)。
乔纳森·莱夫勒

我想知道在写入标准输出之前设置信号处理程序是否会更好?它减少了漏洞窗口,如果进程正在写入管道并且管道缓冲区已满,则漏洞窗口可能会很大。
乔纳森·莱夫勒

@JonathanLeffler感谢您的评论。在- 1上添加了一个sizeof绝对不好。我从来没有想过如果另一个信号到达会发生什么,我想说man signal在Linux和BSD上默认情况下会阻止进一步的信号,但是不能保证POSIX,这就是为什么sigaction会优先考虑。
西罗Santilli郝海东冠状病六四事件法轮功
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.