JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Copy-on-Write and How fork Stays Cheap

fork() seems to duplicate a whole process — every byte of its memory — in an instant. If that were literal, copying gigabytes would make it crawl. This guide reveals the trick: parent and child share the same physical pages, marked read-only, and a page is copied only the moment someone writes to it. It is the page-fault machinery from the last guide, turned to a different and beautiful purpose.

The problem fork() seems to create

Back in the Processes rung you met fork(), the system call that creates a new process by cloning the one that calls it. The child gets its own copy of everything: the same code, the same open file descriptors, and — the part we care about here — its own private address space, a full duplicate of the parent's memory. After the fork the two are independent: a variable the child changes does not change in the parent, and vice versa. That independence is the whole point, and it is real.

But read that literally and it sounds ruinously expensive. Suppose the parent is a database using 4 GiB of memory. A naive fork() would have to find 4 GiB of free physical memory, then copy all 4 GiB, byte for byte, before the child could run a single instruction. That copy alone might take a noticeable fraction of a second — an eternity for an operation programs do constantly. Worse, it is usually wasted: the most common thing a child does right after fork() is immediately call exec() to replace its whole memory image with a different program. You would copy 4 GiB only to throw it away microseconds later.

So the kernel faces a tension. The child must behave as if it has a complete, private, independent copy of the parent's memory — that is what the program expects and relies on. Yet actually making that copy up front is slow and almost always wasted. The resolution is one of the most elegant tricks in operating systems, and it is the subject of this guide: give the child a copy that is logical immediately but physical only on demand.

Share first, copy later

Here is the key observation that makes the trick work. Two copies of identical data are indistinguishable as long as nobody changes either one. If the parent and child both only read a page, they cannot tell whether they are reading their own private copy or a single shared original — the bytes are the same either way. The only moment a separate copy actually matters is when one of them writes, because that is the first instant their two versions would diverge. So why copy before that moment? Why not share a single physical copy right up until the write that forces them apart?

That is exactly what copy-on-write does. When fork() runs, the kernel does not copy the parent's pages. Instead it makes the child's page table point at the very same physical frames the parent is already using. Now both processes' page tables map their pages to one shared set of frames. The copy is, at this instant, purely an entry in two page tables — a few thousand small mappings, not gigabytes of data. This is why fork() returns almost instantly no matter how large the parent is: it copies page tables, never page contents.

But sharing raises an obvious danger. If both page tables point at the same frame and the parent writes to it, the child would see the change too — its supposedly private copy would be corrupted, and the independence we promised would be a lie. We need a way to intercept the very first write to any shared page, so we can quietly make a real copy before the write lands. The mechanism for that is sitting right here in the virtual-memory rung, in the page-table entry bits.

The read-only trap

Recall from guide 2 that every page-table entry carries permission bits, and one of them is the writable bit. Recall from guide 3 that an access the MMU finds illegal — including a write to a read-only page — raises a page fault that hands control to the kernel. Copy-on-write splices those two facts together. When fork() shares the pages, it marks every shared page-table entry read-only, in both the parent's and the child's tables — even though the data is logically writable for each of them. Reads sail straight through and stay shared, for free. But the first write, from either side, hits that read-only bit.

That write triggers a protection page fault, and now the kernel gets its chance. It looks at the faulting page and recognizes the situation: this is not a real permission error, it is a shared copy-on-write page that someone is finally trying to modify. So it performs the copy it deferred — but for one page only. It allocates a fresh physical frame, copies the contents of the shared page into it, repoints the writing process's page-table entry at the new private frame, sets that entry back to writable, and restarts the instruction that faulted. The write now lands on the process's own private page. The other process still maps the original frame, untouched. They have diverged, exactly at the page that needed it, and not one byte sooner.

fork():
    parent page  ->  [ frame 0x9000 ]  <-  child page    (both READ-ONLY)

child writes to its page:
    write -> read-only PTE -> protection page fault -> kernel
    kernel copies frame 0x9000 into a fresh frame 0xA000

after the fault is serviced:
    parent page  ->  [ frame 0x9000 ]   (READ-ONLY, still shared with nobody now)
    child page   ->  [ frame 0xA000 ]   (WRITABLE, private copy)
