Chapter 26 — Part 6: The SIGCHLD Signal

Chapter 26 — Part 6: The SIGCHLD Signal
Asynchronous Child Monitoring, Handlers, and SIG_IGN Behavior
Topic
SIGCHLD
Level
Advanced
Examples
4 Programs

The Problem with Polling

We’ve seen two approaches to child reaping:

  • Blocking wait(): Freezes the parent — can’t do anything else
  • WNOHANG polling: Wastes CPU time; adds complexity

The clean solution is SIGCHLD: the kernel automatically sends this signal to the parent whenever a child changes state (terminates, stops, or resumes). The parent just needs to set up a signal handler.

When is SIGCHLD Delivered?
Event in Child SIGCHLD sent to parent? Condition
Child exits normally (exit/return) ✓ Always
Child killed by signal ✓ Always
Child stopped by signal ✓ Optional Only if SA_NOCLDSTOP is NOT set
Child resumed by SIGCONT ✓ Optional Linux 2.6.9+, if SA_NOCLDSTOP not set

The Signal Queuing Problem — Why You Must Loop

SIGCHLD is a standard signal — it is NOT queued. If 3 children all die while the handler is running, only 1 SIGCHLD is delivered (or at most 2 — the one running + one pending). You might miss child reaping.

The correct solution: Inside the SIGCHLD handler, loop with waitpid() + WNOHANG until there are no more dead children:

static void sigchld_handler(int sig)
{
    int saved_errno = errno; /* Preserve errno! */
    pid_t child;
    int status;

    /* Loop — reap ALL available dead children */
    while ((child = waitpid(-1, &status, WNOHANG)) > 0) {
        /* process status if needed */
    }

    /* Restore errno — system calls in handlers can change it */
    errno = saved_errno;
}
Always save and restore errno in signal handlers. waitpid() can change errno, which could corrupt the main program’s error checking.

Example 1: Basic SIGCHLD Handler
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

static volatile int num_dead = 0;

static void sigchld_handler(int sig)
{
    int saved_errno = errno;
    pid_t child;
    int status;

    /* Must loop — multiple children may have died */
    while ((child = waitpid(-1, &status, WNOHANG)) > 0) {
        num_dead++;
        if (WIFEXITED(status))
            printf("[Handler] Reaped child PID=%d, exit=%d (total=%d)\n",
                   child, WEXITSTATUS(status), num_dead);
        else if (WIFSIGNALED(status))
            printf("[Handler] Reaped child PID=%d, killed by sig %d\n",
                   child, WTERMSIG(status));
    }

    errno = saved_errno;
}

int main(void)
{
    struct sigaction sa;
    int num_children = 4;

    /* Setup SIGCHLD handler BEFORE forking children */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART; /* Restart interrupted system calls */
    sa.sa_handler = sigchld_handler;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    /* Create children with different sleep times */
    for (int i = 0; i < num_children; i++) {
        pid_t child = fork();
        if (child == 0) {
            sleep(i + 1); /* 1s, 2s, 3s, 4s */
            printf("[Child %d] PID=%d exiting\n", i + 1, getpid());
            _exit(i * 5); /* exit codes 0, 5, 10, 15 */
        }
        printf("[Parent] Created child %d PID=%d\n", i + 1, child);
    }

    /* Parent does its own work while waiting */
    printf("[Parent] Doing other work while children run...\n");
    while (num_dead < num_children) {
        sleep(1);
        printf("[Parent] Still working... dead=%d/%d\n",
               num_dead, num_children);
    }

    printf("[Parent] All children reaped. Done.\n");
    return 0;
}

Example 2: SA_NOCLDSTOP — Control Stop Notifications
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

static void sigchld_handler(int sig)
{
    int saved_errno = errno;
    pid_t child;
    int status;

    while ((child = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED)) > 0) {
        if (WIFEXITED(status))
            printf("[SIGCHLD] Child %d exited with code %d\n",
                   child, WEXITSTATUS(status));
        else if (WIFSTOPPED(status))
            printf("[SIGCHLD] Child %d STOPPED by signal %d\n",
                   child, WSTOPSIG(status));
        else if (WIFCONTINUED(status))
            printf("[SIGCHLD] Child %d CONTINUED\n", child);
        else if (WIFSIGNALED(status))
            printf("[SIGCHLD] Child %d killed by signal %d\n",
                   child, WTERMSIG(status));
    }
    errno = saved_errno;
}

