How does the operating system handle a page fault during address translation

How does the operating system handle a page fault during address translation

Page faults sound like errors, and the name doesn’t help — but the overwhelming majority of page faults a system handles are completely normal, expected events, not signs of anything broken. I want to walk through exactly what happens, step by step, when a CPU tries to translate a virtual address and hits a page fault, and why this mechanism is actually one of the cleverest parts of how virtual memory works.

What Triggers a Page Fault

During address translation, the CPU’s memory management unit (MMU) walks the page tables (after a TLB miss) looking for a valid mapping for the requested virtual address. A page fault is raised whenever that walk can’t complete normally. That covers several distinct situations:

  • The page simply isn’t mapped at all — the process is accessing memory it never allocated (a genuine bug, leading to a segmentation fault signal from the OS).
  • The page is part of the process’s address space but not currently resident in physical RAM — it’s been swapped out, or it’s a file-backed page that hasn’t been loaded yet (this is normal and expected).
  • The page exists but the access violates its permissions — writing to a read-only page, executing from a non-executable page, or a user-mode access to a supervisor-only page.
  • The page is marked copy-on-write, and a write is being attempted, requiring the OS to actually duplicate the page before allowing the write to proceed.

Step-by-Step: What the CPU and OS Do

CPU executes instruction needing virtual address X
        |
        v
   TLB miss --> page table walk
        |
        v
  Page table walk fails / permission check fails
        |
        v
   CPU raises a page fault exception (trap)
        |
        v
  Control transfers to the kernel's page fault handler
        |
        v
   Kernel inspects the faulting address and the process's
   VMA (Virtual Memory Area) records to decide what kind
   of fault this is
        |
   +----+-------------------+------------------+
   |                        |                  |
Minor fault           Major fault         Invalid access
(page exists in       (page must be      (no valid VMA,
 memory, just not      read from disk/    or permission
 mapped for this        swap)              violation)
 process's page
 table yet — e.g.
 shared library
 page already
 loaded by another
 process)
   |                        |                  |
   v                        v                  v
Map existing page,     Allocate a frame,   Deliver SIGSEGV
update PTE, resume     issue disk/swap     (or equivalent)
                        I/O, block the      to the process
                        process, then
                        map page and
                        resume on
                        completion

The kernel’s fault handler (on Linux, this is do_page_fault() / the architecture-specific entry point feeding into the generic fault handling code in mm/memory.c) is the piece of code deciding which branch applies. It does this by consulting the process’s VMAs — the kernel’s record of which virtual address ranges are supposed to be valid for this process, what permissions they carry, and what backs them (an anonymous mapping, a file, a device).

Minor vs Major Faults

This distinction matters a lot for performance analysis, and it’s one of the most commonly misunderstood parts of page fault handling.

Minor (soft) faults happen when the page is already resident in physical RAM — maybe another process mapped the same shared library, or the page was recently evicted from this process’s page table but the physical frame is still around in the page cache — and the kernel just needs to update this process’s page table to point at it. No disk I/O required. These are cheap, on the order of a few microseconds.

Major (hard) faults require the kernel to actually fetch data from disk (or a swap device) before the access can be satisfied. This is orders of magnitude slower — potentially milliseconds on a spinning disk, still meaningfully slower than RAM even on fast NVMe storage. A process’s execution literally blocks (goes to sleep) while this I/O completes.

You can observe this distinction directly on Linux with /usr/bin/time -v (look for “minor page faults” and “major page faults”) or via ps -o min_flt,maj_flt.

Demand Paging: Why Most Faults Are a Feature, Not a Bug

The single biggest reason page faults happen constantly, even on a healthy system, is demand paging. When a process starts, the OS doesn’t load its entire executable and all its data into RAM up front. It sets up VMAs describing what the address space should contain, but the actual physical pages are only loaded lazily, the first time each page is actually touched. That first touch always triggers a page fault — normal, expected, “the OS working as designed” — which then triggers the appropriate load (from the executable file, typically, for code and initialized data).

This is a deliberate design tradeoff: it means starting a program is fast (no need to load everything before execution can begin) and memory that’s never actually accessed is never actually loaded, saving both time and RAM.

Copy-on-Write: A Special Case Worth Calling Out

When fork() creates a child process, the kernel doesn’t duplicate the parent’s entire memory image immediately — that would be wasteful, especially since many fork() calls are immediately followed by exec(), discarding the copy entirely. Instead, both parent and child initially share the same physical pages, marked read-only, with a copy-on-write flag set.

The first time either process tries to write to one of these shared pages, a page fault is triggered specifically because of the read-only permission mismatch. The kernel’s fault handler recognizes this as a COW fault, allocates a genuinely new physical page, copies the original page’s contents into it, updates the writing process’s page table to point at the new private copy, and lets the write proceed. This is a brilliant use of the page fault mechanism to defer real memory copying until it’s actually necessary.

