Page Faults

Every modern operating system runs programs whose combined memory demands exceed physical RAM. Yet those programs never notice. The trick is deception: touch a piece of memory that is not actually resident, and the hardware quietly stops you. The operating system fetches what you need, and your instruction runs again as if nothing happened. This deception is the page fault. It is arguably the single most important interrupt in a modern computer. It is the mechanism that makes Demand Paging and Virtual Memory possible at all. A page fault is not an error in the everyday sense. It is a planned, expected event — a controlled trap deliberately engineered into the hardware-software boundary. Every time you allocate memory with `malloc`, load a shared library, `mmap` a file, or start a program whose code has never been touched, page faults are firing in the background. They stay invisible unless you go looking with tools like `perf` or `vmstat`. Understanding exactly what happens during a fault matters greatly. This includes the trap sequence, the six-step service routine, and the delicate matter of resuming an interrupted instruction. That understanding is the difference between treating virtual memory as magic and understanding it as an engineered system. That system has measurable, tunable performance. This article dissects the page-fault mechanism end to end. We start from The Page Table entry that triggers the trap. We walk through the full servicing algorithm the kernel executes. Then we confront the surprisingly subtle problem of restarting an instruction that failed partway through execution. Think of an instruction that touches two pages, or one that has already modified a register. We then derive the effective access time formula. It lets you reason quantitatively about how catastrophic even a small fault rate can be. We also work a full numeric example. By the end you will be able to trace a page fault from the failed memory access through kernel intervention back to user code. You will be able to explain why some architectures need extra hardware support just to make restart possible. You will also be able to compute — not just recite — the performance cost of paging. This groundwork sets up Page Replacement Algorithms, which decide which page to evict when memory is full. It also sets up Memory-Mapped Files, which turn the page-fault mechanism into a general I/O technique.

Key takeaways
  • A page fault is a hardware trap raised when a memory reference maps to a page-table entry marked "not present," or violates protection bits.
  • Servicing a fault is a well-defined kernel routine: validate the address, locate/allocate a frame, read the page in from backing store, update the page table, resume.
  • The faulting instruction must be restarted, not resumed mid-way — this requires care on architectures where an instruction can touch multiple memory operands.
  • Effective Access Time (EAT) = `(1 − p) × memory_access_time + p × page_fault_service_time`, where `p` is the fault rate.
  • Because `page_fault_service_time` (disk I/O) is roughly 10⁴–10⁵× slower than a memory access, even `p = 0.001` can slow the system down by orders of magnitude.
  • Page faults are the entry point into Page Replacement Algorithms (which frame to evict) and enable Memory-Mapped Files (I/O disguised as memory access).
The core insight

A page fault is a deliberately engineered trap that lets the operating system make a program's memory appear larger and always resident, while secretly fetching data on demand and restarting the instruction that asked for it.

The idea

A page fault is a synchronous trap generated by the memory management unit (MMU) when a running program references a virtual address whose page-table entry has its valid/present bit cleared, or whose access violates the entry's protection bits (for instance, a write to a read-only page). Unlike an interrupt, which is caused by external hardware and is asynchronous to instruction execution, a page fault is a trap: it is caused directly by the instruction currently executing, arrives synchronously, and — crucially — is precise, meaning the processor guarantees the faulting instruction can be identified and later restarted cleanly. There are two broad categories of address-translation failure that a fault can represent. A minor fault occurs when the desired page actually exists in memory already — perhaps it is shared with another process, or was recently evicted but not yet reused — so the kernel merely needs to fix up the page table, no disk I/O required. A major fault occurs when the page genuinely must be read from backing store (a swap area or a file on disk), which is orders of magnitude slower. The term "page fault" without qualification usually refers to the major case, since that is where the interesting cost lies, but the distinction matters for real performance analysis. The entire scheme rests on a contract between hardware and the operating system established when The Page Table is built: each entry carries a present/valid bit. When that bit is 0, the MMU refuses translation and instead vectors control to a fault handler in the kernel, passing along the faulting virtual address (often in a dedicated register, e.g. `CR2` on x86) and an error code describing the nature of the violation (not-present vs. protection, read vs. write, user vs. supervisor). Everything that follows — allocating a frame, reading from disk, updating the table, resuming execution — is software, executed by the operating system's page-fault service routine.

