Virtual Memory Explained

September 14, 2026 • 6 min read

Virtual Memory Explained
Table of Contents

Part of the series:Operating Systems

The scheduling post decided who runs; this post decides what each runner sees. Every process believes it owns gigabytes of private, contiguous memory, yet there is one RAM shared among all tenants. The fiction is virtual memory1, and it also closes a loop opened two series ago: the CPU post waved at malloc as library bookkeeping over brk and mmap without explaining either. We will look at address spaces, paging and page tables, the TLB that makes translation affordable, page faults and swap, and finally how mmap, brk, and malloc fit together.

Address spaces: every process gets its own universe

An address space is the set of virtual addresses a process may use: its code, heap, stacks, shared libraries, and kernel mappings, each at fixed conventional places. The addresses are virtual: they name slots in the process’s private universe, not bytes in RAM. The memory hardware translates every virtual address to a physical one on each access, consulting the current process’s page table, and refuses translations the table forbids.

process A addresses        RAM                process B addresses
0x400000: code      ──▶  frame 91              0x400000: code ──▶ frame 12
0x7fff00: stack     ──▶  frame 7               0x7fff00: stack ──▶ frame 44
0x900000: (unmapped)──▶  fault                 (same numbers, other frames)

Same numbers, different frames, enforced by hardware. That is the whole isolation trick from the processes post, stated as mechanism: separate tables, separate worlds.

Paging: memory in fixed-size sheets

Paging2 divides virtual memory into fixed-size pages (commonly 4 KiB) and RAM into same-size frames. Each page maps to some frame, or to nothing, or to storage. The page table3 records the mapping plus permission bits (read, write, execute, user-accessible) checked on every access.

A virtual address splits into two halves: the page number selects the table entry, the offset selects the byte within the frame:

virtual address (64-bit, 4 KiB pages)
┌──────────────────────────────┬──────────────┐
│ page number (52 bits)         │ offset (12)  │
└──────────────┬───────────────┴──────┬───────┘
               ▼                      │
        page table lookup             │
        page 7 → frame 42, rw         │
               ▼                      ▼
        physical address = frame 42 + offset

Real tables are multi-level (four levels on x86-64): unmapped regions cost no table memory because whole subtrees are simply absent. Permissions ride along for free: mark code pages execute-but-not-write, mark kernel pages supervisor-only, and entire bug classes (overwriting code, user code reading kernel memory) die in hardware with a fault instead of silent corruption.

The TLB: making translation affordable

Translating every access through four table levels in RAM would multiply memory traffic severalfold. The translation lookaside buffer (TLB)4 avoids it: a tiny cache inside the memory unit holding recent translations. Common case, the translation hits the TLB and costs roughly nothing; rare case, the hardware walks the table, caches the result, and continues. Locality saves the day again: programs reuse pages, so a few dozen TLB entries cover most accesses.

The TLB explains two costs from earlier posts. Process switches must invalidate (or retag) TLB entries, since translations belong to one address space: part of the hidden price of a context switch. And huge working sets that thrash the TLB pay a walk per access, one reason large pages (2 MiB, 1 GiB) exist: fewer, bigger translations covering the same memory with fewer entries.

Page faults and swap: memory larger than RAM

A page fault5 fires when translation fails: the page is unmapped, protected, or not currently resident. Faults are not always errors. The kernel deliberately leaves pages non-resident and fills them on demand: the first touch of fresh memory, a page swapped out to disk, a file mapping not yet read, a copy-on-write page shared since fork. The fault handler finds a frame (evicting some victim page, writing it to swap space6 if dirty), loads the contents, updates the table, and resumes the faulting instruction as if nothing happened.

program touches page ──▶ TLB miss, table says "on disk"


                     fault handler: evict victim frame,
                     read page from swap/file, map it,
                     resume instruction

Eviction policy decides which victim to sacrifice: approximations of least-recently-used (clock algorithms) track usage bits the hardware sets, sweeping hands around a circular list. The catastrophic failure has a name: thrashing, when the working set exceeds RAM and every access faults, so the machine spends its time paging instead of computing. The cure is fewer tenants or more RAM; no algorithm outruns arithmetic.

mmap, brk, and malloc: where heap memory comes from

Now the loop closes. The kernel hands out memory in pages through two syscalls. brk (and sbrk) moves the top of the data segment, growing one contiguous heap region upward. mmap7 maps pages anywhere: files into memory, anonymous zeroed pages for fresh heap, shared regions between processes. Modern allocators use both: small allocations extend the brk heap, large ones get dedicated mmap regions (easy to return whole).

And malloc8 itself is not a syscall at all. It is library code managing those kernel pages: carving them into blocks, tracking free lists, splitting and coalescing, and calling brk or mmap only when its pools run dry. free usually returns blocks to the pool without telling the kernel, which is why process memory rarely shrinks after a spike. The CPU post’s one-liner is now a full picture: conventions in userspace, pages from the kernel, translation in hardware.

The big picture

Virtual addresses name a private universe; paging maps it page by page with hardware-checked permissions; the TLB caches translations so the common case stays fast; faults fetch the missing pieces from swap or files on demand; and mmap plus brk supply the raw pages that malloc subdivides. Isolation, oversubscription, and on-demand loading all fall out of one table per process.

But RAM forgets and swap is scratch: neither keeps anything worth keeping. Programs that must survive power loss need names, folders, permissions, and crash-safe updates on persistent storage. That durable world, blocks into files, is the next post’s subject.

Footnotes

  1. Virtual memory - Wikipedia

  2. Paging - Wikipedia

  3. Page table - Wikipedia

  4. Translation lookaside buffer - Wikipedia

  5. Page fault - Wikipedia

  6. Paging - Wikipedia

  7. mmap - Wikipedia

  8. C dynamic memory allocation - Wikipedia