The idea: every access needs a translation
A program only sees virtual addresses. Before the CPU can load or store anything, the MMU (memory management unit) must turn the virtual address into a physical address in RAM. The map lives in RAM too, as a tree of page tables. Reading that tree for every access would make every load four times slower, so the CPU keeps the last few answers in a tiny cache, the TLB (translation lookaside buffer).
This page follows one access at a time: the split of the address, the TLB lookup, and on a miss the four reads of the page walk, the TLB fill, and the final physical address. For page faults, copy-on-write and how Linux fills the tables, see Linux Virtual Memory.
Splitting the address
x86-64 uses 48 of the 64 bits (4-level paging). Bits 63–48 must copy bit 47, so user addresses start with 0x0000 and kernel addresses with 0xffff. The rest is five numbers:
| bits | field | what it picks | 0x7ffd5a3c1e40 |
|---|---|---|---|
| 47–39 | PML4 index | one of 512 entries in the top table (each covers 512 GiB) | 255 |
| 38–30 | PDPT index | an entry in the page-directory-pointer table (1 GiB each) | 501 |
| 29–21 | PD index | an entry in the page directory (2 MiB each) | 209 |
| 20–12 | PT index | an entry in the page table (one 4 KiB page) | 449 |
| 11–0 | offset | the byte inside the page, copied unchanged | 0xe40 |
Each table is one 4 KiB page holding 512 entries of 8 bytes, which is why each index has 9 bits. Linux calls the same four levels PGD, PUD, PMD and PTE.
One walk, step by step
CR3 holds the physical address of the current process's PML4. The MMU reads the entry at CR3 + PML4 index × 8. If its present bit (P) is set, the entry holds the physical address of the next table; the MMU reads that table's entry at its own index, and so on. The PT entry finally gives the frame, and the physical address is the frame plus the offset. That is four dependent memory reads before the one the program asked for (Demo: miss, walk, fill, then hit).
Neighbouring pages share the upper tables: 0x401234 and 0x402010 use the same PML4, PDPT, PD and PT, and differ only in the PT index. The stack at 0x7ffd5a3c1e40 starts at PML4 entry 255 and goes down a different branch of the tree (Demo: another page, another walk). On this page each walk costs four reads either way.
The TLB: a small cache of translations
The TLB stores page → frame pairs with their permission bits. All entries are compared at once (here: fully associative), so a hit costs about a cycle and no memory read. Only after a miss does the MMU walk, and then it writes the result into the TLB (the fill). When the TLB is full, an old entry is evicted; this page uses LRU, least recently used (Demo: TLB full, LRU eviction).
| this page | a recent x86 core (roughly) | |
|---|---|---|
| L1 data TLB | 4 entries, 4 KiB and 2 MiB mixed | 64–96 entries for 4 KiB pages, a separate small array for 2 MiB / 1 GiB |
| L2 (shared) TLB | none | 1,500–3,000 entries |
| hit | no memory read | 0–1 extra cycles (L1), ~7 cycles (L2) |
| miss | 4 memory reads | 4 reads, often served from the data caches and the page-walk caches: tens of cycles, hundreds if they miss to RAM |
The memory a TLB can map without missing is its reach: 64 entries × 4 KiB = 256 KiB. A program that jumps around a bigger working set keeps missing even when all its data is in the CPU caches.
Huge pages: a shorter walk and more reach
A PD entry with the PS (page size) bit set does not point to a page table: it maps a whole 2 MiB page. The walk stops after three reads, the low 21 bits are the offset, and one TLB entry covers what would take 512 entries with 4 KiB pages (Demo: 2 MiB huge page: one miss, then 0x40123456 and 0x401ff000 hit). A PDPT entry with PS = 1 maps 1 GiB.
Linux gives huge pages through hugetlbfs or, automatically, through transparent huge pages (THP); databases and JVMs ask for them because their heaps are far larger than the TLB's reach.
Context switches: flush, or tag with PCID
Two processes can use the same virtual address for different frames. Here A's page 0x401000 is frame 0x5a1000 and B's is 0x9b1000. When the kernel switches process it writes the new PML4 address into CR3, and the TLB must not keep giving A's answers to B.
- PCID off: writing CR3 flushes every TLB entry (except global kernel pages). B starts with an empty TLB, and when A runs again it misses on pages it had just used (Demo: context switch without PCID: 4 misses, 0 hits).
- PCID on: the low 12 bits of CR3 carry a process-context identifier and every TLB entry is tagged with it. Bit 63 of the value written to CR3 says "do not flush". A lookup matches only entries with the current PCID, so A's entries stay and A hits when it comes back (Demo: context switch with PCID: 3 misses, 1 hit).
ARM and RISC-V call the same tag an ASID (address-space identifier). Linux uses PCID on x86 since 4.14; it became important after Meltdown, when kernel page-table isolation (KPTI) started switching CR3 on every system call too.
When the walk finds nothing
If an entry on the way has P = 0, the walk stops and the CPU raises a page fault (#PF) with the address in CR2. No TLB entry is made: the TLB only ever caches present translations (Demo: not present, page fault). The kernel's handler then maps a page and retries the instruction, or sends SIGSEGV; Linux Virtual Memory animates that side, and Paging and Page Replacement shows what happens when RAM is full.
Changing a present entry is the opposite problem: the TLB may still hold the old one. The kernel removes it with INVLPG on this CPU, and with an inter-processor interrupt (a TLB shootdown) on the other CPUs that run the same process.
How to see it on a real system
perf stat -e dTLB-loads,dTLB-load-misses,dtlb_load_misses.walk_completed ./progcounts TLB misses and finished walks.grep -o -w pcid /proc/cpuinfo | head -1shows if the CPU supports PCID.grep -i huge /proc/meminfoshowsAnonHugePages(THP) and thehugetlbfspool.cat /proc/<pid>/pagemap(as root) gives the frame of each virtual page.
See also Virtual Memory in Hardware for a one-level page table and the TLB in the textbook MIPS design, CPU caches for the caches the walk reads through, and Linux Context Switch for where switch_mm writes CR3.
What the page leaves out
Real CPUs have two or three TLB levels, separate instruction and data TLBs, and separate arrays per page size; they are set-associative, not fully associative, and do not use exact LRU. They also keep page-walk caches (paging-structure caches) for the upper levels, so a walk next to a recent one often needs only the PT read. The page leaves out permission bits (R/W, U/S, NX) and the accessed and dirty bits the walk sets, global pages, 1 GiB pages, 5-level paging (LA57, 57-bit addresses), INVPCID, and the second, nested walk of a virtual machine (EPT / NPT), where one miss can cost up to 24 reads.