The MMU: Translation and Protection in One Check
The memory management unit is not a lookup table off to the side. It is a gate every load and store passes through, and it answers two questions at once: where does this address really point, and is this process allowed to touch it that way?
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
One structure, two answers
A page-table entry does not just say where a page lives. It carries the frame number *and* the permission bits *and* the status bits, and the MMU consumes all of them in one operation. Asking "is this mapped?" and "may I write to it?" are not two lookups; they are one lookup with two failure modes.
That combination is what makes protection cheap enough to be unconditional. If permission checking were a separate structure consulted after translation, it would double the work on the critical path and every design would be tempted to skip it. Fusing them means the check rides along free with a translation you had to do anyway.
It also explains a class of behaviour that looks arbitrary from software: you cannot have a page that is present but unreadable *without* the hardware trapping, because presence and permission live in the same entry and are evaluated together. Marking a page unreadable is how the OS makes the hardware call it on the next access — a mechanism used for guard pages, copy-on-write and lazy allocation alike.
| Field | Meaning | Consumed by |
|---|---|---|
| Frame number | Where the physical page actually is | MMU, to form the physical address |
| Present / valid | Whether a mapping exists at all | MMU; absent means trap to the OS |
| Read / write / execute | What kinds of access are permitted | MMU, on the same lookup |
| User / supervisor | Which privilege level may access it | MMU, against the current mode |
| Accessed / dirty | Whether it has been read or written since the OS last cleared them | Set by hardware, read by the OS for eviction decisions |
| Cacheability attributes | Whether and how the region may be cached | MMU and cache controller, e.g. for device memory |
On the critical path, so it must overlap
A naive ordering would be: translate the address, then look up the cache with the physical address. That serialises two lookups on the hottest path in the machine, and the latency would be visible on every access.
The standard escape is that the low bits of a virtual address — the offset within the page — are *not* changed by translation. Only the page number is translated. So the cache can begin its lookup using those untranslated offset bits at the same moment the TLB resolves the frame number, and the two results meet at the tag comparison.
This is not an incidental optimisation. It constrains cache design: for the overlap to work, the bits used to index the cache must come from the untranslated part of the address, which puts a ceiling on how large a simple L1 can be for a given associativity. A surprising amount of cache geometry follows from wanting translation to be free. The scope note matters here — the specific arrangement varies, and some designs use physically-indexed caches with different trade-offs.
When the check fails
A failed check does not return an error code. The instruction is aborted before it takes architectural effect, and control transfers to the OS with enough information to describe exactly what happened: the faulting address, the kind of access attempted, and whether the mapping was absent or merely forbidden.
The precision matters enormously. Because the instruction did not complete, the OS can resolve the situation — allocate a frame, page the data back in, copy a shared page before allowing the write — and then restart the very same instruction, which now succeeds. The program cannot tell that anything happened except by timing.
This is the mechanism underneath a long list of features that look unrelated from software: demand paging, copy-on-write after fork, memory-mapped files, guard pages at the end of a stack, and the segmentation fault you get for dereferencing null. All of them are the OS deliberately arranging for the MMU to trap, then doing something useful in the handler. The OS side of that story is Page Faults.
faulting virtual address : 0x7f3c_a940_1000 access type : write privilege at fault : user mapping present : yes permission allowed write : no <-- cause instruction pointer : 0x0000_5561_2f04 the instruction did NOT retire; architectural state is unchanged. the OS may fix the mapping and restart it, or deliver a signal. present=no + any access -> demand paging, or an invalid pointer present=yes + write denied -> copy-on-write, or a genuinely read-only page user access to supervisor -> a privilege violation
Key points
- The MMU resolves the mapping and validates permissions in a single lookup, on every access, unconditionally.
- One page-table entry carries the frame number, the permission bits and the status bits together — which is why the combined check is affordable.
- Translation overlaps the cache lookup because the page offset is not translated; this constrains cache geometry.
- A failed check aborts the instruction precisely, so the OS can fix the situation and restart it transparently.
- Demand paging, copy-on-write, guard pages and segfaults are all the same mechanism: the OS arranging for the MMU to trap.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Load/store unit → MMU: the virtual address and the access type (read, write, or execute) are presented together.
- 2MMU → TLB: a matching entry is sought; the permission and privilege bits come back with the frame number, not separately.
- 3MMU → permission check: the requested access type is validated against the entry's bits and the current privilege level.
- 4MMU → cache: on success the physical address is formed and the access proceeds, having overlapped with the L1 set lookup.
- 5MMU → trap: on failure the instruction is aborted before retirement and the OS receives the faulting address, access type and cause.
- • Treating protection as a software convention that a sufficiently privileged program can talk its way around; it is a hardware gate on every access.
- • Assuming a segfault means memory corruption. It usually means an access the MMU was correctly configured to refuse.
- • Expecting
mprotectto be cheap because it changes no data — it changes tables the hardware caches, which is the expensive part. - • Believing a page can be "present but silently unreadable". Presence and permission are evaluated together; failing either traps.
Consequences, controls and cost
- • Permission changes are not free: they require editing tables and invalidating cached translations, sometimes across every core.
- • A null-pointer dereference fails predictably because the OS deliberately leaves the low page unmapped so the MMU will trap.
- • Copy-on-write after `fork` costs nothing until a write happens, because the trap is what triggers the copy.
- • Device memory must be marked uncacheable through these same attributes, or writes intended for hardware would sit in a cache.
- • The precision of the fault is what makes restartable instructions possible, and therefore what makes demand paging viable at all.
- • Avoid frequent permission changes on hot memory — each one costs table edits and translation invalidation across cores.
- • Batch `mprotect`-style operations over large regions rather than per-page where the interface allows it.
- • Use guard pages deliberately for stack and buffer boundaries; you get a precise fault instead of silent corruption, at the cost of a page of address space.
- • For device or shared-memory regions, get the cacheability attributes right — this is a correctness matter, not a performance one.
- • Otherwise: almost nothing. The check is unconditional and not under program control; what you can control is how often you make the OS rewrite the entries.
- • Count minor page faults with `perf stat`: high minor-fault rates usually mean demand paging or copy-on-write, not I/O.
- • Watch for TLB shootdown activity when permissions change frequently — it appears as cross-core interrupt traffic disproportionate to the work.
- • Trace `mprotect` and `madvise` calls; a hot loop of permission changes is usually an allocator or GC behaviour worth knowing about.
- • On a fault-heavy workload, separate minor from major faults before drawing any conclusion — they differ by orders of magnitude in cost.
- • Fusing translation and protection makes the check affordable but makes permission changes expensive, because both live in the same cached structure.
- • Overlapping translation with cache lookup removes the latency but constrains L1 geometry, which limits how large a simple L1 can be.
- • Precise faults enable restartable instructions and demand paging, at the cost of hardware complexity in tracking exactly which instruction faulted.
Scope
§224 — what these claims are specific to.
- GENERALThe combined translate-and-check model holds across application-class CPUs with an MMU. MMU-less microcontrollers have no such gate and no process isolation to go with it.
- ISA-SPECIFICThe exact permission bits, fault-reporting registers and privilege encodings differ between x86-64, AArch64 and RISC-V. The structure of the check is the same; the field layout is not.