Copy-on-Write
After fork the parent and child share every page read-only; the first write to a page faults, the kernel copies just that page, and the two processes diverge one page at a time — so forking a 10 GB process is cheap until someone writes, which is exactly the property Redis snapshots and process spawning depend on.
The problem
fork() must give the child an exact copy of a 10 GB parent, and it must return in microseconds, and the child will probably call exec and throw the copy away — or write to 1% of it. Copying 10 GB is 2–3 seconds. How can a copy be both complete and free?Fork without copying
The insight is that a copy is only observable when one side writes. Until then, two processes reading the same bytes might as well read the same frames. So fork copies the page tables, not the pages: the child gets its own tables whose entries point at the parent’s frames, every writable private page is marked read-only in *both* tables, and each frame’s reference count is incremented. The child is a complete, independent process — its own PID, its own address space in the sense that matters (Creating Processes: fork, exec, wait) — that happens to share all of its physical memory with its parent.
The cost of the fork is therefore proportional to the *size of the page tables*, not to the memory: about 2 MB of tables per GB of 4 kB mappings, which the kernel walks and duplicates. A 10 GB process forks in tens of milliseconds rather than seconds; a 100 MB process forks in well under a millisecond. Pages that were already read-only (code, shared libraries) need no marking and are shared exactly as they were before.
The write, the fault, the copy
Now the child stores to a heap page. The PTE says read-only; the CPU raises a page fault with "write, user" in the error code (Page Faults). The handler finds the region and sees it is *supposed* to be writable — this is not a permission violation but a CoW page. If the frame’s refcount is greater than one, it allocates a new frame, copies 4 kB into it, points the child’s PTE at the copy with write permission, decrements the original frame’s refcount, and returns; the store re-executes into the private copy. If the refcount was already one (the other side has since copied or exited), it simply flips the PTE to writable — no copy at all. Either way the parent’s view is untouched.
The divergence is incremental and lazy. A child that touches 1% of its pages has 1% of the memory copied; a child that immediately calls exec copies nothing, because exec discards the whole address space before any write. That was the case the design targeted: the shell forks and execs a thousand times a minute, and each fork must cost microseconds. vfork and posix_spawn go further by not even copying the page tables (the child borrows the parent’s memory until exec), and are what modern runtimes use to spawn processes — Python’s subprocess uses vfork or posix_spawn where it can since 3.8/3.11.
- Child: store to VA 0x5a1000PTE: frame 81, present, read-only↓
- CPU: page fault (write to RO page)CR2 = 0x5a1000, error = write | user↓
- Kernel: region is writable → CoWframe 81 refcount = 2↓
- Allocate frame 4410, copy 4 kB~1 µs including the copy↓
- Child PTE → frame 4410, writable; frame 81 refcount → 1parent PTE unchanged: still frame 81↓
- Resume the storechild writes its private page; parent never sees it
Uses: process creation, snapshots, and old strings
Process creation is the origin: every fork on every Unix since the 1980s. Snapshots are the modern star. Redis’s BGSAVE forks; the child walks the *entire* dataset and writes it to the RDB file while the parent keeps serving writes, and copy-on-write guarantees the child sees a consistent point-in-time image at zero cost for pages the parent does not touch. The same trick gives a forking database a consistent snapshot for a backup, a test framework a pristine process state to reset to, and Android its zygote: one warmed-up JVM process forked for every app so that framework classes are shared across all of them.
The kernel uses it internally too: the zero page is a single all-zero frame mapped read-only into every untouched anonymous page, so reading fresh memory costs nothing and the first *write* allocates. MAP_PRIVATE file mappings (Memory Mapping) — every executable’s data segment — are CoW against the page cache. KSM (kernel same-page merging) finds identical pages across VMs and merges them CoW to overcommit hypervisor RAM.
And a historical note from user space: before C++11, libstdc++’s std::string was copy-on-write — copies shared a buffer with a reference count and copied on the first mutating call. C++11 effectively banned it (a [] on one copy must not invalidate references in another, and the refcount was a data race under threads), and modern strings copy eagerly with a small-string optimisation instead. Persistent data structures in functional languages, Git’s object store and btrfs/ZFS snapshots are the same idea applied to trees and blocks rather than pages.
The cost model, and where it bites
A fork costs the page-table copy (tens of ms for 10 GB) plus, afterwards, one minor fault and one 4 kB copy — roughly a microsecond — per page written by *either* side. That last word is the trap. In the Redis case the *parent* is the one writing: a write-heavy workload touches many pages during the seconds the child takes to serialise, and each touch copies a page, so memory usage climbs towards double the dataset and the parent’s latency carries a fault per first-touched page. With transparent huge pages enabled the unit is 2 MB, so a single 8-byte write copies half a megabyte; Redis’s documentation tells you to disable THP for exactly this reason, and reports latest_fork_usec so you can see the page-table cost.
Fork in a multi-threaded process is a separate hazard: only the calling thread exists in the child, and any lock another thread held at that instant is held forever in the child — including the allocator’s. That is why fork followed by anything other than exec is unsafe in threaded programs, why Python 3.12 warns about it, and why runtimes moved to posix_spawn. Finally, CoW makes memory accounting non-obvious: two processes sharing 10 GB each report 10 GB RSS, and the machine may or may not be able to afford both writing.
127.0.0.1:6379> INFO persistence rdb_bgsave_in_progress:1 rdb_last_cow_size:1543503872 # 1.4 GB of pages copied because the parent wrote during the save rdb_last_bgsave_time_sec:34 127.0.0.1:6379> INFO stats latest_fork_usec:182341 # 182 ms: copying the page tables of a ~9 GB process
Key points
- fork copies page tables, not pages: shared frames, marked read-only in both, reference-counted.
- The first write to a shared page faults; the kernel copies that one page and gives the writer a private frame. Unwritten pages are never copied.
- Fork cost ∝ page-table size (~2 MB per GB mapped); divergence cost ∝ pages written by either side (~1 µs + 4 kB each).
- Uses: process spawning, Redis BGSAVE and other fork-based snapshots, the zero page, MAP_PRIVATE file mappings, Android zygote, KSM.
- Transparent huge pages turn a 4 kB copy into a 2 MB one; write-heavy parents can double memory during a snapshot.
- fork in a multi-threaded process inherits locked locks; use posix_spawn or vfork+exec for spawning.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why not copy the memory at fork?
Because the child usually execs immediately, and even when it does not, it writes to a small fraction of its pages. Copying lazily makes the common case free and the worst case no worse than an eager copy.
▸Why is the shared page marked read-only in the parent too?
Because the parent writing would be just as visible to the child as the reverse. Both sides must fault on first write so that whichever writes first gets the private copy.
▸Why does Redis fork to save instead of just walking the data?
Walking the data in the serving process would block writes for seconds or require a consistent-snapshot mechanism in user space. The kernel already has one — copy-on-write — and fork invokes it in one call.
▸Why does memory usage grow during a BGSAVE?
Every page the parent writes while the child still references the old version is copied. Under heavy writes that approaches the whole dataset; with huge pages, faster.
Copy-on-write
How it fails
What the failure looks like from inside real software.
- Redis on a THP-enabled host:
latest_fork_usecfine, but memory doubles during BGSAVE and p99 latency spikes as each write copies 2 MB — disable THP. - A Python web worker forks after threads started: the child deadlocks in
mallocon the first allocation because another thread held the allocator lock at fork time. - A 30 GB process forks to spawn a tiny helper: 60 ms stall copying page tables on every spawn;
posix_spawnremoved it. - Two forked workers "share" 8 GB and the operator sizes RAM for 8; both write, both diverge, and the OOM killer arrives at 16.
- A snapshot child holds pre-fork pages alive for minutes; the parent’s freed memory cannot be returned to the OS until the child exits.