Does fork Really Copy Memory? — Copy-on-Write in Practice
The previous post called process creation expensive. But something doesn’t add up: the Unix way to create a process is fork() — cloning the current process wholesale — yet a process using gigabytes forks in a blink. Copying gigabytes in milliseconds? The answer is that the copy is announced but not performed. The trick is copy-on-write (COW), the core sleight of hand in the Linux process model.
What fork means — every process starts as a clone #
Call fork() and a child process appears, almost identical to the parent: same code, same variable values, same open files. Only the return value differs (the parent gets the child’s PID, the child gets 0), which is how each knows who it is. To run a different program, the child follows with exec(), replacing its own memory image with the new program. A shell launching a command is exactly this fork + exec pair.
This raises the question: if the child will exec anyway, copying the parent’s memory is pure waste, isn’t it? Correct — which is why no copy actually happens.
Copy-on-write — the writer pays #
Process memory is managed in pages (typically 4KB), and each process holds a page table — the map from virtual addresses to physical memory. What fork actually copies is the page table, not the memory.
- Right after fork, parent and child page tables point at the same physical pages. Only the map was copied — that’s why forking a gigabyte process is fast.
- In exchange, the kernel marks the shared pages write-protected on both sides.
- The moment either side writes to such a page, the CPU faults, and the kernel copies just that one page for the writer. From then on the two diverge — for that page only.
So copying happens not “all at fork time” but “only the pages that get modified, at the moment they’re modified.” It also makes fork + exec obviously cheap: if the child execs without touching anything, the copying never happens at all.
Where you run into COW in production #
COW isn’t kernel trivia; it surfaces across server operations.
- Redis snapshots (RDB saves) — Redis forks at save time and the child slowly writes that instant’s memory image to disk. COW makes the fork immediate, and the parent keeps serving. But every page the parent modifies during the save gets copied — memory usage grows by exactly that much. The Redis operating lore that “memory can nearly double during a save” is COW’s invoice.
- Pre-fork servers — gunicorn, uWSGI and friends load the application in a master, then fork workers. Workers physically share code and library pages, so ten workers cost far less than ten times the memory. As workers start modifying their own data, sharing decays and usage climbs. Runtimes that write reference counts into every object — Python, famously — dirty pages just by reading, eroding the benefit.
- Container image layers — filesystem-level, but the same idea: overlay filesystems share image layers and copy-up only modified files. “Copy when written,” again.
Why memory math gets confusing — and overcommit #
COW’s side effect is making “how much memory does this process use?” a hard question. Summing ps RSS across ten workers counts shared pages ten times. Use sharing-aware metrics — PSS via smem or /proc/<pid>/smaps_rollup — to get close to the truth. One common cause of the “numbers don’t add up” situation in the memory post is exactly this double counting.
One more: since fork “promises now, copies later,” the kernel can promise more than physical memory exists (overcommit). An 8GB process forking creates a theoretical 16GB of promises while actually using 8GB plus change. Systems with overcommit disabled (vm.overcommit_memory=2) can see large processes fail to fork for lack of memory — and this is why the Redis documentation recommends allowing overcommit.
Summary #
- fork clones a whole process, but only the page table is copied. Physical pages are shared, write-protected.
- Copying happens per page, at modification time (copy-on-write) — which makes fork + exec nearly free.
- Redis save-time memory growth and pre-fork servers’ memory savings (and their decay) are direct consequences of COW.
- Summing worker RSS double-counts shared pages; PSS is the honest number.
- fork separates promise (virtual) from use (physical). Overcommit settings and fork failures of large processes fall out of that separation.
Next: the most misread number in Linux — load average.