TRT
Sign in
Operating Systems

Virtual Memory and the Page-Fault Path

Virtual memory lets each process use an address space that is translated through page tables and hardware translation caches.

The Runtime Theory Team8 min read#virtual-memory#paging#page-faults

Virtual memory lets each process see a private address space whose addresses are names, not places. A 64-bit program may use 0x7f4a00000000; physical RAM may not contain that address at all. Between them sit page tables — per-process, kernel-owned trees that map virtual page numbers to physical frame numbers with permission bits — and a hardware cache of recent translations called the TLB. Pages (4 KB by default) are the unit of mapping, protection and movement between RAM and storage.

The translation path, in order

On every load or store the hardware runs this sequence:

  1. TLB lookup. The TLB holds recently used virtual page → frame entries, tagged with the process's address-space ID. A hit costs about one cycle; it is the fast path that makes virtual addresses nearly free.
  2. Page-table walk on a miss. A four-level x86-64 table means up to four dependent memory accesses (~40–60 cycles if all hit in cache, up to ~100 if they go to DRAM) before the translation is known.
  3. Fault, if the entry says so. No mapping, read-only page being written, or a not-present entry (demand paging, copy-on-write) — the CPU raises a page fault, traps into the kernel, and the handler decides.
c
double *p = malloc(1 << 30);      // 1 GiB mapped, zero pages resident
*p = 1.0;                        // first touch: fault, allocate a frame, resume

Not all faults are errors

The kernel's fault handler branches on the cause:

  • Minor fault: the page needs a frame but no I/O — a demand-zero page, a not-yet-copied copy-on-write page after fork, or a file page already in the page cache. Costs roughly 1–5 microseconds.
  • Major fault: the page must come from storage — tens of microseconds on NVMe, milliseconds on a spinning disk. This is the one that shows up in latency graphs.
  • Invalid access: no legitimate fix-up exists, so the kernel delivers SIGSEGV (or SIGBUS). A genuine bug.

Because a fault can be transparent, programs are written as if all memory is present: fork gives parent and child identical physical pages marked read-only, and whichever writes first pays for a private copy; mmap reserves address space and materializes frames on demand. Measure them:

bash
$ /usr/bin/time -v ./job 2>&1 | grep -E "page faults|Elapsed"
  Minor (reclaiming a frame) page faults: 298
  Major (requiring I/O) page faults: 3
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.42

A program that allocates 1 GB lazily and touches 4 MB reports about a thousand minor faults, not 262,144. A process whose major-fault rate climbs under load is either being starved of RAM (the kernel is reclaiming its pages) or reading data that was never cached.

What goes wrong in production

  • Thrashing. When the sum of working sets exceeds RAM, the kernel spends its time evicting and reloading. Throughput collapses long before an OOM kill: the signal is vmstat reporting high si/so (swap in/out) alongside low CPU.
  • TLB pressure. A large pointer-chasing structure pays the walk on every translation miss — 4–5 extra memory accesses on top of the data access. Huge pages (2 MB / 1 GB) cut TLB entries by 512× and are why databases (hugepages=always) and JVM heaps ask for them.
  • Unexpected major faults from shared libraries. Two processes mapping the same .so share physical pages; faults are minor until the file's pages leave the cache.
  • Fork-without-exec servers. A parent with 8 GB resident that forks for each request makes copy-on-write cheap for a moment — until every child writes, and 8 GB becomes 8 GB × children.

The trace of the page-fault path steps through one fault from instruction to resumed load.

Key takeaways

  • Virtual addresses are per-process names translated by kernel-owned page tables and cached in the TLB: a hit is ~1 cycle, a walk is up to four memory accesses.
  • A page fault is a handler decision, not a bug: minor (frame allocation, COW), major (storage I/O), or invalid (SIGSEGV).
  • Laziness is the feature: demand paging and copy-on-write make fork and large mmaps nearly free until pages are actually touched.
  • Production symptoms map to causes — rising major faults mean reclaim pressure or uncached reads; thrashing shows up as swap traffic; TLB misses show up as hidden memory latency.

Production cost model and latency numbers

To apply Virtual Memory and the Page-Fault Path effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Virtual Memory and the Page-Fault Path, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save