SIGCHLD
Advanced
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.
| 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 |
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;
}
#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;
}
#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.
*/
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)
*/
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;
}
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) |
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
*/
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)
| 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.