How it works

The full lifecycle of a page fault spans hardware and software in six recognizable phases.

1. Address generation and translation attempt. The CPU executes an instruction that references a virtual address. The MMU walks (or consults the TLB for) the page table to translate it.

2. Trap. The MMU discovers the present bit is 0 (or a protection violation), and instead of completing the memory operation it traps: it saves the program counter and processor state needed to resume this exact instruction, switches to kernel mode, and jumps to the fault-handler entry point, passing the faulting address and fault-type code.

3. Validation. The kernel's fault handler checks whether the reference is legal. It consults the process's memory map (e.g., the list of valid virtual memory areas). If the address falls outside any region the process is permitted to touch, this is a genuine error — a segmentation violation — and the process is terminated (typically via `SIGSEGV`). Otherwise the fault is legitimate and demand paging proceeds.

4. Frame acquisition. The kernel needs a physical frame to hold the page. If a free frame exists, it is taken from the free-frame list. If none is free, a victim frame must be selected and, if dirty, written back — this is where a page-replacement policy is invoked (a topic developed in its own right elsewhere).

5. Data fetch and table update. The kernel schedules a disk (or backing-store) read to bring the required page into the chosen frame. The faulting process is blocked and the CPU is rescheduled to other work during this I/O, since it takes milliseconds. When the transfer completes, an interrupt notifies the kernel, which updates the page table entry: sets the frame number, sets the present bit, restores any other necessary state (e.g. clearing a "swapped" flag), and possibly updates the TLB.

6. Restart. The process is placed back on the ready queue; when scheduled, the exact instruction that faulted is re-executed from the beginning. This time, translation succeeds — the present bit is now 1 — and execution proceeds as if the fault never happened. ``` function handle_page_fault(fault_addr, error_code): vma = find_vma(current_process, fault_addr) if vma is None or not permission_ok(vma, error_code): deliver_signal(current_process, SIGSEGV) # illegal access return frame = get_free_frame() if frame is None: victim = select_victim_frame() # replacement policy if victim.dirty: write_back(victim) # flush to backing store invalidate_pte(victim.owner_process, victim.page_no) frame = victim.frame_no block(current_process) # yield CPU during I/O schedule_disk_read(vma.backing_store, fault_addr, frame) wait_for_io_completion() pte = page_table[current_process][page_number(fault_addr)] pte.frame_no = frame pte.present = true pte.dirty = false flush_tlb_entry(fault_addr) unblock(current_process) restart_instruction(current_process) # re-execute faulting instruction ``` The critical, easily-overlooked design detail is step 6: correctness demands that instruction restart reproduces exactly the state the CPU had before the faulting memory access began, not some partially-executed state. For simple instructions (load a register from memory) this is trivial — nothing irreversible happened before the fault. But consider an x86 instruction like `MOVS` that copies a block of memory and updates two pointers and a counter as a side effect, or an instruction with two memory operands where the second operand's page is the one missing. If the first operand's page was already written before the fault on the second operand is detected, naive restart would apply that side effect twice. Architectures handle this in one of two ways:

  • Prevent the problem: before executing an instruction, the CPU (or a preceding decode/microcode step) checks that all operands it will touch are already resident, refusing to begin the instruction at all if any page is missing (used historically on some designs for exactly this reason).
  • Undo the problem: the CPU records enough microarchitectural state (saved registers, a shadow copy of altered values) to reverse partial side effects before repeating the instruction from scratch. Modern x86 processors take this general approach, which is one reason instructions like `REP MOVS` are more complex to implement precisely than a load or a store.

Either way, the OS-visible contract is simple and must hold: the instruction stream, from the process's point of view, behaves as though the fault took zero time and never disturbed any state — this is what makes the page-fault mechanism transparent.

