Ctrl-C 如何终止子进程?

发布于 2024-11-09 12:59:59 字数 293 浏览 0 评论 0原文

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

您能否解释一下它是如何工作的,特别是为什么父(shell)进程没有终止?

shell 是否必须对 CTRL+C 事件进行一些特殊处理,如果是的话,它到底做了什么?

I am trying to understand how CTRL+C terminates a child but not a parent process. I see this behavior in some script shells like bash where you can start some long-running process and then terminate it by entering CTRL-C and the control returns to the shell.

Could you explain how does it work and in particular why isn't the parent (shell) process terminated?

Does the shell have to do some special handling of CTRL+C event and if yes what exactly does it do?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

緦唸λ蓇 2024-11-16 12:59:59

默认情况下,信号由内核处理。旧的 Unix 系统有 15 个信号;现在他们有更多了。您可以检查(或kill -l)。 CTRL+C 是名为 SIGINT 的信号。

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

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

这就是 shell 的作用:

  • 当 shell 以交互模式运行时,它对此模式有特殊的信号处理。
  • 当您运行程序时,例如 find,shell 会:
    • fork本身
    • 并为子进程设置默认信号处理
    • 用给定的命令替换子项(例如使用 find)
    • 当您按 CTRL+C 时,父 shell 会处理此信号,但子 shell 会收到该信号 - 使用默认操作 - 终止。 (孩子也可以实现信号处理)

您也可以在 shell 脚本中捕获信号...

并且您也可以为交互式 shell 设置信号处理,尝试在顶部输入此>~/.profile。 (确保您已经登录并使用另一个终端进行测试 - 您可以锁定自己)

trap 'echo "Dont do this"' 2

现在,每次按 CTRL+C< /kbd> 在你的 shell 中,它会打印一条消息。不要忘记删除线!

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

上面的评论中有一些错误信息(现已删除),所以如果有人对此感兴趣,这是一个非常好的链接 - 信号处理如何工作

Signals by default are handled by the kernel. Old Unix systems had 15 signals; now they have more. You can check </usr/include/signal.h> (or kill -l). CTRL+C is the signal with name SIGINT.

The default action for handling each signal is defined in the kernel too, and usually it terminates the process that received the signal.

All signals (but SIGKILL) can be handled by program.

And this is what the shell does:

  • When the shell running in interactive mode, it has a special signal handling for this mode.
  • When you run a program, for example find, the shell:
    • forks itself
    • and for the child set the default signal handling
    • replace the child with the given command (e.g. with find)
    • when you press CTRL+C, parent shell handle this signal but the child will receive it - with the default action - terminate. (the child can implement signal handling too)

You can trap signals in your shell script too...

And you can set signal handling for your interactive shell too, try enter this at the top of you ~/.profile. (Ensure than you're a already logged in and test it with another terminal - you can lock out yourself)

trap 'echo "Dont do this"' 2

Now, every time you press CTRL+C in your shell, it will print a message. Don't forget to remove the line!

If interested, you can check the plain old /bin/sh signal handling in the source code here.

At the above there were some misinformations in the comments (now deleted), so if someone interested here is a very nice link - how the signal handling works.

时光匆匆的小流年 2024-11-16 12:59:59

首先,阅读有关 POSIX 终端接口的 Wikipedia 文章所有方式通过。

SIGINT 信号由终端线路规则生成,并广播到终端前台进程组中的所有进程。您的 shell 已经为您运行的命令(或命令管道)创建了一个新的进程组,并告诉终端该进程组是其(终端的)前台进程组。每个并发命令管道都有自己的进程组,前台命令管道是 shell 已将进程组编程到终端中作为终端的前台进程组的管道。在前台和后台之间切换“作业”(除了一些细节)是 shell 告诉终端哪个进程组现在是前台进程的问题。

shell 进程本身位于另一个进程组中,因此当其中一个进程组位于前台时,它不会收到信号。就是这么简单。

First, read the Wikipedia article on the POSIX terminal interface all of the way through.

The SIGINT signal is generated by the terminal line discipline, and broadcast to all processes in the terminal's foreground process group. Your shell has already created a new process group for the command (or command pipeline) that you ran, and told the terminal that that process group is its (the terminal's) foreground process group. Every concurrent command pipeline has its own process group, and the foreground command pipeline is the one with the process group that the shell has programmed into the terminal as the terminal's foreground process group. Switching "jobs" between foreground and background is (some details aside) a matter of the shell telling the terminal which process group is now the foreground one.

The shell process itself is in yet another process group all of its own and so doesn't receive the signal when one of those process groups is in the foreground. It's that simple.

凤舞天涯 2024-11-16 12:59:59

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

没有进程一定会被强制关闭(尽管默认情况下,如果您不处理 sigint,我相信该行为是调用 abort(),但我需要查找一下)。

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

如果您想要父 shell 消失,请使用 exec 启动您的程序:

exec ./myprogram

这样,父 shell 就会被子进程替换

The terminal sends the INT (interrupt) signal to the process that is currently attached to the terminal. The program then receives it, and could choose to ignore it, or quit.

No process is necessarily being forcibly closed (although by default, if you don't handle sigint, I believe the behaviour is to call abort(), but I'd need to look that up).

Of course, the running process is isolated from the shell that launched it.

If you wanted the parent shell to go, launch your program with exec:

exec ./myprogram

That way, the parent shell is replaced by the child process

初吻给了烟 2024-11-16 12:59:59

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)< 向整个进程组发送信号/code>
  • 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 上测试。

setpgid POSIX C process group minimal example

It might be easier to understand with a minimal runnable example of the underlying API.

This illustrates how the signal does get sent to the child, if the child didn't change its process group with 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 upstream.

Compile with:

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

Run without setpgid

Without any CLI arguments, setpgid is not done:

./setpgid

Possible outcome:

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

and the program hangs.

As we can see, the pgid of both processes is the same, as it gets inherited across fork.

Then whenever you hit Ctrl+C it outputs again:

sigint parent
sigint child

This shows how:

  • to send a signal to an entire process group with kill(-pgid, SIGINT)
  • Ctrl+C on the terminal sends a kill to the entire process group by default

Quit the program by sending a different signal to both processes, e.g. SIGQUIT with Ctrl+\.

Run with setpgid

If you run with an argument, e.g.:

./setpgid 1

then the child changes its pgid, and now only a single sigint gets printed every time from the parent only:

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

And now, whenever you hit Ctrl+C only the parent receives the signal as well:

sigint parent

You can still kill the parent as before with a SIGQUIT (Ctrl+\) however the child now has a different PGID, and does not receive that signal! This can seen from:

ps aux | grep setpgid

You will have to kill it explicitly with:

kill -9 16470

This makes it clear why signal groups exist: otherwise we would get a bunch of processes left over to be cleaned manually all the time.

Tested on Ubuntu 18.04.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文