Loading learning content...
After fork() returns, two processes exist where one existed before. Both processes execute the same program from the same point (the instruction after fork()), but they are now completely independent entities with separate address spaces, separate execution contexts, and separate fates.
Understanding the relationship between parent and child processes—how they execute, when they run, how they interact—is fundamental to designing correct multi-process applications. The behavior is not just a technical detail; it shapes the entire architecture of Unix systems.
This page explores execution dynamics between parent and child. You will understand why execution order is undefined, how the scheduler treats parent and child, practical implications of concurrent execution, common coordination patterns, and how to design robust multi-process applications that don't depend on execution order.
One of the most important principles in Unix process programming is that the execution order of parent and child after fork() is undefined. POSIX does not guarantee which process runs first, and this non-guarantee is intentional.
Why Order is Undefined:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
/* * Demonstrating Undefined Execution Order * * Run this multiple times - you may see different orderings! * This is expected and correct behavior. */ #include <stdio.h>#include <unistd.h>#include <sys/wait.h> int main() { printf("Before fork: PID %d\n", getpid()); pid_t pid = fork(); if (pid == -1) { perror("fork"); return 1; } /* * Which prints first? UNDEFINED! * * On a single-core system: Scheduler chooses one * On multi-core: They may interleave or print simultaneously * * Factors affecting order: * - System load * - Kernel scheduler algorithm (CFS, etc.) * - Number of CPUs * - Priority settings * - Random timing variations */ if (pid == 0) { printf("Child: I am PID %d, running...\n", getpid()); printf("Child: Step 1\n"); printf("Child: Step 2\n"); printf("Child: Step 3\n"); _exit(0); } else { printf("Parent: I am PID %d, child is %d\n", getpid(), pid); printf("Parent: Step A\n"); printf("Parent: Step B\n"); printf("Parent: Step C\n"); wait(NULL); } return 0;} /* * Possible outputs (among many): * * Run 1: Run 2: * Before fork: PID 1000 Before fork: PID 1000 * Parent: I am PID 1000... Child: I am PID 1001... * Parent: Step A Parent: I am PID 1000... * Child: I am PID 1001... Child: Step 1 * Parent: Step B Parent: Step A * Child: Step 1 Child: Step 2 * ... ... * * NEVER write code that assumes a particular order! */Code that works on your machine might fail in production, on a different kernel version, or on a machine with different core count. The scheduler's choice can change between runs, between reboots, or even between forks in the same program. Always design for concurrency, never for order.
Understanding how the scheduler treats parent and child helps explain the observed behavior after fork().
Linux CFS (Completely Fair Scheduler):
Modern Linux uses the Completely Fair Scheduler, which treats the newly created child process fairly relative to the parent:
1234567891011121314151617181920212223242526272829303132333435363738
Linux CFS and fork():====================== BEFORE FORK:┌─────────────────────────────────────────────────────┐│ CFS Red-Black Tree (runqueue) ││ ││ Process A (vruntime: 100ms) ││ / \ ││ Process B Parent ││ (vruntime: (vruntime: 150ms) ││ 80ms) │└─────────────────────────────────────────────────────┘ AFTER FORK:┌─────────────────────────────────────────────────────┐│ CFS Red-Black Tree (runqueue) ││ ││ Process A (vruntime: 100ms) ││ / \ ││ Process B Parent ││ (vruntime: (vruntime: 150ms) ││ 80ms) \ ││ Child ││ (vruntime: ~150ms) ││ (inherits parent's vruntime) │└─────────────────────────────────────────────────────┘ CFS Policy:1. Child inherits parent's vruntime (virtual runtime)2. This prevents fork() from being used to gain unfair CPU time3. Child might run first if its vruntime is slightly adjusted4. Both compete fairly for CPU time going forward Historical Note:- Older Linux kernels ran parent first (avoid COW copy on child stack)- Then child-first became default (assume fork+exec pattern)- Current: Scheduler decides based on fairness metrics| Scheduler | Typical Behavior | Rationale |
|---|---|---|
| Linux CFS | Fair competition based on vruntime | Prevent fork() from gaming scheduler |
| FreeBSD ULE | Parent often continues first | Locality: parent data likely in cache |
| macOS GCD | Integration with dispatch queues | Optimize for fork+exec pattern |
| Solaris | Priority-based with inheritance | Parent priority inherited by child |
Linux has a tunable (sched_child_runs_first in older kernels, now deprecated) that could force child to run first. This was intended to make fork+exec more efficient (child runs, execs, before parent wastes time on COW). Modern kernels handle this more intelligently without explicit flags.
On multi-core systems, parent and child can execute truly simultaneously. This is fundamentally different from single-core interleaving and has important implications.
Single-Core vs Multi-Core:
12345678910111213141516171819202122232425262728293031323334353637383940
SINGLE-CORE EXECUTION (Time-sliced):==================================== Time →├────┤ Parent runs ├────┤ Context switch, Child runs ├────┤ Context switch, Parent runs ├────┤ Context switch, Child runs Only ONE process executes at any instant. Interleaving, but not true parallelism. MULTI-CORE EXECUTION (True Parallelism):======================================== Core 0 Core 1Time → ├────────────┤ ├────────────┤ │ Parent │ │ Child │ │ Running │ │ Running │ │ │ │ │ ├────────────┤ ├────────────┤ BOTH processes execute at the SAME instant. True parallelism, not just interleaving. IMPLICATIONS: 1. Output interleaving is possible: printf("A") in parent and printf("1") in child might produce "A1", "1A", or even "A1" atomically (depends on stdio buffering) 2. Shared resources (files, pipes) may be accessed simultaneously - need proper synchronization 3. Race conditions are real-time, not just scheduler quirks 4. Performance can genuinely scale with cores (if workload permits)1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
/* * Demonstrating True Parallel Execution * Run on multi-core system to observe simultaneous execution */ #define _GNU_SOURCE#include <stdio.h>#include <unistd.h>#include <sys/wait.h>#include <sched.h>#include <time.h> long long get_time_ns() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000000000LL + ts.tv_nsec;} void busy_work(int iterations) { volatile long sum = 0; for (int i = 0; i < iterations; i++) { sum += i; }} int main() { printf("System has %d CPUs\n", sysconf(_SC_NPROCESSORS_ONLN)); long long fork_time = get_time_ns(); pid_t pid = fork(); if (pid == 0) { /* CHILD */ long long start = get_time_ns(); int cpu = sched_getcpu(); /* Which CPU are we on? */ printf("Child PID %d started on CPU %d at +%lld ns\n", getpid(), cpu, start - fork_time); busy_work(100000000); long long end = get_time_ns(); printf("Child finished at +%lld ns (duration: %lld ms)\n", end - fork_time, (end - start) / 1000000); _exit(0); } /* PARENT */ long long start = get_time_ns(); int cpu = sched_getcpu(); printf("Parent PID %d started on CPU %d at +%lld ns\n", getpid(), cpu, start - fork_time); busy_work(100000000); long long end = get_time_ns(); printf("Parent finished at +%lld ns (duration: %lld ms)\n", end - fork_time, (end - start) / 1000000); wait(NULL); /* * On multi-core system: * - Parent and child may run on different CPUs * - Both may start nearly simultaneously * - Total time ≈ one worker time (parallel execution) * * On single-core system: * - Both run on CPU 0 * - Total time ≈ two worker times (sequential) */ return 0;}Use sched_setaffinity() to pin parent or child to specific CPUs if needed. This is useful for benchmarking, NUMA optimization, or ensuring cache locality. However, usually let the scheduler decide—it generally knows best.
After fork(), parent and child are independent processes. This independence is fundamental and has significant implications:
What Independence Means:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
/* * Demonstrating Process Independence After fork() */ #include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h> int shared_var = 100; /* NOT actually shared after fork! */ void signal_handler(int sig) { printf("Process %d received signal %d\n", getpid(), sig);} int main() { signal(SIGUSR1, signal_handler); pid_t pid = fork(); if (pid == 0) { /* CHILD */ printf("Child: Initial shared_var = %d\n", shared_var); /* Modify 'shared' variable - only affects child */ shared_var = 999; printf("Child: After modification, shared_var = %d\n", shared_var); /* Sleep to allow parent to send signal */ sleep(2); /* Child can exit - parent keeps running */ printf("Child: Exiting... parent unaffected\n"); exit(42); } /* PARENT */ sleep(1); /* Let child modify its copy */ printf("Parent: shared_var = %d (unaffected by child!)\n", shared_var); /* Send signal to child - each process has own signal handling */ printf("Parent: Sending SIGUSR1 to child %d\n", pid); kill(pid, SIGUSR1); /* Wait for child, but parent continues independently */ int status; waitpid(pid, &status, 0); printf("Parent: Child exited with status %d\n", WEXITSTATUS(status)); /* Parent keeps running after child dies */ printf("Parent: Still running after child exit\n"); printf("Parent: shared_var still = %d\n", shared_var); return 0;} /* * Output: * Child: Initial shared_var = 100 * Child: After modification, shared_var = 999 * Parent: shared_var = 100 (unaffected by child!) * Parent: Sending SIGUSR1 to child 12345 * Process 12345 received signal 10 * Child: Exiting... parent unaffected * Parent: Child exited with status 42 * Parent: Still running after child exit * Parent: shared_var still = 100 */Global variables named 'shared' are NOT shared between parent and child. The name is a misnomer. Each process has its own copy after fork(). For true sharing, use mmap with MAP_SHARED, POSIX shared memory (shm_open), or System V shared memory (shmget).
Since execution order is undefined, robust applications need explicit coordination mechanisms. Here are the primary patterns:
Pattern 1: wait() for Synchronization
12345678910111213141516171819202122232425262728293031323334353637
/* * Pattern: Using wait() for Parent-Child Synchronization * * wait() BLOCKS the parent until child exits. * This creates a synchronization point. */ #include <stdio.h>#include <unistd.h>#include <sys/wait.h> int main() { pid_t pid = fork(); if (pid == 0) { /* Child does work */ printf("Child: Doing work...\n"); sleep(2); printf("Child: Work complete!\n"); _exit(0); } /* * Parent WILL NOT proceed past wait() until child exits. * This guarantees ordering: child completes before parent continues. */ printf("Parent: Waiting for child...\n"); wait(NULL); printf("Parent: Child finished, continuing...\n"); /* * GUARANTEED: The above print happens AFTER child's prints. * wait() provides this synchronization. */ return 0;}Pattern 2: Pipes for Coordination
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
/* * Pattern: Using Pipes for Parent-Child Coordination * * Pipes allow synchronized communication without wait(). * Useful when parent needs to signal child, or vice versa. */ #include <stdio.h>#include <unistd.h>#include <sys/wait.h>#include <string.h> int main() { int parent_to_child[2]; /* Pipe: parent writes, child reads */ int child_to_parent[2]; /* Pipe: child writes, parent reads */ pipe(parent_to_child); pipe(child_to_parent); pid_t pid = fork(); if (pid == 0) { /* CHILD */ close(parent_to_child[1]); /* Close write end */ close(child_to_parent[0]); /* Close read end */ /* Wait for signal from parent */ char buf[32]; read(parent_to_child[0], buf, sizeof(buf)); /* BLOCKS until data */ printf("Child: Received signal: %s\n", buf); /* Do work */ printf("Child: Starting work...\n"); sleep(1); printf("Child: Work done!\n"); /* Signal parent we're done */ write(child_to_parent[1], "DONE", 5); close(parent_to_child[0]); close(child_to_parent[1]); _exit(0); } /* PARENT */ close(parent_to_child[0]); /* Close read end */ close(child_to_parent[1]); /* Close write end */ /* Do some setup first */ printf("Parent: Preparing environment...\n"); sleep(1); /* Signal child to start */ printf("Parent: Signaling child to start\n"); write(parent_to_child[1], "START", 6); /* Wait for child to complete */ char buf[32]; read(child_to_parent[0], buf, sizeof(buf)); /* BLOCKS until data */ printf("Parent: Child signaled: %s\n", buf); close(parent_to_child[1]); close(child_to_parent[0]); wait(NULL); return 0;}Pattern 3: Signals for Coordination
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
/* * Pattern: Using Signals for Parent-Child Coordination * * Signals provide asynchronous notification between processes. * Lightweight but less information-rich than pipes. */ #include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h> volatile sig_atomic_t ready = 0; void usr1_handler(int sig) { (void)sig; ready = 1;} int main() { signal(SIGUSR1, usr1_handler); pid_t pid = fork(); if (pid == 0) { /* CHILD */ /* Wait for parent's signal */ while (!ready) { pause(); /* Sleep until signal received */ } printf("Child: Received signal, starting work!\n"); sleep(1); printf("Child: Work complete!\n"); /* Signal parent we're done */ kill(getppid(), SIGUSR1); _exit(0); } /* PARENT */ ready = 0; /* Ensure fresh start */ printf("Parent: Preparing...\n"); sleep(1); /* Signal child to start */ printf("Parent: Signaling child\n"); kill(pid, SIGUSR1); /* Wait for child's signal */ while (!ready) { pause(); } printf("Parent: Child signaled completion!\n"); wait(NULL); return 0;}wait(): Simple, child-completion only. Pipes: Bidirectional, can pass data, synchronous. Signals: Asynchronous, lightweight, limited information. Shared memory + semaphores: Complex but powerful for tight coupling. Choose based on your communication needs.
Different applications require different parent-child relationships. Here are the major patterns:
Pattern 1: Fork-Wait (Synchronous Child)
123456789101112131415
/* * Fork-Wait: Parent spawns child, waits for completion * Use case: Running external commands, sequential task execution */ pid_t pid = fork();if (pid == 0) { /* Child: Do work or exec */ execlp("gcc", "gcc", "program.c", "-o", "program", NULL); _exit(127);}/* Parent: Wait for child */int status;waitpid(pid, &status, 0);/* Continue after child completes */Pattern 2: Fork-and-Continue (Asynchronous Child)
123456789101112131415161718
/* * Fork-and-Continue: Parent spawns child, continues working * Use case: Background tasks, parallel processing * MUST handle SIGCHLD to avoid zombies! */ /* Set up SIGCHLD handler to reap children */signal(SIGCHLD, sigchld_handler); pid_t pid = fork();if (pid == 0) { /* Child: Do background work */ do_background_work(); _exit(0);}/* Parent: Continue immediately */printf("Spawned background worker %d\n", pid);do_main_work(); /* Child runs in parallel */Pattern 3: Worker Pool
123456789101112131415161718192021222324252627282930
/* * Worker Pool: Parent spawns multiple children for parallel work * Use case: Web servers, parallel computation, request handling */ #define NUM_WORKERS 4pid_t workers[NUM_WORKERS]; /* Spawn workers */for (int i = 0; i < NUM_WORKERS; i++) { workers[i] = fork(); if (workers[i] == 0) { /* Worker child */ worker_loop(i); /* Process requests */ _exit(0); }} /* Parent: Manage workers, handle signals, etc. */while (running) { /* Accept connections, dispatch to workers via pipe/socket */ /* Handle SIGCHLD to detect crashed workers */ /* Optionally restart failed workers */} /* Cleanup: Kill workers */for (int i = 0; i < NUM_WORKERS; i++) { kill(workers[i], SIGTERM);}while (wait(NULL) > 0); /* Reap all */Pattern 4: Daemon Creation (Double-Fork)
1234567891011121314151617181920212223242526272829303132
/* * Daemon Creation: Child becomes independent background process * Use case: Services that outlive terminal session * The double-fork ensures daemon is orphaned (adopted by init) */ pid_t pid1 = fork();if (pid1 < 0) exit(1);if (pid1 > 0) exit(0); /* Parent 1 exits immediately */ /* Child 1: Become session leader */setsid(); pid_t pid2 = fork();if (pid2 < 0) exit(1);if (pid2 > 0) exit(0); /* Child 1 exits */ /* Child 2: This is the daemon *//* - Has no controlling terminal *//* - Parent PID is init (1) *//* - Runs independently of original terminal */ /* Standard daemon setup */chdir("/");close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO); /* Daemon main loop */while (1) { do_daemon_work();}The double-fork daemon pattern is traditional but outdated. Modern systems should use systemd with Type=simple or Type=forking. systemd handles session management, logging, automatic restart, and resource limits. Double-fork is still useful when systemd isn't available.
Debugging parent-child interactions is challenging due to concurrent execution. Here are essential techniques:
set follow-fork-mode child to debug child after forkset detach-on-fork offecho 1 > /proc/sys/kernel/core_uses_pid123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
/* * Debugging Helper Macros and Functions */ #include <stdio.h>#include <unistd.h>#include <sys/types.h>#include <string.h>#include <errno.h> /* Debug macro with automatic PID and location */#define DEBUG_LOG(fmt, ...) \ fprintf(stderr, "[PID %d] %s:%d: " fmt "\n", \ getpid(), __func__, __LINE__, ##__VA_ARGS__) /* Wait for debugger attachment */void wait_for_debugger(const char *name) { fprintf(stderr, "[PID %d] %s waiting for debugger...\n", getpid(), name); fprintf(stderr, "Attach with: gdb -p %d\n", getpid()); fprintf(stderr, "Then: set var waiting=0 and continue\n"); volatile int waiting = 1; while (waiting) { sleep(1); } fprintf(stderr, "[PID %d] Debugger attached, continuing\n", getpid());} /* Debug fork with logging */pid_t debug_fork(void) { DEBUG_LOG("Calling fork()"); pid_t pid = fork(); if (pid == -1) { DEBUG_LOG("fork() failed: %s", strerror(errno)); } else if (pid == 0) { DEBUG_LOG("Fork returned, I am the child"); } else { DEBUG_LOG("Fork returned, I am the parent, child=%d", pid); } return pid;} /* Example usage */int main() { pid_t pid = debug_fork(); if (pid == -1) return 1; if (pid == 0) { /* Uncomment to wait for debugger in child */ // wait_for_debugger("child"); DEBUG_LOG("Child working..."); sleep(1); DEBUG_LOG("Child exiting"); _exit(0); } DEBUG_LOG("Parent waiting..."); wait(NULL); DEBUG_LOG("Parent done"); return 0;}In GDB: info inferiors shows all processes being debugged. inferior N switches to process N. set schedule-multiple on lets all processes run. These commands are essential for debugging fork-heavy applications.
We've explored the dynamics of parent and child execution after fork(). Let's consolidate the key concepts:
What's Next:
We'll explore the dark side of fork(): fork bombs. These dangerous constructs can bring systems to their knees, and understanding how they work—and how to prevent them—is essential knowledge for any systems programmer.
You now understand the execution dynamics between parent and child processes. You know why execution order is undefined, how schedulers treat forked processes, and how to design robust applications that use explicit coordination instead of relying on timing. Next, we'll examine fork bombs and protection mechanisms.