Ctrl-C 如何终止子进程?
我试图了解 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
默认情况下,信号由内核处理。旧的 Unix 系统有 15 个信号;现在他们有更多了。您可以检查
(或kill -l)。 CTRL+C 是名为
SIGINT
的信号。处理每个信号的默认操作也在内核中定义,通常它会终止接收信号的进程。
所有信号(除了
SIGKILL
)都可以由程序处理。这就是 shell 的作用:
find
,shell 会:fork
本身您也可以在 shell 脚本中
捕获
信号...并且您也可以为交互式 shell 设置信号处理,尝试在顶部输入此
>~/.profile
。 (确保您已经登录并使用另一个终端进行测试 - 您可以锁定自己)现在,每次按 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 nameSIGINT
.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:
find
, the shell:fork
s itselfYou 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)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.
首先,阅读有关 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.
终端向当前连接到终端的进程发送 INT(中断)信号。然后程序接收它,并可以选择忽略它或退出。
没有进程一定会被强制关闭(尽管默认情况下,如果您不处理 sigint,我相信该行为是调用
abort()
,但我需要查找一下)。当然,正在运行的进程与启动它的 shell 是隔离的。
如果您想要父 shell 消失,请使用
exec
启动您的程序:这样,父 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
:That way, the parent shell is replaced by the child process
setpgid
POSIX C 进程组最小示例通过底层 API 的最小可运行示例可能会更容易理解。
这说明了如果子进程没有使用
setpgid
更改其进程组,信号如何发送到子进程。main.c
GitHub 上游。
编译:
不使用
setpgid
运行如果没有任何 CLI 参数,
setpgid
未完成:可能的结果:
程序挂起。
正如我们所看到的,两个进程的 pgid 是相同的,因为它是通过
fork
继承的。然后,每当您按下 Ctrl+C 时,它都会再次输出:
这显示了如何:
kill(-pgid, SIGINT)< 向整个进程组发送信号/code>
通过向两个进程发送不同的信号来退出程序,例如 SIGQUIT Ctrl+\。
使用
setpgid 运行
如果您使用参数运行,例如:
那么子级会更改其 pgid,现在每次仅从父级打印一个 sigint:
现在,每当您按下 Ctrl+C 时,只有父级也会收到信号:
您仍然可以像以前一样使用 SIGQUIT 杀死父级(Ctrl+\) 但是,孩子现在有不同的 PGID,并且不会收到该信号!这可以从以下内容中看出:
您必须使用以下命令显式杀死它:
这清楚地表明了信号组存在的原因:否则我们将一直留下一堆需要手动清理的进程。
在 Ubuntu 18.04 上测试。
setpgid
POSIX C process group minimal exampleIt 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
GitHub upstream.
Compile with:
Run without
setpgid
Without any CLI arguments,
setpgid
is not done:Possible outcome:
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:
This shows how:
kill(-pgid, SIGINT)
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.:
then the child changes its pgid, and now only a single sigint gets printed every time from the parent only:
And now, whenever you hit Ctrl+C only the parent receives the signal as well:
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:
You will have to kill it explicitly with:
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.