给定一个 shell 进程(例如sh
)及其子进程(例如cat
),如何使用 shell 的进程 ID模拟Ctrl+的行为?C
这是我尝试过的:
运行sh
然后cat
:
[user@host ~]$ sh
sh-4.3$ cat
test
test
SIGINT
从cat
另一个终端发送:
[user@host ~]$ kill -SIGINT $PID_OF_CAT
cat
收到信号并终止(如预期)。
向父进程发送信号似乎不起作用。为什么信号cat
发送到其父进程时没有传播到sh
?
这不起作用:
[user@host ~]$ kill -SIGINT $PID_OF_SH
答案1
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
第二列是进程组 ID。
如何向进程组发送信号?
现在我们知道了进程组 ID 是什么,我们需要模拟向整个组发送信号的 POSIX 行为。
这可以通过在组 ID 前面kill
添加 来完成。 例如,如果您的进程组 ID 是 1234,您将使用:-
kill -INT -1234
使用终端号模拟CTRL+ 。C
所以上面介绍了如何将CTRL+模拟C为手动过程。但是,如果您知道 TTY 号码,并且想要为该终端模拟CTRL+该怎么办?C
这变得非常容易。
让我们假设$tty
是您想要定位的终端(您可以通过tty | sed 's#^/dev/##'
在终端中运行来获取它)。
kill -INT -$(ps h -t $tty -o tpgid | uniq)
这将向前台进程组发送一个 SIGINT $tty
。
答案2
作为vinc17 说,没有理由发生这种情况。当您键入信号生成键序列(例如Ctrl+ C)时,信号将发送到连接到(关联)终端的所有进程。生成的信号没有这样的机制kill
。
然而,像这样的命令
kill -SIGINT -12345
将向所有进程发送信号进程组12345;看杀死(1) 和杀死(2)。 shell 的子进程通常位于 shell 的进程组中(至少,如果它们不是异步的),因此将信号发送到 shell PID 的负数可能会执行您想要的操作。
哎呀
作为vinc17 指出,这不适用于交互式 shell。这是一个替代方案可能工作:
杀死 -SIGINT -$(回显 $(ps -pshell的PIDtpgid=))
ps -pPID_of_shell
获取 shell 上的进程信息。
o tpgid=
指示ps
仅输出终端进程组 ID,不带标头。如果小于 10000,ps
则显示前导空格;这$(echo …)
是去除前导(和尾随)空格的快速技巧。
我确实让它在 Debian 机器上进行了粗略测试。
答案3
这个问题包含它自己的答案。将 发送SIGINT
到cat
进程是对按+kill
时发生的情况的完美模拟。CtrlC
更准确地说,中断字符(^C
默认情况下)发送SIGINT
到终端前台进程组中的每个进程。如果cat
您运行的是涉及多个进程的更复杂的命令,则必须终止进程组才能达到与^C
.
当您在没有后台操作符的情况下运行任何外部命令时&
,shell 会为该命令创建一个新的进程组,并通知终端该进程组现在位于前台。 shell 仍位于其自己的进程组中,不再位于前台。然后 shell 等待命令退出。
这就是您似乎成为一个常见误解的受害者的地方:认为 shell 正在做一些事情来促进其子进程和终端之间的交互。那不是真的。一旦完成设置工作(进程创建、终端模式设置、管道创建和其他文件描述符重定向以及执行目标程序),shell只是等待。您输入的内容cat
不会通过 shell,无论是普通输入还是生成信号的特殊字符(如 )^C
。进程cat
可以通过自己的文件描述符直接访问终端,并且终端能够直接向进程发送信号,cat
因为它是前台进程组。外壳已经脱离了。
进程死亡后cat
,shell 将收到通知,因为它是进程的父进程cat
。然后 shell 变得活跃并再次将其自身置于前台。
这是一个增强你的理解的练习。
在新终端的 shell 提示符下,运行以下命令:
exec cat
该exec
关键字使 shell 执行cat
而不创建子进程。外壳被替换为cat
.以前属于 shell 的 PID 现在是 的 PID cat
。ps
在不同的终端中验证这一点。输入一些随机行,然后看到cat
它们会重复给您,证明尽管没有 shell 进程作为父进程,它仍然表现正常。现在按Ctrl+会发生什么C?
回答:
SIGINT 被传递到 cat 进程,该进程终止。因为它是终端上的唯一进程,所以会话结束,就像您在 shell 提示符下说“退出”一样。实际上猫曾是你的外壳一段时间了。
答案4
setpgid
POSIX C 进程组最小示例
通过底层 API 的最小可运行示例可能会更容易理解。
这说明了如果子进程没有使用 更改其进程组,信号如何发送到子进程setpgid
。
主程序
#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);
}
编译:
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)
- 终端上的 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 上测试。