AnyLearn
All lessons
Computer Scienceintermediate

Virtual Memory: Page Tables, TLBs and Faults

Virtual memory is not a trick for pretending you have more RAM. It is the hardware and kernel machinery that gives every process a private, relocatable address space. This lesson covers multi-level page tables, why a TLB miss costs real cycles, demand paging, copy-on-write, mmap, swapping, huge pages, and how the OOM killer picks a victim.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 11

Virtual addresses buy four things, not one

The popular explanation, "virtual memory lets you use more memory than you have", is the least interesting of its benefits. Indirection between the address a program uses and the address the DRAM chip sees buys four properties at once:

  • Isolation. A pointer in process A simply cannot name a byte in process B, because A's page tables never map it. This is enforced by hardware on every single access.
  • Relocation. The linker can place your code at a fixed virtual address and the loader is free to put it anywhere physical.
  • Overcommit and laziness. A mapping can exist with no physical page behind it until first touch.
  • Uniform permissions. Read, write, execute, and user versus kernel access are bits in the translation itself, checked for free.

The cost of all four is that every load and store now requires a translation.

Full lesson text

All 11 steps on one page, for reading, reference, and search.

Show

1. Virtual addresses buy four things, not one

The popular explanation, "virtual memory lets you use more memory than you have", is the least interesting of its benefits. Indirection between the address a program uses and the address the DRAM chip sees buys four properties at once:

  • Isolation. A pointer in process A simply cannot name a byte in process B, because A's page tables never map it. This is enforced by hardware on every single access.
  • Relocation. The linker can place your code at a fixed virtual address and the loader is free to put it anywhere physical.
  • Overcommit and laziness. A mapping can exist with no physical page behind it until first touch.
  • Uniform permissions. Read, write, execute, and user versus kernel access are bits in the translation itself, checked for free.

The cost of all four is that every load and store now requires a translation.

2. Why translation is a tree, not a table

A flat array mapping every 4 KiB page of a 48-bit address space would need 2 to the 36 entries, hundreds of gigabytes per process. Unworkable. So translation is a sparse radix tree instead.

On x86-64 with the standard 4-level paging, a canonical virtual address splits into five fields: four 9-bit indices and a 12-bit offset. Nine bits selects one of 512 entries in a 4 KiB table, and each entry is 8 bytes. Four levels of 9 bits plus the 12-bit offset gives 48 usable bits, which is 256 TiB of address space.

CR3 holds the physical address of the top table, and switching CR3 is what switches address spaces. Intel's 5-level paging, enabled by setting bit 12 of CR4 (the LA57 bit) and first shipped in Ice Lake, adds a fifth 9-bit index for 57-bit addresses, 128 PiB.

3. One address, four memory reads

A page walk on x86-64 costs up to four dependent memory accesses before the real access even starts. That is what the TLB exists to avoid.

flowchart LR
  R["CR3: physical address of top table"] --> A
  V["Virtual address"] --> A["Bits 47 to 39 index level 4 table"]
  A --> B["Bits 38 to 30 index level 3 table"]
  B --> C["Bits 29 to 21 index level 2 table"]
  C --> D["Bits 20 to 12 index level 1 table"]
  D --> F["Page table entry: frame number plus permission bits"]
  O["Bits 11 to 0: offset within the page"] --> P
  F --> P["Physical address"]

4. The TLB, and what a miss really costs

The TLB is a small, fully associative cache of recent virtual-to-physical translations sitting in front of the page walker. A hit is essentially free, folded into the pipeline. A miss triggers the walk.

Mel Gorman measured the miss cost directly in his LWN huge pages series. On an Intel Core Duo T2600 he found the cost of a TLB miss at 19 clock cycles, or 8.80 nanoseconds; on an AMD Athlon 64 3000+ it was 37 cycles, 18.17 nanoseconds; and on a PPC970MP he calculated roughly 563 cycles. Same concept, an order of magnitude apart across architectures.

The number that actually matters is coverage. Gorman's x86 machine reported a 128-entry data TLB for 4 KiB pages. 128 times 4 KiB is 512 KiB of memory reachable without a walk. A process with a 2 GiB working set and random access misses constantly.

5. Page faults: minor, major, and invalid

When the walk finds no valid translation, the CPU raises a page fault: a synchronous exception that hands control to the kernel with the faulting address and an error code describing the access.

