The Runtime Theory
KernelInternalsmemory

What happens when you call fork()?

A step-by-step walk from the clone() syscall through task_struct duplication, copy-on-write page tables, the child's first run, and execve replacing it all.

The Runtime Theory Team3 min read07 steps

layer stack

Kernel

HWHardware
KKernel
RTRuntime
APPApplication
SYSSystem
CLIClient
NETNetwork
TLSCrypto
SRVServer

trace spine

  1. 01 fork() becomes clone()
  2. 02 copy_process: task_struct duplicated
  3. 03 dup_mm: page tables copied read-only (COW)
  4. 04 fd table, signals, namespace shared or copied
  5. 05 Child scheduled; both return from clone
  6. 06 First write: COW page fault copies the page
  7. 07 execve: the copied world is discarded
On this page

fork() is the oldest trick in the Unix book: one call, two processes. The kernel's job is to make that seem free. The reality is a careful dance of refcounts, read-only page tables, and deferred copying — plus one big lie: all that careful work is thrown away moments later by exec.

Step 1 — fork() becomes clone()

glibc's fork() calls the clone(SIGCHLD) syscall (clone3 on modern kernels). The kernel sees CLONE_VM off, CLONE_FILES off: this is a process fork, not a thread.

Step 2 — copy_process: the task_struct

The kernel allocates a new task_struct, copies the parent's (PID namespace, credentials, signal state, security context), assigns a new PID, and — critically — creates a second kernel stack and a fresh thread_info with the child's own need_resched flags. Cheap: a few allocations, ~µs.

Step 3 — dup_mm: the page tables

Here's the expensive-looking part that must be made cheap. dup_mm()dup_mmap() walks the parent's VMA list and for each mapping calls copy_page_range(): for a 1GB address space that's ~262,000 PTE entries. The trick: every PTE is marked read-only and flagged _PAGE_COW (or, on new x86-64 kernels, the write bit is cleared with the COW bit set). No data is copied. Shared file mappings (MAP_SHARED) stay shared; anonymous pages become COW. Cost: page-table copy only — a few ms for a 1GB process, and zero per byte of actual data.

Step 4 — the rest of the process

File descriptors: the fd table is copied (each struct file gets a refcount bump — the underlying file is shared, the table isn't). Signal handlers, timers, and the fs context (cwd, root) are copied or refcounted. The child inherits the parent's memory view, but from now on they diverge independently.

Step 5 — both return from clone

The child is placed on the runqueue and eventually scheduled (often on the same CPU — scheduler wakeup logic biases to the parent's CPU for cache warmth). Here's the punchline: the clone() syscall returns twice — in the parent with the child's PID, in the child with 0. The same kernel stack frame produces two different return values.

Step 6 — the first write: COW strikes

The child's first write to any inherited page hits the COW mechanism: page fault → do_wp_page() → the PTE is read-only → the kernel checks the page's refcount. If it's 1 (child is sole owner — the common fast path), the kernel just makes the PTE writable in place, ~1µs. If the page is shared, a fresh page is allocated and the 4KB copied. This is the real price of fork: paid lazily, per page, exactly on the pages both sides actually mutate.

Step 7 — execve throws it all away

execve() calls exec_mmap(): the entire COW'd address space — every page table, every VMA, all that lazy-copy machinery — is torn down and replaced with a fresh, empty address space for the new binary. fork() without exec is why the copy existed at all; fork+exec is why COW exists instead of eager copying.

What it costs

  • fork() of a 1GB process: ~2-10ms (page-table copy, dominated by cache misses on 262k PTEs).
  • fork() of a small process: ~50-100µs.
  • COW faults after fork: ~1-3µs per page actually written.
  • execve of a typical binary: ~1-5ms (new mm, dynamic linker, faulting in code pages).
bash
$ /usr/bin/time -v sh -c 'for i in $(seq 1000); do /bin/true; done' 2>&1 | grep -E "user|Minor"
	User time (seconds): 0.31        # ~300µs per fork+exec+exit cycle
	Minor (reclaiming a frame) page faults: 48,211   # ~48 minor faults per cycle

The takeaway: fork is cheap because it copies structure, not data — and the structure is copied twice per lifecycle (once on fork, once discarded on exec). If you're spawning thousands of processes per second, the win is in avoiding the fork entirely: vfork, posix_spawn, or a persistent worker pool.