首页 文章

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

提问于
浏览
7

这是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 回答

  • 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不会产生信号,并且只是被解释为正常输入 .

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

相关问题