JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

fork(): One Process Becomes Two

Guide 1 taught you what a running process is. Now meet the strangest system call in Unix: fork(), the one call that returns twice — once in a parent and once in a brand-new child that is an almost-perfect copy of it. We will see exactly what gets copied, how the two halves tell themselves apart, and the honest traps that catch every beginner.

The call that returns twice

Almost every function you have written returns once. You call it, it does its work, you get one value back. fork() breaks that habit in the most unsettling way: you call it once, and it returns twice — once in the original process, and once in a new process the kernel creates as a near-exact duplicate of the caller. That new process is the child; the one that called fork() is the parent. After the call, two processes are running the same program, both sitting on the line right after fork(), both about to execute the next statement. The single thread of control you started with has split into two.

How does each half know which one it is? By the return value, and this is the heart of the whole idea. In the child, fork() returns `0`. In the parent, fork() returns the process ID of the new child — a positive number you can later use to wait for it or signal it. And if the kernel could not create the child at all (it ran out of memory or hit a process limit), fork() returns `-1` in the parent and no child exists. So one and the same line of code yields three possible outcomes, distinguished entirely by the number that comes back.

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

int main(void) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");           /* -1: no child was created */
        return 1;
    } else if (pid == 0) {
        printf("child:  my pid is %d\n", getpid());
    } else {
        printf("parent: my child is %d\n", pid);
    }
    return 0;
}
The canonical fork() skeleton. The three branches are -1 (failure, checked first), 0 (we are the child), and positive (we are the parent, and the value is the child's PID).

What the child inherits — a copy, not a share

The child is a duplicate of the parent's process image: it gets its own copy of the entire address space — the same code, the same heap, the same stack with the same local variables holding the same values, the same global variables — frozen at the instant of the fork. Crucially these are copies, not shared storage. If the parent now sets a variable `int n = 5` to `7`, the child still sees `5`; if the child changes it, the parent never notices. The two processes have separate virtual address spaces, so the address `0x601040` means a different physical byte in each. A variable that read the same a microsecond ago is now two independent variables that merely started equal.

But one resource is genuinely shared after the fork: open files. The child inherits a copy of the parent's file descriptor table, so descriptor 1 (standard output) in the child refers to the same underlying open file as descriptor 1 in the parent. They are two pointers into one shared open-file entry — including a single shared read/write position. That sharing is not an accident; it is exactly what makes shell pipelines and output redirection work, because the child a shell forks can write to a file or pipe the parent set up. We will lean on this heavily when fork() meets exec() and the standard streams in the next guides.

Copy-on-write: why duplicating an address space is cheap

Read the last section and a fair question follows: if fork() copies the whole address space — heap, stack, globals, perhaps hundreds of MiB — is it not painfully slow? In the earliest Unix it really did copy everything, and it was slow. Modern kernels use a trick from the virtual-memory rung: copy-on-write. At fork() the kernel does not duplicate the pages. Instead it lets parent and child share the very same physical pages and marks every one of them read-only. Reads from either side just work. Only when one side tries to write a page does the MMU trap into the kernel, which then makes a private copy of just that one page for the writer and lets the write proceed.

The payoff is large and worth understanding rather than memorising. fork() now costs roughly the price of copying the page tables, not the pages themselves, so it is fast and uses little extra memory at first. A child that reads a lot but writes little stays almost free. And the common case in real life — fork() immediately followed by exec() to run a different program — never touches most of those shared pages at all, because exec() throws the whole inherited address space away. Copy-on-write is the reason the fork-then-exec pattern, which would look wasteful on paper, is in fact the efficient backbone of how every Unix shell launches commands.

The traps every beginner steps in

fork() is small but sharp, so let me be honest about the failure modes rather than show only the happy path. First, always check the return value. fork() can fail with `-1` when the system is under memory pressure or you hit the per-user process limit; a program that assumes success will then treat the parent as if it were the child and corrupt its own logic. This is the check-every-call discipline applied to a syscall that fails in ways that are easy to provoke and ugly to debug.

Second, do not assume an order. After fork(), the kernel is free to run the parent first, the child first, or interleave them on different cores. If both print, the lines can appear in either order — that is not a bug in fork(), it is the nondeterminism of two independent processes, and any code that relies on one running before the other has a race condition. If you truly need ordering, you must arrange it explicitly with wait() (guide 4) or some form of communication; you cannot get it for free from fork().

Where fork() is going

Two clarifications keep the layers crisp. fork() creates a new process, not a new thread: the child has its own address space and its own copy of everything, which is exactly why a change in one is invisible to the other. A thread (a later rung) shares one address space — a different tool for a different job. And fork() is a system call, not an ordinary function: it crosses into the kernel, which is what lets it do something no user-mode code could ever do — conjure a second process out of one.

On its own, fork() just gives you a second copy of the same program — useful, but not yet how you launch `ls` or `gcc`. The real power appears when fork() is paired with exec() to swap the child's program for a new one, the fork-then-exec pattern that the next guide builds. And once you can make children, you owe it to keep track of them: guide 4 covers wait() and reading a child's exit result, and guide 5 the zombies and orphans you create if you forget. fork() is the first half of nearly everything a Unix system does to run another program; you now understand the half that does the splitting.