The handler consults the process's list of virtual memory areas and picks one of three answers:

  • Minor fault. The mapping is legitimate and the data is already in RAM, or needs only a zero page. No disk touched. This covers first touch of freshly allocated memory and hits on the page cache.
  • Major fault. The mapping is legitimate but the content must come from storage, from a file or from swap. The task blocks and the scheduler runs somebody else.
  • Invalid. No VMA covers the address, or the access violates the permission bits. The kernel delivers SIGSEGV.

A segfault is therefore not a special crash mechanism. It is the ordinary fault path reaching its third case.

6. Demand paging and copy-on-write are the same trick

Both are the kernel deliberately installing a page table entry that will fault, then doing the real work in the handler.

Demand paging. malloc() of a gigabyte returns instantly because nothing is backed yet. The VMA exists; the page table entries do not. Each first touch takes a minor fault and gets a physical frame then. This is why resident set size climbs as a program warms up while virtual size was already huge from the start.

Copy-on-write. On fork(), the kernel does not copy the address space. It copies the page tables and marks every writable private page read-only in both parent and child. The first write from either side traps as a protection fault, the handler allocates a fresh frame, copies 4 KiB into it, restores write permission for that one process, and returns. Pages nobody writes are never duplicated.

7. mmap is the raw interface to all of it

mmap() is how userspace asks for a VMA directly. Two independent axes decide what you get.

// File-backed, private: the classic executable/library mapping.
// Reads come from the page cache; writes trigger copy-on-write
// and are never visible in the file.
void *p = mmap(NULL, len, PROT_READ|PROT_WRITE,
               MAP_PRIVATE, fd, 0);

// File-backed, shared: writes go to the page cache pages that
// ARE the file. Other mappers of the same file see them.
void *q = mmap(NULL, len, PROT_READ|PROT_WRITE,
               MAP_SHARED, fd, 0);

