POSIX Signals - SIGCHLD
SIGCHLD (Child Status Changed, signal number 17 on Linux x86_64) is an asynchronous signal sent by the operating system kernel to a parent process whenever one of its child processes changes execution state:
- The child process terminates (exits normally or dies from a signal).
- The child process is stopped (via
SIGSTOPorSIGTSTP). - The child process is resumed (via
SIGCONT).
While the default action for SIGCHLD is to ignore the signal, improper handling of SIGCHLD is the primary cause of Zombie processes (<defunct>) leaking in production systems.
The Zombie Process Lifecycle
When a child process terminates, the operating system frees its virtual memory, closed file descriptors, and CPU allocations.
However, the kernel cannot immediately remove the process entry from the global process table. It must retain a skeleton entry containing:
- The child's Process ID (PID).
- The child's exit status code (or the terminating signal number).
- Resource usage statistics (CPU time, page faults).
This skeleton entry is a Zombie Process (Z state in ps or top). It remains in the kernel process table until the parent process "reaps" it by calling wait() or waitpid().
[ Child Process Runs ] ──► Child exits via exit(0)
│
▼
[ Kernel cleans up memory & files, creates ZOMBIE entry ]
│
▼
[ Kernel delivers SIGCHLD to Parent Process ]
│
┌────────────────┴────────────────┐
│ Parent calls waitpid() │ Parent ignores SIGCHLD
▼ ▼
[ Zombie entry removed from table ] [ Zombie remains indefinitely ]
[ Exit status delivered to parent ] [ Leaks PIDs until PID_MAX hit! ]
Solution 1: The Non-Blocking Reaping Loop
The standard method for preventing zombies is to register a SIGCHLD handler that reaps terminated children using waitpid():
[!CRITICAL] Standard POSIX signals coalesce. If three child processes terminate simultaneously, the kernel sets the pending
SIGCHLDbit only once! If your handler callswaitpid()only once, the remaining two children become permanent zombies.Therefore, you must loop with
WNOHANGuntilwaitpid()returns0or-1.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
void sigchld_handler(int sig) {
int saved_errno = errno; // Preserve errno across signal handler
int status;
pid_t pid;
// WNOHANG: Non-blocking; returns 0 if children exist but none have exited
// Loop drains all pending terminated children
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
// Child exited normally (e.g. exit(0))
} else if (WIFSIGNALED(status)) {
// Child terminated by a signal (e.g. SIGKILL, SIGSEGV)
}
}
errno = saved_errno;
}
int main() {
struct sigaction sa = {
.sa_handler = sigchld_handler,
.sa_flags = SA_RESTART | SA_NOCLDSTOP, // SA_NOCLDSTOP: Only fire on termination
};
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
// Spawn workers...
return 0;
}
Solution 2: Automatic Reaping with SA_NOCLDWAIT
If your parent process does not care about the exit status codes of its children and simply wants them cleaned up automatically, POSIX provides the SA_NOCLDWAIT flag:
struct sigaction sa;
sa.sa_handler = SIG_DFL;
sa.sa_flags = SA_NOCLDWAIT;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
When SA_NOCLDWAIT is enabled:
- The kernel never creates zombie entries for terminating children.
- Children are reaped and purged from the process table immediately upon exit.
- Any subsequent call to
wait()orwaitpid()by the parent blocks until all children terminate and then returns-1witherrno == ECHILD.
Frequently Asked Questions (FAQ)
Why must waitpid() be called in a while loop inside a SIGCHLD handler?
Because POSIX standard signals are unqueued and coalesce; if multiple children terminate concurrently while a signal is pending, only a single SIGCHLD signal is delivered. Calling waitpid() once per signal would reap only the first child, leaving all other terminated children as permanent zombies. The while ((pid = waitpid(-1, &status, WNOHANG)) > 0) loop ensures all terminated children are drained.
Can you kill a zombie process using kill -9?
No. A zombie process is already dead; it has no running code, no stack, and no CPU execution context to receive a signal. The only way to eliminate a zombie process is for its parent to call waitpid(), or for the parent to terminate, which causes the init process (PID 1) or a subreaper to adopt and automatically reap the zombie.
What is the difference between SA_NOCLDSTOP and SA_NOCLDWAIT?
SA_NOCLDSTOP prevents SIGCHLD from being generated when a child is merely paused or resumed (SIGSTOP/SIGCONT), whereas SA_NOCLDWAIT instructs the kernel to immediately purge terminated children without creating zombie entries.
Related Topics
- POSIX Signals Architecture & sigaction: Signal delivery, masks, and async-signal safety.
- POSIX Process Groups & Sessions: Terminal session topology and daemon detachment.
- Linux - fork() System Call: Process creation and Copy-On-Write memory.
