Loading learning content...
In operating systems, processes are not created in isolation—they emerge from existing processes through a carefully orchestrated mechanism that establishes a parent-child relationship. This hierarchical model is not merely an organizational convenience; it is a fundamental architectural decision that shapes resource management, privilege inheritance, termination semantics, and the very identity of every running program on a system.
When you launch a web browser, execute a terminal command, or start a background service, you are witnessing the instantiation of this relationship. The shell that interprets your command becomes the parent, and the newly created browser or program becomes its child. This relationship carries profound implications for how processes share resources, communicate, and ultimately cease to exist.
By the end of this page, you will understand the complete semantics of parent-child relationships: how they are established, what properties they inherit, how they govern resource sharing, and why this model has become the universal standard across Unix-like, Windows, and real-time operating systems.
The parent-child paradigm for process creation arose from a fundamental insight in operating system design: creation implies responsibility. When a process creates another process, the creator naturally assumes oversight responsibilities—managing the child's lifecycle, collecting its termination status, and ensuring proper cleanup when the child concludes execution.
One might question why processes need parents at all. Couldn't an operating system simply create processes as independent entities? Theoretically, yes—but this approach creates significant practical problems:
Resource Accounting: Without a parent, who is responsible for the resources a process consumes? Who reclaims memory if the process crashes?
Termination Status: When a process completes, its exit status (success, failure, error code) needs to be delivered somewhere. Without a parent, this information is lost.
Privilege Boundaries: A newly created process needs some initial set of permissions. Inheriting from a parent provides a natural, auditable source of these privileges.
Process Group Organization: Processes often need to work together. Parent-child relationships provide a natural grouping mechanism.
The terminology 'parent' and 'child' deliberately invokes biological reproduction. Just as biological offspring inherit traits from their parents, child processes inherit attributes from their creators. And just as parents bear responsibility for their children, parent processes must properly manage their offspring's lifecycle.
When a parent process creates a child, several critical operations occur atomically from the perspective of the participating processes:
What the Parent Does:
What the Kernel Does:
What the Child Receives:
12345678910111213141516171819202122232425262728293031323334353637
#include <stdio.h>#include <unistd.h>#include <sys/types.h> /** * Demonstrates the fundamental parent-child relationship * * Key observations: * - Parent and child have different PIDs * - Child's PPID equals parent's PID * - Both execute after fork() but see different return values */int main() { printf("Before fork(): Process %d\n", getpid()); pid_t pid = fork(); // Create child process if (pid < 0) { // Fork failed perror("fork failed"); return 1; } else if (pid == 0) { // Child process executes this branch printf("Child Process:\n"); printf(" My PID: %d\n", getpid()); printf(" My Parent's PID (PPID): %d\n", getppid()); printf(" fork() returned to me: %d\n", pid); } else { // Parent process executes this branch printf("Parent Process:\n"); printf(" My PID: %d\n", getpid()); printf(" My Child's PID: %d\n", pid); printf(" fork() returned to me: %d\n", pid); } return 0;}The output demonstrates the core relationship:
Before fork(): Process 1234
Parent Process:
My PID: 1234
My Child's PID: 1235
fork() returned to me: 1235
Child Process:
My PID: 1235
My Parent's PID (PPID): 1234
fork() returned to me: 0
Notice how both processes execute the same code after fork(), but the return value differentiates them. The parent receives the child's PID, enabling it to track and manage the child. The child receives 0, indicating it is the newly created process.
Every process in a running system possesses two essential identifiers that define its existence within the process hierarchy:
The Process ID (PID) is a non-negative integer that uniquely identifies a process within the operating system. PIDs are assigned by the kernel at process creation time and are guaranteed to be unique among all currently running processes.
Critical Properties of PIDs:
| Property | Description |
|---|---|
| Uniqueness | No two simultaneously running processes share the same PID |
| Positive Integers | PIDs are always positive (0 is reserved for the swapper/null process) |
| Recycling | PIDs are reused after a process terminates, but with safeguards |
| Monotonic Assignment | Typically assigned sequentially, wrapping around after reaching maximum |
| 32-bit Limit | Modern systems typically support up to 4,194,304 PIDs (configurable) |
PIDs are recycled after processes terminate. If you store a PID and later try to signal or interact with it, you might accidentally target a completely different process that was assigned the same PID. Always verify process identity through additional means (process start time, file descriptor validation) for security-critical operations.
The Parent Process ID (PPID) is the PID of the process that created the current process. This single link creates the chain that forms the entire process hierarchy.
Semantics of PPID:
Set at Creation: PPID is established when a process is created via fork() or equivalent
Immutable Under Normal Circumstances: A process's parent remains constant throughout its lifetime—with one critical exception
Orphan Reparenting: If a parent terminates before its child, the child becomes an orphan, and its PPID changes to that of the init process (PID 1) or a designated subreaper
Queryable: Any process can retrieve its PPID via getppid() system call
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
#include <stdio.h>#include <unistd.h>#include <sys/types.h>#include <sys/wait.h> /** * Explores PID/PPID relationships through multiple generations * * This creates a three-generation process tree: * Grandparent (original) -> Parent -> Child */int main() { printf("=== Generation 0: Grandparent ===\n"); printf("My PID: %d, My PPID: %d (shell/init)\n\n", getpid(), getppid()); pid_t parent_pid = fork(); if (parent_pid == 0) { // First child (becomes "parent" in next generation) printf("=== Generation 1: Parent ===\n"); printf("My PID: %d, My PPID: %d (grandparent)\n\n", getpid(), getppid()); pid_t child_pid = fork(); if (child_pid == 0) { // Second child (grandchild of original) printf("=== Generation 2: Child ===\n"); printf("My PID: %d, My PPID: %d (parent)\n", getpid(), getppid()); // Sleep to observe orphan adoption if parent dies first sleep(2); printf("After sleep - My PPID: %d (may change if parent died)\n", getppid()); } else if (child_pid > 0) { // Parent waits for grandchild waitpid(child_pid, NULL, 0); } } else if (parent_pid > 0) { // Grandparent waits for parent waitpid(parent_pid, NULL, 0); } return 0;}Certain PIDs carry special significance in operating systems:
| PID | Name | Role |
|---|---|---|
| 0 | Swapper/Sched/Null | Kernel process for scheduling (not a user process) |
| 1 | init/systemd/launchd | First user-space process, ancestor of all user processes, adopts orphans |
| 2 | kthreadd (Linux) | Parent of all kernel threads |
The Init Process (PID 1)
PID 1 occupies a singular position in the operating system hierarchy:
Traditional Unix used a simple 'init' program. Modern systems like Linux use 'systemd', macOS uses 'launchd', and Windows has different boot process semantics. Despite implementation differences, the fundamental concept—a root process that anchors the hierarchy—remains universal.
When a child process is created, it inherits a rich set of attributes from its parent. Understanding precisely what is inherited, what is copied, and what is reset is essential for writing correct concurrent programs.
Process attributes at creation time fall into three categories:
Let's examine each attribute category in detail:
| Attribute | Inheritance Type | Notes |
|---|---|---|
| Real User ID (UID) | Copied | Determines ownership; affects permission checks |
| Effective User ID (EUID) | Copied | Used for permission checks; may change via setuid binaries |
| Real/Effective Group ID | Copied | Similar to UID for group membership |
| Supplementary Group IDs | Copied | Additional group memberships |
| Process Group ID (PGID) | Copied | For job control; child joins parent's group |
| Session ID | Copied | Terminal session membership |
| Controlling Terminal | Copied | TTY association for job control |
| Current Working Directory | Copied | Path context for relative file operations |
| Root Directory | Copied | Changed by chroot; affects path resolution |
| File Mode Creation Mask (umask) | Copied | Default permissions for new files |
| Signal Mask | Copied | Which signals are blocked |
| Environment Variables | Copied | Key-value configuration pairs |
| Open File Descriptors | Copied (shared underlying file) | Same file table entries; shared offset |
| Resource Limits (rlimits) | Copied | CPU, memory, file size limits |
| Nice Value | Copied | Scheduling priority adjustment |
| Attribute | Child's Value | Notes |
|---|---|---|
| Process ID (PID) | New, unique | Kernel-assigned identifier |
| Parent Process ID (PPID) | Parent's PID | Links to creator |
| Process Times | Reset to zero | CPU time accounting starts fresh |
| Pending Signals | Cleared | No inherited pending signals |
| File Locks | Not inherited | Locks held by parent stay with parent |
| Semaphore Adjustments | Cleared | POSIX semaphore undo operations |
| Record Locks | Not inherited | fcntl() advisory locks |
| Timer Events | Not inherited | Interval timers, alarms |
| Async I/O Operations | Not inherited | Outstanding async reads/writes |
When file descriptors are inherited, parent and child share the SAME underlying file table entry. This means they share the file offset. If the child reads 100 bytes from a file, the parent's file position also advances 100 bytes. This shared state can cause race conditions if not carefully managed.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <stdio.h>#include <unistd.h>#include <fcntl.h>#include <sys/wait.h>#include <string.h> /** * Demonstrates file descriptor inheritance and shared file offset * * CRITICAL LEARNING: * - Child inherits open file descriptors * - File position is SHARED between parent and child * - Writes interleave unless coordinated */int main() { // Open file before forking - child will inherit this fd int fd = open("shared_file.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open"); return 1; } // Write initial content before fork const char* initial = "Initial content from parent before fork\n"; write(fd, initial, strlen(initial)); pid_t pid = fork(); if (pid == 0) { // Child process // Notice: child did NOT call open() - it inherited fd // Check file position - shared with parent! off_t pos = lseek(fd, 0, SEEK_CUR); printf("Child: Current file position: %ld\n", (long)pos); // Write from child const char* child_msg = "Child's contribution\n"; write(fd, child_msg, strlen(child_msg)); printf("Child: Wrote message, new position: %ld\n", (long)lseek(fd, 0, SEEK_CUR)); } else if (pid > 0) { // Parent process wait(NULL); // Wait for child to finish writing // Parent's file position has advanced due to child's write! off_t pos = lseek(fd, 0, SEEK_CUR); printf("Parent: File position after child wrote: %ld\n", (long)pos); // Write from parent const char* parent_msg = "Parent's contribution after child\n"; write(fd, parent_msg, strlen(parent_msg)); close(fd); } return 0;}Environment variables represent another critical inherited attribute. They form a simple key-value store that configures program behavior:
Importantly, the child receives a copy of the environment at fork time. Subsequent modifications by either parent or child do not affect the other.
// Parent sets environment before fork
setenv("MY_CONFIG", "parent_value", 1);
pid_t pid = fork();
if (pid == 0) {
// Child can read parent's value
printf("Child sees: %s\n", getenv("MY_CONFIG")); // "parent_value"
// Child modifies - parent unaffected
setenv("MY_CONFIG", "child_modified", 1);
} else {
sleep(1);
// Parent still sees original value
printf("Parent sees: %s\n", getenv("MY_CONFIG")); // "parent_value"
}
The parent-child relationship is not merely structural—it carries obligations. A parent process has responsibilities toward its children that, if neglected, lead to system-level problems.
When a child process terminates, it does not simply vanish. Instead, it enters a zombie state, where it remains in the process table with its exit status preserved, waiting for its parent to collect this information.
The Zombie Problem:
exit() or returns from main())wait() or waitpid() to collect exit statusIf the parent never waits:
Each zombie process consumes a slot in the kernel's process table. While individual zombies use minimal memory, accumulated zombies can exhaust the maximum PID limit (typically 32,768 or higher). Long-running daemons that create children without properly waiting are common culprits of this resource exhaustion.
123456789101112131415161718192021222324252627282930313233343536
#include <stdio.h>#include <unistd.h>#include <sys/wait.h> /** * Demonstrates zombie process creation and reaping * * Run this and use "ps aux | grep Z" to observe zombie state */int main() { pid_t pid = fork(); if (pid == 0) { // Child: exit immediately printf("Child (PID %d) exiting immediately...\n", getpid()); _exit(42); // Exit with status 42 } // Parent: deliberately don't wait printf("Parent (PID %d) created child (PID %d)\n", getpid(), pid); printf("Sleeping for 30 seconds WITHOUT calling wait()...\n"); printf("Run: ps aux | grep %d to see zombie state\n", pid); sleep(30); // During this time, child is a zombie // Now properly reap the zombie int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("Finally reaped child. Exit status was: %d\n", WEXITSTATUS(status)); } return 0;}What happens when a parent terminates before its child? The child becomes an orphan, but the operating system does not leave it unmanaged.
The Reparenting Mechanism:
wait() to reap adopted childrenThis mechanism ensures that:
Subreaper Processes (Linux 3.4+):
Modern Linux allows any process to mark itself as a "subreaper" using prctl(PR_SET_CHILD_SUBREAPER, 1). When a process's parent terminates, instead of being adopted by init, it's adopted by the nearest subreaper ancestor. This is useful for container init processes and service managers.
12345678910111213141516171819202122232425262728293031323334353637383940
#include <stdio.h>#include <unistd.h>#include <sys/wait.h> /** * Demonstrates orphan process adoption * * Watch the child's PPID change from parent to init (1) */int main() { pid_t pid = fork(); if (pid == 0) { // Child process printf("Child (PID %d) started. Original PPID: %d\n", getpid(), getppid()); // Wait for parent to die sleep(2); // Check PPID again - should now be 1 (init) printf("Child: After parent died, my new PPID: %d\n", getppid()); if (getppid() == 1) { printf("Child: I've been adopted by init!\n"); } // Continue execution as an orphan sleep(1); printf("Child: Orphan exiting normally. Init will reap me.\n"); } else if (pid > 0) { // Parent exits immediately, abandoning child printf("Parent (PID %d) exiting, leaving child (PID %d) as orphan\n", getpid(), pid); _exit(0); // Parent exits without waiting } return 0;}Sometimes orphaning is intentional. The 'double-fork' technique creates a grandchild and lets the middle process exit, making the grandchild an orphan adopted by init. This is a classic Unix idiom for creating daemon processes that are completely detached from any controlling terminal.
The parent-child relationship intersects with two other critical process management mechanisms: signals and process groups. Understanding these interactions is essential for building robust process hierarchies.
A process group is a collection of processes that can receive signals together. By default:
Job Control Scenario:
parent_shell (PGID=1000)
├── child1 (PGID=1000)
├── child2 (PGID=1000)
└── child3 (PGID=1000)
When you press Ctrl+C in a terminal, SIGINT is sent to the entire foreground process group—all four processes receive it, not just the shell.
Signals interact with parent-child relationships in specific ways:
| Signal | Default Behavior | Parent-Child Implications |
|---|---|---|
| SIGCHLD | Ignored | Sent to parent when child terminates/stops; enables async child reaping |
| SIGKILL | Terminate (uncatchable) | Cannot be blocked or handled; terminates process immediately |
| SIGSTOP | Stop (uncatchable) | Cannot be blocked or handled; stops process immediately |
| SIGTERM | Terminate | Default termination request; allows cleanup |
| SIGHUP | Terminate | Traditionally sent when terminal disconnects; often causes children to terminate |
| SIGINT | Terminate | Ctrl+C sends to foreground process group |
| SIGQUIT | Core dump | Ctrl+\ sends to foreground process group |
SIGCHLD is the primary mechanism for asynchronous child management. When a child:
The kernel sends SIGCHLD to the parent. This enables non-blocking child reaping:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h>#include <errno.h> volatile sig_atomic_t child_terminated = 0; /** * SIGCHLD handler for asynchronous child reaping * * CRITICAL: This handler must be async-signal-safe * - Uses only async-signal-safe functions * - Handles multiple children that may terminate simultaneously * - Saves and restores errno */void sigchld_handler(int sig) { int saved_errno = errno; // Handler may interrupt code that set errno pid_t pid; int status; // Reap ALL terminated children (multiple may have died) while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { // In real code, log or record this information // Here we just set a flag (async-signal-safe) child_terminated++; } errno = saved_errno; // Restore errno} int main() { // Install SIGCHLD handler struct sigaction sa; sa.sa_handler = sigchld_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // Don't notify on stop/continue if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(1); } // Create multiple children for (int i = 0; i < 5; i++) { pid_t pid = fork(); if (pid == 0) { // Child: sleep random time (0-3 seconds) then exit srand(getpid()); sleep(rand() % 4); printf("Child %d (PID %d) exiting\n", i, getpid()); _exit(i); } } // Parent continues doing work while children run printf("Parent: Working while children run in background...\n"); for (int i = 0; i < 10; i++) { printf("Parent: Work iteration %d, children reaped so far: %d\n", i, child_terminated); sleep(1); } printf("Parent: Total children reaped: %d\n", child_terminated); return 0;}Signal handlers execute asynchronously, potentially interrupting any code in the main program. Only 'async-signal-safe' functions can be called from handlers. printf() is NOT safe (though often used in examples); write() is safe. Production code should minimize handler work—set a flag and handle in the main loop.
While the parent-child paradigm is universal, its implementation varies across operating systems. Understanding these differences is crucial for writing portable code and for appreciating different design philosophies.
Windows takes a fundamentally different approach than Unix. Instead of fork-and-exec, Windows uses CreateProcess(), which creates a new process and loads a new program in one operation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
#include <windows.h>#include <stdio.h> /** * Windows process creation demonstration * * Key differences from Unix: * - CreateProcess() combines fork+exec * - Uses HANDLEs instead of PIDs * - Explicit handle inheritance control * - Startup info configures child process */int main() { STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); // Create child process running notepad if (!CreateProcess( NULL, // Application name (NULL = use cmdline) "notepad.exe", // Command line NULL, // Process security attributes NULL, // Thread security attributes FALSE, // Handle inheritance (FALSE = don't inherit) 0, // Creation flags NULL, // Environment (NULL = inherit parent's) NULL, // Current directory (NULL = inherit) &si, // Startup info &pi)) // Process info (receives handles) { printf("CreateProcess failed: %d\n", GetLastError()); return 1; } printf("Parent: Created child process\n"); printf(" Child Process ID: %lu\n", pi.dwProcessId); printf(" Child Thread ID: %lu\n", pi.dwThreadId); printf(" Process Handle: %p\n", pi.hProcess); // Wait for child to complete printf("Waiting for child to exit...\n"); WaitForSingleObject(pi.hProcess, INFINITE); // Get exit code DWORD exitCode; GetExitCodeProcess(pi.hProcess, &exitCode); printf("Child exited with code: %lu\n", exitCode); // Close handles (important: not doing this leaks resources) CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return 0;}Real-time operating systems often have simplified process models focused on determinism:
| RTOS Aspect | Typical Approach |
|---|---|
| Process Model | Often single address space with threads (tasks) |
| Creation Cost | Minimized for predictable timing |
| Parent-Child | May be flat (no hierarchy) or simplified |
| Fork Semantics | Rarely supported; creation is explicit |
| Resource Inheritance | Typically minimized for isolation |
Example: FreeRTOS Task Creation
// FreeRTOS uses tasks, not processes
// No parent-child hierarchy; tasks are peers
xTaskCreate(
vTaskFunction, // Task function
"TaskName", // Name
STACK_SIZE, // Stack size
NULL, // Parameters
PRIORITY, // Priority
&taskHandle // Handle (output)
);
Libraries like POSIX threads (pthreads) and cross-platform frameworks (Qt, Boost.Process) abstract these differences. However, fundamental semantic differences—like Windows not having fork()—mean some patterns simply don't translate directly. Design with the target platform's capabilities in mind.
We have explored the foundational concept of parent-child relationships in process creation—a model that underpins virtually all modern operating systems. Let's consolidate the essential knowledge:
What's Next:
The parent-child relationship creates a structure; the next page explores how these structures form complete process trees—the hierarchical arrangement of all processes in a running system. We'll examine how to visualize, traverse, and manage these trees, and understand the implications of tree structure for system administration and process control.
You now understand the fundamental parent-child relationship that governs process creation. This relationship—with its inheritance rules, responsibilities, and lifecycle management—is the foundation upon which all process hierarchies are built. Next, we'll see how these relationships combine to form complete process trees.