What you'll learn
- Quick answer
- Why Virtual Memory Exists
- Pages and Frames: The Actual Unit of Translation
- The Page Table: Where the Mapping Actually Lives
- The TLB: A Cache for Translations
- Page Faults and Eviction Under Memory Pressure
- A Real Bug This Simulation Hit (And Why It Matters)
- Why This Matters Day to Day
- FAQ
Quick Answer
Virtual memory gives every process its own private address space that gets translated to real physical RAM addresses behind the scenes, in fixed-size chunks called pages. A page table holds that mapping, a small hardware cache called the TLB speeds up the lookup, and when a page a process needs isn't actually in RAM, the CPU triggers a page fault so the OS can load it from disk.
Why Virtual Memory Exists
Without virtual memory, every running program would need to know the real, physical RAM addresses it's allowed to touch, and the OS would need to make sure no two programs ever pick the same address at the same time. That's fragile and doesn't scale - it's also why old single-tasking systems really could crash the whole machine if one program wrote to the wrong address.
Virtual memory solves this by giving every process its own private address space, starting at address 0, as if it owned the entire machine. The CPU's memory management unit (MMU) translates every one of those virtual addresses into a real physical address at the moment of access, using a mapping the OS controls. Two processes can both think they're using address 0x1000 and never collide, because the OS maps each process's 0x1000 to a different real physical location, or to none at all if that process shouldn't have access - which is also where memory isolation between processes comes from.
Pages and Frames: The Actual Unit of Translation
Virtual memory doesn't translate addresses one byte at a time - that would need an impossibly large mapping table. Instead, both virtual and physical memory are divided into fixed-size chunks. A chunk of virtual memory is a page; the matching chunk of physical memory is a frame. Most systems use 4 KB pages by default (x86's standard page size, though huge pages of 2 MB or 1 GB exist for workloads that benefit from fewer, bigger mappings).
A virtual address splits into two parts: a virtual page number (which page) and an offset within that page (which byte). The MMU only needs to translate the page number - the offset passes through unchanged, since a page and its matching frame are the same size.
PAGE_SIZE = 4096 # 4 KB
vpn = virtual_addr // PAGE_SIZE
offset = virtual_addr % PAGE_SIZE
# physical_addr = (page_table[vpn] * PAGE_SIZE) + offset
The Page Table: Where the Mapping Actually Lives
The page table is the OS-maintained data structure mapping each process's virtual page numbers to physical frame numbers. Here's a real simulation - 3 physical frames available, a program touching 6 addresses across 4 distinct virtual pages, run in Python:
virtual=0x0000 -> vpn=0 offset=0x000 | pfn=2 -> physical=0x02000
virtual=0x1000 -> vpn=1 offset=0x000 | pfn=1 -> physical=0x01000
virtual=0x0004 -> vpn=0 offset=0x004 | pfn=2 -> physical=0x02004
virtual=0x2000 -> vpn=2 offset=0x000 | pfn=0 -> physical=0x00000Notice the second access to vpn 0 (at virtual address 0x0004) lands on the same physical frame (2) as the first access, just 4 bytes further in - exactly what the offset arithmetic guarantees. Every address inside one page always maps to the same frame.
The TLB: A Cache for Translations
Walking a full page table on every single memory access would be slow, so CPUs cache recent virtual-to-physical translations in a small, very fast piece of hardware called the Translation Lookaside Buffer (TLB). A TLB hit skips the page table walk entirely.
Running the same 6-address sequence with a 4-entry TLB tracked alongside the page table produced this real hit/miss trace:
virtual=0x0000 -> vpn=0 [TLB MISS -> page table (PAGE FAULT)]
virtual=0x1000 -> vpn=1 [TLB MISS -> page table (PAGE FAULT)]
virtual=0x0004 -> vpn=0 [TLB HIT]
virtual=0x2000 -> vpn=2 [TLB MISS -> page table (PAGE FAULT)]
TLB hits: 1, TLB misses: 3 (so far)The third access is the interesting one: same page (vpn 0) as the very first access, so the translation is still sitting in the TLB and the page table never gets touched at all.
Page Faults and Eviction Under Memory Pressure
With only 3 physical frames and a program touching 4+ distinct pages, something has to give. Extending the access sequence with a 4th and 5th distinct page, and a repeat access to an early page, produced this real trace (LRU - least recently used - eviction policy):
virtual=0x2000 -> vpn=2 [PAGE FAULT] (fills last free frame)
virtual=0x3000 -> vpn=3 [PAGE FAULT]
[evict] vpn=1 written back to disk, frame 1 reused, TLB entry invalidated
virtual=0x1000 -> vpn=1 [PAGE FAULT] (vpn 1 was evicted - has to fault back in)
Total page faults: 5
TLB hits: 1, TLB misses: 5, TLB hit rate: 16.7%This is demand paging in miniature: a page only gets loaded into a physical frame when something actually touches it, and when frames run out, the OS picks a victim (here, the least recently used page) to write back out and reclaim its frame for the new page.
A Real Bug This Simulation Hit (And Why It Matters)
Building this simulation surfaced two genuine bugs worth walking through, because they're not simulation artifacts - they're the exact class of bug real MMU designs have to guard against.
Bug 1 - LRU tracking that ignored TLB hits. The first version of the simulation only updated a page's "last used" timestamp when the page table was actually walked - meaning a page repeatedly hit in the TLB looked "cold" to the eviction logic, even though it was being used constantly. That produced a wrong eviction choice. The fix: real hardware sets a page's "accessed" bit on every reference, TLB hit or not, so the simulation's LRU tracking has to update on every access too, not just on a full page-table walk.
Bug 2 - a stale TLB entry after eviction. After fixing bug 1, a second, more serious bug appeared: when the OS evicted vpn 1 from the page table and reused its physical frame for a different page, the TLB still had vpn 1's old mapping cached. The next access to vpn 1 got a TLB "hit" - and confidently returned the wrong physical frame, one that now belonged to a completely different page.
This is exactly why real CPUs perform what's called a TLB shootdown: whenever the OS evicts a page (or changes its mapping, or a process exits), it must explicitly invalidate that page's TLB entry, on every core that might have cached it, before the frame gets reused. Skip this step and you get memory corruption - one process silently reading or writing another page's data through a translation that's already lying to it. Fixing this in the simulation (and re-running it) gave the correct final trace shown in the previous sections.
Why This Matters Day to Day
You don't manage page tables directly as an application developer, but the consequences of this machinery show up constantly:
- Thrashing: if a process's working set (the pages it actively needs) doesn't fit in the physical RAM it's been given, it spends most of its time faulting pages in and out instead of doing real work - this is why a container or VM sized too small for its workload can suddenly become far slower than a linear resource cut would predict.
- Swap / page file: what this post calls "disk" in the simulation is exactly what your OS calls swap space (Linux) or the page file (Windows) - the backing store for pages that don't currently fit in RAM.
- Why memory isolation is a hardware guarantee, not just an OS convention: a process literally cannot address another process's physical memory, because its virtual addresses only ever get translated through its own page table.
