Operating Systems 📂 Memory Management · 4 of 5 46 min read

Virtual Memory & Demand Paging — Page Faults, EAT, Copy-on-Write

Master Virtual Memory and Demand Paging from Galvin's Operating System Concepts through three interactive step-by-step animations. Watch the valid bit flag page faults across a sequence of accesses, walk through the complete 7-stage page fault handling routine, and translate five addresses on a mixed valid/invalid page table. Two worked numericals cover EAT calculations (41× and 5× slowdowns) and address translation with faults.

Section 01

The Story That Explains Virtual Memory

The Library with 10× Books but Only 1× Reading Room
Imagine a university library that owns 10 million books but has physical shelves for only 1 million. Students walk in and request books by title — they don't know or care whether the book is on the shelf right now or in deep storage two miles away.

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.
💡
The Core Idea

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.


Section 02

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.

📈
Programs Bigger than RAM
the killer feature
A program with a 4 GB address space can run happily on a machine with 512 MB physical RAM. Only the "hot" pages need to be resident.
👥
More Programs at Once
higher multiprogramming
Since each program uses less RAM at a time, more programs can share the same machine. CPU utilisation rises; throughput rises.
Faster Program Load
start executing sooner
Programs don't need the full binary loaded before starting. Just load the first few pages (main function, static data) and let demand paging bring in the rest as needed. Startup time drops dramatically.
Virtual vs Physical Address Space
Virtual (4 GB per process) Physical RAM (512 MB) Disk (Swap) page 0 (code) page 1 (code) page 2 (data) page 3 (data) ... page N (heap) ... page M (stack) page 0 → f4 page 2 → f1 page M → f7 page 1 → f3 page N → f2 page 3 page 4 page 5 ... page 100 ... page M−1 MMU maps swap out / in Virtual space (huge) → some pages in RAM (few), rest on disk. Illusion of infinite memory.

Section 03

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.

📋 What Happens on First Access
Access
Process references a virtual address. MMU looks up the page table entry.
In memory?
If the valid bit is set → get the frame number, proceed as normal.
Not in memory
If valid bit is clear → the page is on disk (or invalid). Hardware triggers a page fault.
OS handles
OS finds a free frame, reads the page from disk into it, updates the page table, restarts the instruction.
Retry
Instruction re-runs. Now the page is in memory. Access succeeds. Process never knew it happened.
🔑
Why "Lazy" Works

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.


Section 04

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."

v — Valid
Page is in memory, frame number is meaningful
Access proceeds normally — physical address computed
Frame number field points at a real frame
i — Invalid
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

Section 05

🎮 Interactive — Detecting a Page Fault

Page table with a mix of valid and invalid entries. Watch how the MMU treats each access.

PageFrameValid
04v
16v
2i
32v
4i
57v
6i
70v
Valid bit check — some accesses hit, some fault
CPU requested page: offset: PAGE TABLE LOOKUP frame: valid bit: ✅ HIT physical: access: complete 🛑 PAGE FAULT action: trap to OS will need: load from disk Access #: 0 Hits: 0 Faults: 0 Fault rate: — Access sequence: pages 0, 3, 2, 5, 4, 7 (page size 100 bytes for simplicity)
Step 0 of 6
START Six accesses coming: pages 0, 3, 2, 5, 4, 7. Some are valid (in RAM), some invalid (would fault). Click Next to fire each one.

Section 06

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

Full page fault handling sequence — 7 stages
Page Fault Handling — 7 Stages CPU (user) accessing page "?" OS Kernel idle (waiting) PAGE TABLE ENTRY valid: i frame: Physical RAM frames available Disk (Swap) page stored here CURRENT STAGE Ready — click Next
Step 0 of 7
START A user process tries to access page 2. The page table says valid=invalid. Click Next to walk through the 7 stages of page fault handling.

Section 07

Pure Demand Paging vs Prepaging

💣 Pure Demand Paging
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
🔥 Prepaging
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)

Section 08

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).

📈
The Formula

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

⚠️
Why Page Faults Must Be Rare

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.


Section 09

Numerical Problem 1 — Compute Effective Access Time