int main(void)
{
    struct sigaction sa;
    pid_t child;

    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sigchld_handler;

    /* WITHOUT SA_NOCLDSTOP = receive SIGCHLD for stops too */
    sa.sa_flags = 0; /* No SA_NOCLDSTOP */
    sigaction(SIGCHLD, &sa, NULL);

    child = fork();
    if (child == 0) {
        printf("[Child] PID=%d. Try: kill -STOP %d\n", getpid(), getpid());
        for (;;) pause(); /* wait for signals */
    }

    printf("[Parent] Child PID=%d created.\n", child);
    printf("[Parent] Send SIGSTOP, SIGCONT, SIGTERM to child to see events.\n");
    printf("[Parent] kill -STOP %d  then  kill -CONT %d  then kill -TERM %d\n",
           child, child, child);

    /* Wait until child is killed */
    int status;
    waitpid(child, &status, 0);
    return 0;
}
/*
 * Test:
 *   Run in one terminal
 *   In another: kill -STOP <child_pid>
 *               kill -CONT <child_pid>
 *               kill -TERM <child_pid>
 * With SA_NOCLDSTOP flag set, stop/continue events would be suppressed.
 */

Example 3: SIG_IGN — Auto-Reap Children

Setting SIGCHLD to SIG_IGN tells the kernel to automatically discard child status — no zombies, no need to call wait().

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int main(void)
{
    /* Set SIGCHLD disposition to SIG_IGN BEFORE forking */
    /* This prevents child zombies — kernel auto-reaps them */
    signal(SIGCHLD, SIG_IGN);

    printf("[Parent] SIGCHLD set to SIG_IGN\n");

    for (int i = 0; i < 3; i++) {
        pid_t child = fork();
        if (child == 0) {
            sleep(1);
            printf("[Child %d] PID=%d exiting\n", i + 1, getpid());
            _exit(0);
        }
        printf("[Parent] Created child %d PID=%d\n", i + 1, child);
    }

    sleep(3); /* Wait for children to exit */

    /* Try to wait — should get ECHILD since no zombies exist */
    pid_t result = wait(NULL);
    if (result == -1 && errno == ECHILD)
        printf("[Parent] wait() returned ECHILD — no zombies (correct!)\n");

    printf("[Parent] Done.\n");
    return 0;
}
/*
 * Important notes:
 *   - Must set SIG_IGN BEFORE fork()
 *   - Children's exit status is discarded — can't retrieve it
 *   - Existing zombies are NOT removed by this; only future children
 *   - Default SIGCHLD disposition is "ignore" but explicitly setting
 *     SIG_IGN has DIFFERENT behavior (this auto-reap)
 */

Example 4: Handling Race Condition with sigprocmask + sigsuspend

A race condition occurs if a child exits between the parent’s “are all children done?” check and the pause()/sigsuspend() call. The solution is to block SIGCHLD before the check and atomically unblock it in sigsuspend.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

static volatile int num_live = 0;
static volatile int sig_count = 0;

static void sigchld_handler(int sig)
{
    int saved_errno = errno;
    pid_t child;
    int status;
    sig_count++;

    while ((child = waitpid(-1, &status, WNOHANG)) > 0) {
        num_live--;
        printf("[Handler] Reaped PID=%d (live remaining=%d)\n",
               child, num_live);
    }
    errno = saved_errno;
}

int main(int argc, char *argv[])
{
    int num_children = 3;
    int sleep_secs[] = {1, 2, 3};
    sigset_t block_mask, empty_mask;
    struct sigaction sa;

    /* Setup handler */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = sigchld_handler;
    sigaction(SIGCHLD, &sa, NULL);

    /* Block SIGCHLD BEFORE creating children */
    /* This prevents SIGCHLD arriving before sigsuspend() loop */
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGCHLD);
    sigprocmask(SIG_SETMASK, &block_mask, NULL);

    num_live = num_children;

    for (int i = 0; i < num_children; i++) {
        pid_t child = fork();
        if (child == 0) {
            printf("[Child %d] PID=%d sleeping %ds\n",
                   i + 1, getpid(), sleep_secs[i]);
            sleep(sleep_secs[i]);
            _exit(0);
        }
    }

    /* sigsuspend atomically:
     *   1. Unblocks SIGCHLD
     *   2. Sleeps until a signal arrives
     *   3. Re-blocks SIGCHLD after handler returns
     * This eliminates the race condition between the check
     * (num_live > 0) and the sleep.
     */
    sigemptyset(&empty_mask);
    while (num_live > 0) {
        if (sigsuspend(&empty_mask) == -1 && errno != EINTR) {
            perror("sigsuspend");
            exit(EXIT_FAILURE);
        }
    }

    printf("[Parent] All children done. SIGCHLD caught %d time(s).\n",
           sig_count);
    printf("[Parent] Note: %d children but possibly fewer signals — normal!\n",
           num_children);
    return 0;
}
Why fewer SIGCHLD than children? If 2 children die while the handler is running, only 1 extra SIGCHLD is queued (signals are not queued multiple times). The loop inside the handler reaps both children in one invocation.

SA_NOCLDWAIT Flag
struct sigaction sa;
sa.sa_handler = SIG_DFL; /* or your handler */
sa.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa, NULL);

