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

graph TD fault["Page fault at address A<br/>(read / write / exec)"] --> vma{"Is A inside a mapped region<br/>with a compatible permission?"} vma -->|"no"| segv["SIGSEGV<br/>(null pointer, freed memory, stack overflow,<br/>write to read-only)"] vma -->|"yes"| kind{"Why is the PTE missing or read-only?"} kind -->|"anonymous, never touched"| zero["Allocate a zeroed frame, map it<br/>(minor)"] kind -->|"file-backed, page in page cache"| map["Map the cached page<br/>(minor)"] kind -->|"file-backed, not in page cache"| read["Read from disk into page cache, map<br/>(major)"] kind -->|"write to copy-on-write page"| cow["Copy the page, map the copy writable<br/>(minor)"] kind -->|"swapped out"| swap["Read from swap, map<br/>(major)"] kind -->|"file mapping beyond end of file,<br/>or I/O error reading it"| bus["SIGBUS"]

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.