How Virtual Memory Works: Pages, Page Tables, and Page Faults
Why virtual memory exists
A process does not own a private bank of RAM chips. It owns a private address space: virtual addresses the CPU will use on its behalf. The operating system and the memory-management unit translate those addresses into physical frames, or they refuse the access with a page fault.
This article explains that translation: pages, page tables, the TLB, page faults, demand paging, copy-on-write, and what happens when RAM is full. Isolation of address spaces is why two processes can both use address 0x400000 without colliding. That is a different topic from how those processes are scheduled.
What virtual memory provides
Virtual memory is not merely disk pretending to be RAM. Swap is one tool. The system itself is address translation plus protection.
- Isolation: process A cannot touch process B's pages unless the kernel maps the same frame into both spaces.
- A contiguous illusion: heap, stack, and mapped files look linear even when frames are scattered.
- Overcommit and sharing: virtual mappings can exceed RAM; unused pages need no frame; read-only libc text can be shared.
- Protection bits: read, write, execute. Illegal access becomes a fault, which is how copy-on-write and W^X are enforced.
The unit of translation is the page. On most general-purpose systems the base size is 4 KiB. Huge pages (2 MiB, 1 GiB) reduce page-table pressure. A frame is a page-sized chunk of physical memory.
Virtual addresses are indexes
On a 64-bit process the programmer writes 64-bit pointers. Hardware does not use all 64 bits as a physical index. Typical user-space canonical addresses on x86-64 use 48 bits, or 57 with five-level paging. The kernel splits a virtual address into page-table indexes and an offset inside the page (12 bits for 4 KiB pages).
With four-level paging, bits 11:0 are the offset. Higher bits select entries in the PML4, page-directory-pointer table, page directory, and page table. The last entry holds a physical frame number plus flags: present, writable, user-accessible, no-execute, accessed, dirty.
The offset is copied unchanged onto the physical address. Translation never rearranges bytes inside a page. That is why alignment matters for DMA, file mapping, and huge pages.
Page tables are trees
A flat table of every 4 KiB page in a 48-bit space would need 2^36 entries. Hardware walks a sparse tree instead. Empty subtrees are missing. Mapping 8 MiB of heap does not allocate tables for the rest of a 128 TiB theoretical range.
Each process has its own top-level page-table pointer. On x86-64 that pointer lives in CR3. A context switch loads a new CR3, or an ASID/PCID-tagged root, so the next user instruction translates against a different tree. That is the hardware meaning of address space.
Kernel mappings are usually present in every process tree so a syscall can run without swapping trees. Those pages are marked supervisor-only. After Meltdown-class attacks, kernels also isolate most kernel secrets from user page tables (KPTI).
The TLB
A four-level walk is four extra memory reads per load if done naively. Processors cache recent translations in the Translation Lookaside Buffer. A TLB hit turns a virtual address into a frame without touching page tables.
TLB capacity is limited and often split by instruction versus data and by page size. A context switch must not let the new process reuse the old process's translations. Tagged TLBs keep entries from several address spaces and match the current tag.
When the kernel unmaps a page, flips a writable bit for copy-on-write, or installs a new anonymous page, it must invalidate cached translations on every CPU that might hold them. That shootdown is visible in programs that remap memory often.
What a page fault is
A page fault is the MMU saying this virtual address has no usable translation right now. The CPU records the faulting instruction and the access type, then enters the kernel.
Not every fault is a bug.
- Invalid: the address is outside any virtual memory area the process created. On Unix-like systems this becomes SIGSEGV.
- Protection: the mapping exists but the access violates flags. Sometimes this is a signal. Sometimes it is copy-on-write: allocate a private frame, copy, mark writable, retry the instruction.
- Not present but valid: the mapping exists; the page was never installed or was evicted. The kernel fills a frame from zeros, a file, or swap, installs a present PTE, and returns to the same instruction.
That last case is demand paging. Starting a program does not copy the whole binary into RAM. The loader maps the file. The first fetch on each page pulls that page from the executable. Large memory-mapped datasets work the same way: you pay for pages you touch.
Anonymous pages, file pages, and swap
File-backed pages come from an executable, a shared library, or mmap of a file. If RAM is tight and the page is clean, the kernel can drop it and reread the file later. Dirty file pages must be written back first.
Anonymous pages have no file: heap, stack, MAP_ANONYMOUS, private CoW copies. If RAM is tight they go to swap. If swap is full and nothing else can be reclaimed, allocation fails or the OOM killer runs.
Swap is optional. Embedded systems and some latency-critical servers disable it. They still use page tables, protection, and demand paging from files. They refuse the extra latency of paging anonymous memory to disk.
A worked walk through one load
A process executes a load from virtual address 0x7f1234002008 with 4 KiB pages.
- The CPU splits offset 0x008 from the page number.
- It probes the TLB. On a hit it checks cached permissions and completes the load.
- On a miss the walker reads each table level from the current root. If every Present bit is set and permissions allow a user read, it fills the TLB and the load completes.
- If Present is clear, a fault runs. The kernel finds the VMA. For a first-touch anonymous mapping it allocates a frame, zeros it (or uses a shared zero page until a write), installs the PTE, and returns. The instruction runs again and hits.
From the program's point of view the pointer was always valid. The first access paid for the frame. Walking a freshly allocated 1 GiB buffer therefore stalls on faults and zeroing, not only on cache misses.
Copy-on-write after fork
After fork the child needs its own address space but not an immediate copy of every page. The kernel duplicates the page-table tree and marks writable pages read-only in both processes. Frames stay shared.
The first write faults. The handler allocates a new frame, copies the page, and gives the writer a private writable mapping. Pages never written stay shared. Fork of a large process is cheap until someone dirties memory. That is why fork-heavy servers can show a sudden RSS jump after workers start mutating heaps.
What developers actually debug
- SIGSEGV on a pointer that looked valid: use-after-free, a racing munmap, stack overflow past the guard page, or a mapping past the end of a file. On Linux, /proc/pid/maps shows the layout.
- RSS growth after fork: copy-on-write as workers write inherited heaps.
- Latency on first touch: demand paging and zero-fill. MAP_POPULATE, an explicit walk, or mlock if the budget forbids faults.
- Thrashing: the working set does not fit in RAM. Faster disks only hide mild cases.
- TLB shootdown from frequent mmap, mprotect, or JIT permission flips: kernel time and cross-CPU interrupts rather than user-space cache misses.
What this article is not
Virtual memory is the translation layer. malloc and a garbage collector decide which virtual pages to request. They do not replace page tables. NUMA placement, huge-page pools, and IOMMU translation for devices are related but separate.
Related reading on this site: Processes vs Threads Explained and How Caching Works.
Common misconceptions
Virtual memory does not mean swap. Laptops with swap off still translate every user pointer.
64-bit pointers do not mean 16 exabytes of RAM. They mean a large virtual address space. Physical RAM and OS limits are smaller.
malloc does not return physical memory. It returns a virtual address. The frame often appears at first write.
A page fault is not always a crash. Minor faults are how pages get installed. Only illegal addresses or unfixable permissions become signals.
Takeaways
Every user pointer is translated. The page table names the frame, if any, and the permitted access. The TLB caches that answer. A fault is the kernel's chance to install a page, copy a page, or reject the access. Isolation between processes is a different page-table root, not a software convention.
Allocator behavior, fork cost, first-touch latency, and OOM reports follow from when frames are actually bound to virtual pages.