What Happens With a Genuinely Invalid Access

If the kernel checks the process’s VMAs and finds no valid mapping at all for the faulting address — or finds one but the requested operation (write, execute) isn’t permitted by that VMA — this is a real error. The kernel delivers SIGSEGV (segmentation violation) to the offending process, which by default terminates it (often producing the familiar “Segmentation fault (core dumped)” message). This is the case people usually mean when they informally say a program “crashed with a page fault.”

Page Faults Across Operating Systems

  • Linux handles this through the generic fault handler in mm/memory.c, dispatching to architecture-specific low-level trap entry code first.
  • Windows uses a broadly similar model — the Memory Manager’s fault handler distinguishes soft faults (page already in the working set list or standby list) from hard faults (requiring disk I/O), and Windows’ own Task Manager / Resource Monitor explicitly surfaces “hard faults per second” as a performance metric.
  • macOS/iOS, on XNU, follow the same demand-paging and copy-on-write model, with the vm_fault() code path performing analogous logic.
  • Classic UNIX systems established this whole model decades ago; demand paging and copy-on-write both trace back to research and commercial UNIX implementations in the 1970s-80s before becoming near-universal.

Troubleshooting Page-Fault-Related Performance Issues

  1. High major fault rates usually indicate memory pressure forcing pages out to swap, or a working set genuinely larger than available RAM — check vmstat 1 (the si/so swap-in/swap-out columns) and /proc/meminfo.
  2. A sudden spike in minor faults after deploying new code can indicate excessive mmap()/munmap() churn or aggressive lazy-loading patterns worth investigating with perf stat -e faults.
  3. Consistent SIGSEGV crashes point to an actual out-of-bounds access bug — tools like AddressSanitizer or gdb core dump analysis are the right next step, not page-fault tuning.
  4. On COW-heavy workloads (fork-heavy servers), high fault rates immediately after fork() are expected; if it’s a bottleneck, consider vfork()/posix_spawn() patterns or avoiding fork-per-request architectures.

Best Practices

  • Don’t be alarmed by nonzero minor fault counts — they’re a normal cost of demand paging, not a sign of a problem.
  • Watch major fault rates specifically as a proxy for memory pressure; they correlate strongly with real user-perceptible slowdowns.
  • For memory-mapped file workloads, consider madvise(MADV_WILLNEED) to hint the kernel to prefetch pages before they’re needed, converting potential major faults into background I/O rather than blocking foreground faults.
  • Design fork-heavy server architectures with COW behavior in mind — avoid writing to large shared data structures immediately after fork if you want to preserve COW’s memory-saving benefit.

Summary

A page fault is the mechanism the OS uses whenever address translation can’t be completed directly from the page tables — and far from always being an error, it’s the load-bearing trick behind demand paging, copy-on-write, and swapping. The kernel’s fault handler inspects the process’s VMA records to classify the fault as a cheap minor fault (just update the page table), an expensive major fault (go fetch data from disk), a COW duplication, or a genuine invalid access resulting in SIGSEGV. Every major operating system implements some version of this same model, because it’s fundamentally the right way to make virtual memory both fast to start up and efficient to run.

FAQs

Is a page fault always an error? No — the vast majority of page faults (minor faults, demand paging, copy-on-write) are completely normal and expected. Only invalid-access faults resulting in SIGSEGV represent genuine errors.

What’s the difference between a minor and major page fault? A minor fault is satisfied without disk I/O (the page is already in RAM, just not mapped for this process yet); a major fault requires reading data from disk or swap, making it dramatically slower.

How does copy-on-write use page faults? After fork(), parent and child share read-only pages; the first write to a shared page triggers a fault that the kernel uses as the trigger to actually duplicate the page before letting the write proceed.

What causes a segmentation fault? A SIGSEGV is delivered when a page fault handler determines the access is genuinely invalid — no valid mapping exists for the address, or the access violates the mapping’s permissions.

References

  • Linux kernel source, mm/memory.c, handle_mm_fault()
  • Bovet, D., Cesati, M., Understanding the Linux Kernel, memory management chapters
  • Microsoft Docs, “Managing Memory-Mapped Files” and Memory Manager internals
  • Silberschatz, A., Galvin, P., Gagne, G., Operating System Concepts, virtual memory chapter
Total
2
Shares

Leave a Reply

Previous Post
Describe the role of the page table entry (PTE) in a page table

Describe the role of the page table entry (PTE) in a page table

Next Post
What is the purpose of the Translation Lookaside Buffer (TLB) in virtual memory

What is the purpose of the Translation Lookaside Buffer (TLB) in virtual memory

Related Posts