The Story That Explains Virtual Memory
The librarian's rule: only fetch a book when someone actually asks for it. Most of the collection sleeps quietly in storage. When a request comes in for a book that's not on the shelf, the librarian sends a runner to bring it. If the shelf is full, she sends an older, less-requested book back to storage to make room. From the student's perspective, the library appears to hold all 10 million books at all times — the "virtual" collection is much larger than the "physical" shelves.
That's Virtual Memory. The program thinks it has gigabytes of RAM; the OS quietly juggles what's actually in physical memory versus on the disk. The runner-fetching-a-book moment is a page fault, and the whole scheme is called demand paging — pages come in only when demanded.
A process's logical address space can be much larger than the physical memory it actually uses at any moment. Only the pages the program is currently touching need to be in RAM; the rest live on disk and get pulled in on demand. This makes it possible to run a 4 GB program on a machine with 512 MB of RAM.
Introduction to Virtual Memory
Traditional memory management demanded that the entire program fit in physical memory before it could run. This wastes RAM (unused error handlers, rarely triggered code paths) and limits program size to available RAM. Virtual Memory throws that assumption away.
Demand Paging — Only Bring In What's Needed
Demand paging is the concrete technique that makes virtual memory work. Simple rule: a page is loaded into physical memory only when the CPU actually references it. Not before. Not speculatively. Only when demanded.
Most programs exhibit locality of reference — they touch a small "working set" of pages for a while, then move to another set. Loading everything upfront wastes time on pages that will never be used. Loading on demand costs one fault per new page and pays off dramatically for cold code paths.
The Valid/Invalid Bit
Every page table entry gets a new bit: the valid bit (v/i). It doesn't mean "is this a legal address in this process's space" — it means "is this page currently in physical memory."
| Page is in memory, frame number is meaningful |
| Access proceeds normally — physical address computed |
| Frame number field points at a real frame |
| Either: page is on disk (needs to be brought in) |
| Or: address is not part of process's logical space (bug) |
| Hardware raises a page fault — OS decides which case |
🎮 Interactive — Detecting a Page Fault
Page table with a mix of valid and invalid entries. Watch how the MMU treats each access.
| Page | Frame | Valid |
|---|---|---|
| 0 | 4 | v |
| 1 | 6 | v |
| 2 | — | i |
| 3 | 2 | v |
| 4 | — | i |
| 5 | 7 | v |
| 6 | — | i |
| 7 | 0 | v |
Page Fault Handling — What the OS Actually Does
When the valid bit is clear, the hardware traps to the OS. The OS then executes a carefully choreographed sequence to bring the missing page into memory and restart the instruction.
# Page fault service routine (simplified from Galvin)
def page_fault_handler(faulting_address):
# Step 1: check if reference is legal
if not in_process_address_space(faulting_address):
terminate_process("SIGSEGV")
return
# Step 2: locate the page on the backing store (disk)
disk_location = find_page_on_disk(faulting_address)
# Step 3: find a free frame in physical memory
frame = get_free_frame()
if frame is None:
frame = page_replacement() # evict a victim first
# Step 4: read the page from disk into the frame
disk_read(disk_location, frame) # this is slow — millions of cycles
# Step 5: update the page table
p = faulting_address / PAGE_SIZE
PageTable[p].frame = frame
PageTable[p].valid = True
# Step 6: restart the faulted instruction
restart_instruction()
🎮 Interactive — Page Fault Handling, Step by Step
Pure Demand Paging vs Prepaging
| Start process with zero pages in memory |
| The very first instruction fault brings in page 0 |
| Every new page = page fault (many faults at startup) |
| No wasted RAM — but poor startup performance |
| Load predicted set of pages before the process starts |
| Batches disk reads (more efficient than one at a time) |
| Risk: some prepaged pages never used → wasted work |
| Modern OSes use hybrid heuristics (read ahead by locality) |
Effective Access Time (EAT) for Demand Paging
With demand paging, the vast majority of accesses hit RAM at normal speed (memory access time ma). Occasionally a page fault occurs and costs the huge disk service time (millions of times slower).
Let p = page fault probability (0 ≤ p ≤ 1),
ma = memory access time,
Tfault = time to service a page fault.
EAT = (1 − p) × ma + p × Tfault
Typical numbers: ma = 200 ns, Tfault = 8 ms = 8 000 000 ns. The fault is 40 000× slower than a normal memory access. Even a 0.1% fault rate would multiply access time by 8×. Real systems must keep p below ~1 in 100 000 to maintain acceptable performance.
Numerical Problem 1 — Compute Effective Access Time
Given
| Memory access time (ma) = 200 ns |
| Page-fault service time (Tfault) = 8 ms = 8 000 000 ns |
| Page fault rate (p) = varies per part |
| EAT = (1 − p) × ma + p × Tfault |
| Slowdown = EAT / ma |
Part (a) — Fault Rate = 0.001 (one in 1000)
EAT = 0.999 × 200 + 0.001 × 8 000 000
= 199.8 + 8000
= 8199.8 ns
≈ 8.2 μs
Slowdown = 8199.8 / 200 ≈ 41×
A page fault rate of just 1 in 1000 makes memory 41 times slower. That's a 4000% slowdown from a 0.1% event. Disk is truly enormous relative to memory.
Part (b) — Fault Rate = 0.0001 (one in 10 000)
EAT = 0.9999 × 200 + 0.0001 × 8 000 000
= 199.98 + 800
= 999.98 ns ≈ 1 μs
Slowdown ≈ 5×
Part (c) — What Fault Rate Keeps Slowdown Under 10%?
We want EAT ≤ 1.1 × ma = 220 ns:
EAT ≤ 220
(1 − p) × 200 + p × 8 000 000 ≤ 220
200 − 200p + 8 000 000p ≤ 220
7 999 800p ≤ 20
p ≤ 20 / 7 999 800
p ≤ 2.5 × 10⁻⁶
p ≤ 1 in 400 000 accesses
To keep demand paging cheap (under 10% slowdown), the OS must ensure fewer than 1 fault per 400 000 memory accesses. This is why page replacement algorithms, working sets, and TLBs are studied so intensively — every fault avoided pays back thousands of memory accesses' worth of latency.
Numerical Problem 2 — Address Translation with Page Faults (Interactive)
Setup: Page size = 1024 bytes. Process has 8 pages logically. Page table below (some pages are in RAM, some are still on disk):
| Page | Frame | Valid |
|---|---|---|
| 0 | 3 | v |
| 1 | — | i |
| 2 | 1 | v |
| 3 | 5 | v |
| 4 | — | i |
| 5 | 2 | v |
| 6 | 7 | v |
| 7 | — | i |
Copy-on-Write — A Beautiful Optimisation
When a process forks (creates a child), the child normally gets an identical copy of the
parent's address space. Naively, this means duplicating potentially gigabytes of memory —
even if the child immediately calls exec() and throws it all away.
fork() becomes essentially free.
fork() is used millions of times a day in Unix. Without COW, every fork
would copy the parent's entire memory (often multi-GB). With COW, most forks are
near-instant. This one optimisation is why the Unix "fork + exec" pattern remained
practical as programs grew from KBs to GBs.
Real-World Applications
mmap() maps files into virtual address space, deferring the actual disk read
until pages are touched. Enables zero-copy IO for databases, media players, and browsers.
Golden Rules — Virtual Memory & Demand Paging
EAT = (1 − p) × ma + p × Tfault. Because Tfault ≫ ma, even a tiny p degrades performance catastrophically.fork() near-free: parent and child share pages read-only; only on write does the OS actually duplicate. Fundamental to Unix.