简体   繁体   English

Ctrl-C 如何终止子进程?

[英]How does Ctrl-C terminate a child process?

I am trying to understand how CTRL + C terminates a child but not a parent process.我试图了解CTRL + C如何终止子进程而不是父进程。 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.我在bash等一些脚本 shell 中看到了这种行为,您可以在其中启动一些长时间运行的进程,然后通过输入CTRL - C来终止它,然后控制返回到 Z2591C98B70119FE624898B1E424。

Could you explain how does it work and in particular why isn't the parent (shell) process terminated?您能否解释一下它是如何工作的,特别是为什么父(shell)进程没有终止?

Does the shell have to do some special handling of CTRL + C event and if yes what exactly does it do? shell 是否必须对CTRL + C事件进行一些特殊处理,如果是,它到底做了什么?

Signals by default are handled by the kernel.默认情况下,信号由 kernel 处理。 Old Unix systems had 15 signals;旧的 Unix 系统有 15 个信号; now they have more.现在他们有更多了。 You can check </usr/include/signal.h> (or kill -l).您可以检查</usr/include/signal.h> (或 kill -l)。 CTRL + C is the signal with name SIGINT . CTRL + C是名称为SIGINT的信号。

The default action for handling each signal is defined in the kernel too, and usually it terminates the process that received the signal. kernel 中也定义了处理每个信号的默认操作,通常它会终止接收到信号的进程。

All signals (but SIGKILL ) can be handled by program.所有信号(但SIGKILL )都可以由程序处理。

And this is what the shell does:这就是 shell 所做的:

  • When the shell running in interactive mode, it has a special signal handling for this mode.当 shell 在交互模式下运行时,它对该模式有特殊的信号处理。
  • When you run a program, for example find , the shell:当您运行程序时,例如find ,shell:
    • fork s itself fork本身
    • and for the child set the default signal handling并为孩子设置默认信号处理
    • replace the child with the given command (eg with find)用给定的命令替换孩子(例如用 find )
    • when you press CTRL + C , parent shell handle this signal but the child will receive it - with the default action - terminate.当您按下CTRL + C时,父 shell 会处理此信号,但孩子会收到它 - 使用默认操作 - 终止。 (the child can implement signal handling too) (孩子也可以实现信号处理)

You can trap signals in your shell script too...您也可以在 shell 脚本中trap信号...

And you can set signal handling for your interactive shell too, try enter this at the top of you ~/.profile .你也可以为你的交互式 shell 设置信号处理,试着在你的顶部输入这个~/.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.现在,每次您在 shell 中按CTRL + C时,它都会打印一条消息。 Don't forget to remove the line!不要忘记删除线!

If interested, you can check the plain old /bin/sh signal handling in the source code here .如果有兴趣,您可以在此处查看源代码中的普通旧/bin/sh信号处理。

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

First, read the Wikipedia article on the POSIX terminal interface all of the way through.首先,通读有关 POSIX 终端界面的 Wikipedia 文章

The SIGINT signal is generated by the terminal line discipline, and broadcast to all processes in the terminal's foreground process group . SIGINT信号由终端线路规程生成,并广播到终端前台进程组中的所有进程。 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.您的 shell 已经为您运行的命令(或命令管道)创建了一个新进程组,并告诉终端该进程组是其(终端的)前台进程组。 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.每个并发命令管道都有自己的进程组,前台命令管道是shell已经编程到终端的进程组作为终端的前台进程组。 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.在前台和后台之间切换“作业”(除了一些细节)是 shell 告诉终端哪个进程组现在是前台的问题。

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. shell 进程本身位于另一个进程组中,因此当其中一个进程组处于前台时不会收到信号。 It's that simple.就是这么简单。

The terminal sends the INT (interrupt) signal to the process that is currently attached to the terminal.终端向当前连接到终端的进程发送 INT(中断)信号。 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).没有进程必须被强制关闭(尽管默认情况下,如果你不处理 sigint,我相信行为是调用abort() ,但我需要查看它)。

Of course, the running process is isolated from the shell that launched it.当然,运行进程与启动它的shell是隔离的。

If you wanted the parent shell to go, launch your program with exec :如果您想要父 shell 到 go,请使用exec启动您的程序:

exec ./myprogram

That way, the parent shell is replaced by the child process这样,父 shell 被子进程替换

setpgid POSIX C process group minimal example setpgid POSIX C 进程组最小示例

It might be easier to understand with a minimal runnable example of the underlying API.使用底层 API 的最小可运行示例可能更容易理解。

This illustrates how the signal does get sent to the child, if the child didn't change its process group with setpgid .这说明了如果子进程没有使用setpgid更改其进程组,信号是如何发送给子进程的。

main.c 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 . GitHub 上游

Compile with:编译:

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

Run without setpgid不使用setpgid运行

Without any CLI arguments, setpgid is not done:如果没有任何 CLI arguments, setpgid不会完成:

./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 .正如我们所见,两个进程的 pgid 是相同的,因为它是通过fork继承的。

Then whenever you hit Ctrl + C it outputs again:然后,每当您按 Ctrl + C时,它都会再次输出:

sigint parent
sigint child

This shows how:这显示了如何:

  • to send a signal to an entire process group with kill(-pgid, SIGINT)使用kill(-pgid, SIGINT)向整个进程组发送信号
  • Ctrl + C on the terminal sends a kill to the entire process group by default终端Ctrl + C默认向整个进程组发送kill

Quit the program by sending a different signal to both processes, eg SIGQUIT with Ctrl + \ .通过向两个进程发送不同的信号来退出程序,例如 SIGQUIT 与Ctrl + \

Run with setpgid使用setpgid运行

If you run with an argument, eg:如果您使用参数运行,例如:

./setpgid 1

then the child changes its pgid, and now only a single sigint gets printed every time from the parent only:然后孩子改变它的pgid,现在每次只从父母那里打印一个单一的信号:

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:现在,每当您点击Ctrl + C时,只有父级也会收到信号:

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:您仍然可以像以前一样使用 SIGQUIT ( Ctrl + \ ) 杀死父级,但是子级现在具有不同的 PGID,并且没有收到该信号:这可以从:

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.在 Ubuntu 18.04 上测试。

CTRL + C is a map to the kill command. CTRL + C是杀死命令的 map。 When you press them, kill sends a SIGINT signal, that's interrupts the process.当你按下它们时,kill 会发送一个 SIGINT 信号,这会中断进程。

Kill: http://en.wikipedia.org/wiki/Kill_(command)杀戮: http://en.wikipedia.org/wiki/Kill_(command)

SIGINT: http://en.wikipedia.org/wiki/SIGINT_(POSIX)信号: http://en.wikipedia.org/wiki/SIGINT_(POSIX)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM