JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Pages, Page Tables, and the MMU

The previous guide promised that every program gets its own private illusion of memory. This guide opens the machinery that makes the illusion real: memory is sliced into fixed-size pages, a per-process table records where each page actually lives, and a small piece of hardware translates every address on the fly — fast enough that you never notice it is happening.

From last guide's illusion to this guide's machinery

In the first guide of this rung you saw the central trick of virtual memory: every process believes it owns a vast, private, simple address space starting near 0x0, even though the real RAM is shared, scattered, and smaller. The pointer you print, like 0x7fffffffe4a0, is a virtual address — a fiction the hardware maintains. Somewhere underneath, a different number, the physical address, names an actual byte in the actual memory chips. That guide told you the illusion exists. This guide is about how the trick is performed — the concrete data structures and the one piece of hardware that turn a fiction into a working machine.

The naive way to map every virtual address to a physical one would be a giant lookup table with one entry per byte — but with a 48-bit address space that is hundreds of trillions of entries per process, which is absurd. The whole design rests on one cost-saving idea: do not translate byte by byte. Instead, slice memory into fixed-size chunks and translate a whole chunk at once. Those chunks are pages, and they are the unit in which the entire system thinks about, protects, and moves memory.

Pages: cut the address in two

A page is just a fixed-size, aligned block of the address space — on almost every common system today it is 4 KiB, that is 4096 bytes. Physical memory is carved into blocks of the same size called frames (or page frames). Translation, then, is not about individual bytes at all: it maps one virtual page to one physical frame, and every byte inside that page comes along for the ride at the same offset. If virtual page P lives in physical frame F, then virtual byte (P, offset) is physical byte (F, offset) — the offset is identical on both sides.

This is why the 4 KiB size shows up directly in the bits of an address. Because 4096 is 2^12, the bottom 12 bits of any address are the offset within a page, and they are never translated — they pass through untouched. Everything above those 12 bits is the page number, and that is the only part the hardware actually looks up. Splitting an address is therefore pure bit-slicing: mask off the low 12 bits to get the offset, shift the rest down to get the page number. No arithmetic, just a cut.

a 4 KiB page => offset is the low 12 bits (2^12 = 4096)

virtual address 0x00405abc, split:

  bits:   ... 0000 0100 0000 0101 | 1010 1011 1100
          \_________________________/ \___________/
               page number 0x405          offset 0xabc

  page_number = vaddr >> 12        =  0x405
  offset      = vaddr & 0xFFF      =  0xabc

if page 0x405 maps to frame 0x131, the physical address is:

  paddr = (0x131 << 12) | 0xabc   =  0x131abc
Translation only rewrites the high bits (page number -> frame number); the low 12 bits (the offset) are copied straight through. The page number is the only thing ever looked up.

The page table: a per-process map of where each page lives

Now we need the actual lookup: given a page number, which frame is it in? That answer lives in the page table — a data structure the operating system builds and the hardware reads. Crucially, each process has its own page table, which is exactly how two processes can both use virtual address 0x405000 yet land on completely different physical memory: their tables point that page at different frames. The table is the concrete object behind last guide's promise of private address spaces.

Each row of the table is a page table entry (PTE). A PTE stores the frame number for that page plus a handful of single-bit flags that the rest of this rung depends on: a present bit (is this page actually in RAM right now, or does touching it need the OS — the seed of demand paging in the next guide), permission bits like writable and user-accessible (the basis of per-page protection), an accessed bit and a dirty bit (has this page been read, has it been written — used to decide what to evict). The frame number answers where; the flag bits answer under what rules.

The MMU and the TLB: doing this on every single access, fast

Here is the part that has to be believed to be appreciated: this translation happens on every memory access your program makes — every instruction fetch, every load, every store, billions of times a second. No software could keep up, so it is done in hardware by the memory management unit (MMU), a unit sitting between the CPU core and memory. When the CPU emits a virtual address, the MMU walks the page table, finds the frame, builds the physical address, and only then does the access reach RAM. Your program never sees this happen; from its side it just dereferenced a pointer.

But walking a four-level tree means up to four extra memory reads just to translate one address — that would make every access five times slower, which is unacceptable. The fix is a cache: the translation lookaside buffer (TLB), a tiny, very fast table inside the MMU that remembers recent page-number-to-frame translations. On an access the MMU checks the TLB first; a hit gives the frame in one cycle and the slow page-table walk is skipped entirely. Only a miss triggers the full walk, and its result is then cached in the TLB for next time. Because programs touch the same pages over and over (this is locality), the TLB hit rate is normally well above 99%, and translation becomes nearly free.

  1. Your code dereferences a pointer, so the CPU emits a virtual address — split by the hardware into a page number (high bits) and an offset (low 12 bits).
  2. The MMU looks up that page number in the TLB. On a hit it has the frame number immediately and jumps to the last step; on a miss it must do the slow walk.
  3. On a miss, the MMU walks the multi-level page table down to the page table entry, reads the frame number and the flag bits, and caches the result in the TLB.
  4. It checks the flags — if the page is not present, or the access violates its permissions, the MMU does not translate but raises a fault to the OS (the subject of the next two guides); otherwise it forms the physical address frame*4096 + offset and lets the access reach RAM.

What the flags buy you, and where this is honestly a simplification

Notice how much falls out of one table of frame numbers plus a few flag bits per entry. Mark a page's present bit clear and the OS can pretend it has more RAM than it does, paging data in only when touched — that is demand paging, the next guide. Mark a page's writable bit clear but share its frame between two processes, and a write triggers a fault the OS turns into a private copy — that is copy-on-write, the guide after. Clear the user-accessible bit on the kernel's pages and a user program physically cannot read them; this is the per-page enforcement of memory protection. The hardware checks these bits on every access for free, which is why protection in modern systems is cheap and absolute rather than a software afterthought.

Be honest about two things this clean picture glosses over. First, the page size is not a law of nature: 4 KiB is the common default, but most CPUs also support huge pages (2 MiB and 1 GiB on x86-64), which cover more memory per TLB entry and so cut TLB misses for workloads with large, hot regions — at the cost of coarser granularity. Second, the TLB is per-core and not magically kept in sync with the page table: when the OS changes a mapping, it must explicitly flush the stale TLB entry, and on a context switch it generally invalidates entries so the next process does not see the previous one's translations. Forgetting a flush is a real and nasty class of OS bug — the hardware would happily keep using a translation the software believes it deleted.

Where this leaves you

Step back and the illusion is no longer mysterious. Memory is sliced into 4 KiB pages; each process carries a page table mapping its pages to physical frames, with a few flag bits per entry deciding presence and permissions; the MMU performs the translation on every access; and the TLB caches recent translations so the common case is nearly free. The private, simple address space each program enjoys is just this map, read by hardware, billions of times a second.

We left two flag bits deliberately loaded, and the next guides pull on them. When the MMU finds a page table entry with its present bit clear, it cannot translate — so it hands control to the OS as a page fault. The next guide, on page faults and demand paging, shows what the kernel does in that handler: find or fetch the page, install a valid mapping, and resume your instruction as if nothing happened — which is the mechanism that lets a program use more memory than the machine physically has. You now hold the map; next you will watch the OS fill it in on demand.