The Runtime Theory
KernelDSAmemory

What happens when the CPU can't find a page?

A step-by-step walk from a TLB miss and hardware page-table walk, through minor and major faults, copy-on-write, and swap-in, to the re-executed instruction.

The Runtime Theory Team3 min read07 steps

layer stack

Kernel

HWHardware
KKernel
RTRuntime
APPApplication
SYSSystem
CLIClient
NETNetwork
TLSCrypto
SRVServer

trace spine

  1. 01 TLB miss and hardware page-table walk
  2. 02 Page fault exception raised
  3. 03 VMA lookup and access check
  4. 04 Minor fault: zero page or page cache hit
  5. 05 Major fault: disk read or swap-in
  6. 06 COW: copy-on-write fault on a shared page
  7. 07 PTE installed, TLB filled, instruction re-executed
On this page

Every process on a modern OS runs on a fiction: it owns the whole address space, all 48 bits of it. The MMU enforces that fiction, and the page fault is the moment fiction and physics meet — when the CPU wants a virtual page that has no physical page behind it.

Step 1 — TLB miss and the hardware walk

Your code does mov (%rbx), %eax. The CPU checks the TLB (translation lookaside buffer, ~1-5ns on hit). On a miss, the hardware walks the page table: 4 levels for a 48-bit address (PML4 → PDPT → PD → PT → PTE), each level a memory load that usually hits L2/L3. That walk costs ~20-100ns on cache hits.

Step 2 — the exception

The walk finds a PTE whose present bit is 0. The MMU can't resolve it, so it raises a page fault: on x86, the CPU pushes the faulting address into CR2 and a fault error code onto the stack, then jumps to the kernel's page_fault handler. This is an exception, not an interrupt — the kernel knows exactly which instruction caused it.

Step 3 — VMA lookup and access check

do_page_faulthandle_mm_faultfind_vma(), a lookup in the process's VMA (virtual memory area) red-black tree: is this address mapped at all? Is the access type legal (read on read-only, write on writable)? Three outcomes:

  • Address in no VMA → SIGSEGV.
  • Write to a read-only, non-COW VMA → SIGSEGV.
  • Otherwise → we have work to do.

Note the lock: mmap_lock (formerly mmap_sem) is taken for the VMA lookup, which is why multi-threaded mmap churn under a page-fault storm becomes a bottleneck.

Step 4 — minor fault

The page exists conceptually but not physically. For anonymous memory, the kernel maps the shared zero page first (read-only), then on first write does a COW fault (step 6) that allocates a real, zeroed page. For file-backed memory, the page may already be in the page cache — the kernel wires it into the PTE. Cost: ~0.5-2µs, zero I/O. This is why "minor faults" are mostly harmless — they're the steady-state of lazily-mapped memory.

Step 5 — major fault

The page isn't anywhere in RAM. For a file mapping: allocate a page, submit a bio to the block layer (with readahead — the kernel speculatively reads the next 16-32 pages), wait on the I/O completion (interrupt-driven wakeup), then wire the page in. For anonymous memory swapped out: find the swap slot in the swap cache, read from the swap device, insert the page into the LRU. Cost: 100µs to 10ms — orders of magnitude more than the fault machinery itself. The process sleeps on the page lock while disk works.

Step 6 — copy-on-write

A write fault on a page shared via COW (from fork(), or the zero page): the kernel checks whether the PTE has the COW bit (dirty/accessed bookkeeping + _PAGE_COW on x86-64). If the page has exactly one reference, the kernel just flips it writable — the "refcount one" fast path. Otherwise: allocate a new page, copy 4KB (or 2MB for THP), install a private writable PTE, drop the old reference. Cost: ~1-2µs plus the copy — the price of every fork()-and-write.

Step 7 — return and re-execute

The handler returns via iret; the CPU re-executes the exact instruction that faulted — now it finds the PTE present and the TLB is filled by the walk. To the process, nothing happened at all: no error, no return value, just a few hundred nanoseconds (or milliseconds) of invisible time.

What it costs

  • Minor fault: ~0.5-2µs (zeroing, PTE install, TLB fill).
  • COW fault: ~1-3µs, dominated by the 4KB copy.
  • Major fault: 100µs-10ms — disk latency dominates.
  • Fault storms: mmap/munmap churn serializes on mmap_lock; a single thread faulting in a 1GB map can starve other threads' address-space operations.
bash
$ /usr/bin/time -v ./cold-start 2>&1 | grep -E "faults"
	Minor (reclaiming a frame) page faults: 84,311
	Major (requiring I/O) page faults: 12

84k minor faults at ~1µs each ≈ 84ms of invisible time; the 12 major faults could be anything from 1ms to a full disk seek. The rule of thumb: minor faults are noise, major faults are the thing you should be optimizing away.