Given

📋 Parameters
Memory access time (ma) = 200 ns
Page-fault service time (Tfault) = 8 ms = 8 000 000 ns
Page fault rate (p) = varies per part
📋 Formula
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 / 20041×
🚩
Whoa

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:

EAT220
(1 − p) × 200 + p × 8 000 000220
200200p + 8 000 000p ≤ 220
7 999 800p ≤ 20
       p ≤ 20 / 7 999 800
       p ≤ 2.5 × 10⁻⁶
       p ≤ 1 in 400 000 accesses
🏆
The Punchline

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.


Section 10

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):

PageFrameValid
03v
1i
21v
35v
4i
52v
67v
7i
Multi-address translation with fault detection
Logical Address
p = ?, d = ?
Page Table Lookup
frame = ?, valid = ?
Result
Awaiting first address…
Access #: 0 Hits: 0 Faults: 0 Fault rate:
Step 0 of 5
START Five addresses to translate: 500, 1500, 2500, 4500, 5500. Click Next for the first.

Section 11

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.

📋 Copy-on-Write (COW) Protocol
Fork
Instead of copying, both parent and child share the same physical pages.
Mark
All shared pages are marked read-only in both processes' page tables.
Read
Reads work normally in both processes — no change needed.
Write
The first write triggers a fault. OS notices "this was COW-shared"; makes a private copy for the writer; marks it writable; restarts the write.
Payoff
Pages that both processes only read (or never touch) are never duplicated. fork() becomes essentially free.
🔑
Why This Matters

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.


Section 12

Real-World Applications

💾
Every Modern OS
Linux, Windows, macOS all use demand paging plus copy-on-write for fork. Without them, running dozens of GB-scale programs on a laptop with 8 GB RAM would be impossible.
Universal · Demand paging
📱
Mobile Devices
Android and iOS rely heavily on demand paging. Apps in the background have most of their pages paged out, so foreground apps get the RAM they need.
iOS · Android · Low-mem
🖥️
Memory-Mapped Files
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.
mmap · Zero-copy
📂
Shared Libraries
libc.so is memory-mapped into every process using it. Pages are demand-loaded on first access. Same code, one copy, shared by hundreds of processes.
.so · .dll · Shared code
🔬
Docker Container Startup
Containers use copy-on-write file systems (overlayfs) — same idea as memory COW applied to disk. Instant startup, minimal disk usage.
Docker · OverlayFS · COW
🎮
Game Level Loading
Modern games mmap gigabytes of asset files. Only the assets currently visible on screen get paged in. Open-world games use this for near-seamless streaming.
Asset streaming

Section 13

Golden Rules — Virtual Memory & Demand Paging

🔑 Galvin's Non-Negotiable Rules
1
Virtual memory decouples the logical address space (large) from the physical memory (small). A program's virtual space can be much larger than the RAM installed.
2
Demand paging loads a page into RAM only when first accessed. No prediction, no pre-loading — trust locality of reference to keep the working set small.
3
Every page table entry has a valid bit. Clear = page not in RAM. On access, hardware triggers a page fault.
4
Page fault handling: trap to OS → verify legal address → find free frame (or evict) → read page from disk → update page table → restart instruction. Six or seven distinct stages.
5
A page fault costs millions of cycles because disk is millions of times slower than RAM. This is why page faults must be rare (typically < 1 in 100 000).
6
EAT formula for demand paging: EAT = (1 − p) × ma + p × Tfault. Because Tfault ≫ ma, even a tiny p degrades performance catastrophically.
7
The instruction must be restartable after a page fault. This means the OS must be able to restore CPU state exactly as it was just before the faulting instruction — a subtle constraint that shaped modern instruction set design.
8
Pure demand paging starts with zero pages in RAM — every startup page is a fault. Prepaging loads a predicted set upfront. Modern OSes blend both via read-ahead.
9
Copy-on-Write makes fork() near-free: parent and child share pages read-only; only on write does the OS actually duplicate. Fundamental to Unix.
10
Virtual memory is not free — it depends on a working page replacement algorithm to decide which page to evict when RAM fills up. That's the topic of the next chapter.