Figure
The page-fault trap and service sequence
CPU executes LOAD R1, [addr] MMU: page table walk present bit = 0 TRAP → kernel mode save PC, pass fault_addr Validate address legal? else SIGSEGV Find / evict a frame replacement policy if full Disk read (block process) ~ms — huge relative delay Update PTE, flush TLB present = 1 Restart instruction re-fetch, re-decode, re-execute LOAD from the very beginning translation now succeeds — execution continues as if fault never occurred

Complexity & correctness

Time cost of a single fault. A minor fault (page already in memory, just not mapped for this process) costs roughly a TLB shootdown plus a few page-table updates: microseconds. A major fault requires a disk seek and transfer, dominated by mechanical or flash latency: typically 1–10 milliseconds for spinning disks, tens of microseconds for SSDs — still 10³–10⁵ times slower than a DRAM access (roughly 100 nanoseconds). This asymmetry is the entire reason page-fault rate matters so much, and it is quantified precisely below.

Space cost. No extra asymptotic memory is required beyond what demand paging already uses: one present bit per page-table entry (already present in The Page Table format) and O(1) bookkeeping per fault (which frame, which victim). The kernel-side data structures involved (free-frame lists, per-process VMAs) are the same ones demand paging needs regardless of fault frequency.

Correctness argument. The precise-trap contract is what makes the whole edifice sound. Define the observable process state as the tuple of (register file, memory contents, program counter) visible to the user program at instruction boundaries. The correctness invariant is: > For any instruction `I` that triggers a page fault, the state observed immediately before `I` completes (after the fault is serviced and `I` is restarted) must be identical to the state that would have existed had `I` executed successfully on the first attempt, with no partial side effects from the aborted attempt persisting. This invariant holds because: 1. The MMU detects the missing translation before any store to memory or any architecturally-visible register write is committed (or, if partial writes occurred, the hardware undoes them before restart) — call this the no-partial-commit property. 2. The trap handler is guaranteed to preserve the entire register file and program counter of the interrupted process (standard trap/interrupt save-state discipline, the same mechanism used for any interrupt). 3. Page-table updates performed during service (steps 4–5 above) only ever change entries from invalid to valid for the process's own address space; they never alter any already-valid mapping the instruction had already used before faulting, so subsequent re-execution behaves identically to the never-faulted case for any operands already resolved. 4. Since the OS suspends only the faulting process (not the CPU itself) during the I/O wait, other processes continue running — a scheduling/throughput property, not a correctness one, but essential for why the enormous latency of a major fault does not stall the whole system. Given (1)–(3), re-executing `I` from scratch after the page is made present is behaviorally indistinguishable from an alternate universe in which the page had been resident all along — which is precisely the illusion demand paging promises the programmer.

Tradeoffs. The mechanism buys enormous flexibility (processes can vastly overcommit physical memory, programs need not be fully loaded before running, memory-mapped files become possible) at the cost of unpredictable, sometimes very large, per-access latency and a nontrivial hardware requirement (precise traps, undoable or checked instructions). Real-time systems often avoid or bound this unpredictability by locking critical pages in memory (`mlock`), trading flexibility back for predictability.

Worked example

Setup. Suppose the memory-access time is `ma = 100 ns`, and the page-fault service time (dominated by disk I/O) is `pf = 8 ms = 8,000,000 ns`. Define the effective access time (EAT): ``` EAT = (1 − p) × ma + p × pf ``` where `p` is the probability that any given memory reference faults.

p (fault rate)(1−p)·map·pfEAT (ns)Slowdown vs. no faults
0100.00100.01×
0.0000001 (10⁻⁷)≈100.00.8≈100.8≈1.008×
0.000001 (10⁻⁶)≈100.08.0≈108.01.08×
0.00001 (10⁻⁵)≈100.080.0≈180.01.8×
0.0001 (10⁻⁴)≈100.0800.0≈900.09×
0.001 (10⁻³)≈99.98000.0≈810081×

Reading the table. With `p = 10⁻⁶` — one fault per million references, seemingly negligible — the system already runs 8% slower than a fault-free machine. At `p = 10⁻³`, a rate that sounds "small" in casual conversation, the effective access time balloons by 81×. This nonlinearity is exactly why operating-system courses hammer the point: because `pf ≫ ma` by four to five orders of magnitude, even tiny fault probabilities dominate the average.