Similar effect to SIG_IGN: terminates children are not converted to zombies. However, unlike SIG_IGN, SIGCHLD may still be delivered to the parent (behavior is implementation-specific in SUSv3). Linux does deliver SIGCHLD with SA_NOCLDWAIT.

Approach Zombies? Can retrieve status? SIGCHLD delivered?
waitpid() in handler No (when reaped) Yes Yes
SIGCHLD = SIG_IGN No (auto-reaped) No No
SA_NOCLDWAIT No (auto-reaped) No Yes (on Linux)

Clean Signal-Caused Termination in Handler

If your signal handler catches a normally-terminal signal and you want the parent to know the child was killed by that signal (not that it exited cleanly), re-raise the signal after cleanup:

void signal_handler(int sig)
{
    /* ... perform cleanup ... */

    /* Disestablish this handler, then re-raise the signal.
     * This causes the process to terminate with the correct
     * "killed by signal" status visible to the parent's wait().
     * If we called exit() instead, parent would see normal exit.
     */
    signal(sig, SIG_DFL);
    raise(sig);  /* Terminate with the original signal */
}
/*
 * Without this pattern:
 *   Parent sees: WIFEXITED — misleading, child "exited"
 *
 * With this pattern:
 *   Parent sees: WIFSIGNALED with WTERMSIG == sig — accurate
 */

Key Terms:

SIGCHLD sigaction() SA_NOCLDSTOP SA_NOCLDWAIT SIG_IGN sigsuspend() sigprocmask() saved_errno async-signal-safe race condition

Interview Questions

Q1. What is SIGCHLD and when is it sent?

SIGCHLD is sent to a parent process when one of its children changes state: terminates (normally or by signal), or optionally when stopped/resumed by a signal.

Q2. Why must the SIGCHLD handler loop with waitpid(WNOHANG)?

Because SIGCHLD is not queued — if multiple children die rapidly, only one signal is delivered. A single waitpid() call would reap only one child. The loop reaps all available dead children per handler invocation.

Q3. Why do you need to save and restore errno in a signal handler?

System calls like waitpid() inside the handler can modify errno. If the main program checks errno after a failed syscall and the handler changes it in between, the main program reads the wrong error code.

Q4. What is SA_NOCLDSTOP and when would you use it?

SA_NOCLDSTOP prevents SIGCHLD from being sent when a child is stopped by a signal. Use it when you only care about child termination and don’t want stop events triggering your handler.

Q5. What is the difference between SIGCHLD = SIG_IGN vs the default (which is also ignore)?

The default “ignore” simply discards the signal. Explicitly setting SIG_IGN has an additional effect: children are auto-reaped immediately on exit without becoming zombies, and wait() returns ECHILD.

Q6. Describe the race condition in SIGCHLD handling and how to fix it.

If a child dies between the “any children alive?” check and the pause()/sigsuspend() call, the SIGCHLD is missed and sigsuspend blocks forever. Fix: block SIGCHLD with sigprocmask() before the check, then use sigsuspend() which atomically unblocks SIGCHLD while sleeping.

Q7. Is printf() safe to call inside a SIGCHLD handler?

No. printf() is not async-signal-safe. It uses internal locks that can cause deadlock if the main program is also calling printf() when the signal arrives. For production code, use only async-signal-safe functions (write(), etc.) in handlers.

Q8. Why should the SIGCHLD handler be established BEFORE creating any children?

If a child terminates before the handler is installed, whether SIGCHLD is generated retroactively is implementation-specific. Installing the handler first ensures you don’t miss any early exits.

Q9. What does SA_RESTART do in the context of SIGCHLD?

SA_RESTART causes system calls interrupted by SIGCHLD to automatically restart (rather than returning -1 with EINTR). This prevents spurious failures in the main loop when the SIGCHLD handler fires.

Q10. Can you use waitpid() with WUNTRACED from inside a SIGCHLD handler?

Yes. This lets you detect stopped children from within the handler as well. Combine: waitpid(-1, &status, WNOHANG | WUNTRACED)

Chapter 26 Summary — Key Takeaways
Concept Key Point
wait() Blocks, waits for ANY child, returns child PID
waitpid() Target specific child, WNOHANG, WUNTRACED
W* macros Always use macros to decode status; never inspect raw bits
waitid() Returns siginfo_t, supports WNOWAIT peek
Zombie Dead child awaiting parent’s wait(); immune to SIGKILL
Orphan Child whose parent died; adopted by init (PID 1)
SIGCHLD handler Loop with WNOHANG; save/restore errno; install before fork
SIG_IGN Auto-reap children; status discarded; wait() → ECHILD

Chapter 26 Complete! Continue with Process Execution

You’ve mastered child process monitoring — from basic wait() to SIGCHLD-driven async reaping.

Back to Course Index Next Chapter →

Leave a Reply

Your email address will not be published. Required fields are marked *