Loading learning content...
When a process creates a child, a fundamental question arises: What happens next? Does the parent continue executing alongside the child? Does it wait for the child to complete? Does the child immediately replace itself with a different program?
These execution options determine the relationship between parent and child execution flows and profoundly affect program structure, responsiveness, resource utilization, and correctness. Understanding these patterns is essential for designing everything from simple shell commands to complex distributed systems.
By the end of this page, you will master the three primary execution models: concurrent execution (parent and child run simultaneously), synchronous execution (parent waits for child), and the fork-exec pattern (child replaces itself with a new program). You'll understand when to use each pattern and how to implement them correctly.
In the concurrent execution model, after fork() returns, both parent and child continue executing simultaneously. The operating system's scheduler interleaves their execution on available CPUs.
Before fork(): One process executing
↓
[fork()]
↓
After fork(): Two processes executing
Parent (fork returns child PID)
Child (fork returns 0)
| Aspect | Behavior |
|---|---|
| Scheduling | OS decides which runs when; no guaranteed order |
| Interleaving | Parent and child statements interleave unpredictably |
| CPU Utilization | Both can run on separate cores simultaneously |
| Independence | Each has own instruction pointer, stack, registers |
| Synchronization | Required if accessing shared resources |
After fork(), there is NO guarantee whether parent or child executes first, or in what order their statements interleave. Code that assumes a specific order will have race conditions that may only manifest under certain system loads or timing conditions. Always use explicit synchronization when order matters.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <stdio.h>#include <unistd.h>#include <sys/wait.h> /** * Demonstrates concurrent execution after fork() * * Run multiple times - output order will vary! */int main() { printf("Before fork: Single process (PID %d)\n", getpid()); pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } // Both parent and child execute the code below // The order of outputs is NON-DETERMINISTIC if (pid == 0) { // Child process for (int i = 0; i < 5; i++) { printf(" [Child %d] iteration %d\n", getpid(), i); usleep(10000); // 10ms - simulate work } } else { // Parent process for (int i = 0; i < 5; i++) { printf(" [Parent %d] iteration %d\n", getpid(), i); usleep(10000); // 10ms - simulate work } } // Both processes reach here if (pid == 0) { printf("Child exiting\n"); } else { printf("Parent waiting for child...\n"); wait(NULL); printf("Parent: Child has exited\n"); } return 0;} /* * Sample output (order will vary): * * Before fork: Single process (PID 12345) * [Parent 12345] iteration 0 * [Child 12346] iteration 0 * [Child 12346] iteration 1 * [Parent 12345] iteration 1 * [Parent 12345] iteration 2 * [Child 12346] iteration 2 * ... */Ideal Use Cases:
Independent Tasks: Parent and child perform unrelated work that can proceed in parallel (e.g., downloading multiple files)
Pipeline Processing: Parent produces data, child consumes it (with proper IPC synchronization)
Responsive UI: Fork a child for long computation so parent can continue handling user input
Worker Pool: Create multiple children that all work on different tasks concurrently
Server Concurrency: Fork a child for each client connection while parent accepts more connections
Caution Required When:
In the synchronous execution model, the parent blocks (suspends its execution) until the child completes. This creates a clear sequential flow where the child's work fully completes before the parent continues.
Unix provides several wait variants with different capabilities:
| Function | Waits For | Key Features |
|---|---|---|
wait(int *status) | Any child | Simplest; blocks until any child terminates |
waitpid(pid, status, opts) | Specific child or group | Can specify which child; options for non-blocking |
waitid(idtype, id, siginfo, opts) | Flexible specification | More detailed status; POSIX-standard |
wait3(status, opts, rusage) | Any child | Also returns resource usage statistics |
wait4(pid, status, opts, rusage) | Specific child | Combined waitpid + resource usage |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <time.h> /** * Demonstrates synchronous execution with wait() * * Parent blocks until child completes, then continues */int main() { printf("Parent: Starting (PID %d)\n", getpid()); time_t start = time(NULL); pid_t pid = fork(); if (pid == 0) { // Child: simulate 3 seconds of work printf("Child: Starting work...\n"); for (int i = 0; i < 3; i++) { printf("Child: Working... (%d/3)\n", i + 1); sleep(1); } printf("Child: Work complete, exiting with status 42\n"); exit(42); // Exit with specific status } // Parent waits for child printf("Parent: Waiting for child (PID %d) to complete...\n", pid); int status; pid_t finished = wait(&status); // BLOCKS HERE time_t end = time(NULL); printf("Parent: wait() returned\n"); printf("Parent: Finished child PID: %d\n", finished); // Decode exit status if (WIFEXITED(status)) { printf("Parent: Child exited normally with status %d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Parent: Child killed by signal %d\n", WTERMSIG(status)); } printf("Parent: Total elapsed time: %ld seconds\n", end - start); printf("Parent: Now continuing with post-child work...\n"); return 0;} /* * Output: * Parent: Starting (PID 12345) * Parent: Waiting for child (PID 12346) to complete... * Child: Starting work... * Child: Working... (1/3) * Child: Working... (2/3) * Child: Working... (3/3) * Child: Work complete, exiting with status 42 * Parent: wait() returned * Parent: Finished child PID: 12346 * Parent: Child exited normally with status 42 * Parent: Total elapsed time: 3 seconds * Parent: Now continuing with post-child work... */waitpid() provides fine-grained control via its options parameter:
pid_t waitpid(pid_t pid, int *status, int options);
pid parameter:
| Value | Meaning |
|---|---|
> 0 | Wait for specific child with this PID |
-1 | Wait for any child (like wait()) |
0 | Wait for any child in same process group |
< -1 | Wait for any child in process group abs(pid) |
options flags:
| Flag | Effect |
|---|---|
WNOHANG | Return immediately if no child exited (non-blocking) |
WUNTRACED | Also report stopped (not just terminated) children |
WCONTINUED | Also report continued (SIGCONT'ed) children |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
#include <stdio.h>#include <unistd.h>#include <sys/wait.h>#include <errno.h> /** * Demonstrates waitpid() options, especially WNOHANG * * Non-blocking wait allows parent to poll for child completion * while doing other work */int main() { pid_t pid = fork(); if (pid == 0) { // Child: do some work printf("Child: Starting 5-second task...\n"); sleep(5); printf("Child: Done!\n"); _exit(0); } // Parent: use non-blocking wait to poll printf("Parent: Polling for child completion (non-blocking)...\n"); int status; pid_t result; int iterations = 0; while (1) { result = waitpid(pid, &status, WNOHANG); // Non-blocking! if (result > 0) { // Child has exited printf("\nParent: Child %d finished\n", result); break; } else if (result == 0) { // Child still running printf("."); fflush(stdout); iterations++; sleep(1); // Do other work or just poll periodically } else { // Error perror("waitpid"); break; } } printf("Parent: Polled %d times before child completed\n", iterations); if (WIFEXITED(status)) { printf("Parent: Child exited with status %d\n", WEXITSTATUS(status)); } return 0;} /* * Output: * Parent: Polling for child completion (non-blocking)... * Child: Starting 5-second task... * ..... * Child: Done! * Parent: Child 12346 finished * Parent: Polled 5 times before child completed * Parent: Child exited with status 0 */If a parent creates multiple children and calls wait() or waitpid(-1, ...), it receives the status of whichever child terminates first. To wait for all children, call wait() in a loop until it returns -1 with errno == ECHILD (no children left).
Perhaps the most important process creation pattern in Unix is fork-exec: the parent forks a child, and the child immediately replaces itself with a new program using exec(). This is how every command you run in a shell is started.
Unix deliberately separates process creation (fork) from program loading (exec). This separation enables powerful patterns:
The exec() family replaces the current process image with a new program. There are several variants with different interfaces:
| Function | Arguments | Environment | Path Search |
|---|---|---|---|
execl() | List (variadic) | Inherited | No |
execlp() | List (variadic) | Inherited | Yes (PATH) |
execle() | List (variadic) | Specified | No |
execv() | Array (argv[]) | Inherited | No |
execvp() | Array (argv[]) | Inherited | Yes (PATH) |
execve() | Array (argv[]) | Specified | No |
execvpe() | Array (argv[]) | Specified | Yes (PATH) |
Naming Convention:
l = arguments as list (variadic)v = arguments as vector (array)p = search PATH for executablee = specify environment explicitly1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> /** * Demonstrates the fork-exec pattern * * This is exactly how shells execute commands */ int run_command(const char *program, char *const argv[]) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; } if (pid == 0) { // Child: exec the new program // Note: execvp searches PATH execvp(program, argv); // If we reach here, exec failed perror("execvp"); _exit(127); // Convention: 127 = command not found } // Parent: wait for child int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { return WEXITSTATUS(status); } return -1;} int main() { printf("=== Fork-Exec Pattern Demo ===\n\n"); // Example 1: Run 'ls -la' printf("Running: ls -la\n"); printf("-------------------\n"); char *ls_args[] = {"ls", "-la", NULL}; // Must be NULL-terminated int result = run_command("ls", ls_args); printf("\nCommand exited with status: %d\n\n", result); // Example 2: Run 'date' printf("Running: date\n"); printf("--------------\n"); char *date_args[] = {"date", NULL}; result = run_command("date", date_args); printf("\nCommand exited with status: %d\n\n", result); // Example 3: Run command that doesn't exist printf("Running: nonexistent_command\n"); printf("----------------------------\n"); char *bad_args[] = {"nonexistent_command", NULL}; result = run_command("nonexistent_command", bad_args); printf("Command exited with status: %d (127 = not found)\n", result); return 0;}Replaced (New Program):
Preserved (From Before exec):
A successful exec() call NEVER returns—the calling program is completely replaced. Any code after exec() only runs if exec() failed. This is why examples always have error handling immediately after exec(): execvp(...); perror("exec failed"); _exit(127);
One of the most powerful uses of the fork-exec separation is setting up I/O redirection between fork() and exec(). The child manipulates its file descriptors, inherits them through exec(), and the new program runs with the redirected I/O without knowing anything changed.
When you type ls > output.txt, the shell does:
close(1) (close stdout)open("output.txt", O_WRONLY|O_CREAT) — gets fd 1 (lowest available)exec("ls", ...)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/wait.h> /** * Demonstrates I/O redirection in fork-exec pattern * * Equivalent to: ls -la > listing.txt */int main() { printf("Running 'ls -la > listing.txt'\n"); pid_t pid = fork(); if (pid == 0) { // Child: set up redirection before exec // Close stdout (fd 1) close(STDOUT_FILENO); // Open file - it will get fd 1 (lowest available) int fd = open("listing.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd != STDOUT_FILENO) { // Just in case, dup2 to be safe dup2(fd, STDOUT_FILENO); close(fd); } // Now exec ls - its stdout goes to the file char *args[] = {"ls", "-la", NULL}; execvp("ls", args); perror("exec failed"); _exit(127); } // Parent waits wait(NULL); printf("Done. Check listing.txt for output.\n"); // Show what's in the file printf("\n--- Contents of listing.txt ---\n"); char *cat_args[] = {"cat", "listing.txt", NULL}; pid = fork(); if (pid == 0) { execvp("cat", cat_args); _exit(127); } wait(NULL); return 0;}Pipes between commands (ls | grep foo) use similar redirection between cooperating processes:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> /** * Implements: ls -la | grep ".txt" * * Two children connected by pipe: * - Child 1: ls -la (stdout -> pipe write end) * - Child 2: grep .txt (stdin <- pipe read end) */int main() { int pipefd[2]; // Create pipe: pipefd[0] = read end, pipefd[1] = write end if (pipe(pipefd) < 0) { perror("pipe"); return 1; } printf("Running: ls -la | grep '.txt'\n"); printf("----------------------------------\n"); // Fork first child (ls) pid_t pid1 = fork(); if (pid1 == 0) { // Child 1: ls // Redirect stdout to pipe write end dup2(pipefd[1], STDOUT_FILENO); // Close both pipe ends (we've duplicated what we need) close(pipefd[0]); close(pipefd[1]); // Exec ls char *args[] = {"ls", "-la", NULL}; execvp("ls", args); _exit(127); } // Fork second child (grep) pid_t pid2 = fork(); if (pid2 == 0) { // Child 2: grep // Redirect stdin to pipe read end dup2(pipefd[0], STDIN_FILENO); // Close both pipe ends close(pipefd[0]); close(pipefd[1]); // Exec grep char *args[] = {"grep", ".txt", NULL}; execvp("grep", args); _exit(127); } // Parent: close pipe ends (children have their own copies) close(pipefd[0]); close(pipefd[1]); // Wait for both children waitpid(pid1, NULL, 0); waitpid(pid2, NULL, 0); printf("\n----------------------------------\n"); printf("Pipeline complete.\n"); return 0;}A common bug is forgetting to close pipe ends in parent or child. If the write end isn't closed in all processes that don't need it, readers will block forever waiting for more data. If the read end isn't closed, writers may block when the buffer fills. Always close both ends in processes that don't use them.
Let's analyze when to use each execution pattern and their tradeoffs.
| Pattern | Description | Best For | Example |
|---|---|---|---|
| Concurrent (fork only) | Parent continues while child runs | Parallel work, background tasks | Web server handling multiple requests |
| Synchronous (fork + wait) | Parent blocks until child completes | Sequential dependencies | Shell running single command |
| Fork-exec | Child replaces itself with new program | Running different executables | Shell, process launcher |
| Fork-exec + wait | Run program, wait for completion | Scripted command execution | Build systems, installers |
| Fork-monitor | Parent supervises multiple children | Service managers, pools | systemd, prefork servers |
| Daemonize (double-fork) | Detach from terminal completely | Background services | System daemons |
A common pattern for robust services is a monitor process that supervises workers:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <signal.h> #define NUM_WORKERS 3 volatile sig_atomic_t running = 1; void handle_sigint(int sig) { running = 0;} /** * Monitor pattern: supervisor manages worker processes * * - Starts N workers * - Restarts any worker that crashes * - Gracefully shuts down on SIGINT */int main() { signal(SIGINT, handle_sigint); pid_t workers[NUM_WORKERS] = {0}; printf("Monitor starting %d workers...\n", NUM_WORKERS); // Start initial workers for (int i = 0; i < NUM_WORKERS; i++) { workers[i] = fork(); if (workers[i] == 0) { // Worker process printf("Worker %d (PID %d) started\n", i, getpid()); while (1) { sleep(5); // Simulate work // Randomly "crash" some workers for demo if (rand() % 10 == 0) { printf("Worker PID %d simulating crash\n", getpid()); _exit(1); } } } } // Monitor loop while (running) { int status; pid_t pid = waitpid(-1, &status, 0); // Wait for ANY child if (pid <= 0) continue; // Find which worker died for (int i = 0; i < NUM_WORKERS; i++) { if (workers[i] == pid) { printf("Monitor: Worker %d (PID %d) exited, restarting...\n", i, pid); // Restart the worker if (running) { workers[i] = fork(); if (workers[i] == 0) { printf("Worker %d (PID %d) restarted\n", i, getpid()); while (1) sleep(5); } } break; } } } // Shutdown: kill all workers printf("\nMonitor: Shutting down workers...\n"); for (int i = 0; i < NUM_WORKERS; i++) { if (workers[i] > 0) { kill(workers[i], SIGTERM); } } // Wait for all to exit while (wait(NULL) > 0); printf("Monitor: All workers stopped, exiting.\n"); return 0;}To create a proper daemon (background service), processes must detach from their controlling terminal. The classic pattern uses double-fork:
// Step 1: First fork
pid = fork();
if (pid > 0) exit(0); // Parent exits
// Step 2: Become session leader (detach from terminal)
setsid();
// Step 3: Second fork (prevent terminal reacquisition)
pid = fork();
if (pid > 0) exit(0); // First child exits
// Step 4: We are now
// - Orphaned (adopted by init)
// - Session leader (no controlling terminal)
// - Not a process group leader (can't acquire terminal)
// Step 5: Close all file descriptors
for (int i = 0; i < getdtablesize(); i++) close(i);
// Step 6: Redirect stdin/out/err to /dev/null
open("/dev/null", O_RDWR); // stdin
dup(0); // stdout
dup(0); // stderr
// Step 7: Change to safe directory
chdir("/");
// Now we are a proper daemon
Why double-fork? After setsid(), the process is a session leader and COULD acquire a controlling terminal if it opens one. The second fork ensures the final child is NOT a session leader and cannot accidentally acquire a terminal.
Robust process management requires handling numerous edge cases and error conditions.
fork() can fail for several reasons:
| Condition | errno | Cause |
|---|---|---|
| EAGAIN | Resource limit | Too many processes (user or system limit) |
| ENOMEM | Out of memory | Insufficient memory for new process structures |
pid_t pid = fork();
if (pid < 0) {
if (errno == EAGAIN) {
fprintf(stderr, "Too many processes, cannot fork\n");
// Possibly wait and retry
} else if (errno == ENOMEM) {
fprintf(stderr, "Out of memory for fork\n");
// Cannot recover easily
}
return -1;
}
exec() can fail for many reasons:
| errno | Cause |
|---|---|
| ENOENT | File not found |
| EACCES | Permission denied (not executable) |
| ENOEXEC | Invalid executable format |
| E2BIG | Argument list too long |
| ENOMEM | Out of memory |
| ETXTBSY | Executable currently open for writing |
execvp(program, args);
// Only reached if exec failed
switch (errno) {
case ENOENT:
fprintf(stderr, "%s: command not found\n", program);
_exit(127); // Standard "not found" code
case EACCES:
fprintf(stderr, "%s: permission denied\n", program);
_exit(126); // Standard "not executable" code
default:
perror(program);
_exit(1);
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <errno.h>#include <sys/wait.h> /** * Robust fork-exec with proper error handling * * Handles all common failure modes and reports meaningful errors */ typedef struct { int exit_code; int signal_num; int error_type; // 0=normal, 1=signaled, 2=fork_failed, 3=exec_failed char error_msg[256];} CommandResult; CommandResult run_command_robust(const char *program, char *const argv[]) { CommandResult result = {0}; // Create pipe for child to report exec errors int error_pipe[2]; if (pipe(error_pipe) < 0) { result.error_type = 2; snprintf(result.error_msg, sizeof(result.error_msg), "pipe failed: %s", strerror(errno)); return result; } pid_t pid = fork(); if (pid < 0) { result.error_type = 2; snprintf(result.error_msg, sizeof(result.error_msg), "fork failed: %s", strerror(errno)); close(error_pipe[0]); close(error_pipe[1]); return result; } if (pid == 0) { // Child close(error_pipe[0]); // Close read end // Set close-on-exec on write end // If exec succeeds, pipe is closed automatically // If exec fails, we write the error fcntl(error_pipe[1], F_SETFD, FD_CLOEXEC); execvp(program, argv); // Exec failed - write errno to pipe int err = errno; write(error_pipe[1], &err, sizeof(err)); _exit(127); } // Parent close(error_pipe[1]); // Close write end // Try to read exec error from child int child_errno; ssize_t n = read(error_pipe[0], &child_errno, sizeof(child_errno)); close(error_pipe[0]); if (n > 0) { // Child failed to exec result.error_type = 3; if (child_errno == ENOENT) { snprintf(result.error_msg, sizeof(result.error_msg), "%s: command not found", program); result.exit_code = 127; } else if (child_errno == EACCES) { snprintf(result.error_msg, sizeof(result.error_msg), "%s: permission denied", program); result.exit_code = 126; } else { snprintf(result.error_msg, sizeof(result.error_msg), "%s: %s", program, strerror(child_errno)); result.exit_code = 1; } waitpid(pid, NULL, 0); // Still need to reap child return result; } // Exec succeeded - wait for command completion int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { result.error_type = 0; result.exit_code = WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { result.error_type = 1; result.signal_num = WTERMSIG(status); } return result;} int main() { printf("=== Robust Fork-Exec Demo ===\n\n"); // Test 1: Successful command printf("Test 1: Running 'echo hello'\n"); char *args1[] = {"echo", "hello", NULL}; CommandResult r1 = run_command_robust("echo", args1); printf("Result: exit_code=%d, type=%d\n\n", r1.exit_code, r1.error_type); // Test 2: Command not found printf("Test 2: Running 'nonexistent_cmd'\n"); char *args2[] = {"nonexistent_cmd", NULL}; CommandResult r2 = run_command_robust("nonexistent_cmd", args2); printf("Result: %s\n\n", r2.error_msg); // Test 3: Permission denied printf("Test 3: Running '/etc/passwd' (not executable)\n"); char *args3[] = {"/etc/passwd", NULL}; CommandResult r3 = run_command_robust("/etc/passwd", args3); printf("Result: %s\n", r3.error_msg); return 0;}The error_pipe technique shown above is how professional shells detect exec() failures. The pipe is set close-on-exec, so if exec() succeeds, the pipe closes and read() returns 0. If exec() fails, the child writes errno before exiting, and parent receives specific error information.
Execution options determine the relationship between parent and child processes after creation. Choosing the right pattern affects program structure, performance, and correctness.
ls | grepWhat's Next:
With execution options covered, the final page of this module examines address space options—how the child's memory layout can be configured, including the choice between shared address spaces (threads), complete copies (fork), and the modern vfork() optimization for the immediate-exec case.
You now understand the full spectrum of execution options available during process creation. From concurrent execution to synchronous waiting to the fork-exec pattern, these patterns form the foundation of Unix process management and are essential knowledge for systems programming.