使用SIGTERM对子进程调用kill会终止父进程,但使用SIGKILL调用它会使父进程保持活动状态

时间:2016-11-08 02:28:01

标签: c linux signals fork

这是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 个答案:

答案 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。如果成功分配errnoforward_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_SUCCESSEXIT_FAILURE。我之所以留下这个,是因为它有时会在实践中使用,当你想要返回与返回的子进程相同或类似的退出状态代码时。所以,它是为了说明。如果你发现自己处于一种情况,当你的程序应该返回与它分叉和执行的子进程相同的退出状态时,这仍然比设置机制让进程使用杀死子进程的相同信号自杀处理。如果你需要使用它,请在那里加上一个醒目的注释,并在安装说明中注明,以便那些在可能不需要的架构上编译程序的人可以修复它。