用SIGTERM调用subprocess终止父进程,但用SIGKILL调用subprocess使父进程保持活动状态

这是如何防止subprocess中的SIGINT传播并杀死父进程的延续?

在上面的问题中,我了解到SIGINT并不是从孩子到家长冒泡,而是发给整个前台进程组,意味着我需要编写一个信号处理程序来防止父母在我按CTRL + C时退出CTRL + C

我试图实现这一点,但这是问题。 关于具体的kill系统调用,我调用终止子,如果我通过SIGKILL ,一切都按预期工作,但如果我通过SIGTERM ,它也终止父进程,稍后在shell提示符显示Terminated: 15

尽pipeSIGKILL有效,但我想使用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时,它不能拦截信号,所以我的杀人电话按预期工作。 这只是一个黑暗刺中。

有人可以解释为什么发生这种情况?

正如我注意到,我并不兴奋我的handle_sigint函数。 是否有一个更加标准的杀死互动式subprocess的方法?

Solutions Collecting From Web of "用SIGTERM调用subprocess终止父进程,但用SIGKILL调用subprocess使父进程保持活动状态"

你的代码中有太多的错误(从不清除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不会引起信号,只是被解释为正常的输入。

您可以使用.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()是编译器提供的内置__atomic_load_n() ; 对于海湾合作委员会,详情请看这里 。 他们避免了只在部分分配子pid时发生信号的问题。 在一些常见的体系结构中,这不会发生,但是这只是一个小心的例子,所以原子访问被用来确保只有一个完整的(旧的新的)值被看到。 我们可以完全避免使用这些,如果我们在转换过程中临时阻塞相关信号。 同样,我决定原子访问更简单,在实践中可能会很有趣。

forward_handler()函数以原子方式获得子进程的PID,然后验证它是非零的(我们知道我们有一个子进程),并且我们不转发子进程发送的信号(只是为了确保我们不会导致一个信号风暴,两个互相轰炸的信号)。 siginfo_t结构中的各个字段在man 2 sigaction手册页中列出。

forward_signal()函数为指定的信号signum安装上述处理程序。 请注意,我们首先使用memset()将整个结构清零。 如果将结构中的某些填充转换为数据字段,则以这种方式清除可确保将来的兼容性。

struct sigaction.sa_mask字段是一组无序的信号。 设置在掩码中的信号在执行信号处理程序的线程中被阻止传递。 (对于上面的示例程序,我们可以放心地说,这些信号在信号处理程序运行时被阻塞;只是在多线程程序中,信号只能在用于运行处理程序的特定线程中被阻塞。

使用sigemptyset(&act.sa_mask)清除信号掩码非常重要。 简单地将结构设置为零是不够的,即使它在许多机器上实际上可能工作。 (我不知道,我甚至都没有检查过,我更喜欢强壮和可靠,而不是懒惰和脆弱的任何一天!)

使用的标志包括SA_SIGINFO因为处理程序使用三参数形式(并使用si_pid字段)。 SA_RESTART标志仅在那里,因为OP希望使用它; 它只是意味着如果可能的话,C库和内核尝试避免返回errno == EINTR错误,如果使用当前在系统调用中阻塞的线程(如wait() )传递信号。 您可以删除SA_RESTART标志,并添加一个调试fprintf(stderr, "Hey!\n"); 在父进程的循环中的适当位置,看看会发生什么。

如果没有错误, sigaction()函数将返回0,否则返回-1 。 如果forward_handler成功分配,则forward_signal()函数返回0,否则返回非零的errno数字。 有些人不喜欢这种返回值(他们宁愿只是返回一个错误,而不是errno值本身),但我有一些不合理的理由喜欢这个成语。 一定要改变它,如果你想的话。

现在我们到main()

如果您不带参数运行该程序,或者使用单个-h--help参数,则会打印一个使用情况摘要。 再次,这样做只是我喜欢的东西 – getopt()getopt_long()更常用于解析命令行选项。 对于这种微不足道的程序,我只是硬编码参数检查。

在这种情况下,我故意将使用输出保留得很短。 再加一段关于这个程序到底是什么的,真的会好得多。 这些类型的文本 – 特别是代码中的注释(解释意图 ,代码应该做什么的概念,而不是描述代码的实际内容)是非常重要的。 自从我第一次获得付款编写代码以来,已经过去了二十多年了,而且我仍然在学习如何评论 – 更好地描述我的代码的意图,所以我认为越早开始工作,更好。

fork()部分应该是熟悉的。 如果它返回-1 ,则分叉失败(可能是由于限制或某些原因),那么打印出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)习语,因为它允许执行命令行上指定的另一个命令,而不必担心解析命令行或执行一个外壳(有时候是不可取的)。

man 7 signal手册页解释了在fork()exec()处发生什么信号处理。 简而言之,信号处理程序通过fork()继承,但在exec()重置为默认值。 幸运的是,这正是我们想要的。

如果我们先分叉,然后安装信号处理程序,那么我们就会有一个窗口,在这个窗口期间子进程已经存在,但是父进程仍然具有信号的默认配置(大多是终止)。

相反,我们可以在分叉之前使用例如sigprocmask()在父进程中阻塞这些信号。 阻断信号意味着它被“等待”; 在信号解除阻塞之前它不会被传送。 在子进程中,信号可能会一直处于阻塞状态,因为无论如何,通过exec()将信号处置重置为默认值。 在父进程中,我们可以 – 或者在分叉之前,无关紧要 – 安装信号处理程序,最后解除阻塞信号。 这样我们就不需要原子的东西,甚至不检查孩子的pid是否为零,因为孩子的pid在任何信号传递之前都会被设置为其实际值。

while循环基本上只是一个围绕着waitpid()调用的循环,直到我们开始退出的确切的子进程,或者发生了一些有趣的事情(子进程以某种方式消失)。 这个循环包含了非常小心的错误检查,以及如果信号处理程序没有SA_RESTART标志的情况下安装正确的EINTR处理。

如果我们分叉的子进程退出,我们检查退出状态和/或导致死亡的原因,并将诊断消息打印到标准错误。

最后,程序以一个可怕的黑客结束:而不是返回EXIT_SUCCESSEXIT_FAILURE ,而是返回我们用waitpid获得的整个状态字,当子进程退出。 我之所以这样做,是因为它有时在实践中用于返回与返回的子进程相同或相似的退出状态代码。 所以,这是为了说明。 如果您发现自己处于一种情况,即您的程序应该返回与子进程相同的退出状态并执行分叉并执行,则仍然比设置机器以使该进程使用杀死该孩子的相同信号自杀处理。 如果您需要使用这个功能,请在这里提出一个突出的评论,并在安装说明中提供一个说明,以便那些在可能不需要的架构上编译程序的人可以修复它。