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

Copy-on-Write and Memory-Mapped Files

The page fault machinery you have just built is not only a safety net for memory that does not fit. Turn it around and it becomes a creative tool: two processes can share one copy of memory until the instant someone writes, and a file on disk can become a region you simply read and write like an array. This guide shows how the same trap powers both tricks, and where the swap space behind it all lives.

The fault as a tool, not a problem

So far in this rung the page fault has played the role of an accident: a process touched a page that was not in RAM, the hardware trapped into the kernel, and the kernel scrambled to fetch the missing page and restart the instruction. That was guide 3's whole story. But step back and notice what the fault really is — a programmable hook that fires precisely when a process touches a particular page. The kernel gets to decide, on the spot, what that touch should mean.

That reframing is the key to this whole guide. Once you see the fault as a doorbell the kernel can answer however it likes, two beautiful tricks fall out. First, the kernel can let many processes share one physical copy of a page and only split them apart on the first write — that is copy-on-write. Second, the kernel can map a file on disk straight into a process's address space, so that reading and writing memory quietly reads and writes the file — that is a memory-mapped file. Both are built from nothing more than the valid bit, the protection bits in the page table, and the fault handler you already understand.

Copy-on-write: cheap fork

Recall fork() from the processes rung: it creates a child process that is an almost-exact duplicate of its parent, with its own copy of the entire address space. Taken literally, that is alarmingly expensive. If the parent uses 500 MB, fork would have to copy 500 MB of pages before the child runs even one instruction. Worse, the very common pattern is fork immediately followed by exec(), which throws away that whole fresh copy and loads a brand-new program. You would have copied half a gigabyte just to discard it microseconds later. That is absurd, and copy-on-write is the fix.

The idea is to lie, cleverly and safely. When fork runs, the kernel does not copy any data pages at all. Instead it gives the child a page table that points at the very same physical frames the parent uses — they become shared pages — and it marks every one of those entries read-only in both processes. Now parent and child are looking at one physical copy of memory. As long as they only read, that is perfectly correct: reads do not change anything, so sharing is invisible. The expensive copy has simply not happened.

The magic happens on the first write. Suppose the child tries to store a value into a shared page. The page is marked read-only, so the hardware raises a protection fault — the same trap machinery as a page fault, just triggered by a forbidden write rather than a missing page. The kernel's handler recognizes this is a copy-on-write page, not a real error. It makes a fresh copy of just that one page into a new free frame, updates the writing process's page table to point at the private copy, marks both copies writable again, and restarts the instruction. The write now lands on the process's own page; the other process keeps the original. Only the pages that are actually modified ever get copied — so a fork-then-exec costs almost nothing, because exec replaces the address space before more than a handful of pages are ever written. This is why fork is cheap on every modern Unix-like system: the cost has been deferred to the moment of writing, and often that moment never comes.

After fork(), before any write:

  parent page table --\
                       >--> [ frame 7 ]  (read-only, shared)
  child  page table --/

Child writes to that page -> protection fault -> COW copy:

  parent page table -----> [ frame 7 ]  (writable again)
  child  page table -----> [ frame 9 ]  (private copy, writable)
Before the write both tables point at one frame; the first write splits off a private copy and leaves the other process untouched.

Memory-mapped files: a file that is just an array

Now point the same machinery at a file. Normally you read a file with explicit calls like read(fd, buf, n), which copy bytes from the kernel's disk buffers into your own buffer, and write them back the same way. A memory-mapped file skips all that ceremony. With one call (the classic name is mmap), you ask the kernel to map a region of a file directly into your address space. From then on, the file simply is a stretch of memory: byte 0 of the file is at some address, byte 100 is a hundred bytes further along, and you read or modify the file by reading or modifying those addresses, exactly as if it were an array in memory.

