The gap the last guide left open
Guide 2 walked you through the machinery of translation: a virtual address is split into a page number and an offset, the MMU looks the page number up in the page table, finds a page table entry holding the matching physical frame, and pastes the offset back on. Every memory access your program makes goes through this lookup. But that whole story quietly assumed the page table entry actually points somewhere — that the page really is sitting in physical RAM. This guide is about the moment that assumption fails, which, it turns out, is not an error but the normal way memory gets allocated.
Here is the key fact the MMU relies on. Every page table entry carries, alongside the frame number, a small set of flag bits — and one of them is the present bit. If present is 1, the entry holds a valid physical frame and translation succeeds in hardware, invisibly. If present is 0, the MMU cannot finish the translation: the page is not in physical memory right now. The hardware does not guess and it does not give up — it does the one thing it is wired to do when it cannot translate, which is raise a fault and hand control to the operating system.
What a page fault actually is
A page fault is a trap the CPU raises, mid-instruction, when it tries to translate a virtual address whose page table entry has present = 0. It is not a bug, not a crash, and emphatically not a segmentation fault — keep those two crisply apart, because the names look alike. A segfault means the address was invalid: your program had no business touching it. A page fault means the address was perfectly valid but the page just was not loaded yet. One is your mistake; the other is the system doing its job.
When the fault fires, the CPU does the same thing it does for any trap, which you met back in the syscall rung: it switches from user mode into kernel mode and jumps to a fixed handler the kernel registered ahead of time — the page-fault handler. Crucially, the hardware also records which virtual address faulted (on x86-64 it lands in a control register, cr2) so the kernel knows exactly which page to fix. The handler's job is to ask one question: is this address something this process is allowed to access, and if so, what should be at that page? Then it acts accordingly.
Demand paging: why pages arrive late on purpose
Now the payoff. Demand paging is the policy of bringing a page into physical memory only at the moment it is first touched — on demand — rather than up front. When you call malloc() for 100 MiB, or your program starts and the loader maps a 50 MiB executable, almost nothing is copied into RAM right away. The kernel just records, in your page tables, that these virtual pages exist and where their contents will come from, leaving every present bit at 0. Not one physical frame is committed until your code actually reads or writes a given page.
So the page fault is not an accident demand paging tolerates — it is the trigger demand paging is built on. The first time you write to a freshly malloc'd page, its present bit is 0, the MMU faults, the kernel handler sees a valid heap page, grabs a free physical frame, zeroes it (so you never see another process's old data), flips present to 1, and restarts your instruction. Your store completes. From inside your program, nothing unusual happened: you wrote a byte and it stuck. Underneath, a whole kernel round-trip just allocated physical memory exactly where and when you needed it, and nowhere else.
This laziness is what makes virtual memory feel almost free. A process can map a sparse 1 GiB array and only ever touch a few scattered kilobytes of it; only those touched pages cost real RAM. It is why the address space can be vast and mostly empty without waste, and it is the same lazy machinery that the next guide's copy-on-write and the rung's final guide's mmap() both ride on. Get demand paging and you have the engine under both.
Minor faults, major faults, and the cost of each
Not all page faults cost the same, and the difference matters enormously for performance. The system distinguishes a minor fault from a major fault by one thing: whether the kernel had to touch the disk. A minor fault is the cheap, common case — the data the page needs is already somewhere in RAM, so the handler only has to fix up the page table. Zeroing a fresh heap page is a minor fault. So is wiring up a page whose contents are already cached in memory, or a copy-on-write break (next guide). No disk, just a quick kernel detour: microseconds.
A major fault is the expensive one: the page's contents live on a slower device and must be read in before your instruction can proceed. The classic case is the first read of a page of an executable or a memory-mapped file — its bytes are on disk, so the handler issues a real disk read and blocks your process until it finishes. The other source is swapping: under memory pressure the kernel evicts an idle page to a swap area on disk to free its frame; when you next touch that page, a major fault must read it back. A disk round-trip is on the order of milliseconds — thousands of times slower than a minor fault — which is exactly why major faults are the ones you watch.
page fault | +-- address valid for this process? | | | no --> deliver SIGSEGV (the segfault) | yes | | | +-- contents already in RAM? | yes --> fix page table, present=1 (MINOR fault, ~us) | no --> read from disk/swap, then (MAJOR fault, ~ms) | fix page table, present=1 | +-- in all valid cases: restart the faulting instruction
When demand paging turns against you: thrashing
Demand paging plus swapping is a beautiful illusion right up until the working set stops fitting. Your working set is the modest collection of pages your program actively uses over any short stretch of time — thanks to locality, real programs touch far fewer pages than they have mapped. As long as the working set fits in RAM, faults are rare and mostly minor, and everything hums. But push memory demand past what is physically available and the kernel is forced to evict pages that are still in active use to make room — only for the program to fault them right back in moments later, evicting some other still-needed page to do so.
That vicious cycle is thrashing: the machine spends nearly all its time servicing major faults — shuttling pages to and from the disk — and almost none running your code. The telltale sign is a system that goes from responsive to molasses-slow while the CPU sits mostly idle and the disk light is pinned; throughput collapses even though no single program is doing anything obviously wrong. Each program is just demanding a working set that no longer fits, and the kernel's honest attempt to give everyone a turn degenerates into pure overhead.
Where this leaves you
You can now read the present bit's whole story. A page table entry with present = 0 is not broken — it is an invitation. When the MMU hits it, it raises a page fault, the kernel inspects the faulting address, and one of two things happens: either the access was illegal and you get a segfault, or it was legal and the kernel makes the page present and reruns your instruction as if nothing happened. That second path, used deliberately, is demand paging — the policy of allocating physical frames lazily, exactly on first touch, which is what lets a program map far more than it uses.
Keep the two cost classes in mind, because they are how page faults show up in real performance work: a minor fault is a microsecond kernel detour with no disk; a major fault is a millisecond disk read, and a storm of them under memory pressure is thrashing. With this in hand you are ready for the next guide, where the same fault-on-touch trick gets one more flag bit — write-protect — to pull off copy-on-write, the lazy sharing that makes fork() cheap.