Turning the formula around: budgeting a target slowdown. Suppose we want at most a 10% slowdown, i.e. `EAT ≤ 1.10 × ma = 110 ns`. Solve for `p`: ``` 110 = (1 − p)(100) + p(8,000,000) 110 = 100 − 100p + 8,000,000p 10 = 7,999,900 p p ≈ 1.25 × 10⁻⁶ ``` So to keep the slowdown under 10%, fewer than roughly 1 in 800,000 memory references may fault — a concrete, derivable target that explains why systems invest heavily in large physical memory, good Page Replacement Algorithms, and prefetching: the acceptable fault rate is astonishingly small given the disk/memory speed gap.

A second case — faster backing store. Repeat with an SSD-backed swap area, `pf = 60 µs = 60,000 ns` instead of 8 ms: ``` EAT(p=10⁻³) = (0.999)(100) + (0.001)(60,000) ≈ 99.9 + 60 ≈ 159.9 ns (≈1.6× slowdown, versus 81× on spinning disk) ``` This demonstrates concretely why SSD-backed swap and NVMe storage dramatically change the economics of paging: the same fault rate that was catastrophic against a hard disk becomes merely noticeable against flash, because the gap between `ma` and `pf` shrank by over two orders of magnitude.

Common pitfalls & traps

PitfallWhy it happensFix / correct reasoning
Confusing a page fault with a fatal errorBeginners see "fault" and assume crashMost faults are expected and resolved transparently; only an out-of-VMA or permission-violating access becomes `SIGSEGV`
Assuming the faulting instruction "continues" from where it stoppedIntuition from normal function calls/returnsThe instruction is restarted from its beginning, not resumed mid-execution; state before the fault must be fully preserved or undone
Forgetting multi-operand instructions can double-fault or double-apply side effectsSimple load/store examples dominate textbooksReal ISAs need either an "all operands present" pre-check or undo logic for instructions like block moves
Treating fault rate `p` as negligible because it "looks small"Not internalizing the `ma` vs `pf` magnitude gapAlways compute `p × pf` explicitly — a 10⁻⁶ rate against millisecond service time already costs microseconds per average access
Ignoring minor vs. major faults in performance analysisTooling often reports only aggregate "faults"Minor faults (already-resident page, just remap) cost far less than major faults (disk I/O); blending them hides the real bottleneck
Blocking the entire CPU instead of just the faulting process during I/O waitMisunderstanding scheduling during faultsThe OS reschedules other runnable processes while the faulting one awaits I/O — this is what keeps overall throughput acceptable

Pioneers & history

The page fault as a formal mechanism emerged from the same lineage that produced virtual memory and paging itself — the effort, beginning in the late 1950s, to let programs run without being bound by the size of physical memory.

Origins · 1 minTom Kilburn and how this was discovered
TK
Tom Kilburn
1921–2001 · University of Manchester, Atlas computer project

Kilburn led the hardware team behind the Atlas computer (early 1960s), whose "one-level store" is widely credited as the first working implementation of paged virtual memory with automatic fault-driven page replacement — the direct ancestor of the page-fault mechanism used in every modern OS.

History
1959
— Manchester Atlas project begins designing the "one-level store," the first paging system with automatic page-fault handling.
1962
— Atlas becomes operational, demonstrating hardware-triggered faults transparently fetching pages from drum storage.
1969–1970
— Multics and early Unix-adjacent systems formalize demand paging and precise-trap requirements for restartable instructions.
1985
— Intel 80386 introduces full paging support with precise page faults in the x86 line, including `CR2` fault-address reporting.
1990
s–2000s — Widespread adoption of `mmap`-based demand paging turns the page fault into a general file-I/O primitive across Unix and Windows kernels.

Where this matters

Every general-purpose operating system — Linux, Windows, macOS, the BSDs — relies on page faults as the trigger for demand paging, copy-on-write process creation (`fork` maps parent and child to the same read-only pages, faulting only on the first write), and memory-mapped file I/O via `mmap`, where reading a file becomes indistinguishable from touching memory. Language runtimes exploit the same mechanism for generational garbage collectors (write-protected pages fault to detect mutation) and for guard pages that catch stack overflows. Hypervisors use nested/second-level page faults to implement memory overcommitment across virtual machines, and container runtimes rely on the same primitive for lazy image loading. Database systems that memory-map large files depend on predictable fault-service latency, which is precisely why understanding the effective-access-time tradeoff — and the deeper questions of which page to evict, covered under Page Replacement Algorithms — is central to performance engineering, not just OS theory.

