为什么 SIGINT 在发送到其父进程时没有传播到子进程?

rob*_*b87 77 shell process signals

给定一个 shell 进程(例如sh)及其子进程(例如cat),如何使用 shell 的进程 ID模拟Ctrl+的行为C


这是我尝试过的:

运行sh然后cat

[user@host ~]$ sh
sh-4.3$ cat
test
test
Run Code Online (Sandbox Code Playgroud)

发送SIGINTcat从另一终端:

[user@host ~]$ kill -SIGINT $PID_OF_CAT
Run Code Online (Sandbox Code Playgroud)

cat 收到信号并终止(如预期)。

向父进程发送信号似乎不起作用。为什么信号cat在发送到其父进程时没有传播到sh

这不起作用:

[user@host ~]$ kill -SIGINT $PID_OF_SH
Run Code Online (Sandbox Code Playgroud)

phe*_*mer 108

如何CTRL+C作品

第一件事是了解CTRL+是如何C工作的。

当您按下CTRL+ 时C,您的终端模拟器会发送一个 ETX 字符(文本结束/0x03)。
TTY 被配置为当它接收到这个字符时,它会向终端的前台进程组发送一个 SIGINT。可以通过执行stty -a和查看来查看此配置intr = ^C;。的POSIX说明书说,当接收时INTR,它应发送一个SIGINT给该终端的前台进程组。

什么是前台进程组?

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

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

ps ax -O tpgid
Run Code Online (Sandbox Code Playgroud)

第二列将是进程组 ID。

如何向进程组发送信号?

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

这可以kill通过-在组 ID 前面放置一个来完成。
例如,如果您的进程组 ID 是 1234,您将使用:

kill -INT -1234
Run Code Online (Sandbox Code Playgroud)

使用终端号模拟CTRL+ C

所以上面涵盖了如何模拟CTRL+C作为手动过程。但是,如果您知道 TTY 号码,并且想为该终端模拟CTRL+C呢?

这变得非常容易。

让我们假设$tty是您想要定位的终端(您可以通过tty | sed 's#^/dev/##'在终端中运行来获得它)。

kill -INT -$(ps h -t $tty -o tpgid | uniq)
Run Code Online (Sandbox Code Playgroud)

这将向任何前台进程组发送 SIGINT $tty。  

  • 值得指出的是,直接来自终端的信号会绕过权限检查,因此 Ctrl+C 总是能够成功传递信号,除非您在终端属性中将其关闭,而 `kill` 命令可能会失败。 (11认同)
  • +1,用于`向终端的前台进程组发送一个SIGINT。` (6认同)

G-M*_*ca' 18

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

但是,像这样的命令

kill -SIGINT -12345
Run Code Online (Sandbox Code Playgroud)

将信号发送给进程组12345 中的所有进程;请参阅kill(1)kill(2)。shell 的子进程通常在 shell 的进程组中(至少,如果它们不是异步的),因此将信号发送到 shell 的 PID 的负数可能会做你想要的。


哎呀

正如vinc17 指出的那样,这不适用于交互式 shell。这是一个可能有效的替代方法:

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

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

我确实在 Debian 机器上进行了粗略测试。


小智 12

这个问题有它自己的答案。将 发送SIGINTcat进程kill是对按下Ctrl+时发生的情况的完美模拟C

更准确地说,中断字符(^C默认情况下)发送SIGINT到终端前台进程组中的每个进程。如果不是cat运行涉及多个进程的更复杂的命令,则必须终止进程组才能达到与^C.

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

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

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

这里有一个练习来增加你的理解。

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

exec cat
Run Code Online (Sandbox Code Playgroud)

exec关键字使外壳程序在cat不创建子进程的情况下执行。外壳由cat. 以前属于 shell 的 PID 现在是cat. ps在不同的终端中验证这一点。输入一些随机的行并查看cat它们是否重复给您,证明尽管没有作为父进程的 shell 进程,它仍然表现正常。现在Ctrl+会发生什么C

回答:

SIGINT 被传送到 cat 进程,该进程死亡。因为它是终端上的唯一进程,所以会话结束,就像您在 shell 提示符下说“退出”一样。实际上,猫曾经是您的外壳。

  • 关键是外壳 * 不* 将 SIGINT 发送给它的孩子。SIGINT 来自终端驱动程序,并发送到所有前台进程。 (2认同)

vin*_*c17 5

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

如果 shell 传播了接收到的SIGINT,例如跟随一个真正的Ctrl+ C,这将意味着子进程将接收SIGINT信号两次,这可能会产生不需要的行为。