Memory that does not exist yet
A guided walk through virtual addresses, page faults, copy-on-write, and why VSZ/RSS/PSS disagree — before Firecracker and microVMs.
Ask a program where its memory lives and it points at an address. Ask the machine the same question and the answer may be: nowhere yet.
That is not a bug. The address is virtual. It names a spot in this process's address space — not a particular RAM chip. Linux can give you the address first and supply real storage later, when you first touch it.
People often hear “virtual memory = disk as extra RAM.” That is one side effect. The useful idea is simpler:
Virtual memory is an indirection layer between the addresses a program uses and the physical frames the hardware can access.
This lesson walks that layer top to bottom. Terms are defined before they show
up. You will get annotated terminals and a visual model of the address space.
On macOS there is no /proc — use the macOS toggle where you see one.
By the end you should be able to say, in plain language: what a virtual address
is, what a page fault does, why fork is cheap until someone writes, and why
memory dashboards disagree.
1. An address is not a RAM chip
Suppose a program does:
ask for 1 GiB of memory
write one byte at the start
If that succeeds, the process owns a range of virtual addresses. It has not necessarily consumed 1 GiB of RAM. Linux may wait until each page is first touched before providing a physical frame.
Three different quantities (do not mix them):
- address space — ranges of virtual addresses the process is allowed to use
- resident memory — pages from those ranges currently backed by physical RAM
- committed / promised — what the system has said it should be able to provide if you eventually touch it (subject to overcommit policy)
A huge address range can hold almost no resident pages. A page can be resident
and shared by a hundred processes. A malloc can succeed and a later write can
still fail under memory pressure.
Pages (the unit of translation)
The CPU does not usually translate every byte alone. Memory is divided into fixed-size pages. On typical Linux x86-64 / arm64, a base page is 4 KiB.
Think of a virtual address as:
[ which page? ][ byte inside that page ]
Translation finds a physical frame for the page number. The offset inside the page stays the same.
You do not need the bit math yet. You need the habit: systems talk in pages.
2. Your process has a map of allowed regions
Linux records contiguous chunks of address space as VMAs — virtual memory areas. A VMA answers: “is this range allowed, and what is it for?”
Examples of what a VMA might say:
- readable / writable / executable
- anonymous (just zeros / heap) or backed by a file (your binary, a
.so) - private or shared
- stack, heap, code, library…
Empty gaps between VMAs are real. Touching a gap is illegal even if the machine has free RAM.
Session · your address map
Look at the regions your process is allowed to use
A virtual address is only legal if it sits inside a mapped region. This session shows that map and names the fields.
Linux — /proc/$$/maps
Words you just saw
- address range
- Start–end virtual addresses for one region (one VMA).
- perms (rwxp)
- read / write / execute / private(p) or shared(s).
- pathname
- Backing object: your binary, a .so, [heap], [stack], or empty for anonymous.
- VMA
- Virtual Memory Area — one contiguous “allowed” region in the address space.
Checkpoint: an address is only meaningful if a VMA covers it. The map is policy — “you may use these ranges.” It is not yet the live translation the CPU uses on every load and store.
3. Two layers: the permit and the street address
Here is the part that usually goes too fast. Slow it down.
Your process has two related ideas, and they are not the same:
- The permit (VMA) — “Is this virtual address in an allowed region?”
If no → crash (SIGSEGV), even if the machine has free RAM. - The street address (PTE) — “Right now, which physical RAM frame backs
this page — if any?”
A page can be allowed (permit yes) and still have no frame yet (street address missing).
A page table is just the kernel’s book of those street addresses for pages that are in play. Each entry is a PTE (page-table entry). It can say:
- present or absent
- writable or read-only
- executable or not
The sentence that unlocks the rest of this lesson:
“Not present” does not mean “invalid.”
- Permit yes, PTE absent → often a page fault the kernel can repair
(lazy allocation, first touch, copy-on-write, …) - Permit no → illegal → usually
SIGSEGV
Click around in the visual below. Hollow mapped pages are “allowed but not in RAM yet.” Dashed gaps are illegal. Filled pages already have a frame.
Visual · permit vs street address
Click the address space. Watch RAM appear only when needed.
Think of two layers. The top strip is virtual pages (what the program names). The bottom strip is physical frames (real RAM). A mapping can exist before a frame does.
Virtual address space (what the program sees)
page 4 · ~0x404000
Physical frames (real RAM in this toy model)
What just happened
Click a page in the mapped band. Gray gaps are illegal. Hollow mapped pages need a fault.
- VMA (permit)
- Yes — this range is allowed
- PTE (street address)
- Absent → needs a fault
- Result
- Page fault → retry
You can ignore the TLB for now (a cache of recent translations). Just remember: the CPU wants a present PTE; if it does not have one, it asks the kernel.
4. First touch: the page fault as a slow “yes”
When the CPU cannot finish a translation (or permissions disagree), it pauses the instruction and asks the kernel. That pause is a page fault.
The kernel checks the permit, then tries to fix the street address:
- Is this address covered by a VMA?
- Is this kind of access allowed (read / write / execute)?
- Can we give it a frame (or fix permissions)?
If yes: install the PTE, return, and the CPU retries the exact same instruction. Your program usually does not see an error — the access just took longer.
If no: the kernel cannot repair it → signal.
Two common labels:
- Minor fault — fixed without waiting on disk (fresh zero page, or page already in the page cache)
- Major fault — must wait for storage I/O (cold file page)
“Minor” is not “harmless forever.” It only means “no disk wait this time.”
The visual plays the story. Underneath, the same demo is in Python and C++ (switch panes) — every step explained, not a cryptic blob.
Use anonymous mmap, not bytearray or value-initialized new char[N]().
Those often touch pages while allocating, so “after alloc” and “after touch”
can look identical. That does not disprove demand paging — it means the demo
allocated eagerly.
Visual · reserve then touch
Mapped space can be big while RAM stays small
Watch sixteen virtual pages. Allocating marks them as allowed. Touching them one-by-one is what pulls in RAM.
Virtual mapping
0 KiB
Resident now
0 KiB
Story step
not allocated
Press play. First we “malloc” the band (mapped, hollow). Then we write one byte per page (resident, filled).
Session · reserve then touch in code
Same demo in Python and C++
Anonymous mmap reserves; the touch loop pays. Python and C++ panes — short versions. Avoid bytearray / zeroing new[].
Python
Words you just saw
- mmap(NULL, N, … MAP_ANONYMOUS)
- Ask the OS for N bytes of anonymous address space. Usually lazy on Linux — almost no RAM until touch.
- bytearray / new char[N]()
- Often eager: CPython may zero bytearray; value-init new[] zeroes too. That hides demand paging.
- VmRSS
- Current resident size from /proc (Linux). Prefer this over ru_maxrss (high-water only).
- stride 4096
- One write per 4 KiB page — each first write can fault in a new frame.
What to notice
- Python: paste the heredoc. C++: save touch_pages.cpp, then g++ -O0 touch_pages.cpp -o touch_pages && ./touch_pages
- You want after alloc small, after touch ~64 MiB larger.
Checkpoint: malloc / mmap often reserve. Touching pays. The payment
mechanism is the page fault.
One caution: Linux may overcommit — promise more anonymous memory than it could hold all at once if everyone touched everything. A successful allocation is not a guarantee that every future write succeeds under pressure.
5. Where do the bytes come from? (mmap, cache, COW)
mmap creates a VMA (a permit). It does not mean “read the whole file into
RAM now.”
- Anonymous mapping — no file. Pages start as zeros. Frames appear on touch. Heaps are often built this way.
- File-backed mapping — addresses correspond to file offsets. First touch may fill from the page cache (or from disk into that cache), then install a PTE.
Buffered read() and file-backed mmap() usually share the same page cache.
Clean cache is “used” RAM that is often reclaimable — which is why staring only
at free misleads.
Copy-on-write in one picture
For private mappings (and for memory after fork):
- Reads can share one physical frame among processes
- The first write copies that one page and points only the writer at the private copy
That is copy-on-write (COW). Not “copy the whole heap on fork.” The next block shows the three moments side by side — before fork, right after, and after one write — so you can see that only the touched page grows RAM.
Session · copy-on-write
Share until someone writes
fork does not duplicate the whole heap. Read the three moments below left → right. Only the written page becomes private.
1 · Before fork
One process, four resident pages. Four physical frames in RAM.
- Parent
- → F0 F1 F2 F3
- Child
- (does not exist)
- RAM
- F0 F1 F2 F3
Physical cost: 4 pages
2 · Right after fork
Child appears. Both point at the same frames. No full copy.
- Parent
- → F0 F1 F2 F3
- Child
- → F0 F1 F2 F3
- RAM
- F0 F1 F2 F3 (still 4)
Physical cost: still 4 pages
Both PTEs mark the pages read-only so a write will fault.
3 · Child writes page 0
Only page 0 is copied. Pages 1–3 stay shared.
- Parent
- → F0 F1 F2 F3
- Child
- → F0′ F1 F2 F3
- RAM
- F0 F0′ F1 F2 F3
Physical cost: 5 pages (+1)
F0′ is the private copy. One write ≠ copy the whole heap.
The only idea
- After fork, summing parent RSS + child RSS double-counts the shared frames.
- A write copies one page (typically 4 KiB), not the whole buffer.
- That is why fork + exec is cheap: the child usually replaces its address space before copying most pages.
in words
Words you just saw
- COW
- Copy-on-write — keep sharing a frame until a write proves you need your own.
- write fault
- First write to a shared read-only page: kernel copies the page, installs a private writable PTE, retries the store.
Checkpoint: sharing until write is why fork is cheap — and why a child that
dirties a huge inherited buffer suddenly costs RAM.
6. Why memory numbers disagree
Dashboards argue because they answer different questions:
- VSZ / VIRT — how much address space is mapped? (includes untouched)
- RSS — how much resident memory can this process reach?
(shared pages counted fully in each process) - PSS — what fair share of resident memory should we attribute here?
(shared pages divided among mappers) - Private / USS — what would likely free if only this process exited?
Summing RSS across processes double-counts shared libraries. That is the wrong question, not bad arithmetic. Hollow-vs-filled pages from earlier are the same idea: mapped ≠ resident.
Session · memory numbers
Same process, three honest answers
VSZ, RSS, and PSS answer different questions. None is “the” memory number.
ps + smaps
Words you just saw
- VSZ
- How much address space is mapped (including untouched).
- RSS
- How much resident memory this process can reach (shared pages counted fully).
- PSS
- Fair share — divide each shared page among the processes that map it.
Words you just saw
- Pick by question
- Mappings → VSZ. Reach → RSS. Fair share → PSS. What one exit frees → private/USS.
Working set (short): residency changes over time. You can have 2 GiB resident and only actively need 100 MiB. Under pressure, cold clean file pages can be dropped and faulted back later.
7. A microVM adds another translation layer
Inside a guest, a process still uses guest virtual addresses. The guest kernel still has VMAs and page tables. Virtualisation adds a second step:
guest virtual address
→ guest page tables →
guest physical address
→ second-stage translation →
host physical frame
Same idea — an address at one layer, a translation to backing at the next — with a different owner for each fault. File that away for Firecracker. Today’s foundation does not change.
What to keep
- A virtual address is not a physical location.
- A VMA is the permit; a PTE is the current street address.
- Absent PTE can be repairable; unmapped cannot.
- A page fault is often how lazy memory works — not always a crash.
- COW shares until write; only the written page becomes private.
- VSZ ≠ RSS ≠ PSS — pick the number that matches your question.
Next: a pathname is not a file on disk — how names become inodes, pages, and blocks, and why a microVM root disk is a filesystem inside a file.