fork() makes a complete copy of your process — address space, file descriptors, everything. Called on a 10 GB process, it returns in well under a millisecond. Those two sentences can’t both be true naively, and the trick that reconciles them — copy-on-write — is one of those kernel ideas that pays compound interest: it explains why fork+exec isn’t absurd, how Redis snapshots without pausing, why forking servers “share” memory until they don’t, and one famous way production machines run out of memory without allocating any.

Copy the Map, Not the Territory

What fork actually duplicates is the page tables — the map of the address space, kilobytes-to-megabytes even for huge processes — not the memory. Both processes’ entries now point at the same physical frames, and every writable page is downgraded to read-only in both:

text
  parent PTEs ──▶ [frame A][frame B][frame C] ◀── child PTEs
                        all marked read-only, refcount 2

Reads proceed at full speed, indefinitely shared. A write — by either process — traps: the page fault handler sees a write to a read-only page in a writable VMA, recognizes the COW case, copies that one 4 KB page, points the writer’s PTE at the copy (writable), and restarts the instruction. The process pays for its divergence page by page, on demand, and pages nobody writes are never copied at all.

The design bet is workload-shaped, and it’s usually right: the most common thing to do after fork is exec, which throws the whole address space away — eager copying would be pure waste. (History corroborates: pre-COW Unix really did copy, which is why vfork — “borrow the parent’s memory, parent freezes until exec” — existed. Modern posix_spawn bundles fork+exec for the spawn-a-program case and is what glibc actually optimizes; use it and skip the folklore.)

Where the Bill Comes Due

COW turns “how much memory does fork cost” into a question about write patterns, and that has sharp production edges.

The Redis case is the canonical one. BGSAVE forks; the child serializes a frozen snapshot of the dataset — consistency for free via COW, no locks, the parent keeps serving. But every write the parent handles during the dump copies a page. A write-hot 20 GB Redis can approach 2× memory during a save — and the failure is spiky: fine for months, then a traffic surge coincides with BGSAVE and the OOM killer arrives. The vm.overcommit_memory=1 advice in every Redis deployment guide is exactly this: the kernel must be willing to promise memory that COW will probably never claim, or fork itself fails with ENOMEM at the worst time.

The same physics, quieter: forking web workers (classic Apache prefork, gunicorn) start out sharing nearly everything — then runtime writes erode the sharing. The infamous version: CPython’s reference counting writes into every object it touches, so even read-only workloads in forked Python workers un-share memory steadily (Instagram once patched CPython's GC — gc.freeze — specifically to stop refcount/GC writes from shredding COW). Measure reality with smaps_rollup: Shared_Clean vs Private_Dirty is the COW ledger, and PSS is the honest per-worker cost.

Two more inherited-by-COW facts worth filing: the child gets a copy of file descriptor numbers but shares the file objects — offsets move for both (why you fflush before fork, lest buffered output print twice); and fork in a multithreaded process copies only the calling thread, with every lock frozen exactly as some now-nonexistent thread left it — the reason “fork only then exec, or fork from a thread that owns nothing” is a rule and pthread_atfork is mostly an apology.

One Mechanism, Many Costumes

Once you see COW as “share until write, fault to diverge,” it shows up everywhere: mmap(MAP_PRIVATE) of a file (your process’s view of libc is COW over the page cache), memory deduplication in VMs (KSM merges identical guest pages, un-merges on write), Btrfs/ZFS snapshots and cp --reflink (same idea over disk blocks), and fuzzer fork-servers (AFL forks per test case precisely because COW makes each iteration’s memory state disposable). It’s the same trade every time: cheap logical copies, deferred physical cost, metadata tracking who diverged.

Takeaways

  • fork copies page tables and marks pages read-only; the first write to each page pays for a 4 KB copy via the fault handler.
  • Cost is proportional to writes after fork, not process size — which is why fork+exec is cheap and snapshot-fork (Redis) is a memory-spike machine.
  • Refcounting/GC runtimes (CPython) write everywhere and quietly defeat COW sharing in forked workers; audit with Private_Dirty/PSS.
  • After fork: fds share offsets, only the calling thread survives, and frozen locks make fork-without-exec hazardous in threaded programs.
  • COW is a general systems pattern — MAP_PRIVATE, KSM, reflink snapshots, fork-servers — not a fork-specific trick.