// Anonymous: no file. This is what large malloc() calls become.
void *r = mmap(NULL, len, PROT_READ|PROT_WRITE,
               MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

File-backed versus anonymous decides where a major fault reads from. Private versus shared decides whether a write copies or publishes. Every mapping in /proc/self/maps is one of these four combinations.

8. Reclaim and swapping

When free memory runs low the kernel reclaims pages rather than failing allocations. It keeps pages on active and inactive LRU lists, split into file-backed and anonymous.

File-backed clean pages are cheapest to reclaim: drop them, since the file still has the content. Dirty file pages must be written back first. Anonymous pages have nowhere to go unless swap exists, which is the entire purpose of swap: it makes anonymous memory reclaimable.

vm.swappiness, documented in the kernel's sysctl guide and defaulting to 60, biases the choice between evicting file pages and swapping anonymous ones. Setting it to 0 does not disable swap, it only makes the kernel avoid anonymous reclaim until it is nearly forced.

The failure mode is thrashing: the working set exceeds RAM, so every fault evicts a page someone is about to fault back in. Throughput collapses while the CPU sits idle waiting on I/O.

9. Huge pages: buying TLB coverage

If a 128-entry TLB covers only 512 KiB with 4 KiB pages, the fix is bigger pages. x86-64 supports 2 MiB pages by stopping the walk one level early, and 1 GiB pages by stopping two levels early. One TLB entry then covers 512 times or 262144 times as much memory.

The kernel's transparent hugepage documentation names both wins: "a single TLB entry will be mapping a much larger amount of virtual memory in turn reducing the number of TLB misses", and taking one fault per 2 MiB region rather than 512, "reducing the enter/exit kernel frequency by a 512 times factor".

The gotcha is that 2 MiB of physically contiguous free memory has to exist. Under fragmentation, THP in always mode can stall an allocation while the kernel compacts memory. The documented default is madvise: huge pages only where the application asked via madvise(MADV_HUGEPAGE).

10. How the OOM killer chooses

Linux overcommits by default: mmap() succeeds for more memory than exists, on the bet that most of it is never touched. When that bet loses and reclaim cannot free enough, the kernel invokes the OOM killer rather than returning failure to a caller that would not check it anyway.

select_bad_process() scores every task with oom_badness(). The score is essentially the task's resident memory plus page tables plus swap, normalised against total memory to a 0 to 1000 scale, so the heaviest user is the default victim. The kernel picks the highest scorer and kills it.

Userspace steers this through /proc/PID/oom_score_adj, documented in proc_pid_oom_score_adj(5) with a range of -1000 to +1000. The extremes are absolute: -1000 makes a process exempt, +1000 adds the equivalent of all system RAM and effectively volunteers it. Read the current verdict from /proc/PID/oom_score.

11. Gotchas worth internalising

  • Virtual size is not memory used. A process with 40 GiB of VSZ and 300 MiB of RSS is normal. Only RSS costs physical frames.
  • Freeing memory does not always return it. Small free() calls go back to the allocator, not the kernel. Only munmap() or madvise(MADV_DONTNEED) releases frames.
  • The OOM killer kills the biggest, not the guilty. A leaking helper can get your database killed instead. That is what oom_score_adj is for.
  • Swap off is not the safe setting. With no swap, anonymous memory is unreclaimable, so pressure goes straight to the OOM killer instead of degrading gracefully.
  • Page-table memory is real memory. Every mapped 4 KiB page costs 8 bytes of PTE, plus the upper levels. Thousands of processes mapping the same huge file is a measurable cost.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why is x86-64 address translation organised as a four-level tree rather than one flat array?
    • A tree lets the CPU translate several addresses in parallel.
    • A flat table for a 48-bit space at 4 KiB granularity would need 2^36 entries per process, hundreds of gigabytes.
    • The tree structure is what allows pages to have different permission bits.
    • Hardware can only index tables of at most 512 entries.
  2. A process calls fork(), and the child immediately writes one byte to a large private heap buffer. What does the kernel do?
    • Nothing special, since fork already duplicated the parent's physical pages at call time.
    • It raises a major page fault and reads the page back from swap.
    • It takes a protection fault on the read-only PTE, allocates one fresh frame, copies that single page, and restores write permission for the child.
    • It marks the entire buffer shared so both processes observe the write.
  3. Mel Gorman's LWN measurements put a TLB miss at 19 cycles on a Core Duo T2600 and roughly 563 cycles on a PPC970MP. What is the main reason 2 MiB huge pages help?
    • They make each individual TLB miss cheaper to service.
    • They remove the need for permission checks on each access.
    • They let the kernel skip the page fault handler entirely.
    • One TLB entry covers 512 times more memory, so far fewer accesses miss at all.
  4. A program mmaps a file with MAP_SHARED and PROT_WRITE, then writes to the mapping. Which statement is correct?
    • The write lands on page cache pages backing the file, so other processes mapping the same file observe it.
    • The write triggers copy-on-write and stays private to this process.
    • The write goes directly to the storage device, bypassing the page cache.
    • The write is rejected unless the file was opened with O_DIRECT.
  5. A container host runs out of memory. Which process is the kernel's OOM killer most likely to select?
    • The process that made the allocation request which could not be satisfied.
    • The oldest process on the system, by start time.
    • The one with the highest oom_badness score, driven mostly by resident memory plus swap and adjusted by oom_score_adj.
    • The process with the most open file descriptors.

Related lessons

Computer Science
intermediate

Filesystems and the I/O Path

A file has no name; the name is a directory entry pointing at an inode. This lesson follows the kernel's I/O path: the VFS abstraction over every filesystem, the page cache and its writeback thresholds, buffered versus direct I/O, what fsync actually promises and what it does after a failure, ext4 journaling modes, and how a write finally reaches flash.

12 steps·~18 min
Computer Science
intermediate

Syscalls and the Kernel Boundary

User code cannot touch a disk, a network card, or another process's memory without crossing into the kernel, and the crossing is not free. This lesson covers ring transitions and the syscall path, interrupts versus traps versus exceptions, measured boundary costs, why vDSO and io_uring exist, and how namespaces plus cgroups turn ordinary kernel features into containers.

11 steps·~17 min
Computer Science
intermediate

Processes and Scheduling: What the Kernel Actually Runs

A process is an address space plus a control block, and the thing Linux actually schedules is neither. This lesson walks the kernel side: what a context switch physically costs, how per-CPU run queues and preemption flags work, how nice values become weights, and why CFS was replaced by EEVDF in Linux 6.6.

11 steps·~17 min
Computer Science
advanced

What Has Actually Been Verified, and What It Cost

Two landmark systems carry machine-checked proofs of real code: a C compiler and an operating system kernel. Their published effort figures give the honest price of full verification, and their trusted computing bases show exactly what a proof still leaves unproved. This lesson uses both to decide where verification pays.

8 steps·~12 min