One shared page becoming two private ones, on the first write. Only this page is copied; every page neither side wrote stays shared at frame-level.

Why this makes fork() cheap

Now the payoff is clear. The cost of fork() no longer scales with how much memory the parent uses — it scales with how much the parent and child change afterward. Copy 4 GiB of page tables' worth of mappings, mark everything read-only, and return: that is fast and constant-ish regardless of the 4 GiB behind it. From then on the price is paid one page at a time, only for pages actually written, and only by whichever side writes them. A child that reads a lot but writes little costs almost nothing. This is the same lazy spirit as demand paging from the last guide: do no work until a fault proves the work is genuinely needed.

The win is biggest for the most common pattern of all: fork-then-exec. When a shell runs a command, it forks and the child immediately calls exec(), which discards the entire inherited address space and loads a new program. With copy-on-write, the child between fork() and exec() barely touches any inherited pages — so almost nothing is ever copied. The kernel set up a few thousand read-only mappings, the child wrote to a handful of them (perhaps a stack page or two for the exec() call), and then exec() threw all the mappings away. A 4 GiB parent forked and exec'd a new program having copied only a trivial handful of pages. That is why launching a process on Unix is fast even from a huge parent.

Seeing it, and where it shows up

You can watch copy-on-write happen in the fault counters. Recall from guide 3 the split between a minor fault (resolved in memory) and a major fault (needs disk). A copy-on-write fault is always a minor fault: the page is already present in RAM, the kernel only has to allocate a frame and copy one page, no disk involved. So a process that forks and then dirties many shared pages will show a spike in minor faults but no major faults. Tools like `/usr/bin/time -v ./a.out` report "minor (reclaiming a frame) page faults", and a fork-heavy or write-after-fork-heavy program lights that number up.

Walk through what happens to one ordinary global variable across a fork, to make it concrete.

  1. Before fork(), the parent has an int counter living in some page; its page-table entry is normal and writable, mapping to physical frame 0x9000.
  2. fork() runs. The kernel copies the page table into the child and marks that page read-only in both processes. counter now lives in one shared frame, 0x9000, mapped read-only by parent and child alike — no data copied.
  3. The child does counter = counter + 1. The write hits the read-only entry, the MMU raises a protection page fault, and the kernel takes over.
  4. The kernel allocates a fresh frame 0xA000, copies the page (including counter's current value) into it, repoints the child's entry at 0xA000 as writable, and restarts the add. The child's counter increments in its private page; the parent's counter, still at 0x9000, is unchanged.
  5. Every other page the child never wrote stays shared at frame level forever — or until exec() discards the whole mapping. The promised independence held, paid for one page at a time.

Copy-on-write is bigger than fork(). The same read-only-share-then-copy idea backs shared zero-pages (a freshly allocated region maps every page to one shared all-zero frame until you write), and it reappears the moment you meet mmap() in the next guide, where a file or region can be mapped with private copy-on-write semantics. And it lives outside the kernel too: many language runtimes and data structures use copy-on-write to share immutable-looking objects cheaply and split them only on mutation. Once you see the pattern — share while read-only, trap the first write, copy just that piece — you will spot it everywhere.

Where this leaves you

You now understand the trick that lets a process clone itself without paying to copy its memory. fork() gives the child a copy that is logical at once but physical only on demand: the kernel shares the parent's frames, marks every shared page-table entry read-only, and lets the first write trap into a page fault that copies exactly one page into a private frame and restarts the write. The cost scales with what changes, not with what exists — which is precisely why fork() stays cheap, and why fork-then-exec launches a fresh program in a flash even from an enormous parent.

Notice the pattern across this whole rung: nearly every feature is one combination of page-table-entry bits plus a fault handler. Demand paging is a not-present bit; memory protection is the writable and no-execute bits; copy-on-write is a present-but-read-only bit. The MMU enforces the bits, the fault hands control to the OS, and the OS decides what the fault means. The final guide turns this same machinery outward, letting your program ask for mappings on purpose with mmap() — including the copy-on-write kind you just learned to recognize.