Loading learning content...
In a hierarchical process model, every process (except init) has a parent. But what happens when a parent process terminates before its children? This question touches on fundamental operating system design decisions with significant implications for system stability, resource management, and application behavior.
Different systems take different approaches:
Understanding these mechanisms is essential for designing robust multi-process applications, implementing proper daemon behavior, and working with modern container orchestration systems.
By the end of this page, you will understand: orphan process handling and reparenting to init, the role of init/systemd as the ultimate parent, process groups and session-based termination, SIGHUP propagation when terminals close, how containers implement cascading termination, and best practices for managing process hierarchies in production.
When a parent process terminates while its children are still running, those children become orphaned processes. The operating system must decide:
UNIX's Answer: Reparenting to init
In UNIX-like systems, orphaned processes are reparented to init (PID 1). This design choice has profound implications:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> void print_process_info(const char* name) { printf("%s: PID=%d, PPID=%d\n", name, getpid(), getppid());} int main() { printf("=== Orphan Process Demonstration ===\n\n"); print_process_info("Parent"); pid_t child = fork(); if (child == 0) { // Child process print_process_info("Child (before parent exit)"); // Wait for parent to exit sleep(2); // Now check PPID - should be 1 (init) or subreaper print_process_info("Child (after parent exit)"); // Verify we're orphaned if (getppid() == 1) { printf("Child: I've been adopted by init (PID 1)\n"); } else { printf("Child: Adopted by subreaper (PID %d)\n", getppid()); } printf("Child: Continuing to run as orphan...\n"); sleep(2); printf("Child: Exiting normally\n"); _exit(0); } // Parent process printf("Parent: Created child PID %d\n", child); printf("Parent: Exiting WITHOUT waiting for child\n"); // Exit immediately - child becomes orphan // Do NOT call wait() return 0;} /* * Output: * === Orphan Process Demonstration === * * Parent: PID=1000, PPID=999 * Parent: Created child PID 1001 * Parent: Exiting WITHOUT waiting for child * Child (before parent exit): PID=1001, PPID=1000 * Child (after parent exit): PID=1001, PPID=1 * Child: I've been adopted by init (PID 1) * Child: Continuing to run as orphan... * Child: Exiting normally */Modern Linux systems allow processes to become 'subreapers' using prctl(PR_SET_CHILD_SUBREAPER). A subreaper adopts orphaned descendants instead of init. This is used by systemd, container runtimes, and session managers to manage process trees without polluting init's responsibilities.
The init process (PID 1) has a critical responsibility: preventing zombie accumulation. When any process terminates, it becomes a zombie until its parent calls wait(). For orphaned processes, init is the parent that must call wait().
Init's Zombie-Reaping Loop:
// Simplified init reaping logic
while (1) {
pid_t child = wait(NULL); // Wait for ANY child
if (child > 0) {
// Child reaped, zombie cleaned up
}
// Handle other init duties...
}
Why This Matters:
Without init's reaping:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <signal.h> void show_zombies() { // Count zombie children (Linux-specific) char cmd[256]; snprintf(cmd, sizeof(cmd), "ps --ppid %d -o pid,stat,comm 2>/dev/null | grep -c Z || echo 0", getpid()); printf("Zombie count: "); fflush(stdout); system(cmd);} void demonstrate_zombie_accumulation() { printf("\n=== Zombie Accumulation Demo ===\n"); printf("(Demonstrating what happens without wait())\n\n"); // Create several children that will become zombies for (int i = 0; i < 5; i++) { pid_t pid = fork(); if (pid == 0) { // Child: exit immediately _exit(i); } printf("Created child PID %d (will become zombie)\n", pid); } // Don't reap them - let them become zombies sleep(1); printf("\nChildren have exited, parent hasn't called wait():\n"); system("ps --ppid $PPID -o pid,stat,comm 2>/dev/null | head -10"); show_zombies(); // Now reap them printf("\nNow reaping zombies with wait()...\n"); while (wait(NULL) > 0); printf("After reaping:\n"); show_zombies();} void demonstrate_sigchld_handling() { printf("\n=== Automatic Reaping with SIGCHLD ===\n"); // Option 1: Ignore SIGCHLD - kernel auto-reaps // signal(SIGCHLD, SIG_IGN); // Option 2: Use SA_NOCLDWAIT flag struct sigaction sa; sa.sa_handler = SIG_DFL; sa.sa_flags = SA_NOCLDWAIT; // Don't create zombies sigemptyset(&sa.sa_mask); sigaction(SIGCHLD, &sa, NULL); printf("Set SA_NOCLDWAIT - children won't become zombies\n"); for (int i = 0; i < 5; i++) { pid_t pid = fork(); if (pid == 0) { _exit(0); } printf("Created child PID %d\n", pid); } sleep(1); printf("\nAfter children exit (no zombies due to SA_NOCLDWAIT):\n"); show_zombies();} int main() { demonstrate_zombie_accumulation(); demonstrate_sigchld_handling(); return 0;}Three ways to prevent zombie accumulation: (1) Call wait()/waitpid() to reap children, (2) Set signal(SIGCHLD, SIG_IGN) to auto-reap, (3) Use sigaction with SA_NOCLDWAIT flag. For daemons that create many short-lived children, SIG_IGN is often the simplest solution.
While parent-child relationships determine inheritance and zombie handling, process groups and sessions provide a mechanism for group-wide termination via signals.
Process Group (PGID):
ls | grep foo | wcSession (SID):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h>#include <string.h> void sigterm_handler(int signum) { char buf[100]; int len = snprintf(buf, sizeof(buf), "PID %d received SIGTERM\n", getpid()); write(STDOUT_FILENO, buf, len); _exit(0);} void print_ids(const char* name) { printf("%s: PID=%d, PPID=%d, PGID=%d, SID=%d\n", name, getpid(), getppid(), getpgrp(), getsid(0));} void demonstrate_group_signal() { printf("\n=== Process Group Signals ===\n"); // Create new process group with self as leader setpgid(0, 0); // PGID = PID pid_t group_leader = getpid(); print_ids("Group Leader"); // Create children in our group pid_t children[3]; for (int i = 0; i < 3; i++) { children[i] = fork(); if (children[i] == 0) { // Child: install signal handler and wait signal(SIGTERM, sigterm_handler); char name[32]; snprintf(name, sizeof(name), "Child %d", i); print_ids(name); // Wait for signal while (1) pause(); _exit(0); // Never reached } } sleep(1); // Let children set up printf("\nLeader: Sending SIGTERM to entire group (PGID %d)\n", group_leader); // Send signal to process group // Negative PID means "process group" kill(-group_leader, SIGTERM); // Note: Leader also receives the signal, but we haven't installed // a handler, so we continue // Reap children for (int i = 0; i < 3; i++) { waitpid(children[i], NULL, 0); } printf("Leader: All children terminated\n");} void demonstrate_new_session() { printf("\n=== Session Creation ===\n"); pid_t pid = fork(); if (pid == 0) { // Child: print initial session info printf("Child before setsid():\n"); print_ids("Child"); // Create new session pid_t new_sid = setsid(); if (new_sid < 0) { perror("setsid"); _exit(1); } printf("\nChild after setsid():\n"); print_ids("Child (session leader)"); printf("Note: SID == PID, no controlling terminal\n"); _exit(0); } wait(NULL);} int main() { printf("Process Groups and Sessions Demo\n"); printf("=================================\n"); print_ids("Original Process"); demonstrate_group_signal(); demonstrate_new_session(); return 0;}kill(-pgid, signal) sends a signal to all processes in group pgid. kill(0, signal) sends to all processes in the caller's group. Be careful: the sender typically receives the signal too! Install handlers or check PID before sending.
When a terminal (or SSH session) closes, the kernel sends SIGHUP (hangup signal) to the session's foreground process group. This is a form of cascading termination—closing a terminal can kill all processes started from it.
The SIGHUP Propagation Chain:
Survival Strategies:
Two ways for processes to survive terminal closure:
| Method | How It Works | Usage |
|---|---|---|
| nohup command | Ignores SIGHUP, redirects output | nohup ./script.sh & |
| disown | Shell stops tracking the job | ./script.sh & disown |
| setsid | Creates new session (no terminal) | setsid ./script.sh |
| tmux/screen | Runs in persistent pseudo-terminal | tmux new -d ./script.sh |
| systemd unit | Process managed by systemd | systemctl start myservice |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <fcntl.h>#include <sys/stat.h>#include <string.h>#include <time.h> #define LOG_FILE "/tmp/sighup_demo.log" void log_message(const char* msg) { int fd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd >= 0) { time_t now = time(NULL); char buf[256]; int len = snprintf(buf, sizeof(buf), "[%ld] PID %d: %s\n", now, getpid(), msg); write(fd, buf, len); close(fd); }} void sighup_handler(int signum) { log_message("Received SIGHUP - ignoring and continuing"); // Note: In real daemons, SIGHUP often means "reload config"} void demonstrate_nohup_behavior() { // Simulate nohup: ignore SIGHUP signal(SIGHUP, sighup_handler); // Redirect stdout/stderr to files (like nohup does) // freopen("/dev/null", "w", stdout); // freopen("/dev/null", "w", stderr); log_message("Started - SIGHUP will be ignored"); printf("Process running with SIGHUP handler\n"); printf("Log file: %s\n", LOG_FILE); printf("Try: kill -HUP %d\n", getpid()); printf("Then check the log file\n\n"); // Simulate work for (int i = 0; i < 30; i++) { char msg[64]; snprintf(msg, sizeof(msg), "Still running... iteration %d", i); log_message(msg); sleep(2); } log_message("Exiting normally");} void daemonize() { // Standard daemonization: survive terminal close // First fork: exit parent pid_t pid = fork(); if (pid > 0) exit(0); if (pid < 0) exit(1); // Create new session: escape terminal setsid(); // Second fork: prevent reacquiring terminal pid = fork(); if (pid > 0) exit(0); if (pid < 0) exit(1); // Now we're a proper daemon // Change to root directory chdir("/"); // Close stdin/stdout/stderr close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); // Redirect to /dev/null open("/dev/null", O_RDONLY); // stdin open("/dev/null", O_WRONLY); // stdout open("/dev/null", O_WRONLY); // stderr // Install SIGHUP handler (often used for config reload) signal(SIGHUP, sighup_handler); log_message("Daemonized - immune to terminal hangup"); // Daemon work loop while (1) { log_message("Daemon heartbeat"); sleep(10); }} int main(int argc, char* argv[]) { printf("SIGHUP Demonstration\n"); printf("====================\n\n"); if (argc > 1 && strcmp(argv[1], "--daemon") == 0) { printf("Daemonizing... check %s for output\n", LOG_FILE); daemonize(); // Never returns } printf("Run with --daemon to daemonize\n"); printf("Running in foreground with SIGHUP handling...\n\n"); demonstrate_nohup_behavior(); return 0;}Modern daemons often use SIGHUP as a 'reload configuration' signal rather than ignoring it. The double-fork daemonization pattern shown above is traditional; with systemd, you can use 'Type=simple' or 'Type=forking' in unit files and let systemd handle the session management.
Containers fundamentally change process termination semantics. Within a container:
Why Containers Need Cascading Termination:
Containers represent isolated environments. When a container stops:
How It Works (Docker/containerd):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h>#include <string.h>#include <errno.h> /* * Proper Container Init Process Pattern * * When running as PID 1 in a container, you must: * 1. Forward signals to children * 2. Reap zombies (no real init is watching) * 3. Exit cleanly on SIGTERM * * This is what tools like 'tini' and 'dumb-init' do. */ static volatile sig_atomic_t shutdown_requested = 0;static pid_t main_child = 0; void signal_handler(int signum) { if (signum == SIGTERM || signum == SIGINT) { shutdown_requested = 1; // Forward to main child if (main_child > 0) { kill(main_child, signum); } } else if (signum == SIGCHLD) { // Reap all zombies int status; pid_t pid; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { if (pid == main_child) { // Main application exited shutdown_requested = 1; } } }} int container_init(char* const argv[]) { // We're PID 1 in a container if (getpid() != 1) { fprintf(stderr, "Warning: Not running as PID 1\n"); } printf("Container init starting...\n"); // Set up signal handling struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGCHLD, &sa, NULL); // Spawn the main application main_child = fork(); if (main_child == 0) { // Child: exec the actual application execvp(argv[0], argv); fprintf(stderr, "exec failed: %s\n", strerror(errno)); _exit(127); } printf("Started main application (PID %d)\n", main_child); // Init loop: monitor children while (!shutdown_requested) { pause(); // Wait for signals } printf("Shutdown requested, waiting for children...\n"); // Wait for main child int status; if (main_child > 0) { waitpid(main_child, &status, 0); if (WIFEXITED(status)) { printf("Main app exited with code %d\n", WEXITSTATUS(status)); return WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { printf("Main app killed by signal %d\n", WTERMSIG(status)); return 128 + WTERMSIG(status); } } return 0;} int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <command> [args...]\n", argv[0]); fprintf(stderr, "Container init wrapper\n"); return 1; } return container_init(argv + 1);}When PID 1 exits in a PID namespace (container), the kernel sends SIGKILL to all other processes in that namespace. There's no graceful shutdown for other processes—they're terminated immediately. This is why container init wrappers like tini or dumb-init are important for proper signal handling.
Linux provides subreaping as a mechanism for non-init processes to adopt orphaned descendants. This is essential for:
PR_SET_CHILD_SUBREAPER:
A process that sets this flag becomes the 'init' for its descendants:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
#define _GNU_SOURCE#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/prctl.h>#include <sys/wait.h>#include <signal.h> void print_ancestry(const char* name) { printf("%s: PID=%d, PPID=%d\n", name, getpid(), getppid());} int main() { printf("=== Subreaper Demonstration ===\n\n"); print_ancestry("Main process (will become subreaper)"); // Become a subreaper if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) { perror("prctl"); return 1; } printf("\nSet as subreaper - orphaned descendants will be adopted\n\n"); // Create child pid_t child = fork(); if (child == 0) { print_ancestry("Child (intermediate)"); // Child creates grandchild pid_t grandchild = fork(); if (grandchild == 0) { printf("\nGrandchild: waiting for parent to exit...\n"); sleep(2); // Parent (child) should have exited by now print_ancestry("Grandchild (after parent exit)"); if (getppid() != 1) { printf("Grandchild: I was adopted by subreaper (PID %d), not init!\n", getppid()); } _exit(42); // Exit with identifiable code } printf("Child: created grandchild PID %d, exiting without waiting\n", grandchild); _exit(0); // Exit, orphaning grandchild } printf("Main: created child PID %d\n", child); // Wait for direct child int status; pid_t reaped = waitpid(child, &status, 0); printf("Main: reaped direct child PID %d\n", reaped); printf("Main: waiting for orphaned grandchild...\n"); // As subreaper, we should also be able to reap the grandchild while ((reaped = wait(&status)) > 0) { if (WIFEXITED(status)) { printf("Main: reaped adopted child PID %d (exit code %d)\n", reaped, WEXITSTATUS(status)); } } printf("\nAll descendants reaped by subreaper.\n"); return 0;} /* * Expected output: * * Main process (will become subreaper): PID=1000, PPID=999 * * Set as subreaper - orphaned descendants will be adopted * * Child (intermediate): PID=1001, PPID=1000 * Main: created child PID 1001 * Child: created grandchild PID 1002, exiting without waiting * * Grandchild: waiting for parent to exit... * Main: reaped direct child PID 1001 * Main: waiting for orphaned grandchild... * Grandchild (after parent exit): PID=1002, PPID=1000 <-- Adopted by subreaper! * Grandchild: I was adopted by subreaper (PID 1000), not init! * Main: reaped adopted child PID 1002 (exit code 42) * * All descendants reaped by subreaper. */If multiple ancestors are subreapers, the closest one adopts the orphan. This allows nested subreaping—for example, systemd is a subreaper, but a container runtime within a systemd-managed service can also be a subreaper for its containers.
systemd represents the modern approach to process lifecycle management in Linux. It provides comprehensive control over process hierarchies:
Key systemd Features for Termination:
123456789101112131415161718192021222324252627282930313233
[Unit]Description=My Service with Proper Termination HandlingAfter=network.target [Service]Type=simpleExecStart=/usr/local/bin/myservice # Termination settings# KillMode=control-group # Kill all processes in cgroup (default)# KillMode=process # Kill only main process# KillMode=mixed # SIGTERM to main, SIGKILL to others# KillMode=none # Don't kill any processes # How long to wait for graceful shutdown before SIGKILLTimeoutStopSec=30 # Which signal to send firstKillSignal=SIGTERM # Optionally send this signal after timeout before SIGKILL# SendSIGHUP=yes# SendSIGKILL=yes # Default: yes # Restart settingsRestart=on-failureRestartSec=10 # Resource limitsLimitNOFILE=65535 [Install]WantedBy=multi-user.targetsystemd Stop Sequence:
KillSignal (default: SIGTERM) to processesTimeoutStopSec secondsSendSIGKILL=yes, send SIGKILLKillMode Comparison:
| KillMode | SIGTERM Recipients | SIGKILL after Timeout |
|---|---|---|
| control-group | All processes in service's cgroup | All processes |
| process | Main process only | Main process only |
| mixed | Main process only | All processes |
| none | None | None |
1234567891011121314151617181920212223242526272829303132
#!/bin/bash# systemd service management commands # View service status including process treesystemctl status myservice # Stop with configured timeout and signal escalationsystemctl stop myservice # Restart (stop + start)systemctl restart myservice # Send signal to main processsystemctl kill myservice --signal=SIGHUP # Send signal to all processes in service cgroupsystemctl kill myservice --signal=SIGTERM --kill-who=all # View processes in service cgroupsystemctl status myservice | grep -A 20 "CGroup:" # Or use systemd-cglssystemd-cgls -u myservice.service # View service journal for termination logsjournalctl -u myservice --lines=100 # Monitor service in real-timejournalctl -u myservice -f # View what systemd will do on stopsystemd-analyze critical-chain myservice.serviceUnlike traditional init, systemd uses cgroups to track ALL processes spawned by a service—even if they fork, double-fork, or try to escape. This prevents orphaned processes from surviving service restart and ensures complete resource cleanup. 'systemctl status' shows the full cgroup tree.
We've explored the complete landscape of cascading termination—from traditional UNIX orphaning to modern container semantics and systemd service management.
Module Complete:
You've now mastered process termination in all its forms. From the graceful exit() to the brutal SIGKILL, from orphaned children to cascading container shutdowns, you understand how processes end their lives and what happens to the resources they held. This knowledge is fundamental for:
Congratulations! You've completed the Process Termination module. You now have deep knowledge of how processes end—covering normal and abnormal termination, exit status semantics, resource cleanup, and cascading termination through process hierarchies. This foundational knowledge will serve you well in systems programming, debugging, and understanding modern containerized environments.