Loading learning content...
When a process terminates—whether gracefully through exit() or abruptly due to a crash—it leaves behind a trail of consumed resources: open files, allocated memory, held locks, network connections, shared memory segments, and more. The operating system must reclaim these resources to prevent leaks that would eventually exhaust system capacity.
This cleanup isn't just housekeeping. Improper resource cleanup can cause:
Understanding resource cleanup is essential for writing robust systems software and for debugging issues when cleanup fails.
By the end of this page, you will understand: how the kernel closes file descriptors, memory reclamation for heap, stack, and mapped regions, lock release semantics for various lock types, shared resource reference counting, network connection cleanup, and the guarantees the kernel provides about cleanup completeness.
Every process maintains a file descriptor table—an array of references to open files, pipes, sockets, and other I/O resources. When a process terminates, the kernel must close all file descriptors and handle the consequences.
Kernel Actions on File Descriptor Cleanup:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/wait.h>#include <sys/stat.h>#include <string.h>#include <errno.h> void demonstrate_fd_inheritance() { // Open a file before fork int fd = open("/tmp/testfile.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open"); return; } printf("Parent opened fd %d\n", fd); pid_t pid = fork(); if (pid == 0) { // Child inherits the file descriptor printf("Child: inherited fd %d\n", fd); // Write something const char* msg = "Hello from child\n"; write(fd, msg, strlen(msg)); // When child exits, the fd is closed // But the file object persists (parent still has it) printf("Child: exiting (fd will be closed)\n"); _exit(0); } wait(NULL); // Parent's fd is still valid printf("Parent: fd %d is still open\n", fd); // Can continue writing const char* msg2 = "Hello from parent\n"; write(fd, msg2, strlen(msg2)); // Parent closes its copy close(fd); printf("Parent: closed fd\n");} void demonstrate_pipe_cleanup() { int pipefd[2]; if (pipe(pipefd) < 0) { perror("pipe"); return; } printf("\nPipe created: read=%d, write=%d\n", pipefd[0], pipefd[1]); pid_t pid = fork(); if (pid == 0) { // Child: close read end, use write end close(pipefd[0]); const char* msg = "Message from child"; write(pipefd[1], msg, strlen(msg)); printf("Child: wrote to pipe, exiting\n"); // When child exits, pipefd[1] is closed // This sends EOF to reader _exit(0); } // Parent: close write end, read from child close(pipefd[1]); char buffer[100]; ssize_t n; // Keep reading until EOF (child's exit closes write end) while ((n = read(pipefd[0], buffer, sizeof(buffer) - 1)) > 0) { buffer[n] = '\0'; printf("Parent received: %s\n", buffer); } if (n == 0) { printf("Parent: EOF received (child exited and closed write end)\n"); } close(pipefd[0]); wait(NULL);} void show_fd_limits() { struct rlimit rl; getrlimit(RLIMIT_NOFILE, &rl); printf("\nFile descriptor limits:\n"); printf(" Soft limit: %lu\n", rl.rlim_cur); printf(" Hard limit: %lu\n", rl.rlim_max); // Count current open fds int count = 0; for (int fd = 0; fd < (int)rl.rlim_cur; fd++) { if (fcntl(fd, F_GETFD) != -1) { count++; } } printf(" Currently open: %d\n", count);} int main() { printf("File Descriptor Cleanup Demo\n"); printf("============================\n\n"); show_fd_limits(); printf("\n--- FD Inheritance Test ---\n"); demonstrate_fd_inheritance(); printf("\n--- Pipe Cleanup Test ---\n"); demonstrate_pipe_cleanup(); // Cleanup unlink("/tmp/testfile.txt"); return 0;}Kernel close() only flushes kernel buffers. User-space buffers (stdio) are NOT automatically flushed when a process is killed by a signal. Use _exit() in forked children that did NOT exec() to prevent double-flushing. For critical data, call fsync() before termination.
Reference Counting in the Kernel
File descriptors don't map directly to files—there's an intermediate layer:
Process A: fd 3 ─────┐
├──► struct file ──► struct inode ──► disk
Process B: fd 5 ─────┘ (refcount=2) (refcount=1)
When Process A closes fd 3:
When Process B closes fd 5:
This reference counting ensures that shared file descriptors (via fork or IPC) remain valid as long as any process holds them.
Memory is the most significant resource a process consumes. The kernel must efficiently reclaim all memory when a process terminates, including:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/mman.h>#include <sys/wait.h>#include <unistd.h>#include <fcntl.h> void show_process_memory() { char path[64]; snprintf(path, sizeof(path), "/proc/%d/status", getpid()); FILE* f = fopen(path, "r"); if (!f) return; char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "VmSize:", 7) == 0 || strncmp(line, "VmRSS:", 6) == 0 || strncmp(line, "VmData:", 7) == 0 || strncmp(line, "VmStk:", 6) == 0) { printf(" %s", line); } } fclose(f);} void demonstrate_heap_cleanup() { printf("\n--- Heap Memory Cleanup ---\n"); pid_t pid = fork(); if (pid == 0) { printf("Child: Initial memory state\n"); show_process_memory(); // Allocate 100MB of heap memory size_t size = 100 * 1024 * 1024; char* buffer = malloc(size); if (!buffer) { perror("malloc"); _exit(1); } // Touch the memory to actually allocate it memset(buffer, 'A', size); printf("Child: After allocating 100MB\n"); show_process_memory(); // Exit WITHOUT freeing // Kernel will reclaim all memory printf("Child: Exiting without free()\n"); _exit(0); } wait(NULL); printf("Parent: Child exited - its memory is reclaimed\n");} void demonstrate_mmap_cleanup() { printf("\n--- mmap Memory Cleanup ---\n"); // Create a file for mmap int fd = open("/tmp/mmaptest", O_RDWR | O_CREAT | O_TRUNC, 0644); write(fd, "Test data for mmap cleanup demo", 32); pid_t pid = fork(); if (pid == 0) { // mmap the file char* mapped = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mapped == MAP_FAILED) { perror("mmap"); _exit(1); } printf("Child: mmapped file at %p\n", (void*)mapped); // Modify the mapped memory strcpy(mapped, "Modified by child"); // Exit without munmap // Kernel will sync and unmap printf("Child: Exiting without munmap()\n"); _exit(0); } wait(NULL); // Parent: verify the write was synced char buffer[64]; lseek(fd, 0, SEEK_SET); read(fd, buffer, sizeof(buffer)); printf("Parent: File contents after child exit: '%s'\n", buffer); close(fd); unlink("/tmp/mmaptest");} void demonstrate_shared_memory() { printf("\n--- Shared Memory Cleanup ---\n"); // Create anonymous shared memory char* shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) { perror("mmap"); return; } strcpy(shared, "Shared between parent and child"); pid_t pid = fork(); if (pid == 0) { printf("Child: Modifying shared memory\n"); strcpy(shared, "Modified by child"); // Child exits, but shared region persists for parent _exit(0); } wait(NULL); // Parent can still access shared memory printf("Parent: Shared memory says: '%s'\n", shared); // Only when parent unmaps is the memory truly freed munmap(shared, 4096); printf("Parent: Unmapped shared memory\n");} int main() { printf("Memory Reclamation Demo\n"); printf("=======================\n"); demonstrate_heap_cleanup(); demonstrate_mmap_cleanup(); demonstrate_shared_memory(); printf("\nAll tests complete.\n"); return 0;}Unlike user-space memory management (where leaks are possible), the kernel tracks all memory a process uses. When the process exits, ALL memory is reclaimed—the kernel cannot leak memory that a dead process used. This is why 'memory leak' bugs don't persist after process termination.
Locks present a critical cleanup challenge. A process that dies while holding locks can leave other processes blocked forever—unless the system provides automatic lock release.
Different lock types have different cleanup guarantees:
File Locks (flock() and fcntl())
This is why file locks are "advisory" and tied to the process—the kernel can clean them up.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/file.h>#include <sys/wait.h>#include <errno.h>#include <string.h> void demonstrate_flock_cleanup() { printf("\n--- flock() Cleanup ---\n"); int fd = open("/tmp/locktest", O_RDWR | O_CREAT, 0644); if (fd < 0) { perror("open"); return; } pid_t pid = fork(); if (pid == 0) { // Child: acquire exclusive lock printf("Child: Acquiring exclusive lock...\n"); if (flock(fd, LOCK_EX) < 0) { perror("flock"); _exit(1); } printf("Child: Lock acquired\n"); // Hold lock for a moment sleep(1); // Exit WITHOUT releasing lock // Kernel will release it printf("Child: Exiting with lock held\n"); _exit(0); } // Parent: give child time to acquire lock sleep(0.5); // Try to get lock (should block until child exits) printf("Parent: Trying to acquire lock...\n"); int start = time(NULL); if (flock(fd, LOCK_EX) < 0) { perror("flock"); } else { int elapsed = time(NULL) - start; printf("Parent: Got lock after %d seconds (child exited)\n", elapsed); flock(fd, LOCK_UN); } wait(NULL); close(fd); unlink("/tmp/locktest");} void demonstrate_fcntl_lock_cleanup() { printf("\n--- fcntl() Lock Cleanup ---\n"); int fd = open("/tmp/locktest2", O_RDWR | O_CREAT, 0644); if (fd < 0) { perror("open"); return; } pid_t pid = fork(); if (pid == 0) { struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 100, // Lock first 100 bytes }; printf("Child: Acquiring region lock (bytes 0-100)...\n"); if (fcntl(fd, F_SETLKW, &fl) < 0) { perror("fcntl"); _exit(1); } printf("Child: Region lock acquired\n"); sleep(1); printf("Child: Exiting with region lock held\n"); // Lock automatically released on exit _exit(0); } sleep(0.5); printf("Parent: Trying to acquire same region...\n"); struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 100, }; int start = time(NULL); if (fcntl(fd, F_SETLKW, &fl) < 0) { perror("fcntl"); } else { int elapsed = time(NULL) - start; printf("Parent: Got region lock after %d seconds\n", elapsed); fl.l_type = F_UNLCK; fcntl(fd, F_SETLK, &fl); } wait(NULL); close(fd); unlink("/tmp/locktest2");} int main() { printf("Lock Release Semantics Demo\n"); printf("===========================\n"); demonstrate_flock_cleanup(); demonstrate_fcntl_lock_cleanup(); printf("\nAll lock tests complete.\n"); return 0;}POSIX Semaphores and Mutexes
These are more problematic for cleanup:
| Lock Type | Cleanup on Exit | Robust Option |
|---|---|---|
| sem_t (named, sem_open) | NOT automatic | No |
| sem_t (unnamed, in shared mem) | NOT automatic | No |
| pthread_mutex_t (process-private) | Yes (with process) | N/A |
| pthread_mutex_t (process-shared) | NOT automatic | PTHREAD_MUTEX_ROBUST |
Robust Mutexes (PTHREAD_MUTEX_ROBUST):
POSIX provides "robust" mutexes that can detect and recover from owner death:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
#include <stdio.h>#include <stdlib.h>#include <pthread.h>#include <sys/mman.h>#include <sys/wait.h>#include <unistd.h>#include <errno.h>#include <string.h> typedef struct { pthread_mutex_t mutex; int counter;} SharedData; int main() { printf("Robust Mutex Demo\n"); printf("=================\n\n"); // Create shared memory for the mutex SharedData* shared = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (shared == MAP_FAILED) { perror("mmap"); return 1; } // Initialize robust, process-shared mutex pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST); pthread_mutex_init(&shared->mutex, &attr); pthread_mutexattr_destroy(&attr); shared->counter = 0; pid_t pid = fork(); if (pid == 0) { printf("Child: Locking mutex...\n"); pthread_mutex_lock(&shared->mutex); printf("Child: Mutex locked, counter = %d\n", shared->counter); shared->counter = 100; // DIE while holding the mutex! printf("Child: Dying with mutex held...\n"); _exit(0); // Simulates crash } wait(NULL); printf("Parent: Child has exited\n"); // Now try to lock the mutex printf("Parent: Attempting to lock mutex...\n"); int result = pthread_mutex_lock(&shared->mutex); if (result == EOWNERDEAD) { printf("Parent: Got EOWNERDEAD - previous owner died!\n"); printf("Parent: Counter value: %d (may be inconsistent)\n", shared->counter); // Recover: make the mutex usable again // Note: you must verify/repair shared state first! pthread_mutex_consistent(&shared->mutex); printf("Parent: Mutex marked consistent\n"); // Can now use normally shared->counter = 200; pthread_mutex_unlock(&shared->mutex); printf("Parent: Mutex unlocked\n"); } else if (result == 0) { printf("Parent: Got lock normally (unexpected)\n"); pthread_mutex_unlock(&shared->mutex); } else { printf("Parent: Error locking: %s\n", strerror(result)); } pthread_mutex_destroy(&shared->mutex); munmap(shared, sizeof(SharedData)); return 0;}POSIX named semaphores (sem_open) persist in the system even after all processes using them exit. They must be explicitly removed with sem_unlink(). This is a common source of resource leaks. Use ipcrm or find /dev/shm to clean up orphaned semaphores.
Network sockets require special cleanup handling because they involve not just local resources but also remote endpoints and kernel protocol state.
TCP Connection Cleanup:
When a process with open TCP connections terminates:
This means:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/socket.h>#include <netinet/in.h>#include <netinet/tcp.h>#include <sys/wait.h>#include <errno.h>#include <fcntl.h>#include <signal.h> #define PORT 8888 void show_socket_state(int port) { char cmd[128]; // Show socket state (Linux-specific) snprintf(cmd, sizeof(cmd), "ss -tn state all 'sport = :%d or dport = :%d' 2>/dev/null | head -5", port, port); printf("Socket state:\n"); system(cmd);} void demonstrate_time_wait() { printf("\n--- TIME_WAIT Demonstration ---\n"); int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket"); return; } // Without SO_REUSEADDR, we'd get "Address already in use" // when restarting after TIME_WAIT int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(PORT), .sin_addr.s_addr = INADDR_ANY, }; if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(server_fd); return; } listen(server_fd, 5); printf("Server listening on port %d\n", PORT); pid_t client_pid = fork(); if (client_pid == 0) { // Child: act as client close(server_fd); sleep(1); // Let server set up int client_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr = { .sin_family = AF_INET, .sin_port = htons(PORT), .sin_addr.s_addr = htonl(INADDR_LOOPBACK), }; if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0) { printf("Client: Connected\n"); send(client_fd, "Hello", 5, 0); char buf[100]; recv(client_fd, buf, sizeof(buf), 0); } close(client_fd); printf("Client: Closed connection\n"); _exit(0); } // Server: accept connection int conn_fd = accept(server_fd, NULL, NULL); if (conn_fd >= 0) { printf("Server: Accepted connection\n"); char buf[100]; recv(conn_fd, buf, sizeof(buf), 0); send(conn_fd, "World", 5, 0); // Close connection - this side initiates close close(conn_fd); printf("Server: Closed connection (initiator: enters TIME_WAIT)\n"); } wait(NULL); close(server_fd); // Show TIME_WAIT state printf("\nAfter closing:\n"); show_socket_state(PORT); printf("\nThe port remains in TIME_WAIT for ~60 seconds.\n"); printf("Use SO_REUSEADDR to allow immediate rebind.\n");} void demonstrate_so_linger() { printf("\n--- SO_LINGER Options ---\n\n"); printf("SO_LINGER controls close() behavior:\n"); printf(" l_onoff=0: Normal close (send buffered, FIN)\n"); printf(" l_onoff=1, l_linger=0: Immediate RST, discard data\n"); printf(" l_onoff=1, l_linger>0: Block close() for N seconds\n\n"); int sock = socket(AF_INET, SOCK_STREAM, 0); // Example: Hard close with RST struct linger ling = { .l_onoff = 1, .l_linger = 0, // Immediate RST }; setsockopt(sock, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling)); printf("Set SO_LINGER: abort mode (RST on close)\n"); close(sock);} int main() { printf("Network Socket Cleanup Demo\n"); printf("===========================\n"); // Ignore SIGCHLD for clean demo signal(SIGPIPE, SIG_IGN); demonstrate_time_wait(); demonstrate_so_linger(); return 0;}Always set SO_REUSEADDR on server sockets before bind(). This allows immediate rebinding to a port in TIME_WAIT state from a previous server instance. Without it, restarting a server fails for 1-2 minutes after the previous instance exits.
UDP Socket Cleanup:
UDP sockets are simpler—no connection state exists:
Unix Domain Socket Cleanup:
Unix sockets bound to filesystem paths have a unique issue:
System V IPC resources (shared memory, semaphores, message queues) are kernel-persistent—they survive process termination and must be explicitly removed. This is a critical difference from most other resources.
Cleanup Characteristics:
| Resource Type | Survives Process Exit? | Cleanup Method |
|---|---|---|
| SysV Shared Memory | Yes | shmctl(IPC_RMID) or ipcrm -m |
| SysV Semaphores | Yes | semctl(IPC_RMID) or ipcrm -s |
| SysV Message Queues | Yes | msgctl(IPC_RMID) or ipcrm -q |
| POSIX Shared Memory | Yes | shm_unlink() |
| POSIX Named Semaphores | Yes | sem_unlink() |
| POSIX Message Queues | Yes | mq_unlink() |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
#!/bin/bash# System V IPC Cleanup Demonstration echo "=== System V IPC Cleanup ==="echo # Show current IPC resourcesecho "Current IPC resources:"ipcs # Create some resources via C programcat > /tmp/ipc_create.c << 'EOF'#include <stdio.h>#include <stdlib.h>#include <sys/ipc.h>#include <sys/shm.h>#include <sys/sem.h>#include <sys/msg.h> int main() { key_t key = ftok("/tmp", 'A'); // Create shared memory int shmid = shmget(key, 4096, IPC_CREAT | 0666); printf("Created shared memory: id=%d", shmid); // Create semaphore set int semid = semget(key, 1, IPC_CREAT | 0666); printf("Created semaphore: id=%d", semid); // Create message queue int msgid = msgget(key, IPC_CREAT | 0666); printf("Created message queue: id=%d", msgid); printf("Process exiting (IPC resources remain!)"); return 0;}EOF gcc /tmp/ipc_create.c -o /tmp/ipc_create 2>/dev/null/tmp/ipc_create echoecho "After process exit, IPC resources still exist:"ipcs # Clean upechoecho "Cleaning up with ipcrm..."key=$(printf "%d" $(ipcs -m | awk 'NR>3 && $2!="" {print $2; exit}'))if [ -n "$key" ]; then ipcrm -m $key 2>/dev/null ipcrm -s $key 2>/dev/null ipcrm -q $key 2>/dev/nullfi echoecho "After cleanup:"ipcs rm -f /tmp/ipc_create.c /tmp/ipc_createEvery System V or POSIX IPC resource that isn't explicitly removed is a leak. Common causes: crashes before cleanup, signal handlers that don't clean up, forgotten cleanup in error paths. Always register cleanup handlers with atexit() and signal handlers for robust IPC usage.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <sys/shm.h>#include <sys/sem.h>#include <unistd.h> // Global IPC identifiers for cleanupstatic int g_shmid = -1;static int g_semid = -1; // Cleanup function - called on exit and signalsvoid cleanup_ipc(void) { if (g_shmid >= 0) { printf("Cleaning up shared memory %d\n", g_shmid); shmctl(g_shmid, IPC_RMID, NULL); g_shmid = -1; } if (g_semid >= 0) { printf("Cleaning up semaphore %d\n", g_semid); semctl(g_semid, 0, IPC_RMID); g_semid = -1; }} // Signal handlervoid signal_handler(int signum) { // Can't safely call printf, but cleanup_ipc uses shmctl which is // async-signal-safe enough for our purposes cleanup_ipc(); // Reset handler and re-raise for default behavior signal(signum, SIG_DFL); raise(signum);} int main() { // Register cleanup for normal exit atexit(cleanup_ipc); // Register cleanup for signals signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); signal(SIGHUP, signal_handler); // Note: Can't catch SIGKILL - IPC will leak if killed with -9 // Create IPC resources key_t key = ftok("/tmp", 'R'); g_shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666); if (g_shmid < 0) { perror("shmget"); return 1; } printf("Created shared memory: %d\n", g_shmid); g_semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666); if (g_semid < 0) { perror("semget"); return 1; // atexit will clean up shmid } printf("Created semaphore: %d\n", g_semid); printf("\nPress Ctrl+C or wait 5 seconds...\n"); sleep(5); printf("Normal exit\n"); return 0; // atexit cleanup runs}When a parent process terminates, its children don't automatically die—they're orphaned and reparented to init (PID 1). However, there are scenarios where child cleanup is important:
Orphaned Children:
Process Group Signals:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h>#include <sys/prctl.h> // Linux-specific#include <string.h> // Pattern 1: Kill children when parent dies (Linux-specific)void setup_death_signal() {#ifdef __linux__ // Request SIGKILL when parent dies if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) { perror("prctl"); }#endif} // Pattern 2: Explicit cleanup with signal handlerstatic pid_t g_child_pids[10];static int g_child_count = 0; void kill_all_children(int signum) { for (int i = 0; i < g_child_count; i++) { if (g_child_pids[i] > 0) { kill(g_child_pids[i], SIGTERM); } }} void register_child(pid_t pid) { if (g_child_count < 10) { g_child_pids[g_child_count++] = pid; }} // Pattern 3: Process group managementvoid create_managed_group() { // Create new process group with parent as leader setpgid(0, 0); for (int i = 0; i < 3; i++) { pid_t pid = fork(); if (pid == 0) { // Child: join parent's process group setpgid(0, getppid()); printf("Child %d (PID %d, PGID %d): working...\n", i, getpid(), getpgrp()); while (1) { pause(); } } } // Parent can kill entire group printf("Parent (PID %d, PGID %d): killing process group\n", getpid(), getpgrp()); sleep(1); // Send signal to entire process group kill(0, SIGTERM); // 0 means "my process group" // Reap children while (wait(NULL) > 0);} // Pattern 4: Double-fork to completely orphanvoid daemonize() { pid_t pid = fork(); if (pid > 0) { // Original parent exits exit(0); } if (pid < 0) { perror("fork"); exit(1); } // First child: become session leader setsid(); pid = fork(); if (pid > 0) { // First child exits exit(0); } if (pid < 0) { perror("fork"); exit(1); } // Second child: now a proper daemon // - Not a session leader (can't acquire controlling terminal) // - Parent is init // - Independent of original terminal printf("Daemon running: PID %d, PPID %d, SID %d\n", getpid(), getppid(), getsid(0));} int main() { printf("Child Process Cleanup Patterns\n"); printf("==============================\n\n"); // Register cleanup handler signal(SIGTERM, kill_all_children); signal(SIGINT, kill_all_children); printf("Pattern: Process Group Management\n"); create_managed_group(); printf("\nAll children terminated.\n"); return 0;}On Linux, a child can request to receive a signal when its parent dies using prctl(PR_SET_PDEATHSIG, signum). This is commonly used in container runtimes and service managers to ensure child processes don't outlive their supervisors.
Understanding what the kernel guarantees—and what it doesn't—is essential for robust systems design.
| Resource | Kernel Guarantees | User Must Handle |
|---|---|---|
| Virtual Memory | Always fully reclaimed | Nothing |
| File Descriptors | Always closed | Flushing user-space buffers |
| File Locks (flock/fcntl) | Always released | Nothing |
| POSIX Mutexes (private) | Released with process | Nothing |
| POSIX Mutexes (shared) | NOT automatic | Use robust mutexes |
| POSIX Semaphores (named) | NOT automatic | sem_unlink() |
| SysV IPC | NOT automatic | xxxctl(IPC_RMID) |
| TCP Sockets | FIN sent, may TIME_WAIT | SO_REUSEADDR for restart |
| Unix Socket Files | NOT removed | unlink() in handler |
| Temp Files | NOT removed | unlink() or O_TMPFILE |
| Child Processes | Reparented to init | Kill in handler if needed |
There is no way to perform cleanup when killed with SIGKILL (kill -9). The process terminates immediately. This is why SIGKILL should be a last resort—try SIGTERM first, give the process time to clean up, then SIGKILL only if necessary.
We've explored the complete landscape of resource cleanup during process termination—from automatic kernel guarantees to resources that require explicit management.
What's Next:
We've now covered normal termination, abnormal termination, return status, and resource cleanup for individual processes. But what happens in a process hierarchy when a parent dies? The final page of this module explores cascading termination—how termination propagates through process trees and how systems like init, systemd, and container runtimes manage process lifecycles.
You now have comprehensive knowledge of resource cleanup during process termination. You understand what the kernel handles automatically, what requires explicit cleanup, and the patterns for ensuring robust resource management even when processes crash unexpectedly.