这是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
功能并不感到兴奋 . 是否有更标准的方法来杀死交互式子进程?
1 回答
您的代码中有太多错误(从不清除
struct sigaction
上的信号掩码),任何人都可以解释您所看到的效果 .相反,请考虑以下工作示例代码,例如
example.c
:使用例如编译它
并使用例如
您会注意到Ctrl C不会中断
sqlite3
- 但是,即使您直接运行sqlite3
,它也不会中断 - ;相反,你只是在屏幕上看到^C
. 这是因为sqlite3
以这样的方式设置终端,即Ctrl C不会产生信号,并且只是被解释为正常输入 .您可以使用
.quit
命令从sqlite3
退出,或者在行的开头按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()
是编译器提供的内置函数;对于GCC,see here了解详情 . 它们避免了仅在部分分配子pid时发生信号的问题 . 在一些常见的体系结构中,这不可能发生,但这是一个谨慎的例子,因此原子访问用于确保只看到完全(旧的或新的)值 . 如果我们在转换期间暂时阻止相关信号,我们可以完全避免使用这些信号 . 同样,我认为原子访问更简单,在实践中可能会很有趣 .forward_handler()
函数以原子方式获取子进程PID,然后验证它是非零的(我们知道我们有子进程),并且我们没有转发子进程发送的信号(只是为了确保我们不会产生信号)暴风雨,两人用信号互相轰炸) .siginfo_t
结构中的各个字段列在man 2 sigaction手册页中 .forward_signal()
函数为指定的信号signum
安装上述处理程序 . 请注意,我们首先使用memset()
将整个结构清除为零 . 如果将结构中的某些填充转换为数据字段,则以这种方式清除它可确保将来的兼容性 .struct sigaction
中的.sa_mask
字段是一组无序信号 . 掩码中设置的信号在执行信号处理程序的线程中被阻止传递 . (对于上面的示例程序,我们可以有把握地说,这些信号在信号处理程序运行时被阻止;只是在多线程程序中,信号仅在用于运行处理程序的特定线程中被阻止 . )使用
sigemptyset(&act.sa_mask)
清除信号掩码非常重要 . 简单地将结构设置为零是不够的,即使它在许多机器上实际工作(可能) . (我甚至没有检查过 . 我喜欢坚固可靠,而不是懒惰和脆弱的任何一天!)使用的标志包括
SA_SIGINFO
,因为处理程序使用三参数形式(并使用siginfo_t
的si_pid
字段) .SA_RESTART
旗帜只是因为OP希望使用它;它只是意味着如果可能的话,如果使用当前在系统调用中阻塞的线程(如wait()
)传递信号,则C库和内核会尝试避免返回errno == EINTR
错误 . 您可以删除SA_RESTART
标志,并在父进程的循环中的适当位置添加调试fprintf(stderr, "Hey!\n");
,以查看当时会发生什么 .如果没有错误,
sigaction()
函数将返回0,否则返回-1
并返回errno
. 如果成功分配了forward_handler
,则forward_signal()
函数返回0,否则返回非零的errno数 . 有些人不喜欢这种返回值(他们更喜欢只返回-1表示错误,而不是errno
值本身),但我有一些不合理的理由喜欢这个成语 . 如果你愿意,一定要改变它 .现在我们到达
main()
.如果您运行没有参数的程序,或者使用单个
-h
或--help
参数,则'll print an usage summary. Again, doing this this way is just something I' m喜欢 - getopt()和getopt_long()更常用于解析命令行选项 . 对于这种简单的程序,我只是硬编码参数检查 .在这种情况下,我故意将使用输出留得很短 . 关于该程序的确切内容,增加一个段落真的会好得多 . 这些类型的文本 - 特别是代码中的注释(解释意图,代码应该做什么的想法,而不是描述代码实际执行的内容) - 非常重要 . 它仍然在学习如何评论 - 更好地描述我的代码的意图,所以我认为越早开始研究,越好 .
fork()
部分应该很熟悉 . 如果它返回-1
,则fork失败(可能是由于限制或某些限制),然后打印出errno
消息是一个非常好的主意 . 子进程中的返回值为0
,父进程中的子进程ID为 .execlp()函数有两个参数:二进制文件的名称(PATH环境变量中指定的目录将用于搜索这样的二进制文件),以及指向该二进制文件参数的指针数组 . 第一个参数将是新二进制文件中的
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() . 阻止信号意味着它被制作成"wait";在信号解除阻塞之前不会发送 . 在子进程中,信号可能会被阻塞,因为无论如何信号处置都会重置为
exec()
的默认值 . 在父进程中,我们可以 - 或者在分叉之前,无关紧要 - 安装信号处理程序,最后解锁信号 . 这样我们就不需要原子的东西,甚至也不需要检查子pid是否为零,因为在传递任何信号之前,子pid将被设置为它的实际值!while
循环基本上只是围绕waitpid()
调用的循环,直到我们开始退出的确切子进程,或者有趣的事情发生(子进程以某种方式消失) . 如果要在没有SA_RESTART
标志的情况下安装信号处理程序,此循环包含非常仔细的错误检查以及正确的EINTR
处理 .如果我们分叉的子进程退出,我们检查退出状态和/或它死亡的原因,并将诊断消息打印到标准错误 .
最后,程序以一个可怕的黑客结束:我们返回
EXIT_SUCCESS
或EXIT_FAILURE
,而不是返回EXIT_SUCCESS
或EXIT_FAILURE
,当子进程退出时,我们返回我们用waitpid获得的整个状态字 . 我之所以留下这个,是因为它有时会在实践中使用,当你想要返回与返回的子进程相同或类似的退出状态代码时 . 所以,这是为了说明 . 如果你发现自己处于一种情况,当你的程序应该返回与它分叉和执行的子进程相同的退出状态时,这仍然比设置机制让进程使用杀死子进程的相同信号自杀处理 . 如果您需要使用此功能,请在此处添加一个醒目的注释,并在安装说明中添加注释,以便那些在可能不需要的体系结构上编译该程序的人可以修复它 .