Topic 04

Virtual Memory

Operating Systems

Two Python processes on the same machine can each hold an object at the same address and hold completely different data there. Every address a program uses is a fiction. On every memory access, the hardware translates it through a table that the kernel keeps for that one process, and only the translated address reaches RAM.

That translation is what makes processes private, lets them share libraries, makes fork cheap and makes allocation lazy. It also has a price, and you pay it on every random access into a large data structure. This topic follows one address through the translation and puts a number on the price.

Addresses Are Names, Not Places

A virtual address is looked up, never used directly. The unit of translation is the page, usually 4 KiB. Each page of a process's address space maps to one physical frame of RAM of the same size, or to nothing yet. The low bits of an address, the position inside the page, pass through untouched; only the page number is translated.

So memory that is contiguous to the program may be scattered all over RAM. A 1 MiB array is 256 consecutive pages to the program and 256 frames wherever the kernel found them. Two processes can map the same frame, which is how they share a library, and a page can be mapped to no frame at all until someone touches it.

Two private address spaces over one physical memory
process A: virtual pagesprocess B: virtual pagesphysical RAM: framespage 0page 0page 1page 1page 2page 2not mapped yetpage 3page 4not mapped yetframe 0frame 1: sharedframe 2frame 3frame 4frame 5frame 6frame 7contiguous to the program, scattered in RAM; one frame, a shared library, mapped by both

The Page Table

A flat array would be enormous, so the table that holds the mapping is a tree, of the kind Chapter 6 introduces, indexed by slices of the virtual address. On x86-64, the processor family in most servers, the tree has four levels of 512 entries each. The top 36 bits of a 48-bit address are cut into four 9-bit indexes, one per level, and the last 12 bits are the offset inside a 4 KiB page. Four levels of 512 cover 256 TiB of address space per process. Newer processors add a fifth level for 57-bit addresses.

Every entry carries permission bits as well as a frame number: present, writable, executable, accessible from user mode. The processor checks them on every access. A missing entry is not an error yet. It is a page fault, a trap into the kernel, which decides what the access should mean; the next topic follows that decision.

One address, four table lookups, one byte
a 48-bit virtual address9 bitslevel 4 index9 bitslevel 3 index9 bitslevel 2 index9 bitslevel 1 index12 bitsoffset in pagelevel 4 table512 entriesentrylevel 3 table512 entriesentrylevel 2 table512 entriesentrylevel 1 table512 entriesentry4 KiB framein RAMthe byteeach arrow between tables is one more memory read; the TLB exists to skip all four

The TLB

Walking four levels of table on every access would add up to four memory reads to every load and store. So the processor caches recent translations in the translation lookaside buffer, the TLB. It holds a few thousand entries at most, so with 4 KiB pages it covers only a few megabytes, 8 MiB for a 2,048-entry buffer.

A TLB hit costs nothing extra. A miss costs a page walk: tens of nanoseconds when the table entries are themselves in the processor's cache, and up to four trips to RAM, about a hundred nanoseconds each on the latency ladder in Chapter 4, when they are not. Huge pages change the arithmetic. On x86-64 a page can also be 2 MiB or 1 GiB, and one TLB entry for a 2 MiB page covers 512 times as much memory, so the same few thousand entries cover gigabytes. Databases and large runtimes ask for them to get that reach.

What the Indirection Buys

Isolation comes first: a process cannot even name another process's memory, because every address it can form goes through its own table. Protection comes from the permission bits: code pages are read-only and the stack is not executable, so a write into code or a jump into data faults instead of running. Sharing is a matter of two tables pointing at one frame, which is how one copy of a common library serves every process on the machine.

Copy-on-write, from the previous topic but one, is two tables pointing at one frame with the writable bit cleared. And laziness falls out for free: an allocation only reserves a range of addresses, and a frame is found for each page only when that page is first touched. A program that allocates a gigabyte and touches ten megabytes uses about ten megabytes.

Why Every Process Thinks It Owns All of Memory

Address space is enormous and nearly free, so processes reserve far more than they use. Thread stacks, memory-mapped files and the arenas of language runtimes all reserve large ranges up front. Add up the reservations on one machine and they can be many times its RAM.

The kernel is betting that not everything reserved will be touched at once. Most of the time the bet wins. When it loses, the kernel pushes rarely used pages out to disk, the swapping of the next topic, or it picks a process and kills it to free memory. So running out of memory usually strikes at some ordinary write far away from any allocation: the write is when the page is actually needed.

