Picking the size of a page
Across this whole rung we kept saying "a 4 KB page" as if that number fell from the sky. It did not — it is a design choice, and like most choices in this subject it is a trade-off with no free winner. A page size is always a power of two (so the hardware can split an address into a page number and an offset with no division, just by slicing the bits), but which power? Real systems have shipped pages from 512 bytes up through 4 KB, and modern hardware also offers "huge" pages of 2 MB or even 1 GB. To choose well, you have to know what gets better and what gets worse as the page grows.
Start with the cost of small pages. The smaller each page, the more pages a process needs, and the page table has one entry per page — so a tiny page size means a huge table, eating memory just to describe memory. Smaller pages also mean each TLB entry covers less ground, so a given workload spills out of the TLB sooner and you pay more misses. And on a page fault, the disk overhead is mostly fixed per transfer, so moving many small pages is far less efficient than moving a few big ones. All three pressures push toward bigger pages.
So why not make pages enormous? Because of the waste we met back in the allocation rung. Paging cures external fragmentation, but it leaves a sliver of internal fragmentation in each process's last page: on average a process wastes about half a page, because its final page is rarely filled exactly to the brim. With 4 KB pages that is roughly 2 KB lost per process — trivial. With 1 GB pages it would be on average half a gigabyte wasted per process — absurd. Bigger pages also waste I/O when a fault drags in a whole large page just to touch a few bytes. So small pages waste table space and TLB reach; big pages waste memory and I/O. The classic 4 KB is a middle that has aged well, and huge pages are reserved for special, large, well-behaved workloads.
Make the page BIGGER -> page table: smaller (fewer entries) GOOD
TLB reach: wider (covers more) GOOD
page-fault I/O: fewer, bigger transfers GOOD
internal frag: bigger waste (~half a page) BAD
Make the page SMALLER -> internal frag: tiny waste GOOD
page table: larger BAD
TLB reach: narrower -> more misses BADOne copy, two readers: shared pages
Here is a quiet gift the page table hands us almost for free. Recall that a page table is each process's private index, mapping its page numbers to physical frames — like the index at the back of a book, pointing words to page numbers. Now imagine two different books whose indexes happen to point some entries at the very same physical pages. Two processes can do exactly that: if process A's entry for page 7 and process B's entry for page 12 both name the same frame, then A and B are looking at one shared chunk of RAM through different addresses. That is a shared page, and once you see it, a lot of how an OS saves memory clicks into place.
The everyday payoff is shared code. A shared library like the C standard library, or the read-only text segment of a program you have launched fifty times, never changes while running. So why keep fifty identical copies in RAM? Map them once and let every process's page table point at that single copy. The kernel marks those shared pages read-only in the page-table entries, which is what makes sharing safe — nobody can scribble on code that others depend on. One physical copy, many logical views, and the machine fits far more programs in the same RAM. Note the rule, though: only unchanging, read-only data shares cleanly this way.
Sharing what changes: copy-on-write
Read-only sharing is easy. But what about data that might change? Think back to the process rung, where fork() creates a child as a near-perfect duplicate of its parent's whole address space. Copying every page eagerly would be wasteful and slow, especially since the child very often calls exec() immediately and throws all those pages away. The elegant fix is copy-on-write: at fork time, do not copy anything. Instead let parent and child share every page, but mark them all read-only — even the writable ones — in both page tables.
Now sharing holds as long as both sides only read. The instant one of them writes to a shared page, the hardware sees a write to a read-only page and traps into the kernel — much like a page fault, but a protection fault. The kernel recognizes this is a copy-on-write page, makes a private copy of just that one page for the writer, flips its entry back to read-write, and lets the write proceed on the fresh copy. The other process keeps the original, untouched. So pages are duplicated lazily, one at a time, only when someone actually modifies them — and pages nobody writes are never copied at all.
- fork() runs: instead of copying memory, the kernel makes the child's page table point at the parent's frames, and marks every shared page read-only in both tables.
- Both processes run, reading freely from the same physical frames — fast, cheap, no copying.
- One process tries to write a shared page. The write hits a read-only page and traps into the kernel.
- The kernel allocates a fresh frame, copies just that one page into it, repoints the writer's entry there, and marks it read-write.
- The write completes on the private copy; the other process still sees the unchanged original. Only touched pages ever get duplicated.
The bigger picture of trade-offs
Step back and notice that this entire rung was one long string of trade-offs, not a march toward a single right answer. Paging itself trades away external fragmentation but buys a little internal fragmentation and the overhead of a page table. The page table cures contiguity but creates the double-memory-access problem — every logical access needs one trip to read the table and another to read the data. The TLB fixes the common case but is a small, finite cache that misses on cold or scattered access, and it relies on locality to pay off. Multi-level page tables shrink the table for sparse address spaces but add even more memory trips per miss, while inverted tables shrink it differently at the cost of harder, slower lookups.
There is a thread running through every one of those: each fix optimizes the common case and accepts a worse rare case. The TLB bets that you will reuse pages soon (usually true). Copy-on-write bets that most shared pages stay unwritten (usually true). Demand paging bets that you will not touch most of your address space at once (usually true). When a bet holds, paging feels free; when it fails — random access patterns, a writer touching every page, a working set larger than RAM — the costs come due, sometimes savagely.
Carrying the rung with you
You now hold the whole paging story. Memory is carved into equal-sized frames and each process's view into equal-sized pages; a per-process page table maps page to frame so external fragmentation cannot happen; an address splits into a page number and an offset; the page-table base register lets the hardware find the table; the TLB caches recent translations so the common access costs one memory trip, not two; page-table-entry bits enforce validity, protection, and dirtiness; multi-level, hashed, and inverted tables tame enormous address spaces; and pages can be shared read-only or copied lazily on write. That is a complete, working mental model of how a modern computer pretends every program owns the machine.
Notice what we have quietly assumed throughout: that every page a process needs is sitting in RAM. That assumption is about to fall. The very same page table, with its valid-invalid bit, is the hinge for the next big idea — letting a process run with most of its pages still on disk, fetched only when actually touched. That is virtual memory with demand paging, the topic waiting just beyond this rung. Everything you learned here is the machinery it runs on; you have built the engine, and next you switch it on.