Beej's Guide to Unix IPC — Chapter 2

A fork() Primer

How Unix creates new processes — the foundation of all interprocess communication.

Prerequisites: Basic C + What a process is. That's it.
8
Chapters
4+
Simulations
0
Assumed Knowledge

Chapter 0: Why New Processes?

You have a program. It runs, does its thing, and exits. Simple. But what if you want it to do two things at once? Maybe you want to handle multiple clients, or crunch data in parallel, or spawn a helper that runs a different program entirely.

On Unix, the answer is fork(). It takes your running process and clones it — creating a brand-new child process with its own copy of everything. The parent keeps running, the child keeps running, and now you have two processes where there was one.

This single system call is the foundation of everything else in this guide. Pipes, shared memory, signals — all of it depends on having multiple processes that need to talk to each other. And fork() is how those processes come into existence.

The big idea: fork() doesn't launch a new program. It duplicates the current one. After fork() returns, there are two nearly identical processes running the same code. The only difference? fork() returns different values to parent and child, so they can diverge.
Process Splitting

Click "Fork" to see a process clone itself. Each child is an independent copy of the parent.

What does fork() do?

Chapter 1: The fork() Call

Calling fork() is deceptively simple. You call it with no arguments. It returns, and suddenly there are two processes running. Both continue executing from the exact same point — the line after the fork() call.

c
#include <unistd.h>
#include <stdio.h>

int main(void) {
    printf("Before fork: one process\n");
    fork();
    printf("After fork: TWO processes print this!\n");
    return 0;
}

That "After fork" line prints twice — once from the parent, once from the child. Both processes have their own copy of the program counter, their own stack, their own heap. They share nothing except the original code.

Key insight: The child gets a copy of the parent's memory, not a reference to it. If the parent changes a variable after fork(), the child doesn't see the change, and vice versa. They are completely independent from that moment on.

The pid_t type is used for process IDs. You can get your own PID with getpid() and your parent's PID with getppid().

After fork() returns, how many processes are running?

Chapter 2: Return Values — Who Am I?

If both parent and child run the same code after fork(), how do they know which one they are? The secret is in the return value of fork(). It returns a different value to each process:

Return ValueMeaning
0You are the child process
Positive (PID)You are the parent. The value is the child's PID.
-1Error — fork() failed (no child created)

This is the standard pattern for using fork():

c
pid_t pid = fork();

switch(pid) {
    case -1:
        perror("fork");  /* something went wrong */
        exit(1);
    case 0:
        printf("I am the child! PID: %d\n", getpid());
        break;
    default:
        printf("I am the parent! Child PID: %d\n", pid);
        break;
}
The trick: fork() is called once but returns twice — once in each process. The parent gets the child's PID (useful for wait()), the child gets 0 (and can call getppid() to find its parent).
fork() Return Values

Click "Fork" to see the return value flow. The parent gets the child's PID; the child gets 0.

What does fork() return to the child process?

Chapter 3: Data Copies — Nothing is Shared

Here's a crucial fact that trips up every beginner: after fork(), parent and child have completely separate memory. The child gets a copy of every variable, every array, every heap allocation. Changes in one process are invisible to the other.

c
int x = 42;
if (fork() == 0) {
    /* Child */
    x = 99;
    printf("Child: x = %d\n", x);  /* prints 99 */
} else {
    /* Parent */
    sleep(1);
    printf("Parent: x = %d\n", x); /* prints 42! */
}

The parent still sees x = 42. The child's modification to x happened in its own private copy. This is exactly why we need IPC — if processes could share memory directly, we wouldn't need pipes, message queues, or shared memory segments.

Key insight: This memory isolation is a feature, not a bug. It means processes can't corrupt each other's data. The price is that communication between processes requires explicit mechanisms — which is what the rest of this guide teaches.
Memory Isolation Demo

Click "Fork & Modify" to see parent and child each have independent copies of a variable. The child changes its copy; the parent's value stays the same.