Huge Pages and Memory Limits

Random lookups into a 10 GB in-memory hash table, the structure of Chapter 5, miss the TLB almost every time, because a few megabytes of translations cannot cover 10 GB of randomly chosen pages. Each lookup then pays a page walk on top of its cache miss. The same table backed by 2 MiB huge pages runs measurably faster with no change to the code. It needs about five thousand translations instead of about two and a half million, so a large share of them fit in the TLB, and a miss walks one level fewer through a table small enough to stay in the processor's cache.

A container's memory limit counts pages actually held in RAM, not reserved addresses. A service showing 20 GB of virtual size may be using 300 MB, and the process that gets killed is the one whose touched pages grew, not the one with the biggest reservation. Watching a process's resident and virtual memory on a real machine is covered in Linux Deep Dive, chapter Performance and Troubleshooting.

Virtual Memory vs Swap vs Virtual Size

Virtual memory is the translation of every address through a page table. It is always on, with or without a disk.

Swap is one thing virtual memory makes possible: moving rarely used pages out to disk, as the next topic shows. A server with swap turned off still runs every instruction through virtual memory.

Virtual size is how much address space a process has reserved. It says almost nothing about how much RAM the process uses.

Misconceptions
  • "Virtual memory means using the disk as RAM." That is swap, one optional use of the mechanism. Every memory access on every mainstream server, laptop and phone is translated, disk or no disk.
  • "The number Python's id function returns, or a pointer in C, is where the object sits in RAM." It is a virtual address, meaningful only inside that process. The same number in another process names different data, and the physical frame behind it can change while the program runs.
  • "Allocating 1 GB uses 1 GB." It reserves addresses. Frames are assigned one page at a time on first touch, which is why the allocation succeeds and the program runs out of memory later, at some write far away from it.
  • "A process with a 20 GB virtual size is a memory hog." Reserved address space, such as thread stacks, mapped files and runtime arenas, is not resident memory. The number that predicts memory pressure is touched, unshared pages.
  • "Memory access costs the same wherever the data is." Beyond what the TLB covers, each random access pays for a translation as well as a cache miss. The layout of the data decides both costs.
Why It Matters
  • Judge a process's memory by its resident, unshared pages. Reserved address space costs nothing until it is touched.
  • Keep hot data compact. Fewer pages touched means fewer TLB misses as well as fewer cache misses.
  • Use huge pages for large, long-lived, randomly accessed memory, such as database buffers and big in-memory indexes. One TLB entry then covers 512 times as much.
  • Test memory limits with realistic touched data, not allocation sizes. Exhaustion arrives at the write, not at the allocation.
RelatedCPU caches they cache data, the TLB caches translations, and a random access can miss both (Chapter 4)Memory safety page permissions stop a jump into data, not an overflow inside a page (Chapter 4)Swap the use of virtual memory most often mistaken for all of it

Knowledge Check

A service does random lookups into a 10 GB in-memory table. Moving the table onto 2 MiB huge pages makes it measurably faster with no code change. Why?

  • Huge pages are stored in a faster kind of RAM than ordinary pages
  • Far fewer translations are needed, so many more lookups hit the TLB
  • Huge pages skip the permission checks that ordinary pages require
  • Huge pages let the table be read with one instruction instead of many

A process allocates 1 GB and then touches only the first 10 MB of it. Roughly how much RAM does it use for that allocation?

  • About 1 GB, since the kernel reserves frames when memory is allocated
  • None, because allocated memory is kept on disk until it is freed
  • About 10 MB, since frames are assigned only to pages that are touched
  • About 512 MB, since the kernel commits half an allocation up front

Which number does a container's memory limit enforce?

  • The virtual size, because it is the amount the process may ever use
  • The size of the largest single allocation the process has made
  • The page-table size, because each entry stands for one used frame
  • The pages actually held in RAM, not the addresses it has reserved

Two processes print the address of one of their objects and get the same number. What does that tell you?

  • The two objects share one physical frame and hold the same data
  • Nothing about sharing: each address is translated by its own table
  • One process has read the other's memory through a shared page table
  • The kernel failed to randomize their layouts, which is a security bug

What does a TLB miss add to one memory access, in the worst case?

  • A trip to the disk, because the page table lives in the swap area
  • Nothing, because the processor always keeps a copy of the whole table
  • About a nanosecond, one extra cache lookup that always hits
  • Up to four reads of page-table entries, each possibly a trip to RAM

You got correct