How Unix creates new processes — the foundation of all interprocess communication.
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.
Click "Fork" to see a process clone itself. Each child is an independent copy of the parent.
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.
The pid_t type is used for process IDs. You can get your own PID with getpid() and your parent's PID with getppid().
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 Value | Meaning |
|---|---|
| 0 | You are the child process |
| Positive (PID) | You are the parent. The value is the child's PID. |
| -1 | Error — 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; }
Click "Fork" to see the return value flow. The parent gets the child's PID; the child gets 0.
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.
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.
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().
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.
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.
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 */
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.
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).
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.
| Mechanism | Direction | Related Processes? | Best For |
|---|---|---|---|
| Signals | One-way notification | Any | Simple alerts |
| Pipes | Unidirectional | Parent-child only | Streaming data |
| FIFOs | Unidirectional | Any | Named pipe on disk |
| Message Queues | Bidirectional | Any | Typed messages |
| Shared Memory | Bidirectional | Any | Fastest data sharing |
| Semaphores | Synchronization | Any | Mutual exclusion |
| Unix Sockets | Full-duplex | Any | Client-server |
"The fork() system call is a ticket to power. Power can sometimes be a ticket to destruction." — Beej