If a parent sets x = 10, then fork()s, and the child sets x = 20, what does the parent see when it reads x?

Chapter 4: Zombies — The Undead Processes

When a child process exits, it doesn't completely disappear. A small remnant lingers in the process table, holding the child's exit status. This remnant is called a zombie process (shown as <defunct> in ps output). It waits for the parent to collect its exit status.

Why? Because the parent might want to know how the child exited — did it succeed? Did it crash? What error code did it return? Unix keeps this information around until the parent explicitly asks for it with wait().

The danger: If the parent never calls wait(), zombies accumulate. Each one takes a slot in the process table. Fork enough children without reaping them and you'll fill the table entirely — no new processes can be created. Your sysadmin will not be pleased.
Child Running
Doing its work normally
↓ exit()
Zombie State
Dead, but exit status not collected
↓ parent calls wait()
Fully Reaped
Removed from process table

What happens if the parent dies first? The child gets reparented to init (PID 1), which periodically reaps zombies. But if the parent is alive and just not calling wait(), the zombies stay forever.

What is a zombie process?

Chapter 5: wait() — Reaping Children

The cure for zombies is wait(). When a parent calls wait(), it blocks until any child exits, then collects the child's exit status. If you need to wait for a specific child, use waitpid() with the child's PID.

c
int status;
pid_t child_pid = wait(&status);

if (WIFEXITED(status)) {
    printf("Child %d exited with code %d\n",
           child_pid, WEXITSTATUS(status));
}

The WEXITSTATUS() macro extracts the actual return value from the raw status integer. There are other macros too: WIFEXITED() checks if the child terminated normally, and WIFSIGNALED() checks if it was killed by a signal.

Shortcut: If you don't care about the exit status at all, pass NULL: wait(NULL). This still reaps the zombie — you just discard the status info.

There's also the nuclear option: ignore SIGCHLD. If the parent sets signal(SIGCHLD, SIG_IGN), the kernel automatically reaps children without leaving zombies. No wait() needed. Beej calls this out early in the chapter — it's a pragmatic shortcut for daemons.

c
signal(SIGCHLD, SIG_IGN); /* children auto-reaped, no zombies */
fork(); fork(); fork(); /* go wild */
What happens if you call wait(NULL)?

Chapter 6: Process Tree Simulation

Time to put it all together. In the real world, a parent might fork multiple children, each of which might fork children of their own, forming a tree of processes. Each fork copies the parent's entire state. Each child runs independently.

Interactive Process Tree

Click on any process to fork a child from it. Watch the tree grow. Click "Reap" on a dead child to remove it. Processes that exit without being wait()'d become zombies (shown in yellow).

Click a node to select
Observe: When a process exits, it turns yellow (zombie). Only when its parent reaps it does it disappear. If the parent exits first, orphans are reparented to init (PID 1). This is exactly how Unix manages process lifetimes.
What happens to a child process when its parent exits without calling wait()?

Chapter 7: Beyond fork()

We've covered the essentials of process creation and lifecycle on Unix. fork() is the gateway to all IPC — you can't communicate between processes until you have multiple processes to communicate between.

What's next: Now that we can create processes, the question becomes: how do they talk to each other? The rest of this guide covers progressively more powerful IPC mechanisms — from simple signals to full-duplex sockets.
MechanismDirectionRelated Processes?Best For
SignalsOne-way notificationAnySimple alerts
PipesUnidirectionalParent-child onlyStreaming data
FIFOsUnidirectionalAnyNamed pipe on disk
Message QueuesBidirectionalAnyTyped messages
Shared MemoryBidirectionalAnyFastest data sharing
SemaphoresSynchronizationAnyMutual exclusion
Unix SocketsFull-duplexAnyClient-server

"The fork() system call is a ticket to power. Power can sometimes be a ticket to destruction." — Beej

Why is fork() the foundation of IPC?