Chapter 7: Demand Paging, Page Faults, and Copy-on-Write

Updated

October 31, 2026

Chapter 7: Demand Paging, Page Faults, and Copy-on-Write

When a Linux process allocates virtual memory, creates child processes, or memory-maps massive multi-gigabyte files, the operating system does not eagerly allocate physical DRAM frames or copy data blocks. Instead, Linux relies on Lazy Evaluation: deferring work until the exact instruction cycle where an address is accessed.

This fundamental optimization is powered by Demand Paging and Copy-on-Write (COW), driven by hardware page fault interrupts.

This chapter explores the page fault lifecycle: major vs minor faults, the role of backing files, Copy-on-Write mechanics during fork(), memory sharing semantics (MAP_SHARED vs MAP_PRIVATE), and an architectural walkthrough of the kernel’s fault handler in mm/memory.c.

The Lazy Loading Philosophy: Demand Paging

Consider an executable binary such as /usr/bin/python3 (\(4\text{ MiB}\) in size). When the user executes the command:

  1. The kernel does not read 4 MiB of machine code from NVMe storage into physical RAM.
  2. The kernel merely establishes virtual memory areas (vm_area_struct) covering the addresses and configures the process’s page tables with the Present bit cleared (P = 0).
  3. When the CPU instruction pointer jumps to main(), the MMU encounters P = 0 and triggers an interrupt: a Page Fault.
  4. The kernel reads only the single \(4\text{ KiB}\) page containing main() into RAM, sets P = 1, and resumes the CPU.

Pages containing code for rarely used Python modules (e.g., legacy math libraries or cleanup handlers) are never read from disk if the script never invokes them.

Page Fault Types and Handling Flow

Page faults are not errors; they are the primary mechanism by which virtual memory is dynamically constructed:

┌────────────────────────────────────────────────────────────────────────┐
│                        PAGE FAULT DECISION ENGINE                      │
│                                                                        │
│   MMU detects Present Bit == 0 or Permission Violation                 │
│          │                                                             │
│          ▼ Interrupt 14 (#PF) -> do_page_fault()                       │
│   Find VMA containing faulting address (via mm->mm_mt Maple Tree)      │
│          │                                                             │
│          ├─► Address NOT in any valid VMA ──► SIGSEGV (Invalid Pointer)│
│          │                                                             │
│          ├─► Write to Read-Only VMA       ──► SIGSEGV (Perm Denied)    │
│          │                                                             │
│          ▼ Address IS in a valid VMA:                                  │
│   ┌──────────────────────────────────────────────────────────────┐     │
│   │ Anonymous Memory (Heap/Stack/BSS)                            │     │
│   │ ├─ Allocate zeroed page from Buddy Allocator                 │     │
│   │ └─ Install PTE with RW permissions -> MINOR FAULT            │     │
│   ├──────────────────────────────────────────────────────────────┤     │
│   │ File-Backed Page (Code/Data)                                 │     │
│   │ ├─ Check Page Cache: Cached in RAM?                          │     │
│   │ │  ├─ YES: Map page into PTE -> MINOR FAULT                  │     │
│   │ │  └─ NO:  Issue Block I/O read to disk -> MAJOR FAULT       │     │
│   ├──────────────────────────────────────────────────────────────┤     │
│   │ Copy-on-Write (COW) Fault                                    │     │
│   │ └─ Allocate new private page, copy 4 KiB, set PTE to writable│     │
│   └──────────────────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────────────────┘

Major and Minor Faults: Performance Implications

Page faults are categorized by their I/O requirements:

  • Minor Page Fault: Resolved entirely in memory without accessing secondary storage. Examples: allocating a zero-filled heap page, resolving a COW write, or mapping a file page that is already cached in the kernel page cache.
    • Latency: \(1\text{ to } 5\text{ microseconds}\).
  • Major Page Fault: Requires reading data from physical disk (block device) or pulling a page back from swap storage. The faulting thread must sleep until the I/O completion interrupt fires.
    • Latency: \(50\text{ to } 500\text{ microseconds}\) (NVMe SSD) or \(5\text{ to } 15\text{ milliseconds}\) (spinning HDD).
# Observe major and minor fault rates across the system
sar -B 1 5

A sudden spike in major page faults is the universal indicator of memory thrashing, indicating that the working set of the system exceeds physical RAM.

Copy-on-Write Semantics and Implementation

The Unix process creation model relies on fork(): duplicating the parent process to create an identical child.

If fork() eagerly duplicated every physical page owned by a \(64\text{ GiB}\) in-memory Redis database, process creation would stall for several seconds and consume an additional 64 GiB of RAM.

Instead, Linux uses Copy-on-Write (COW):

┌────────────────────────────────────────────────────────────────────────┐
│                        COPY-ON-WRITE (COW) MECHANICS                   │
│                                                                        │
│   Step 1: fork() is called                                             │
│   ├─ Child receives duplicated page tables pointing to SAME phys frames│
│   ├─ All writable pages are marked READ-ONLY in BOTH page tables       │
│   └─ Inode page reference count incremented (_refcount = 2)           │
│                                                                        │
│   Step 2: Parent or Child attempts write (MOV [addr], 42)              │
│   ├─ MMU triggers Page Fault (#PF) due to Write on Read-Only page      │
│   ├─ Kernel page fault handler detects VMA allows writing (VM_WRITE)   │
│   ├─ Allocates a NEW physical page from the Buddy Allocator            │
│   ├─ Copies 4 KiB data from original page to new page                  │
│   ├─ Updates faulting process PTE to point to new page with R/W bit set│
│   └─ Original page reference count decremented (_refcount = 1)         │
└────────────────────────────────────────────────────────────────────────┘

If the child process immediately calls execve() (e.g., launching a sub-command), zero data pages are ever copied!

Shared Memory Regions and mmap SHARED vs PRIVATE

When mapping files or memory with mmap(): - MAP_PRIVATE: Changes are private to the calling process and enforced via Copy-on-Write. Modifications are never written back to the backing file on disk. - MAP_SHARED: Modifications are visible to all other processes mapping the same region and are flushed directly back to the physical file on disk by kernel writeback daemons.

Source Walkthrough: The Page Fault Handler in mm/memory.c

When an architectural page fault exception fires, control transfers to handle_mm_fault() in mm/memory.c:

vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
                           unsigned int flags, struct pt_regs *regs)
{
    /* 1. Walk the page table tree down to the PMD level */
    pgd = pgd_offset(mm, address);
    p4d = p4d_alloc(mm, pgd, address);
    pud = pud_alloc(mm, p4d, address);
    pmd = pmd_alloc(mm, pud, address);

    /* 2. Check for Transparent Huge Page (THP) fault (2 MiB) */
    if (pmd_none(*pmd) && vma_is_thp(vma))
        return create_huge_pmd(&vmf);

    /* 3. Allocate PTE page table if missing */
    pte = pte_offset_map(pmd, address);

    /* 4. Resolve the specific fault type */
    if (!pte_present(*pte)) {
        if (pte_none(*pte))
            return do_anonymous_page(&vmf); /* Zero-fill new memory */
        else
            return do_swap_page(&vmf);      /* Swap in page from disk */
    }

    if (flags & FAULT_FLAG_WRITE) {
        if (!pte_write(*pte))
            return do_wp_page(&vmf);        /* Copy-on-Write resolution */
    }

    return 0;
}