SkillGridLearn computer science by doing

Virtual memory in earnest: what a page fault really costs

The TLB, the page table walk, LRU against the clock algorithm, why the Belady anomaly exists and why a thrashing machine looks frozen while the processor sits idle.

17 min · 50 XP

Tier 1 established the shape of it: the address the program sees is virtual, the MMU translates it through a page table, and a page that is not in RAM produces a fault that brings it in from disk. That was the map. Now we walk the route, because the details are where the performance actually lives and dies.

The first thing the map hides is that the MMU cannot afford to read the page table on every access. A four-level page table means four dependent reads of RAM before you even touch the data, which would make every access five times slower. So the processor keeps a small cache of recent translations: the TLB, typically 64 to 1536 entries, fully associative, right next to the pipeline. A hit costs a cycle. This is why a program touching 100 MB with good locality runs at nearly full speed even though the page table is enormous: it only ever uses a few thousand distinct pages at a time.

CPUTLBPage table walkerRAMDiskread virtual address 0x2A14miss, no entry for this pageread 4 levels of page table3 or 4 memory reads before the datapresent bit is 0page faultread 4 KiB pageframe 0x1F20install entryretry the instruction

When the TLB misses and the page is not in memory, the whole pipeline stops. The hardware walks the page table, finds the present bit clear, and raises a fault. The instruction is abandoned mid-flight, the trap handler runs, the kernel finds or frees a frame, reads 4 KiB from the device, updates the page table, installs a TLB entry and returns to retry the instruction - which now succeeds. Hardware handles the easy part in nanoseconds; software handles the expensive part in microseconds.

TLB hit: 1 cycle
walk: 3 or 4 reads
RAM: 30 ns
SSD: 0.1 ms
HDD: 8 ms

fault rate: 1/1000
0.001 x 0.1 ms = 100 ns
30 ns -> 130 ns per accessThe cost ladder. The SSD is fast enough that a low fault rate is survivable, and the rotating disk is not.

When a frame is needed and memory is full, something must be evicted. The best possible choice, if you could see the future, is the page used furthest in the future: the optimal policy of Belady. It is unimplementable, but it is the yardstick. The best practical approximation is LRU - evict the page unused for the longest time - and here is the catch that matters: exact LRU is also unimplementable. It requires a timestamp on every access, which means writing to memory on every read, which is exactly what the TLB exists to avoid. So real kernels approximate it, and the approximation they use is the clock algorithm.

The clock algorithm keeps frames in a circular list with one reference bit each. The hardware sets that bit on every access, and hardware can do it for free. When a frame is needed, the hand advances: if the bit is set, the page has been used recently, so clear the bit and give it a second chance; if the bit is clear, it has gone a full revolution untouched, so evict it. That second chance is the clever part - it separates pages used once in a burst from pages genuinely in the working set, at a cost of one bit and one pass of a hand.

for (;;) {
  frame = frames[hand];
  if (frame.ref_bit == 1) {
    frame.ref_bit = 0;
    hand = (hand + 1) % nframes;
  } else {
    evict(frame);
    break;
  }
}The clock algorithm in full. The hand sweeps, giving every recently used page exactly one more chance.

Now the property that makes replacement policies interesting. A stack algorithm is one where the set of pages resident in N frames is always a subset of the set resident in N+1 frames. LRU is a stack algorithm, so giving it more memory can never cause more faults. FIFO is not, and neither is clock, and the consequence is the Belady anomaly: there exist sequences of accesses where four frames produce more faults than three. It is not a rare curiosity - it is the direct symptom of a policy that makes decisions without a global notion of recency, and it is why production kernels care about LRU-ish age tracking rather than plain round-robin.

reference string   1 2 3 4 1 2 5 1 2 3 4 5

3 frames           9 faults
4 frames          10 faults

FIFO    more frames -> more faults
CLOCK   more frames -> more faults
LRU     more frames -> never worse
OPT     more frames -> never worseThe classic reference string. The first five faults differ, and that is enough to make four frames worse than three.

