Why Virtual Memory?
Every process on the machine believes it owns a large, private, contiguous memory starting at the same address; one level of indirection — a per-process map from virtual pages to physical frames — makes that belief true and buys isolation, relocation, lazy allocation, sharing and protection in a single mechanism.
The problem
Progressive depth
The same mechanism at different altitudes — start where you are.
Each process sees its own big, contiguous memory. The kernel keeps a table per process translating its addresses to real RAM, so two processes using the same address get different memory, and untouched addresses cost nothing.
The problem: everyone wants the same addresses
Early machines ran one program at a time with physical addresses: the address in the instruction was the address on the memory bus. Running two programs meant one of two bad options. Relocate at load time — patch every address in the binary to wherever it happened to land — which works until the program is swapped out and reloaded elsewhere, and does nothing to stop it reading its neighbour. Or give each program a base and bound register pair: the hardware adds the base to every address and faults if the result exceeds the bound. That provides isolation and relocation, but only for one contiguous block per program, so memory fragments as programs of different sizes come and go, and there is no way to share a library between two programs or to run a program larger than RAM.
The requirements that accumulated over the 1960s and 70s are the ones we still have: each process wants a contiguous private address space starting wherever its linker chose; the OS wants to place processes anywhere in RAM, move them, and reclaim pieces; processes must not read or corrupt each other or the kernel; shared code (libc is mapped into nearly every process) should exist once in RAM; and the sum of what processes *reserve* should be allowed to exceed what physically exists, because most of it is never touched at the same time.
The trick: one indirection per page
Instead of one base register per process, give every process a map. Chop both the virtual address space and physical RAM into fixed-size pages (4 kB is the common choice; see Paging) and keep, per process, a table that says for each virtual page which physical frame holds it — or that it holds nothing yet. The CPU’s memory management unit (MMU) consults that table on every access, transparently, so the program sees only virtual addresses and the memory bus sees only physical ones. The kernel owns the tables; user code cannot even see them (User Mode vs Kernel Mode).
One indirection, and every requirement falls out. Two processes both using virtual address 0x400000 have two table entries pointing at two different frames — isolation and relocation at once, and a process can be moved by editing its table. An entry marked "not present" costs no RAM until it is touched — lazy allocation and overcommit. Two tables pointing at the same frame — sharing, so libc’s code exists in RAM once. Permission bits on each entry — protection: code is read-only and executable, data is writable and not executable, kernel pages are inaccessible from user mode. And a page that has been written to disk can be marked not-present and restored on demand — swap, and running programs larger than RAM.
What the indirection buys
Each property below is a consequence of the same table; none needs separate hardware. That is the reason virtual memory won over every alternative: it is a single mechanism with a single cost, not five features with five costs.
- Isolation: a process can only form virtual addresses, and its table maps only to frames it was given. There is no instruction that reaches another process’s frame.
- Relocation: binaries link to fixed addresses and still load anywhere; ASLR randomises where, per run, by editing the table, not the binary.
- Lazy allocation / overcommit:
mmapof 4 GB costs a few table entries; frames arrive on first touch (Page Faults, What Happens When I Allocate Memory?). The sum of reservations can exceed RAM. - Sharing: libc, the JVM, a database’s shared buffer pool, and the pages of a forked child (Copy-on-Write) exist once in RAM and appear in many tables.
- Protection: R/W/X per page. A write to code, an execute of the stack, a user access to kernel memory — each is a fault before it is a corruption.
- Persistence and swap: a file can appear as memory (Memory Mapping) and cold memory can be parked on disk (Memory Pressure, Swap and the OOM Killer) because presence is just a bit in the entry.
A short history: segmentation
Between base-and-bound and paging came segmentation: divide a program into a few variable-sized logical segments (code, data, stack, each library), each with its own base, bound and permissions. It solved sharing and protection per segment and matched how programmers thought about programs. It did not solve fragmentation — segments are variable-sized, so RAM fills with unusable holes — and it made the address two-part (segment selector plus offset), which every compiler and programmer had to reason about. 16-bit x86 real mode and 32-bit protected mode carried segments; Multics and the Burroughs machines built whole systems on them.
Paging won because fixed-size pages fragment only inside a page (at most 4 kB wasted per mapping) and never between pages, and because the map can be walked by hardware without the program knowing. x86-64 effectively retired segmentation: the segment bases are forced to zero in 64-bit mode (with FS/GS surviving as thread-local-storage base registers), and ARM never had it. Where segmentation’s ideas live on is in the *permissions* per region — a modern process’s address space is a list of regions with different protections, mapped over pages.
What it costs
The table has to be consulted on every memory access, and it lives in memory — so a naive implementation doubles the cost of every load and store, and a multi-level table (Page Tables) makes it four or five extra loads. The whole design is viable only because the CPU caches translations in the The TLB, which hits on the vast majority of accesses. When it misses, or when a page is not present, the cost is a page-table walk or a trap (Page Faults) — and when the working set exceeds RAM, the cost is thrashing (Memory Pressure, Swap and the OOM Killer). Every remaining lesson in this module is about one of those costs and the machinery that keeps it small.
The other cost is the tables themselves. A process’s page tables are real memory (a few MB for a process with a few GB mapped; the kernel’s own tables for a 1 TB machine are gigabytes), a context switch has to change the active table (Context Switching), and the kernel spends real effort keeping many tables coherent when a shared page changes (TLB shootdowns across cores). Huge pages exist to shrink all three.
Key points
- Virtual memory is a per-process map from virtual pages to physical frames, consulted by the MMU on every access and owned by the kernel.
- The one indirection provides isolation, relocation, lazy allocation and overcommit, sharing, protection and swap — all from the same table.
- Base-and-bound and segmentation solved subsets of the problem and fragmented RAM; fixed-size paging made the map hardware-walkable and fragmentation bounded.
- A process can only form virtual addresses; there is no path to another process’s frame except through a table the kernel wrote.
- The cost is translation on every access, paid for by the TLB, plus page faults and the tables themselves.
- ASLR, shared libraries, fork, mmap and swap are all applications of editing the map.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why not just give each process a base register?
A base register relocates one contiguous block. It cannot share a library between processes, cannot leave holes for lazy allocation, cannot protect code from writes separately from data, and fragments RAM. A per-page map does all of those with one mechanism.
▸Why can processes reserve more memory than exists?
Because a table entry marked not-present costs nothing. Frames are attached on first touch, and most reservations — stacks, sparse tables, oversized buffers — are never fully touched at once. The bet fails under memory pressure, which is where the OOM killer lives.
▸Why is a page fault not an error?
Because "not present" is a normal state in the map: not yet allocated, shared and copy-on-write, on disk. The fault is the kernel’s chance to make the page present. Only an address outside every mapping is an error.
▸Why do all processes see libc at a different address every run?
ASLR: the kernel picks a random virtual base for each mapping and writes it into the table. The physical frames of libc are the same for every process; only the map differs.
How it fails
What the failure looks like from inside real software.
- Believing "free" memory is what is available: the page cache is reclaimable and RSS double-counts shared pages;
MemAvailableis the honest number. - A process with 40 GB virtual and 2 GB resident is fine; a process with 2 GB virtual and 2 GB resident on a 2 GB box is about to thrash — VSZ tells you almost nothing.
- Overcommit accepted a 100 GB reservation on a 64 GB machine; the process touched it and the OOM killer chose a victim at 3 a.m.
- A JIT that writes code into a page mapped R/W and executes it faults under W^X policies; the fix is
mprotectto flip the page to R/X after writing. - An ASLR-disabled binary (for a debugger) behaves differently from production because addresses were being compared or hashed.