clinuxsignalsprocess-group

SIGTERM to all children processes but not parent


I have a C program that operates by responding to signals. Some signals cause the parent to fork. This allows other processing while the parent continues to respond to signals.

When the parent is sent a SIGTERM, I want the forked children to receive a SIGTERM as well. It isn't critical that the children finish handling the SIGTERM before the parent exits.

However, with the below code, the children do not receive a SIGTERM when I call kill(0, SIGTERM) from the parent. From the kill manpage, it looks like all of the children should get this SIGTERM.

I have a signal handler setup for the parent.

static volatile sig_atomic_t done = 0;
const int handled_signals[] = {SIGINT, SIGTERM, 0};

static void set_flag(int signum) {
    switch (signum) {
    /* Intentionally exclude SIGQUIT/SIGABRT/etc. as we want to exit
     * without cleaning up to help with debugging */
    case SIGTERM:
    case SIGINT:
        done = 1;
        break;
    default:
        /* Should be unreachable, but just in case */
        if (signal(signum, SIG_DFL) != SIG_ERR) {
            raise(signum);
        }
    }
}

static int setup_handlers() {
    struct sigaction sa;
    sigset_t block_all;
    int i;

    /* Block all other signals while handling a signal. This is okay as
     * our handler is very brief */
    sigfillset(&block_all);
    sa.sa_mask = block_all;

    sa.sa_handler = set_flag;
    for (i = 0; handled_signals[i] != 0; i++) {
        if (sigaction(handled_signals[i], &sa, NULL)) {
            err_log("Unable to set sigaction");
            return 1;
        }
    }

    /* Ignore SIGCHLD as we don't keep track of child success */
    sa.sa_handler = SIG_IGN;
    if (sigaction(SIGCHLD, &sa, NULL)) {
        err_log("Unable to ignore SIGCHLD");
        return 1;
    }

    return 0;
}

int main() {
    int i;
    sigset_t block_mask, orig_mask;

    setup_handlers();

    /* Block all of our handled signals as we will be using
     * sigsuspend in the loop below */
    sigemptyset(&block_mask);
    for (i = 0; handled_signals[i] != 0; i++) {
        sigaddset(&block_mask, handled_signals[i]);
    }

    if (sigprocmask(SIG_BLOCK, &block_mask, &orig_mask)) {
        err_log("Error blocking signals");
    }

    while (!done) {
        if (sigsuspend(&orig_mask) && errno != EINTR) {
            err_log("sigsuspend");
        }
    }

    /* Kill all children */
    if (kill(0, SIGTERM)) {
        err_log("kill(0, SIGTERM))");
    }
}

After getting a signal that requires a fork, I do the following

static int unregister_handlers() {
    struct sigaction sa;
    int i;

    sa.sa_handler = SIG_DFL;

    for (i = 0; handled_signals[i] != 0; i++) {
        if (sigaction(handled_signals[i], &sa, NULL)) {
            err_log("sigaction unregister");
            return 1;
        }
    }

    if (sigaction(SIGCHLD, &sa, NULL)) {
        err_log("sigaction SIGCHLD unregister");
        return 1;
    }

    return 0;
}

void do_fork() {

    switch(fork()) {
    /* Error */
    case -1:
        err_log("fork");
        break;

    /* Child */
    case 0:
        if (unregister_handlers()) {
            _exit(1);
        }
        do_fork_stuff();
        _exit(0);
        break;

    /* Parent */
    default:
        break;
    }
}

In do_fork_stuff, the child sleeps for 30 seconds. I then call kill(0, SIGTERM) from the parent. The children do not terminate.

What's the reason the children aren't getting the SIGTERM?


Solution

  • Ah, a little help from /proc/[PID]/status solved this.

    $ cat /proc/31171/status
    Name:   myprog
    SigQ:   2/16382
    SigPnd: 0000000000000000
    ShdPnd: 0000000000000000
    SigBlk: 0000000000004203
    SigIgn: 0000000000000000
    SigCgt: 0000000180000000
    

    The blocked signals (SigBlk) were the issue here. While the handlers were unregistered, the children were blocking SIGTERM. Removing the blocked signals resolved the issue.