Page Fault Handling in Virtual Memory Explained

A page fault is an essential hardware-software mechanism that occurs when a CPU attempts to access a virtual memory address not currently mapped to physical RAM. This article explores the nature of page faults, differentiates their types, and details the step-by-step process by which an operating system's Virtual Memory Manager (VMM)—implemented in the C programming language—resolves these faults to maintain seamless program execution.

What Is a Page Fault?

When an application accesses memory, it references virtual addresses rather than physical memory locations. The Memory Management Unit (MMU) uses page tables to translate these virtual addresses into physical addresses.

A page fault is an exception raised by the MMU when this translation cannot be completed. Common reasons include:

  • Demand Paging: The requested page has not yet been loaded from disk into physical memory (RAM).
  • Swapped Pages: The page was temporarily written to disk (swap space) to free up RAM.
  • Permission Violations: A process attempts to write to a read-only page (such as Copy-on-Write) or access kernel-level space from user mode.
  • Invalid Addresses: The virtual address does not belong to the process's allocated address space, leading to a segmentation fault.

Page faults fall primarily into three categories:

  1. Minor Page Fault: The page is present in physical memory but lacks an active mapping in the process's page table (e.g., shared libraries, or memory already cached by the kernel).
  2. Major Page Fault: The required data must be read from secondary storage (such as an SSD or HDD), incurring I/O latency.
  3. Invalid Page Fault: An access violation that causes the kernel to terminate the offending thread or process.

Hardware Detection and the CPU Trap

When an unmapped or restricted address is accessed:

  1. The MMU halts instruction execution and saves the faulting virtual address to a dedicated CPU control register (such as CR2 on x86 architectures).
  2. The CPU generates an interrupt/exception vector (e.g., Interrupt 14 on x86).
  3. The CPU saves the current register state, pushes an error code to the kernel stack, and transfers execution to the kernel’s Interrupt Service Routine (ISR).

How the Kernel VMM Handles the Fault in C

Within an OS kernel written in C, the interrupt handler routes the execution flow to a core routine, commonly designated as do_page_fault(). The VMM resolves the issue through the following sequence:

1. Extracting Fault Information

The handler retrieves the faulting address and the CPU error code to determine whether the access was a read or write operation, and whether it originated in user mode or kernel mode.

struct pt_regs {
    unsigned long ip;
    unsigned long sp;
    unsigned long flags;
    /* Architecture-specific register state */
};

void do_page_fault(struct pt_regs *regs, unsigned long error_code) {
    unsigned long fault_address;
    
    /* Read the faulting address from the architecture register (e.g., CR2) */
    __asm__ volatile("mov %%cr2, %0" : "=r"(fault_address));
    
    handle_mm_fault(current_process, fault_address, error_code);
}

2. Validating the Address Against Memory Regions

The kernel checks if the address falls within a valid virtual memory area (VMA) associated with the current process.

struct vm_area {
    unsigned long vm_start;
    unsigned long vm_end;
    unsigned int vm_flags; /* VM_READ, VM_WRITE, VM_EXEC */
    struct vm_area *vm_next;
};

int handle_mm_fault(struct task_struct *task, unsigned long address, unsigned long error_code) {
    struct vm_area *vma = find_vma(task->mm, address);

    /* Address is not allocated to the process */
    if (!vma || address < vma->vm_start) {
        send_signal(task, SIGSEGV);
        return -1;
    }

    /* Check permissions (e.g., write to a read-only page) */
    if ((error_code & FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) {
        send_signal(task, SIGSEGV);
        return -1;
    }

    return resolve_page(vma, address, error_code);
}

3. Allocating a Physical Frame

If the address is valid, the kernel must locate or allocate an available physical page frame using its internal page allocator.

struct page {
    unsigned long physical_address;
    int ref_count;
};

int resolve_page(struct vm_area *vma, unsigned long address, unsigned long error_code) {
    struct page *physical_page = allocate_physical_frame();
    
    if (!physical_page) {
        /* Run page-replacement algorithms (e.g., LRU) to swap pages to disk */
        physical_page = reclaim_or_swap_page();
    }

4. Reading Data from Storage (Demand Paging)

For pages backed by a file (such as an executable binary) or swap space, the VMM initiates disk I/O to read the page content into the allocated physical frame. During this I/O operation, the requesting thread is moved to a sleeping state, allowing the scheduler to run other threads.

    if (is_file_backed(vma)) {
        read_page_from_disk(vma->file, address, physical_page->physical_address);
    } else {
        /* For anonymous memory, zero the allocated frame to avoid leaking data */
        memset((void *)physical_page->physical_address, 0, PAGE_SIZE);
    }

5. Updating the Page Table Entry (PTE)

Once the data resides in physical RAM, the VMM updates the process's page table hierarchy (PGD, PUD, PMD, PTE) to bind the virtual address to the new physical frame.

    pte_t *pte = get_pte_entry(vma->mm->pgd, address);
    
    /* Set physical address and mark the page as PRESENT and VALID */
    *pte = physical_page->physical_address | PTE_PRESENT | PTE_USER;
    
    if (vma->vm_flags & VM_WRITE) {
        *pte |= PTE_WRITABLE;
    }

6. Invalidating the TLB and Resuming Execution

Because the Translation Lookaside Buffer (TLB) caches virtual-to-physical mappings, the CPU might hold stale translation state. The kernel executes an architecture-specific invalidation instruction (such as invlpg on x86) for that address:

    __asm__ volatile("invlpg (%0)" :: "r"(address) : "memory");
    return 0;
}

After updating the page table and invalidating the TLB, the kernel restores the CPU registers and executes the return-from-interrupt instruction (iret). The processor re-executes the exact instruction that previously triggered the fault. This time, the MMU finds a valid page table entry and transparently completes the memory access without the user program ever knowing a fault occurred.