Photo by Brian Kostiuk on Unsplash
Copy-on-Write in Memory Management: How Linux and BSD Optimize Fork Performance
When a Unix process calls fork(), it creates a complete copy of itself—or at least, that’s the abstraction. In reality, modern operating systems use copy-on-write (CoW) to avoid duplicating gigabytes of memory unnecessarily. This optimization transforms what would be an expensive operation into something that completes in microseconds.
The Problem Fork Presents
The classic fork semantics promise that the child process receives an identical copy of the parent’s address space. A naive implementation would walk through every page of memory the parent owns and duplicate it. For a process with 4GB of allocated memory, that means copying 4GB before the child can begin execution.
Most forked processes immediately call exec() to replace their memory with a new program, making all that copying wasted effort. Even when the child doesn’t exec, parent and child often read far more memory than they write, making full duplication unnecessary.
Copy-on-Write Mechanics
Instead of copying memory, the kernel marks all pages in both parent and child as read-only and shares the underlying physical memory. Both processes point to the same page frames. The page tables are updated with shared mappings, but the actual data remains in place.
When either process attempts to write to a page, the CPU triggers a page fault because the page is marked read-only. The kernel’s fault handler recognizes this as a CoW fault, allocates a new physical page, copies the original page’s contents, updates the writing process’s page table to point to the new page with write permissions, and resumes execution.
From the application’s perspective, each process has independent memory. From the kernel’s perspective, physical memory is only allocated and copied when actually needed.
Reference Counting and Shared Pages
The kernel maintains reference counts for each physical page frame. When fork creates shared mappings, it increments the reference count. When a process writes and triggers CoW, the reference count decrements for the original page and a new page with a reference count of one is allocated.
When a process exits, the kernel walks its page tables and decrements reference counts. Only when a count reaches zero does the physical memory get freed. This allows arbitrary sharing patterns—a page might be shared among several processes created by multiple fork calls.
Performance Implications
CoW fork enables the common Unix pattern where a server forks worker processes. Redis uses this technique for background persistence: the main process continues serving requests while a forked child writes a snapshot to disk. Because most of Redis’s memory is read-only during the snapshot, very little actual copying occurs.
The Python multiprocessing module and similar frameworks benefit similarly. Large read-only data structures—trained models, lookup tables, configuration—remain shared across worker processes until one modifies its copy.
The overhead shifts from upfront copying to lazy, on-demand copying. A process that forks and immediately execs pays almost nothing. A process that forks and both parent and child heavily modify memory pays the full copying cost, spread across many page faults, plus the overhead of handling those faults.
Transparent Huge Pages and CoW
Modern systems support huge pages (2MB or 1GB instead of 4KB) to reduce TLB pressure. When CoW interacts with huge pages, the granularity matters. A write to a single byte in a 2MB huge page might trigger copying the entire 2MB, depending on the kernel’s huge page handling.
Linux can split huge pages on CoW faults, breaking them into normal 4KB pages. This preserves memory efficiency but sacrifices the TLB benefits. The tradeoff between memory overhead and TLB performance makes huge pages with CoW a tuning challenge for workloads with mixed read/write patterns after fork.
Beyond Fork
CoW appears throughout memory management. Memory-mapped files marked MAP_PRIVATE use CoW—multiple processes can map the same file, and modifications create private copies. Container runtimes layer filesystem images using CoW mechanisms. Version control systems like Git use CoW principles for object storage.
The pattern is universal: share until you can’t, then copy only what diverges. Copy-on-write turns an expensive operation into a lazy one, deferring costs until they’re unavoidable and eliminating them entirely when they’re not.