# Virtual Memory: The Lie Every Process Believes

> Every process thinks it has a private, contiguous address space starting at zero. None of them do. Virtual memory is the translation layer that makes the lie hold — and it's why fork, mmap, and swap can exist at all.

URL: https://harshith.in/blog/virtual-memory-basics/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: memory, systems, fundamentals

Run `cat /proc/self/maps` and you'll see your process's memory laid out: code near
the bottom, heap above it, stack near the top, libraries scattered between. Run it
twice and the addresses differ (ASLR), but the *shape* is the same for every process
on the machine — and every one of them believes it owns addresses like
`0x7f8a2c000000`. They can't all own it. None of them do. Every address your code
ever touches is virtual, and the kernel plus the MMU translate each one to wherever
the data physically lives — RAM, disk, or nowhere at all.

## The Mapping Is Per-Page

Translating arbitrary bytes would need a translation entry per byte. Instead, memory
is managed in **pages** — 4 KB on most systems — and the kernel keeps a per-process
**page table** mapping virtual pages to physical page frames:

```text
  virtual page 0x7f8a2c000 ──▶ physical frame 0x1a2f3   (in RAM)
  virtual page 0x7f8a2c001 ──▶ physical frame 0x00b71   (in RAM)
  virtual page 0x7f8a2c002 ──▶ (not present — on disk, or never touched)
```

The MMU walks this table in hardware on every access. Two consequences fall out
immediately: physical memory doesn't need to be contiguous (your "contiguous" 1 GB
buffer is confetti scattered across RAM), and two processes can map the same
physical frame — which is how shared libraries exist in memory exactly once.

## The Page Fault Is a Feature

Touch a page whose entry says "not present" and the MMU raises a **page fault**.
This sounds like an error; it's actually the kernel's main memory-management
mechanism working as designed:

- **Demand paging.** `malloc(1 << 30)` returns instantly because nothing was
  allocated — the kernel just recorded a reservation. Physical frames are assigned
  one at a time, at first touch, via page faults. This is why a program can
  "allocate" far more than it uses.
- **Copy-on-write.** `fork()` doesn't copy the parent's memory. Both processes map
  the same frames, marked read-only. Whoever writes first faults, and only *that
  page* gets copied. A 10 GB process forks in microseconds.
- **Swap and file mappings.** A page evicted to disk, or an `mmap`ed file page not
  yet read, is just "not present." The fault handler reads it back in and the
  faulting instruction retries, none the wiser.

A fault serviced from RAM (a "minor" fault) costs microseconds. One that goes to
disk ("major" fault) costs milliseconds — five orders of magnitude above a cache
hit. `ps` won't show you this; `perf stat -e page-faults,major-faults` will.

## Why Your Memory Numbers Never Agree

Virtual memory is why "how much memory is this process using" has no single answer:

- **VSZ** (virtual size): everything mapped — reservations, files, untouched
  `malloc`s. Nearly meaningless as a cost measure.
- **RSS** (resident set size): pages actually in RAM right now. Closer, but shared
  library pages are counted fully in *every* process using them.
- **PSS** (proportional set size, in `/proc/<pid>/smaps_rollup`): shared pages
  divided among their users. The honest number for "what does killing this free."

A Chrome tab showing 300 MB RSS may cost 80 MB PSS. Capacity planning off RSS
systematically overcounts.

## Protection Comes Free

Each page table entry carries permission bits — readable, writable, executable.
The MMU checks them on every access, which is where segfaults actually come from:
a segfault is a page fault the kernel couldn't or wouldn't satisfy (unmapped
address, write to read-only page). It's also the mechanism behind W^X (no page
both writable and executable), guard pages that catch stack overflows, and
`mprotect`-based tricks like garbage collector write barriers.

The translation itself has a cost — every load needs a page-table lookup — and the
TLB, the cache that makes this survivable, deserves its own post. But the mental
model to keep is this: **memory in a modern OS is not a thing you have, it's a set
of promises the kernel keeps lazily, one page fault at a time.**

## Takeaways

- Every address in user code is virtual; the kernel + MMU translate per 4 KB page.
- Page faults aren't errors — they implement demand paging, copy-on-write fork,
  swap, and mmap.
- `malloc` reserves; first touch allocates. Major faults (disk) cost ~1000× minor.
- VSZ overcounts wildly, RSS double-counts shared pages, PSS tells the truth.
- Segfaults are page faults with no legitimate resolution — permissions live in
  the page table too.
