Operating systems and concurrency
Virtual memory
Why every process sees its own private memory, how the kernel maps it to RAM in 4 KiB pages, why ps shows two memory numbers, and what triggers the OOM killer.
Two programs can both use the address 0x7ffd1234 and refer to different bytes. Each process sees its own flat range of addresses, and the kernel translates those addresses into real RAM. That translation is virtual memory. It keeps programs private from each other and lets them reserve more memory than the machine physically has.
The big idea
Memory is handled in fixed-size pages, usually 4 KiB. For each process, the kernel keeps a page table that maps each virtual page to a physical frame in RAM. The CPU's memory management unit (MMU) walks this table on every access.
Think of each process as having its own floor plan with room numbers. A translation list, the page table, says which real room each numbered room is. The analogy breaks in one place: a real room is only assigned when it is first used.
Concept
Page: a fixed block of memory, usually 4 KiB. Frame: a block of real RAM of the same size. The page table maps pages to frames.
Walking through one translation
Pages are 4096 bytes, which is 2 to the power of 12. So the low 12 bits of an address are the offset inside the page.
Virtual address = 0x0000_7f3a_1234_5678
Offset = low 12 bits = 0x678
Virtual page no. = address >> 12 = 0x7f3a12345
Page table says : page 0x7f3a12345 -> frame 0x1a2b3
Physical address = (0x1a2b3 << 12) | 0x678
= 0x1a2b3000 | 0x678
= 0x1a2b3678The offset passes through unchanged. Only the page number is translated.
The CPU caches recent translations in the TLB (translation lookaside buffer). With 4 KiB pages, 1,536 TLB entries cover 6 MiB. A program that jumps randomly through a 50 GiB in-memory index misses the TLB constantly. Huge pages (2 MiB or 1 GiB) help, because one entry then covers far more memory.
Lazy allocation and page faults
When malloc asks for memory, the kernel usually reserves addresses without assigning RAM. The first touch of a page triggers a page fault. The kernel finds a free frame, zeroes it, updates the table and resumes your instruction.
- A minor fault is served from RAM and takes about a microsecond.
- A major fault needs disk I/O, such as reading a memory-mapped file. It takes about 100 µs on NVMe to about 10 ms on a spinning disk.
This is why ps shows two memory numbers:
| Number | Meaning | Use it for |
|---|---|---|
| VSZ (virtual size) | All reserved address space | Almost nothing. A JVM can show many GB here. |
| RSS (resident set size) | Pages in RAM right now | What you pay for. Shared pages count in every process that maps them. |
Overcommit and the OOM killer
Linux lets processes reserve more virtual memory than RAM plus swap, betting they won't touch it all. When the bet fails, the OOM killer (out-of-memory killer) picks a victim, usually the biggest process, and sends it SIGKILL.
Containers hit the same limit at a smaller scale. A container that passes its cgroup memory limit exits with code 137, and Kubernetes reports OOMKilled: true. The fix is usually a leak hunt or a correct limit, not a restart loop.
Copy-on-write
fork() would be slow if it copied a 2 GB address space. Instead, the child starts with page-table entries that point at the same frames, marked read-only. This is copy-on-write (COW). Only when one process writes to a page does the kernel copy that single 4 KiB page.
Redis uses this for snapshots. The forked child saves a frozen view of memory to disk while the parent keeps serving writes. If the parent rewrites 30% of pages during the snapshot, memory use grows by up to 30% of the dataset. That is why the Redis docs warn about spare memory.
Common mistakes
- Alerting on VSZ. It is reserved space, not use. Alert on RSS or
memory.current. - Assuming a fork copies all memory. It copies page tables only. Pages are copied one at a time, as they are written.
- Ignoring TLB misses. Random access over a large structure can be slow even when the CPU is idle.
Check yourself
- Two processes both use address
0x7ffd1234. Why don't they clash? Each process has its own page table, so the same virtual address maps to different frames. - A container exits with code 137 and
OOMKilled: true. What should you check first? Its memory limit, and whether the process leaks memory.
Key takeaways
- Each process sees a private address space. The page table maps 4 KiB pages to RAM frames.
- Memory is assigned lazily. The first touch causes a page fault.
- Use RSS, not VSZ, to judge memory use.
- The OOM killer and cgroup limits kill processes that use too much. Exit code 137 is the usual sign.
Further learning
David Black-Schaffer's short lecture series explains page tables, TLBs and page faults better than most text. Start with the introduction.
Next, see how the kernel shares CPU time in CPU scheduling.
Related
Processes, threads and the kernel
What the OS gives every program, and how to see it on a live Linux box. Start here for the process model.
Process lifecycle
How a process is born with fork, takes on a new program with exec, and exits with a code. Why zombies build up when a parent never calls wait.
Memory management and garbage collection
How languages decide where values live and when memory is given back, the trade-offs of manual, ownership, reference-counted and tracing approaches, and how to find leaks and size heaps safely.
The memory hierarchy
From registers to SSDs to the network, the latency numbers that dominate performance, and why the tier your data sits in matters more than the code you write.