Page Faults
When a translation finds no present page, the CPU traps to the kernel, which classifies the fault — minor (page exists or can be made cheaply: lazy allocation, page cache, copy-on-write), major (read from disk or swap), or invalid (SIGSEGV) — fixes the page table, and resumes the same instruction.
The problem
The trap
The page walker reads a PTE with the present bit clear — or one whose permissions forbid the access (a write to a read-only page, an execute of an NX page, a user access to a supervisor page). The instruction cannot complete, so the CPU raises the page-fault exception: it records the faulting virtual address (CR2 on x86, FAR_EL1 on ARM) and an error code saying read/write/execute and user/kernel, saves the user state, and enters the kernel’s fault handler (User Mode vs Kernel Mode). The instruction is *not* finished; it will be restarted from scratch if the handler succeeds.
The kernel now has three facts — the address, the kind of access, the current process — and its own description of the process’s regions. From those it answers one question: is this a page that should exist, and if so where is its content? The rest of the handler is the answer.
- Load from VA 0x7f3a4c1fc010TLB miss → page walk → PTE present = 0↓
- CPU raises #PFCR2 = faulting VA; error code = user read; enter kernel on kernel stack↓
- Kernel: find the regionwhich VMA contains the address? none → invalid↓
- Classifyanonymous & never touched → minor; file-backed & in page cache → minor; on disk / swap → major; CoW write → minor↓
- Allocate or locate the framezero a fresh frame, or copy the shared one, or find the page-cache page, or issue a read and sleep↓
- Update the page tablewrite the PTE: frame, present = 1, permissions; no TLB flush needed for a newly present page↓
- Return: re-execute the same instructionnow the walk succeeds; the program never knew
Classifying the fault
A minor (soft) fault is one the kernel can resolve without waiting for I/O. The page was never touched and the region is anonymous: allocate a zeroed frame. The page belongs to a mapped file whose content is already in the page cache: point the PTE at that frame. The page is shared copy-on-write and the access is a write: allocate a frame, copy 4 kB, point the PTE at the copy (Copy-on-Write). The page was recently reclaimed but not yet reused (it is on the kernel’s inactive list or in the swap cache): just re-map it. All of these are a few hundred nanoseconds to a couple of microseconds of kernel work, and a running process incurs thousands to millions of them without anyone noticing.
A major (hard) fault needs data from a device: a file-mapped page not in the page cache, or an anonymous page that was written to swap. The handler issues the read and puts the process to sleep — it is not runnable until the I/O completes (Process States), and the scheduler runs something else. When the block layer’s completion interrupt fires, the page is filled, the PTE is written, the process is woken, and eventually rescheduled to retry its instruction. The cost is dominated by the device: ~100 µs on an NVMe SSD, 5–10 ms on a spinning disk, plus two context switches and whatever the scheduler queue adds. The kernel usually reads ahead (several pages around the faulting one) so that sequential access takes one major fault and many minor ones.
An invalid fault is an address outside every region, or an access the region’s permissions forbid (writing to code, executing data, the stack’s guard page in Stack Overflow). There is nothing to fix; the handler delivers SIGSEGV (or SIGBUS for a mapped-but-unbackable page) and the default action ends the process. Note that the CPU cannot distinguish the three: from its side every case is "present = 0" or "permission denied". The classification is entirely the kernel’s, made from its region list.
| Kind | Cause | Kernel work | Cost | Process state |
|---|---|---|---|---|
| Minor | First touch, page-cache hit, CoW write, recently reclaimed | Allocate/copy/remap a frame, write PTE | ~0.5–2 µs | Stays running |
| Major | File page not cached; anonymous page in swap | Issue device read, sleep, wake, write PTE | ~100 µs (SSD) – 10 ms (HDD) | Sleeps in D state |
| Invalid | No region; permission violation; guard page | Deliver SIGSEGV / SIGBUS | Process terminates by default | Killed unless a handler exists |
Resume the instruction
The property that makes all of this invisible is precise exceptions: the CPU guarantees that when the handler runs, every instruction before the faulting one has completed and the faulting one has had no effect. The handler changes only the page table; it returns to the *same* instruction pointer, and the load executes again — this time the walk finds a present entry and the value arrives. A store to a copy-on-write page is retried and lands in the private copy. A ten-instruction memcpy inner loop touching a fresh buffer faults once per page and never sees a difference except in the clock.
This is also how the kernel implements things that look impossible. Reading a 10 GB file with mmap and a for loop is a sequence of major and minor faults that the loop never sees (Memory Mapping). A process forked from a 10 GB parent shares every page until its first write to each. A user-space fault handler (userfaultfd on Linux) can even resolve faults from another process — how live migration of a VM copies memory lazily while the guest keeps running.
The numbers, and reading them
A minor fault is roughly a microsecond of kernel time — a trap, a region lookup, a frame allocation, a 4 kB zeroing (the expensive part, ~200–500 ns), a PTE write and the return. Touching a fresh 1 GB region is 262,144 of them: about a quarter of a second, which is why a freshly started process spends its first moments faulting and why pre-faulting exists (What Happens When I Allocate Memory?). A major fault is the device’s latency plus scheduling, so its cost is the I/O domain’s (Follow a File Read) and its rate is the number to watch: a process taking hundreds of major faults per second is being paged in from disk on every step, the first sign of Memory Pressure, Swap and the OOM Killer.
Both are counted per process and system-wide. /usr/bin/time -v prints them after a run; ps -o min_flt,maj_flt, perf stat -e page-faults,major-faults, and sar -B show them live. A program whose minor faults scale with the work it does is normal; one whose major faults are non-zero while the data should be in memory is swapping or has a page cache that is being evicted.
$ /usr/bin/time -v ./touch-1gb 2>&1 | grep -E 'faults|Elapsed' Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.29 Major (requiring I/O) page faults: 0 Minor (reclaiming a frame) page faults: 262401 # one per 4 kB page + a few for libraries $ echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null && /usr/bin/time -v ./grep-mapped-file 2>&1 | grep faults Major (requiring I/O) page faults: 2048 # first touch of each 2 MB readahead window from SSD Minor (reclaiming a frame) page faults: 260353 # the rest were satisfied from the page cache
Key points
- A page fault is a CPU exception raised when the walk finds present = 0 or a permission violation; the kernel, not the CPU, decides what it means.
- Minor: resolved without I/O (first touch, page-cache hit, CoW, recently reclaimed) in ~1 µs. Major: needs a device read, the process sleeps, ~100 µs to 10 ms. Invalid: SIGSEGV.
- The faulting instruction re-executes after the handler; precise exceptions make the whole mechanism invisible to the program.
- Lazy allocation, mmap, fork, swap and live migration are all "leave it not present and handle the fault".
- Touching fresh memory costs ~250 ms per GB in minor faults; pre-fault when latency matters.
- Major faults per second is the first indicator of memory pressure;
/usr/bin/time -v,ps,perf statandsar -Bshow the counts.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why let the fault happen instead of mapping everything up front?
Because most reserved memory is never touched, file pages may never be read, and a forked child rarely writes most of its pages. Paying ~1 µs per page actually used beats paying for every page reserved.
▸Why does a major fault put the process to sleep?
The data is 100 µs to 10 ms away. Spinning would waste a core; sleeping lets the scheduler run something else and the completion interrupt wakes the process when the page is filled.
▸Why can the same instruction be retried safely?
Precise exceptions guarantee it had no effect before the fault. Only the page table changed; the program state is exactly what it was, so re-executing is equivalent to having executed once with the page present.
▸Why is a segfault just another page fault?
The CPU reports every not-present or forbidden access the same way. Only the kernel’s region list distinguishes "not yet allocated" from "never allocated"; the latter gets a signal instead of a frame.
Page fault
- Instruction: mov rax, [addr]user mode↓
- MMU: PTE not present (or perm fails)hardware↓
- CPU trap #14, saves faulting addressuser → kernel↓
- Kernel page-fault handlerfind the VMA↓
- Classify: file-backed VMA, page already in the page cache↓
- point the PTE at the cached frameno I/O↓
- Update page table entry, flush TLB entry↓
- Return to user mode, re-execute the instructionit succeeds this time
How it fails
What the failure looks like from inside real software.
- Hundreds of major faults per second on a process whose data should fit in RAM: the page cache is being evicted or the process is swapping — memory pressure, not a code bug.
- A latency spike on the first request after deploy: first-touch faults on every buffer and code page; warm-up or
MAP_POPULATEfixes it. - A memory-mapped file on a network file system: a "memory access" stalls for the network RTT inside a major fault, and the thread shows as D state with no syscall in progress.
SIGBUSon a memory-mapped file that another process truncated: the page is inside the region but has no backing.- A process copying a large buffer under copy-on-write after fork: one minor fault plus a 4 kB copy per page, doubling memory; visible as the child’s RSS climbing towards the parent’s.