这是How to prevent SIGINT in child process from propagating to and killing parent process?
的延续在上面的问题中,我了解到SIGINT
并没有从子节点到父节点,而是发布到整个前台进程组,这意味着我需要编写一个信号处理程序当我点击CTRL + C
时阻止父母退出。
我试图实现这一点,但这就是问题所在。关于我调用以终止孩子的kill
系统调用,如果我传入SIGKILL
,一切都按预期工作,但如果我传入SIGTERM
,它也会终止父进程,显示稍后在shell提示符中Terminated: 15
。
尽管SIGKILL有效,但我想使用SIGTERM是因为从我所读过的内容来看,它似乎就像是一个更好的主意,它给出了它发出信号以终止清理自己的机会的过程起来。
下面的代码是我提出的一个简单的例子
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
pid_t CHILD = 0;
void handle_sigint(int s) {
(void)s;
if (CHILD != 0) {
kill(CHILD, SIGTERM); // <-- SIGKILL works, but SIGTERM kills parent
CHILD = 0;
}
}
int main() {
// Set up signal handling
char str[2];
struct sigaction sa = {
.sa_flags = SA_RESTART,
.sa_handler = handle_sigint
};
sigaction(SIGINT, &sa, NULL);
for (;;) {
printf("1) Open SQLite\n"
"2) Quit\n"
"-> "
);
scanf("%1s", str);
if (str[0] == '1') {
CHILD = fork();
if (CHILD == 0) {
execlp("sqlite3", "sqlite3", NULL);
printf("exec failed\n");
} else {
wait(NULL);
printf("Hi\n");
}
} else if (str[0] == '2') {
break;
} else {
printf("Invalid!\n");
}
}
}
我对有关为什么会发生这种情况的猜测将会截断SIGTERM并杀死整个过程组。然而,当我使用SIGKILL时,它无法拦截信号,因此我的kill调用按预期工作。但这只是在黑暗中刺伤。
有人可以解释为什么会这样吗?
正如我所注意到的,我对handle_sigint
功能并不感到兴奋。是否有更标准的方法来杀死交互式子进程?
答案 0 :(得分:11)
你的代码中有太多的错误(从struct sigaction
上没有清除信号掩码),任何人都可以解释你所看到的效果。
相反,请考虑以下工作示例代码,例如example.c
:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* Child process PID, and atomic functions to get and set it.
* Do not access the internal_child_pid, except using the set_ and get_ functions.
*/
static pid_t internal_child_pid = 0;
static inline void set_child_pid(pid_t p) { __atomic_store_n(&internal_child_pid, p, __ATOMIC_SEQ_CST); }
static inline pid_t get_child_pid(void) { return __atomic_load_n(&internal_child_pid, __ATOMIC_SEQ_CST); }
static void forward_handler(int signum, siginfo_t *info, void *context)
{
const pid_t target = get_child_pid();
if (target != 0 && info->si_pid != target)
kill(target, signum);
}
static int forward_signal(const int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_sigaction = forward_handler;
act.sa_flags = SA_SIGINFO | SA_RESTART;
if (sigaction(signum, &act, NULL))
return errno;
return 0;
}
int main(int argc, char *argv[])
{
int status;
pid_t p, r;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s COMMAND [ ARGS ... ]\n", argv[0]);
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
/* Install signal forwarders. */
if (forward_signal(SIGINT) ||
forward_signal(SIGHUP) ||
forward_signal(SIGTERM) ||
forward_signal(SIGQUIT) ||
forward_signal(SIGUSR1) ||
forward_signal(SIGUSR2)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
p = fork();
if (p == (pid_t)-1) {
fprintf(stderr, "Cannot fork(): %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (!p) {
/* Child process. */
execvp(argv[1], argv + 1);
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
/* Parent process. Ensure signals are reflected. */
set_child_pid(p);
/* Wait until the child we created exits. */
while (1) {
status = 0;
r = waitpid(p, &status, 0);
/* Error? */
if (r == -1) {
/* EINTR is not an error. Occurs more often if
SA_RESTART is not specified in sigaction flags. */
if (errno == EINTR)
continue;
fprintf(stderr, "Error waiting for child to exit: %s.\n", strerror(errno));
status = EXIT_FAILURE;
break;
}
/* Child p exited? */
if (r == p) {
if (WIFEXITED(status)) {
if (WEXITSTATUS(status))
fprintf(stderr, "Command failed [%d]\n", WEXITSTATUS(status));
else
fprintf(stderr, "Command succeeded [0]\n");
} else
if (WIFSIGNALED(status))
fprintf(stderr, "Command exited due to signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status)));
else
fprintf(stderr, "Command process died from unknown causes!\n");
break;
}
}
/* This is a poor hack, but works in many (but not all) systems.
Instead of returning a valid code (EXIT_SUCCESS, EXIT_FAILURE)
we return the entire status word from the child process. */
return status;
}
使用例如
进行编译gcc -Wall -O2 example.c -o example
并使用例如
运行./example sqlite3
您会注意到 Ctrl + C 不会中断sqlite3
- 但是再一次,即使您要运行它也不会sqlite3
直接 - ;相反,你只需在屏幕上看到^C
。这是因为sqlite3
以这样的方式设置终端: Ctrl + C 不会产生信号,只是被解释为正常输入。
您可以使用sqlite3
命令退出.quit
,或者在行的开头按 Ctrl + D 。
您将看到原始程序之后将输出Command ... []
行,然后返回命令行。因此,父进程不会被信号杀死/伤害/打扰。
您可以使用ps f
查看终端进程的树,然后查找父进程和子进程的PID,并向任一进程发送信号以观察发生的情况。
请注意,由于无法捕获,阻止或忽略SIGSTOP
信号,因此反映作业控制信号(如使用 Ctrl + Z时)非常重要)。为了正确的作业控制,父进程需要设置一个新的会话和一个进程组,并暂时从终端分离。这也是非常可能的,但是这里有一点超出范围,因为它涉及会话,进程组和终端的非常详细的行为,以便正确管理。
让我们解构上面的示例程序。
示例程序本身首先安装一些信号反射器,然后分叉子进程,该子进程执行命令sqlite3
。 (您可以将任何可执行文件和任何参数字符串指定给程序。)
internal_child_pid
变量以及set_child_pid()
和get_child_pid()
函数用于以原子方式管理子进程 。 __atomic_store_n()
和__atomic_load_n()
是编译器提供的内置函数;对于海湾合作委员会,see here了解详情。它们避免了仅在部分分配子pid时发生信号的问题。在一些常见的体系结构上,这不可能发生,但这只是一个谨慎的例子,因此原子访问用于确保只看到完全(旧的或 new)值。如果我们在转换期间暂时阻止相关信号,我们可以完全避免使用这些信号。同样,我认为原子访问更简单,在实践中可能会很有趣。
forward_handler()
函数以原子方式获取子进程PID,然后验证它是非零的(我们知道我们有子进程),并且我们没有转发子进程发送的信号(只是为了确保)我们不会引发信号风暴,两个人用信号互相轰炸)。 siginfo_t
结构中的各个字段列在man 2 sigaction
手册页中。
forward_signal()
函数为指定的信号signum
安装上述处理程序。请注意,我们首先使用memset()
将整个结构清除为零。如果将结构中的某些填充转换为数据字段,则以这种方式清除它将确保将来的兼容性。
.sa_mask
中的struct sigaction
字段是一组无序信号。掩码中设置的信号在执行信号处理程序的线程中被阻止传递。 (对于上面的示例程序,我们可以肯定地说,这些信号在信号处理程序运行时被阻止;它只是在多线程程序中,信号仅在用于运行处理程序的特定线程中被阻止。)
使用sigemptyset(&act.sa_mask)
清除信号掩码非常重要。简单地将结构设置为零是不够的,即使它在许多机器上实际工作(可能)。 (我不知道;我甚至没有检查过。我喜欢健壮可靠而不是懒惰和脆弱的任何一天!)
使用的标志包括SA_SIGINFO
,因为处理程序使用三参数形式(并使用si_pid
的{{1}}字段)。 siginfo_t
标志仅在那里,因为OP希望使用它;它只是意味着如果可能的话,如果使用当前在系统调用中阻塞的线程(如SA_RESTART
)传递信号,则C库和内核会尽量避免返回errno == EINTR
错误。您可以删除wait()
标志,并在父进程的循环中的适当位置添加调试SA_RESTART
,以查看当时会发生什么。
如果没有错误,fprintf(stderr, "Hey!\n");
函数将返回0,否则设置为sigaction()
-1
。如果成功分配errno
,forward_signal()
函数返回0,否则返回非零errno数。有些人不喜欢这种返回值(他们更喜欢只返回-1表示错误,而不是forward_handler
值本身),但我因某种不合理的理由而喜欢这个成语。如果你愿意,一定要改变它。
现在我们到达errno
。
如果您运行的程序没有参数,或者使用单个main()
或-h
参数,则会打印使用情况摘要。再一次,这样做只是我喜欢的事情 - getopt()
和getopt_long()
更常用于解析命令行选项。对于这种简单的程序,我只是对参数检查进行了硬编码。
在这种情况下,我故意将使用率输出很短。通过一个关于程序的附加段落真的会好得多。这些类型的文本 - 尤其是代码中的注释(解释 intent ,代码应该做什么的想法,而不是描述代码实际执行的内容) - 非常重要。自从我第一次获得编写代码付款以来已经有二十多年了,我还在学习如何评论 - 更好地描述我的代码的意图,所以我认为越快开始研究,越多越好。
--help
部分应该是熟悉的。如果它返回fork()
,则fork失败(可能是由于限制或某些限制),然后打印出-1
消息是一个非常好的主意。子项中的返回值为errno
,父进程中的子进程ID为。
execlp()
函数有两个参数:二进制文件的名称(PATH环境变量中指定的目录将用于搜索这样的二进制文件),以及一个指向参数的指针数组那个二进制文件。第一个参数将是新二进制文件中的0
,即命令名称本身。
如果将argv[0]
调用与上面的描述进行比较,那么execlp(argv[1], argv + 1);
调用实际上很容易解析。 argv[1]
命名要执行的二进制文件。 argv + 1
基本上等同于(char **)(&argv[1])
,即它是一个以argv[1]
而不是argv[0]
开头的指针数组。再一次,我只是喜欢execlp(argv[n], argv + n)
成语,因为它允许一个人执行在命令行上指定的另一个命令,而不必担心解析命令行或通过shell执行它(有时是彻头彻尾的不受欢迎的。)
man 7 signal
手册页介绍了fork()
和exec()
处的信号处理程序会发生什么情况。简而言之,信号处理程序继承fork()
,但重置为默认值exec()
。幸运的是,这正是我们想要的,这里。
如果我们先分叉,然后安装信号处理程序,我们就有一个窗口,在此窗口中子进程已经存在,但是父进程仍然有信号的默认处置(主要是终止)。
相反,我们可以使用例如阻止这些信号来阻止这些信号。分叉前父进程中的sigprocmask()
。阻止信号意味着它等待&#34;等待&#34 ;;在信号解除阻塞之前不会发送。在子进程中,信号可能会保持阻塞状态,因为无论如何信号处置都会重置为exec()
以上的默认值。在父进程中,我们可以 - 或者在分叉之前,无关紧要 - 安装信号处理程序,最后解锁信号。这样我们就不需要原子的东西,甚至也不需要检查子pid是否为零,因为在任何信号传递之前,子pid将被设置为它的实际值!
while
循环基本上只是围绕waitpid()
调用的循环,直到我们开始退出的确切子进程,或者有趣的事情发生(子进程以某种方式消失)。如果要在没有EINTR
标志的情况下安装信号处理程序,此循环包含非常仔细的错误检查以及正确的SA_RESTART
处理。
如果我们分叉的子进程退出,我们检查退出状态和/或它死亡的原因,并将诊断消息打印到标准错误。
最后,程序以一个可怕的黑客结束:我们返回我们在子进程退出时使用waitpid获取的整个状态字,而不是返回EXIT_SUCCESS
或EXIT_FAILURE
。我之所以留下这个,是因为它有时会在实践中使用,当你想要返回与返回的子进程相同或类似的退出状态代码时。所以,它是为了说明。如果你发现自己处于一种情况,当你的程序应该返回与它分叉和执行的子进程相同的退出状态时,这仍然比设置机制让进程使用杀死子进程的相同信号自杀处理。如果你需要使用它,请在那里加上一个醒目的注释,并在安装说明中注明,以便那些在可能不需要的架构上编译程序的人可以修复它。