Writing a C Program with fork() to Demonstrate the Parent-Child Relationship of Processes
In Unix-like operating systems, a new process is commonly created with the fork() system call. After fork(), execution continues in both the parent process and the child process from the next instruction, but with different return values that let each process identify its role.2 Specifically, fork() returns 0 in the child, the child’s PID in the parent, and -1 on failure.2
A well-designed demonstration program should show at least four things: the different return values of fork(), the use of getpid() and getppid() to print identities, the fact that parent and child execute concurrently, and the use of wait() or waitpid() so the parent can synchronize with the child and avoid leaving a zombie process.2 Modern systems also optimize fork() through copy-on-write rather than immediately duplicating all physical memory pages, so parent and child initially share memory pages safely until one writes to them.2
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ ↩2 ↩3 -
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩ ↩2 -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩ -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩ -
Why Do We Need the fork System Call to Create New Processes? - Describes inheritance, process control, and copy-on-write memory behavior. ↩
System Programming in C - Basics of Fork
Key Idea
A single call to fork() causes two processes to continue execution from the same point, but with different return values.2
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩
Core system calls and process relationships
The kernel records a parent-child relationship when one process creates another with fork(). Every process, except the system’s root process, has a parent, and the child can discover that relationship using getppid(). In a teaching example, printing both getpid() and getppid() makes the hierarchy visible at runtime.2
The parent typically calls waitpid() or wait() to suspend execution until the child changes state or exits. This is important because if the child terminates and the parent never collects its status, the child remains as a zombie entry in the process table until reaped.2 Thus, even a simple demonstration program should include synchronization.
A compact comparison is shown below:
| Function | Purpose | Typical Return |
|---|---|---|
fork() | Create a child process | 0 in child, child PID in parent, -1 on error2 |
getpid() | Get caller's PID | Caller’s PID |
getppid() | Get parent PID | Parent’s PID2 |
wait() / waitpid() | Collect child status | Child PID on success, -1 on error |
Mathematically, if one process calls fork() exactly once and the call succeeds, the process count changes from to . More generally, if each existing process forks once in a round, the count doubles to after rounds, which is why uncontrolled forking can exhaust system resources quickly.
Footnotes
-
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩ ↩2 ↩3 ↩4 ↩5 -
POSIX API 2 Slides - Parent and Child Processes - Teaching material covering
getppid(), zombies, parent-child relationships, andwaitpid()behavior. ↩ ↩2 ↩3 -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩ ↩2 ↩3 -
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩
Important Safety Note
Do not place fork() in an uncontrolled loop while experimenting. Repeated forking can create a large number of processes very quickly and may destabilize the system.
Footnotes
-
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩
How the demonstration program works
- 1Step 1
Use
stdio.h,stdlib.h,unistd.h, andsys/wait.hso the program can print output, callfork(), query process IDs, and wait for child termination.2Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩
-
- 2Step 2
Store the return value in a variable of type
pid_t. This single call causes the operating system to create a child process.2Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩
-
- 3Step 3
If the return value is negative, process creation failed and the program should report the error and terminate.
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩
-
- 4Step 4
When the return value is
0, print the child PID and parent PID usinggetpid()andgetppid(), optionally modify a local variable, then exit.3Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩
-
- 5Step 5
When the return value is positive, print the parent PID and the child PID returned by
fork(). Then callwaitpid()orwait()to collect the child’s exit status.2Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩
-
- 6Step 6
Use macros such as
WIFEXITED(status)andWEXITSTATUS(status)to determine whether the child terminated normally and what status it returned.Footnotes
-
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩
-
Complete C program
The following program demonstrates the parent-child relationship clearly. It shows identity, control flow separation, independent variable state, and parent synchronization.3
1#include <stdio.h> 2#include <stdlib.h> 3#include <unistd.h> 4#include <sys/wait.h> 5 6int main(void) { 7 pid_t pid; 8 int x = 100; // Demonstrates separate logical address spaces after fork 9 int status; 10 11 pid = fork(); 12 13 if (pid < 0) { 14 perror("fork failed"); 15 return EXIT_FAILURE; 16 } 17 else if (pid == 0) { 18 // Child process 19 x += 25; 20 printf("Child Process:\n"); 21 printf(" PID = %d\n", getpid()); 22 printf(" PPID = %d\n", getppid()); 23 printf(" x = %d\n", x); 24 printf(" fork() return value in child = %d\n", pid); 25 return 42; 26 } 27 else { 28 // Parent process 29 printf("Parent Process:\n"); 30 printf(" PID = %d\n", getpid()); 31 printf(" Child PID returned by fork() = %d\n", pid); 32 printf(" x = %d\n", x); 33 34 waitpid(pid, &status, 0); 35 36 if (WIFEXITED(status)) { 37 printf("Parent: child exited normally with status %d\n", 38 WEXITSTATUS(status)); 39 } else { 40 printf("Parent: child did not exit normally\n"); 41 } 42 } 43 44 return EXIT_SUCCESS; 45}
This example is pedagogically useful for several reasons. First, the child prints fork() return value in child = 0, while the parent prints the positive child PID returned by fork(), directly demonstrating role differentiation.2 Second, the variable x becomes 125 in the child but remains 100 in the parent, illustrating separate writable process state after the fork, even though the child begins as a copy of the parent.2 Third, the use of waitpid(pid, &status, 0) ensures the parent waits for that specific child and then decodes the termination result with standard macros.
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ ↩2 -
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩ ↩2 -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩ ↩2 -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩ -
Why Do We Need the fork System Call to Create New Processes? - Describes inheritance, process control, and copy-on-write memory behavior. ↩
1gcc -Wall -Wextra -o fork_demo fork_demo.c
Interpretation and common questions
Return values and observable roles after fork()
A conceptual comparison of outcomes seen by each execution path.
Execution lifecycle of the demonstration
The lifecycle of this program follows a standard POSIX process-creation pattern: create with fork(), differentiate behavior by return value, perform child work, and synchronize in the parent with waitpid().2 This pattern is foundational in operating systems, shells, and server designs.2
A subtle but important point is that waitpid() returns the PID of the child whose state changed, and status macros such as WIFEXITED and WEXITSTATUS decode the encoded termination information. This makes the demonstration more rigorous than simply printing messages, because it confirms not only ancestry but also process completion semantics.
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩ ↩2 -
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩
Runtime timeline of the fork demonstration
Single process starts
T1The program begins as one running process with one PID and one address space."
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩
Child is created
T2The call to fork() causes the kernel to create a new child process related to the caller.2"
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩
Execution splits
T3The parent receives the child PID, while the child receives 0, allowing separate control paths.2"
Footnotes
-
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩
Processes run concurrently
T4Both processes print their information; scheduling determines visible output order."
Footnotes
-
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩
Child terminates
T5The child returns an exit status, which becomes available to the parent."
Footnotes
-
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩
Parent reaps child
T6The parent calls waitpid() to collect the status and complete the demonstration cleanly.2"
Footnotes
-
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩ -
POSIX API 2 Slides - Parent and Child Processes - Teaching material covering
getppid(), zombies, parent-child relationships, andwaitpid()behavior. ↩
Best Practice
Use waitpid(pid, &status, 0) instead of plain wait() when you want the parent to synchronize with a specific child and inspect its exit code precisely.
Footnotes
-
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩
Educational extensions
Once the basic parent-child relationship is clear, the program can be extended in academically meaningful ways:
- Add
sleep()in the child to make scheduling effects easier to observe. - Replace the child code with
exec()to demonstrate the commonfork()+exec()model used by shells.2 - Create multiple children and call
waitpid()repeatedly to study process trees and status handling.2 - Print memory addresses and values before and after modification to reinforce the copy-on-write model.2
These variations help students move from a single demonstration toward deeper understanding of concurrency, synchronization, exit status, and process lifecycle management.2
Footnotes
-
Understanding
fork()in Linux: How Process Creation Really Works - Explains process creation, scheduling behavior, copy-on-write, and common pitfalls. ↩ ↩2 ↩3 -
fork(2) - Linux manual page - Official Linux manual documentation for
fork(), its semantics, and return values. ↩ -
fork, exec, wait and exit - Explains parent-child process IDs, process trees, and the special two-return behavior of
fork(). ↩ -
wait(2) - Linux manual page - Official reference for
wait()andwaitpid(), including return values and status macros. ↩ ↩2 -
POSIX API 2 Slides - Parent and Child Processes - Teaching material covering
getppid(), zombies, parent-child relationships, andwaitpid()behavior. ↩ -
Why Do We Need the fork System Call to Create New Processes? - Describes inheritance, process control, and copy-on-write memory behavior. ↩
Knowledge Check
What value does fork() return in the child process?
Explore Related Topics
User-Level and Kernel-Level Threads in Operating Systems
User‑Level Threads (ULTs) are managed entirely in user space, while Kernel‑Level Threads (KLTs) are created, scheduled, and tracked by the OS kernel, leading to trade‑offs between low overhead and true parallelism.
- ULT operations avoid kernel mode switches, giving creation and context‑switch cost but suffer from the “blocking system call trap” where one blocked thread stalls the whole process.
- KLTs enable true hardware parallelism and non‑blocking concurrency; however, creation and switching incur kernel overhead .
- Mapping models: Many‑to‑One (single kernel thread, parallelism ), One‑to‑One (parallelism ), Many‑to‑Many (hybrid overhead).
- Modern runtimes (e.g., Go goroutines, Java virtual threads) use M:N scheduling to combine ULT speed with KLT scalability.
- Performance charts show ULT operations costing far fewer CPU cycles than KLT equivalents.
CPU Scheduling Case Study: FCFS vs SJF for a Five-Process Workload
The case study compares FCFS and non‑preemptive SJF scheduling for five processes that all arrive at time 0, showing their Gantt charts, individual waiting times, and average waiting times.
- FCFS order A → B → C → D → E; waiting times 0, 10, 11, 13, 14 ms; average ms.
- SJF order B → D → C → E → A (ties broken by arrival order); waiting times 0, 1, 2, 4, 9 ms; average ms.
- With simultaneous arrivals, each process’s waiting time equals the sum of burst times of all jobs scheduled before it.
- Non‑preemptive SJF is optimal for minimizing average waiting time when burst lengths are known.
- The priority column is irrelevant for this comparison.
Rust Programming
Rust is a systems programming language that provides memory‑safe, high‑performance code through its ownership, borrowing, and lifetime model, combined with modern type features and strong tooling.
- Ownership means each value has a single owner; moving transfers ownership and dropping occurs at scope end, preventing leaks and double frees.
- Borrowing uses immutable
&Tor mutable&mut Treferences with strict aliasing rules, ensuring data‑race‑free safe code. - Enums,
Option<T>andResult<T,E>with pattern matching make absence and errors explicit, enhancing reliability. - Traits and generics enable zero‑cost abstractions and polymorphism, while Cargo manages packages, builds, tests, and documentation.
