gpt4 book ai didi

c - 使用SIGTERM在子进程上调用kill会终止父进程,但是使用SIGKILL调用它会使父进程保持事件状态

转载 作者:行者123 更新时间:2023-12-02 17:09:11 25 4
gpt4 key购买 nike

这是How to prevent SIGINT in child process from propagating to and killing parent process?的延续

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

我试图实现这一点,但这就是问题所在。具体来说,我调用的kill syscall终止了子进程,如果我传递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函数并不感到兴奋。有没有更标准的杀死交互式子进程的方式?

最佳答案

您的代码中存在太多错误(由于未清除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信号,因此反射(reflect)作业控制信号是很重要的(就像使用Ctrl + Z时一样)。为了进行适当的作业控制,父进程将需要建立一个新的 session 和一个进程组,并暂时与终端分离。这也是可能的,但是超出了本文的范围,因为它涉及 session ,进程组和终端的相当详细的行为,以便正确管理。

让我们解构上面的示例程序。

示例程序本身首先安装一些信号反射器,然后派生一个子进程,然后该子进程执行 sqlite3命令。 (您可以为该程序添加任何可执行文件和任何参数字符串。)
internal_child_pid变量以及 set_child_pid()get_child_pid()函数用于原子地管理子进程。 __atomic_store_n()__atomic_load_n()是编译器提供的内置组件。有关GCC的信息,请查询 see here。它们避免了仅部分分配子pid时发生信号的问题。在某些常见的体系结构上不会发生这种情况,但这只是作为一个仔细的示例,因此原子访问用于确保仅看到完整的(旧的或新的)值。如果我们在过渡期间暂时屏蔽了相关信号,则可以避免完全使用这些信号。再次,我认为原子访问更简单,并且在实践中可能会很有趣。
forward_handler()函数以原子方式获取子进程PID,然后验证它是否为非零(我们知道我们有一个子进程),并且我们没有转发该子进程发送的信号(只是为了确保我们不会引起信号) Storm ,两人用信号相互轰炸)。 siginfo_t 手册页中列出了 man 2 sigaction结构中的各个字段。
forward_signal()函数为指定的信号 signum安装上述处理程序。请注意,我们首先使用 memset()将整个结构清除为零。如果结构中的某些填充转换为数据字段,则以这种方式清除它可以确保将来的兼容性。
.sa_mask中的 struct sigaction字段是无序的信号集。屏蔽中设置的信号被阻止在执行信号处理程序的线程中传递。 (对于上面的示例程序,我们可以放心地说,这些信号在信号处理程序运行时被阻塞;只是在多线程程序中,信号仅在用于运行处理程序的特定线程中被阻塞。)

使用 sigemptyset(&act.sa_mask)清除信号掩码很重要。仅将结构设置为零是不够的,即使实际上在许多机器上都可以(可能)工作。 (我不知道;我什至没有检查过。我更喜欢鲁棒和可靠,而不是懒惰和脆弱!)

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

如果没有错误, sigaction()函数将返回0,否则将返回 -1设置为 errno的函数。如果成功分配了 forward_signal(),则 forward_handler函数返回0,否则返回非零errno号。有些人不喜欢这种返回值(他们更愿意为错误返回-1,而不是 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)惯用语,因为它允许一个人执行在命令行上指定的另一条命令,而不必担心解析命令行或通过 shell 执行它(有时是完全不希望的)。

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

如果要先 fork ,然后安装信号处理程序,我们将有一个窗口,在该窗口中子进程已经存在,但是父进程仍然具有信号的默认配置(主要是终止)。

相反,我们可以使用例如派生前在父进程中使用 sigprocmask() 。阻塞信号意味着使其“等待”;直到信号被解除阻塞,它才会被传送。在子进程中,信号可能会保持阻塞状态,因为无论如何都会通过 exec()将信号处理重置为默认设置。在父进程中,我们可以-或在派生之前无关紧要-安装信号处理程序,最后释放信号。这样,我们将不需要原子填充,甚至不需要检查子pid是否为零,因为在传递任何信号之前,子pid将被设置为其实际值!
while循环基本上只是 waitpid()调用的循环,直到我们开始的确切子进程退出或发生有趣的事情(子进程以某种方式消失)为止。如果要安装没有 EINTR标志的信号处理程序,则此循环包含非常仔细的错误检查以及正确的 SA_RESTART处理。

如果我们派生的子进程退出,我们将检查退出状态和/或退出的原因,并打印诊断消息为标准错误。

最终,该程序以可怕的骇客告终:当子进程退出时,我们返回通过waitpid获得的整个状态字,而不是返回 EXIT_SUCCESSEXIT_FAILURE。我之所以将其保留下来,是因为当您想返回与子进程返回的相同或相似的退出状态代码时,有时会在实践中使用它。因此,仅用于说明。如果您发现自己的程序应返回与其 fork 并执行的子进程相同的退出状态,那么这仍然比设置机器使进程以杀死该子进程的信号杀死自身更好。处理。如果您需要使用此注释,只需在其中添加醒目的注释,并在安装说明中添加注释,以便那些在可能不希望使用的体系结构上编译程序的人可以对其进行修复。

关于c - 使用SIGTERM在子进程上调用kill会终止父进程,但是使用SIGKILL调用它会使父进程保持事件状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40477988/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com