malloc(1 GB) succeeds instantly. Touching it does not. That asymmetry is demand paging:
the kernel hands out address space as a promise and delivers physical memory page by page,
on first use. Understanding the page fault — the mechanism behind the promise — explains
why binary loading is fast, why mmap is free until you read, and why a "memory leak" can
suddenly become a "page fault storm."
Virtual memory is a promise
An address space is a map, not storage. Page tables describe it with entries that are either present (pointing at a physical page) or not present (pointing at nothing). Demand paging means the kernel installs not-present entries and fills them in on demand:
- Executing a binary:
execve()maps the text and data segments into the address space but reads nothing from disk. Pages fault in as the program actually runs — which is why a 100 MB binary starts in milliseconds. mmap(): installs a VMA (a region descriptor) and zero pages. Reading the region triggers the faults.malloc(): for large allocations, the kernel gives you address space backed by nothing; the first write faults in a zeroed page.
The fault path, step by step
When the CPU hits a not-present entry, the MMU raises a page fault exception and the kernel runs this exact sequence:
1. instruction accesses address X
2. MMU walks page tables — PTE not present → page fault exception
3. CPU traps to kernel; faulting address in CR2 (x86)
4. kernel finds the VMA covering X — no VMA → SIGSEGV
5. file-backed? → look the page up in the page cache
anonymous? → allocate a zeroed physical page
6. page on disk? → queue I/O, sleep the task (major fault)
7. insert PTE, return to user mode, restart the instructionEvery step happens in the context of the faulting thread — the instruction is replayed afterward, which is why a faulted-in page looks as if it was always there.
Minor vs. major faults: two different price tags
The same trap leads to two very different invoices:
| Fault | What happens | Cost |
|---|---|---|
| minor | page already in memory — in the page cache, shared with another process, or freshly zeroed | ~0.5–2 µs |
| major | page must be read from disk (or swapped back in) | ~1–10 ms |
A shared library is loaded once into the page cache; every process that maps it pays only minor faults. A cold file read is a major fault: 1,000 major faults on a spinning disk is seconds of stall. The page cache is the kernel's bet that pages you touched once will be touched again — it converts future major faults into minor ones.
mmap is lazy by construction
mmap never touches disk at open time:
mmap(fd, 0, 1 GiB, PROT_READ, MAP_SHARED, ...)
→ installs a VMA, reads nothing
→ RSS grows page by page as code/reads touch the regionread() works the same way under the hood: it faults the file's pages into the page cache
(major faults on first read, minor afterward) and copies them to your buffer. The difference
is the copy: mmap hands you the cache page directly, eliminating it.
Why first touch costs
- Major faults: disk or swap latency, 1–10 ms each, serialized if not issued
asynchronously (readahead and
posix_fadviseexist to hide this). - Page zeroing: a fresh anonymous page is guaranteed zero; touching 1 GB of fresh memory is ~1 GB of memory-bandwidth work, plus the physical page allocation itself.
- Cold TLB and caches: new pages have no translation cached and no data cached — first access pays both.
- THP (Transparent Huge Pages): faulting in 2 MB huge pages cuts fault count 512× and
TLB pressure, at the cost of memory fragmentation —
madvise(MADV_HUGEPAGE)is the surgical option.
The classic benchmark surprise: allocating a large buffer, touching every page once, shows startup cost proportional to bandwidth, not to the allocation call — and an untouched buffer shows zero cost because there are zero faults.
Measuring faults
ps -o pid,minflt,majflt,cmd -p <pid> # faults since start
cat /proc/<pid>/stat # fields 10 (minflt) and 12 (majflt)
grep -E "anon|file|swap" /proc/<pid>/smaps # what's actually residentA process with a growing majflt counter is either legitimately loading data or
thrashing — faulting pages in only to evict them before reuse. That pattern, not RSS, is
the first thing to check when memory behavior looks wrong.