Paging
Memory is managed in fixed-size pages: each virtual page maps to a physical frame through an entry carrying present, read/write/execute, dirty and accessed bits, and the "not present" state is not an error but the hook the kernel uses to allocate, share and swap lazily.
The problem
Pages and frames
The virtual address space is divided into pages and physical memory into frames of the same size — 4 kB on x86-64 and most ARM64 Linux systems, 16 kB on Apple silicon, 64 kB on some ARM server configurations. A 4 kB page means the low 12 bits of an address are the offset within the page and never translate; only the high bits (the virtual page number) are looked up. A mapping is a page-table entry (PTE) saying "virtual page N is in frame M, with these permissions". The page is the unit of everything: allocation, protection, sharing, swapping, caching. You cannot map, protect or swap half a page.
The choice of size is a trade-off with two sides. Smaller pages waste less memory at the end of each region (internal fragmentation, at most one page per mapping) and let the kernel be finer-grained about what to load and evict. Larger pages need fewer entries — a smaller table, fewer faults to cover a region, and far more memory covered by the same number of TLB entries (The TLB). 4 kB was chosen in the 1980s when RAM was measured in megabytes; it is now widely considered too small, which is why huge pages and 16 kB defaults exist.
What a page-table entry says
An entry is one machine word (8 bytes on 64-bit) that packs the frame number with a set of flag bits. The exact layout is per architecture, but the flags are universal because the kernel needs each of them for something specific.
The present bit is the pivot of the whole system: if it is clear, the CPU does not interpret the rest of the entry and raises a page fault instead, and the kernel is free to use the remaining bits to remember *why* — the page is on swap at this offset, or is part of a file mapping, or has never been touched. Every lazy mechanism in this module (Page Faults, Copy-on-Write, Memory Mapping, swap in Memory Pressure, Swap and the OOM Killer) is the kernel clearing this bit on purpose and handling the fault later.
The accessed and dirty bits are set by the CPU, not by the kernel, when the page is read or written. The kernel reads and clears them periodically to learn which pages are in use (the raw material for reclaim decisions) and which have been modified since they were last written to disk (a clean page can be dropped; a dirty one must be written first). The permission bits (writable, user-accessible, no-execute) turn a violation into a fault before it becomes a corruption: writing to code, executing the stack, or user code touching a kernel page all fault with the same mechanism.
bit 63 NX no-execute: instruction fetch from this page faults bits 52–62 (avail) ignored by hardware; OS may use them bits 12–51 PFN physical frame number (frame address = PFN << 12) bit 8 G global: not flushed on CR3 write (kernel pages) bit 7 PS page size: 1 = this entry maps a 2 MB / 1 GB page directly bit 6 D dirty: set by CPU on write bit 5 A accessed: set by CPU on read or write bit 4/3 PCD/PWT cache policy (uncached for device memory) bit 2 U/S user (1) or supervisor-only (0) bit 1 R/W writable (1) or read-only (0) bit 0 P present: 0 → any access page-faults; OS owns the other bits
4 kB pages and huge pages
x86-64 can map a page at three granularities: 4 kB, 2 MB (one entry in the second-lowest table level, with the PS bit set) and 1 GB. A huge page is one PTE covering 512 or 262,144 base pages — one TLB entry, one fault to materialise, one table entry to manage. For a process with a 40 GB heap the difference is 10 million entries against 20,000, and a TLB that covers 6 MB against one that covers 3 GB.
Linux offers huge pages two ways. hugetlbfs / MAP_HUGETLB reserves them explicitly at boot or runtime (vm.nr_hugepages), guaranteed and never swapped — how PostgreSQL (huge_pages = on), Oracle and DPDK use them. Transparent huge pages (THP) let the kernel opportunistically back anonymous memory with 2 MB pages and merge 4 kB pages later with khugepaged. THP is convenient and occasionally harmful: a 2 MB page is committed the moment one byte is touched (RSS inflation for sparse allocators), finding 2 MB of contiguous free frames can require compaction (latency spikes), and a copy-on-write fault copies 2 MB instead of 4 kB (Redis’s BGSAVE problem in Copy-on-Write). always vs madvise in /sys/kernel/mm/transparent_hugepage/enabled is the switch; databases document which they want.
Apple silicon and iOS use 16 kB base pages; ARM64 Linux can be built for 4, 16 or 64 kB. Code that assumes 4096 — custom allocators, memory-mapped file offsets, JIT code buffers — breaks on those systems; always ask (sysconf(_SC_PAGESIZE), os.sysconf, mmap.PAGESIZE).
| 4 kB page | 2 MB huge page | |
|---|---|---|
| Entries for 1 GB | 262,144 | 512 |
| TLB reach with 1,536 entries | 6 MB | 3 GB |
| Faults to touch 1 GB | 262,144 (~250 ms) | 512 (~ms, but each zeroes 2 MB) |
| Internal fragmentation | ≤ 4 kB per region | ≤ 2 MB per region |
| Copy-on-write cost per fault | Copy 4 kB | Copy 2 MB |
| Swappable | Yes | THP: split first; hugetlbfs: never |
| Best for | General purpose, sparse mappings | Large hot heaps: databases, JVMs, packet buffers |
Page faults as the mechanism for laziness
Everything the kernel does "on demand" is implemented by leaving a present bit clear and catching the fault. A new mmap region has no entries at all; the first touch of each page faults, and the handler finds the region, allocates a zeroed frame, and installs the entry (What Happens When I Allocate Memory?). A mapped file’s pages fault in from the page cache or from disk (Memory Mapping). A forked child’s pages are present but read-only; a write faults and the handler copies (Copy-on-Write). A page written to swap has its entry rewritten to "not present, swap slot 4711"; touching it faults and the handler reads it back (Memory Pressure, Swap and the OOM Killer).
The fault is cheap enough to be used this way — around a microsecond when no I/O is needed — and precise enough: the CPU delivers the faulting address and whether it was a read, a write or an instruction fetch, and after the handler returns the *same instruction* re-executes as if nothing had happened. The program cannot tell that its memory was materialised, copied or fetched from disk in the middle of a load. That transparency is what makes virtual memory an abstraction rather than an API, and Page Faults follows one fault through the kernel step by step.
Key points
- Memory is mapped, protected, shared and swapped in whole pages: 4 kB on x86-64 and most ARM64 Linux, 16 kB on Apple silicon.
- A PTE packs the frame number with present, R/W, user/supervisor, NX, accessed and dirty bits; the CPU sets A and D, the kernel reads them.
- Present = 0 is not an error; it is the kernel’s hook. The remaining bits then record where the page really is (swap slot, file, nowhere yet).
- Permission bits turn writes to code, execution of data and user access to kernel memory into faults before they become corruptions.
- Huge pages (2 MB / 1 GB) cut entries and faults by 512× and multiply TLB reach; THP is automatic and can inflate RSS and spike latency.
- Never hard-code the page size; ask the OS.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why fixed-size pages?
Fixed sizes make the map an array-like structure the hardware can walk, bound waste to one page per region, and let any frame back any page — no fitting, no external fragmentation.
▸Why does the CPU maintain the dirty and accessed bits?
Only the CPU sees every access. Without hardware-maintained bits the kernel would have to trap on every first access and every first write to learn what is used and what is modified — which some architectures actually did, expensively.
▸Why is 4 kB still the default if it is too small?
Compatibility: file formats, allocators, and decades of software assume it, and huge pages exist as an opt-in on top. Apple could change the default because it controls the whole stack.
▸Why can a page fault be transparent?
Because the CPU raises it before the instruction completes and re-executes the instruction afterwards. The handler changes the map, not the program state, so from the program’s side the load merely took longer.
How it fails
What the failure looks like from inside real software.
- THP
alwayson a Redis or MongoDB host: RSS 30% higher than the data, and latency spikes during compaction and fork;madviseorneverfixes both. - A custom allocator assumes 4 kB pages and misaligns
mmapoffsets on an Apple silicon build:EINVALfrommmap, or silent overlap. - A JIT writes machine code into a read/write page and jumps to it:
SIGSEGVwith the NX bit set; the fix ismprotect(PROT_READ|PROT_EXEC)after writing (W^X). hugetlbfspages reserved at boot but unused by the database due to a misconfiguration: 8 GB of RAM invisible to everything else, "missing" fromfree.- Dirty pages of a huge mapped file accumulate faster than writeback drains them:
dirty_ratioreached, every writer stalls in the kernel.