Ask the tutor: How does the choice of page-replacement algorithm change the fault rate `p`, and therefore the effective access time?

Is a page fault always an error?

No. Most page faults are entirely expected and are the core mechanism behind demand paging, copy-on-write, and memory-mapped files. The kernel treats a fault as a request to make a legally-mapped-but-not-yet-resident page available. Only when the faulting address falls outside any virtual memory area the process is permitted to access — or violates protection bits in a way that isn't an intentional trap (like copy-on-write) — does the kernel treat it as an error and deliver a signal such as `SIGSEGV`. In everyday operation, a running program may generate thousands of "good" page faults per second without anything going wrong.

Why must the faulting instruction restart from the beginning rather than resume partway?

CPU pipelines and instruction semantics generally don't support resuming mid-instruction cleanly; the architectural state mid-instruction usually isn't a well-defined, savable point. Instead, hardware guarantees a precise trap: registers and the program counter are saved as if the instruction had not yet begun any visible side effects. After the kernel makes the missing page present, the CPU simply re-fetches and re-executes the same instruction, which this time completes normally. This design keeps the fault handler simple, at the cost of requiring hardware support (or careful decode-time checks) for instructions with multiple memory operands.

What's the difference between a page fault and a segmentation fault?

A page fault is the low-level hardware trap signaling "this virtual address isn't currently mapped to physical memory (or violates protection)." A segmentation fault (`SIGSEGV`) is the outcome the kernel delivers to a process when it determines, after examining the fault, that the access was illegal — for example, dereferencing a null or wild pointer that isn't part of any valid memory region. So every segmentation fault begins life as a page fault, but the overwhelming majority of page faults are legitimate and resolved silently without any signal ever being generated.

How does the effective access time formula justify investing in more RAM?

EAT = (1−p)·ma + p·pf shows that because `pf` is four to five orders of magnitude larger than `ma`, the fault rate `p` dominates performance almost entirely. Adding physical memory reduces contention for frames, which lowers the frequency of major faults (fewer pages need eviction and refetching), directly shrinking `p`. Because the formula is linear in `p` but the coefficient (`pf`) is enormous, even modest reductions in fault rate — achievable with modestly more RAM or a smarter replacement policy — translate into large, measurable reductions in EAT.

Can multiple page faults happen for the same instruction?

Yes, if an instruction references multiple pages that are each not resident. For example, an instruction copying data across a page boundary, or with two separate memory operands, may fault once, get serviced, restart, then fault again on the second missing page, and restart once more. This is safe precisely because of the precise-trap and full-restart discipline: each fault-and-restart cycle behaves as an independent, complete redo of the same instruction, and correctness only requires that no side effects persist across the aborted attempts.

Does a page fault always involve disk I/O?

No — this is the minor/major fault distinction. A minor fault occurs when the page is already resident in physical memory (perhaps shared with another process, or evicted from this process's mapping but still cached) and the kernel only needs to update page-table metadata, no I/O required. A major fault requires reading the page from a swap area or backing file, incurring real disk or flash latency. Performance analyses that only count "total faults" without separating minor and major can drastically misjudge where the real cost lies.

Why does the OS block only the faulting process rather than halting the whole machine?

Because the fault-service routine involves a device I/O operation (a disk or flash read) that takes milliseconds — an eternity in CPU cycles — it would be enormously wasteful to leave the processor idle. Instead, the faulting process is marked blocked/waiting and removed from the running state, and the scheduler dispatches another ready process to use the CPU. When the I/O completes (signaled by a hardware interrupt), the faulting process is marked ready again and eventually rescheduled, at which point the instruction restarts. This overlap of I/O wait with useful computation elsewhere is essential to acceptable system throughput under paging.

Before this

Where this leads