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

m0m*_*eni 7 c linux fork signals

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

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

我试图实现这一点,但这是问题所在.关于kill我调用以终止子进程的系统调用,如果我传入SIGKILL,一切都按预期工作,但如果我传入SIGTERM,它也会终止父进程,Terminated: 15稍后在shell提示中显示.

尽管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");
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我对于为什么会发生这种情况的猜测将会截获SIGTERM,并杀死整个流程组.然而,当我使用SIGKILL时,它无法拦截信号,因此我的kill调用按预期工作.这只是在黑暗中刺伤.

有人可以解释为什么会这样吗?

我个人注意到,我对我的handle_sigint功能并不感到兴奋.是否有更标准的方法来杀死交互式子进程?

Nom*_*mal 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;
}
Run Code Online (Sandbox Code Playgroud)

使用例如编译它

gcc -Wall -O2 example.c -o example
Run Code Online (Sandbox Code Playgroud)

并使用例如

./example sqlite3
Run Code Online (Sandbox Code Playgroud)

你会注意到Ctrl+ C不会中断sqlite3- 但是再说一遍,即使你sqlite3直接跑也不会- 相反,你只是^C在屏幕上看到.这是因为sqlite3Ctrl+ 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()是编译器提供的内置插件; 对于GCC,请参阅此处了解详情.它们避免了仅在部分分配子pid时发生信号的问题.在一些常见的体系结构中,这不可能发生,但这是一个谨慎的例子,因此原子访问用于确保只看到完全(旧的新的)值.如果我们在转换期间暂时阻止相关信号,我们可以完全避免使用这些信号.同样,我认为原子访问更简单,在实践中可能会很有趣.

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

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

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

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

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

sigaction()如果没有错误,或者函数会返回0 -1errno另外设定.forward_signal()如果forward_handler已成功分配,则该函数返回0,否则返回非零的errno数.有些人不喜欢这种返回值(他们更喜欢只返回-1表示错误,而不是errno值本身),但我有些不合理的理由喜欢这个成语.如果你愿意,一定要改变它.

现在我们来了main().

如果您运行没有参数的程序,或使用单个-h--help参数,它将打印使用情况摘要.再一次,这样做只是我喜欢的东西 - 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().简而言之,信号处理程序通过a继承fork(),但重置为默认值exec().幸运的是,这正是我们想要的,这里.

如果我们先分叉,然后安装信号处理程序,我们就有一个窗口,在此窗口中子进程已经存在,但是父进程仍然有信号的默认处置(主要是终止).

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

while循环基本上是围绕在一个循环waitpid()呼叫,直到确切的子过程中,我们开始退出,或者一些有趣的发生(子进程莫名其妙消失).这个循环包含非常仔细的错误检查,以及在EINTR没有SA_RESTART标志的情况下安装信号处理程序时的正确处理.

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

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

  • @AR7:不需要。我写这篇文章是希望其他人也会遇到你的问题,并且也会发现一个有用的解释示例。一次性吸收的内容有很多,但如果您费力浏览所有的文字墙,您也可以做得更好并玩转……如果您对示例或其行为有任何疑问或问题,只需将其添加为此处的评论,这样我就会收到通知;我只是偶尔上网。 (2认同)