Page Faults¶
What a page fault is¶
A page fault is the exception the MMU raises when a memory access cannot be completed using the current page tables: the page is not present, or the access violates its permissions (a write to a read-only page, an instruction fetch from a no-execute page). The CPU stops the faulting instruction and jumps into the kernel's fault handler. The handler either fixes the situation — supplies a page, changes permissions — and resumes the instruction as if nothing happened, or decides the access is illegal and sends the process a signal.
Most page faults are not errors. They are how the kernel implements lazy allocation, file mapping, copy-on-write, and swapping (see Virtual Memory): set up a mapping without doing the work, and let the fault do the work only if and when the page is used.
The decision the handler makes¶
The kernel keeps a list of the process's mapped regions (VMAs —
virtual memory areas, the lines of /proc/<pid>/maps). The first check
is whether the faulting address belongs to any of them with the right
permissions; if not, the access is invalid. Otherwise, the fault is
legitimate work to be done.
Minor vs. major faults¶
| Type | Meaning | Disk I/O | Typical cost |
|---|---|---|---|
| Minor (soft) | The data is already in RAM (or the page is new and only needs zeroing); only the page table needs updating | No | ~0.5–2 µs, mostly zeroing or copying a page plus the kernel entry/exit |
| Major (hard) | The data must be read from storage: a file page not in the page cache, or a swapped-out page | Yes | Device latency: ~10–100 µs (NVMe) to ~10 ms (HDD), plus queueing |
Worked example — counting them. Measured on an Apple Silicon Mac
(16 KB pages) with getrusage:
| Action | Minor faults | Notes |
|---|---|---|
mmap 1 GiB anonymous |
0 | Only a VMA is created |
| Write 1 byte in each page of the first 100 MiB | 6,400 | 100 MiB / 16 KiB = 6,400: exactly one per page |
| Touch the same pages again | 0 | Pages are now mapped |
Each of those 6,400 minor faults zeroes a fresh 16 KB page before the process can see it — the kernel never hands one process another's leftover data.
Worked example — major fault under memory pressure. A process's heap pages were swapped out while the machine was short on RAM. It then reads a variable on one of those pages. The kernel finds the page's swap slot, issues a disk read, and blocks the thread until it completes — milliseconds on a disk, roughly 1,000x the minor case. A process faulting like this repeatedly spends more time waiting for page-ins than working: thrashing.
Faults that are signals¶
| Signal | Cause | Examples |
|---|---|---|
SIGSEGV |
Address in no VMA, or permission violation | Null pointer dereference (page 0 is never mapped), use-after-free after the allocator munmaped the region, stack overflow into the guard page, writing to a string literal |
SIGBUS |
Address is in a valid mapping but the backing cannot be provided | Accessing an mmaped file beyond its current end (file truncated by someone else), an I/O error reading a mapped file page, misaligned access on some architectures |
SIGBUS is the reason mmap-based I/O is dangerous for a database: a
disk error or a concurrently truncated file is reported not as a
Status from a read call but as a signal that kills the process at
whatever load instruction happened to touch the page (see
Linux I/O Stack and Page Cache).
Use-after-free does not reliably fault: the allocator usually keeps freed memory mapped for reuse, so the access silently reads stale or someone else's data. That is why AddressSanitizer exists.
Faults that create the stack¶
The main thread's stack region grows on demand: the kernel reserves
space below the current stack, and a fault just below the stack pointer
extends the mapping — up to ulimit -s. Touching beyond that limit hits
the guard and becomes SIGSEGV. A function with a very large local
array that skips over the guard in one step could jump past it into
other memory, so compilers insert stack probes
(-fstack-clash-protection) that touch each page in turn.
The zero page¶
On Linux, a read of a never-written anonymous page does not even
allocate memory: the kernel maps a single, global, read-only zero
page into every such location. Only the first write allocates a
private zeroed frame (a copy-on-write fault from the zero page). A
process that reads a huge freshly mmaped region sees all zeros, faults
once per page, and still uses almost no RAM.
Measuring page faults¶
# Totals for a command (Linux GNU time)
/usr/bin/time -v ./db_bench --benchmarks=fillrandom
# Major (requiring I/O) page faults: 12
# Minor (reclaiming a frame) page faults: 1048576
# Counters while running
perf stat -e page-faults,minor-faults,major-faults ./db_bench ...
# Where in the code faults happen
perf record -e page-faults -g ./db_bench ...
perf report
# Per-process, live (minflt/s, majflt/s)
pidstat -r 1
# System-wide
sar -B 1 # majflt/s
grep -E 'pgfault|pgmajfault' /proc/vmstat
In code, getrusage(RUSAGE_SELF, &r) returns ru_minflt and
ru_majflt — what the measurements on this page used.
Controlling faults in latency-sensitive code¶
| Technique | Effect | Use |
|---|---|---|
| Pre-touch (write each page once at startup) | Moves first-touch faults out of the request path | Buffer pools, arenas allocated at startup |
MAP_POPULATE (Linux mmap flag) |
Kernel populates all pages during mmap |
Same, in one call |
mlock / mlockall(MCL_CURRENT \| MCL_FUTURE) |
Pages stay resident: never swapped out, never faulted again | Small, critical regions; real-time systems. Limited by RLIMIT_MEMLOCK |
madvise(MADV_WILLNEED) |
Asks the kernel to read a file mapping's pages ahead | mmaped files about to be scanned |
madvise(MADV_DONTNEED) / MADV_FREE |
Gives pages back; next access faults in zeroed pages | Allocators returning memory to the OS |
| Huge pages | One fault per 2 MB instead of per 4 KB | Large, long-lived regions (Paging) |
A storage engine on its I/O path prefers explicit reads (pread
into a buffer it owns) to mmap exactly because it wants I/O to happen
at points it chooses, with errors returned as values, rather than as
page faults at arbitrary loads — possibly while holding a lock.
Common pitfall¶
Treating all page faults as a problem to eliminate. A program that
allocates memory will take one minor fault per page it touches; that is
the normal cost of demand paging, a few microseconds per page, mostly
incurred at startup or when caches warm. The fault rate that matters
is major faults in steady state: a non-zero majflt/s on a
database server means requests are waiting on disk for memory that was
supposed to be resident — swap is in use, or a memory-mapped file
doesn't fit in RAM. Measure minor and major faults separately, and
investigate steady-state major faults first.