The working set of a process is the set of pages it is actively using in a window of time. If that set fits in the memory it has been given, the fault rate is low and matters little. If it does not fit, the process is in trouble: every page it evicts is a page it will need again shortly, so the next access faults, which evicts another page it will need, and so on.

working set: 20 MiB
5 threads: 100 MiB
RAM: 64 MiB

resident set: 12 MiB
fault -> evict -> fault -> evictA concrete case: five threads with 20 MiB each in a container capped at 64 MiB of RAM.

This is thrashing, and it explains the symptom everybody has seen and few can name. The machine appears completely frozen: the mouse stutters, nothing responds, the disk light is solid. You check the processor usage and it is not busy at all. The reason is that a faulting process is blocked, not running. It sits in the wait queue while the device works, so the CPU is idle - and the scheduler then runs a different process, which promptly faults on its own pages, evicts pages belonging to the first one, and blocks too. The processor is idle because every runnable thread is waiting for a 4 KiB read, and the system is slow not because it is computing too much but because it is computing almost nothing between two disk operations. Adding more processes makes it worse, which is the opposite of the intuition that more work keeps the CPU busy.

Two hardware notes that change the numbers. First, transparent huge pages: the kernel can back parts of memory with 2 MiB pages instead of 4 KiB ones, which cuts the number of TLB entries needed by 512 times. That can be a large win for a database scanning a table. Second, and in the other direction, it can be a loss: a 2 MiB page must be written out whole, so a program that dirties 4 KiB of a huge page pays 2 MiB of I/O. This is why the kernel has both always and madvise modes, and why the default has flip-flopped across versions.

Practice

  1. A program touches an address that is mapped but not resident. Order what happens, from the moment the load instruction executes to the moment it finally completes.

    1. 1the faulting instruction is retried and now completes
    2. 2the 4 KiB page is read from the device and the page table is updated
    3. 3the fault handler runs and selects a frame, evicting a page if memory is full
    4. 4the MMU looks up the virtual page in the TLB and misses
    5. 5the hardware walks the page table and finds the present bit clear
    Hints
    • The MMU starts with the address it was handed and only involves the operating system after it has failed on its own.
    • The instruction that faulted is not modified: once the translating entry exists, it is executed again from the beginning.
  2. A server has 64 GiB of RAM and runs 500 worker processes. One page in every hundred thousand accesses faults, and the disk is an SSD. The machine is heavily loaded but the processor is only 30 percent busy. What is the most likely explanation?

    Hints
    • Compute the cost of one access first: one fault in a hundred thousand, each costing about 0.1 ms, on top of a baseline of about 30 ns.
    • Then ask what a thread does during those microseconds, and whether the scheduler can use that time for anyone else.
  3. Match each virtual memory mechanism to the problem it exists to solve.

    Hints
    • One of these entries describes what the hardware does by itself in a single cycle, and the others describe what software does afterwards.
    • The two software paths are separated by cost: one is measured in microseconds and the other in milliseconds.

Questions about this lesson

What is an operating system, really?

It is the manager between your programs and the hardware. It decides which program gets the processor, hands out memory, and keeps files in order. Every program you write runs on top of it.

Why learn this if I just want to write code?

Because when a program is slow, hangs or runs out of memory, the reason is almost always here. Without this you can only guess; with it you can measure.

Is it free?

Yes. The first level of every subject is free and needs no account. Deeper levels and the senior grades are paid — that is what keeps the project running.

What age is this for?

It is written for curious teenagers from about 12 and for adults starting from zero. Nothing assumes prior programming experience; lessons about networking and physics need no mathematics beyond school level.

Do I need to know programming already?

No. The logic, physics and networking tracks start from nothing. The languages track is where you actually write code, and it also starts from the beginning.

Do I need to install anything?

No. Everything runs in the browser: theory, practice and the interactive simulations. There is nothing to download and no environment to configure.

How long does one subject take?

A lesson takes 10 to 15 minutes. A junior grade is roughly 12 to 20 lessons, so expect a few weeks of steady practice rather than one evening.

Do I have to follow the order?

Within a grade, levels open one after another, because each builds on the previous one. Between subjects you are free: start with logic, physics or networking.