Operating System
Virtual Memory: Three Illusions the OS Maintains Without You Noticing
Every programmer believes they own all of RAM. That's false, and the OS's job is making sure you never have to notice it's false. Base-and-bound, paging, the TLB — all three are just different engineering for maintaining the same set of lies, more efficiently.
The three illusions, named explicitly
- Exclusive ownership of the address space — as if no other process exists
- Contiguity — as if memory is one unbroken block, even when it's physically scattered
- More memory than physically exists — running a 12GB workload on 8GB of RAM
None of these are true at the hardware level; all three are true from inside the process. Before virtual memory existed, programmers manually split programs into chunks and swapped pieces in and out of tiny core memory by hand — it automated a job humans used to do themselves.
What breaks without it
- Fragmentation. 1GB split into three running processes plus one free block. One process exits — you don't get a clean free region back, you get a gap between two still-running processes. Enough of this and you can have, say, 850MB technically free with no single contiguous 300MB chunk to give a new process.
- Insufficient memory. A program legitimately needs more address space than the machine physically has.
Logical vs physical address
The CPU never touches physical memory directly. Every address a process uses is a logical (virtual) address; the Memory Management Unit (MMU) translates it into a physical address before anything touches real RAM.
flowchart LR
A["CPU: logical address"] --> B{"MMU"}
B -->|"within bounds"| C["Physical address"]
C --> D["Main Memory"]
B -->|"out of bounds"| E["Trap to OS<br/>(segmentation fault)"]
CPU fetches an instruction referencing a logical address → sends it to the MMU → MMU applies a translation rule → if out of bounds, it traps and the OS steps in (this is a segmentation fault, mechanically) → if valid, the physical address goes out on the memory bus.
Division of labor: the OS allocates physical memory and sets up translation info; the MMU does the actual translation on every memory access. The OS hands the MMU what it needs once per context switch, then is out of the loop until the next trap.
Base and bound — worked examples
A process's memory image gets a starting physical address (base) and a size limit (bound). Logical address X → base + X, and if X exceeds bound, the MMU traps instead of translating.
- Base = 2400, bound = 670, logical address 410.
410 < 670→ valid. Physical address =2400 + 410 = 2810. - Base = 5600, bound = 940, logical address 1260.
1260 > 940→ the MMU traps before touching memory at all. Computing5600 + 1260 = 6860anyway is the mistake — the bound check isn't optional, it's the entire protection mechanism.
Loading base and bound registers is kernel-mode-only for the same reason the trap instruction exists for system calls: if a user process could set its own base and bound, it could widen its own bound or shift its base and read or overwrite any other process's memory. User code can't be trusted to police itself — same principle, applied twice.
Why paging replaced base-and-bound
Base-and-bound requires each process's memory to be one contiguous physical block, which brings fragmentation back. Paging splits a process's virtual address space into fixed-size chunks (commonly 4KB) and scatters them across any free physical frames, tracked in a per-process page table (virtual page number → physical frame number).
This trades one fragmentation problem for a smaller one: paging eliminates external fragmentation (no unusable gaps between allocations) at the cost of internal fragmentation (a process using 4.1KB still consumes two full 4KB pages). Small, bounded internal waste beats unpredictable, system-wide gap accumulation.
The TLB
Naive paging doubles memory traffic: every access needs a page-table read first, then the actual data access. The Translation Lookaside Buffer (TLB) caches recently-used virtual-to-physical mappings on the CPU.
- TLB hit: translation already cached → one memory access, same cost as no paging
- TLB miss: MMU walks the real page table in memory first, populates the TLB, then accesses the data
The TLB caches page table entries (VPN→PFN mappings), not actual memory contents — a different job from a regular CPU cache. Because mappings are specific to one process's address space, the TLB is flushed on every context switch.
Quick recall
- Three illusions: exclusive ownership, contiguous memory, more memory than physically exists
- Logical address → MMU → physical address; MMU translates on every access, OS sets it up once per context switch
- Base+bound: physical = base + logical address, trap if logical > bound. Only the kernel can set base/bound registers.
- Paging fixes base-and-bound's fragmentation: trades external fragmentation for smaller, bounded internal fragmentation
- TLB caches page table entries (not data) to avoid a second memory access per lookup; flushed on every context switch
Quick quiz
1. Base = 5600, bound = 940, logical address = 1260. What happens?
2. What does paging trade external fragmentation for?
3. What does the TLB actually cache?