How does the kernel pull this off without reading the whole file up front? Exactly the way demand paging handles any other address. At map time the kernel sets up the page table for that region but marks the entries invalid — no frames are loaded yet. The first time your code touches a byte in the mapping, you take a page fault; the handler sees the page belongs to a mapped file, reads that one page in from disk, fixes the entry, and restarts your instruction. You read only the pages you actually touch, on demand, with the very same fault path you have been studying all rung.

Writes work in reverse, and here the dirty bit earns its keep. When you store into a mapped page, the hardware sets that page's dirty bit, marking it as modified. The kernel does not rush the change to disk; it lets your dirty pages accumulate and writes them back lazily — when memory is needed, when you ask explicitly, or when you unmap the region. Clean pages, never modified, need no write-back at all, so the dirty bit lets the kernel skip pointless disk I/O. This is also why two processes mapping the same file can share it: they map the same frames, and a change one makes becomes visible to the other, because there is genuinely one copy in memory.

Where the evicted pages go: swap space

All of this raises a question we have dodged until now. When RAM is full and the page replacement you met in guide 4 picks a victim to evict, where does that victim go? If the page came from a mapped file, the answer is easy: write it back to its file on disk, or, if it is clean, just drop it. But what about an ordinary writable page — a piece of stack, heap, or a copy-on-write page that has been privately copied? It has no file to go home to. It needs a holding area on disk reserved for exactly this purpose. That area is swap space.

Swap space is a region of disk — a dedicated partition or a file — that the kernel uses as an overflow extension of RAM. When such a page is evicted, the kernel performs a page-out: it copies the page's contents into a free slot in swap and records, in that page's table entry, where on disk it now lives. The page frame is then free for someone else. Later, if the process touches that page again, it faults; the handler sees the entry is invalid but tagged with a swap location, reads the page back from swap into a fresh frame, and restarts the instruction. It is demand paging again — the backing store is just swap instead of a file.

  1. RAM fills up and page replacement chooses a victim page to make room.
  2. If the victim is clean (its dirty bit is unset), simply discard it — an identical copy already exists on disk or in its mapped file.
  3. If the victim is dirty and has a backing file, write it back to that file; if it is anonymous (stack, heap, copied COW page), page it out to swap space.
  4. Record the new disk location in the page's table entry and mark the entry invalid, so a later access will fault and bring the page back.

Swap space is also what makes overcommit possible — and what makes it dangerous. The kernel can hand out far more virtual memory than the RAM it owns, betting that processes will not all touch all of it at once; copy-on-write and lazy mapping make that bet usually safe. But if the bet goes wrong and everyone touches their pages, the system leans hard on swap, and because disk is thousands of times slower than RAM, it can collapse into thrashing — spending nearly all its time paging in and out rather than computing. Swap lets you run more than fits, but it cannot make disk as fast as memory, and over-leaning on it is how a busy machine grinds to a crawl.

Pulling the rung together

Look at how little new machinery this guide needed. Copy-on-write, memory-mapped files, and swapping in from disk are all the same idea wearing different clothes: a page table entry can be marked invalid or read-only, a touch then traps into the kernel, and the kernel decides what that touch should do — copy a page, read a page from a file, read a page from swap. The single mechanism from guide 2 and guide 3, the fault-and-restart loop resting on the valid bit of the page table entry, is doing all the work.

And all of it works only because of the truths the rest of this rung established. It is correct to defer a copy or a load because the instruction is restartable — the faulting access can be retried with no harm done. It is affordable because of locality of reference and the working set: a process touches only a small, slowly-shifting handful of its pages at any moment, so the faults that fetch the rest are rare. Pull locality away and every one of these clever tricks would fault on nearly every access, and the whole edifice would thrash itself to death.

That is the deep lesson of this whole rung. Virtual memory is not a trick for going faster — every fault is pure overhead, and a thrashing system is dramatically slower than one with enough RAM. What virtual memory buys is possibility: programs larger than RAM that run anyway, forks that cost almost nothing, files you treat as memory, and dozens of processes coexisting in the illusion that each owns the whole machine. The page fault you first met as an emergency turns out to be the quiet engine behind all of it.