Memory Management & Virtual Memory: Deep-Dive Tutorial
📘 The Complete Tutorial
Introduction: Why Memory Management Is the Deepest Topic
Memory management is where hardware meets software at the most fundamental level. Every pointer dereference, every array access, every object creation — all of it flows through the memory subsystem. Understanding this layer means understanding how the CPU, the operating system, and the hardware cooperate to create the illusion of infinite, contiguous memory.
This course takes you from physical RAM chips to GPU tensor allocation.
Module 01: Physical Memory
Physical memory is the actual DRAM installed in your machine. It is organized as:
- Frames: Fixed-size chunks (typically 4 KB on x86-64)
- Banks: Independent memory arrays that can be accessed in parallel
- Ranks: Groups of chips that share a chip-select line
- Channels: Independent data paths between CPU and memory
Physical address space: A flat array of bytes from 0x00000000 to 0xFFFFFFFFFFFF (on 64-bit systems, usually limited to 48 bits of physical address space).
Key insight: Physical memory is scarce, fragmented, and shared among all processes and the kernel.
Module 02: Virtual Memory
Virtual memory is an abstraction layer that gives each process the illusion of owning the entire address space.
Benefits:
- Isolation: Process A cannot read/write Process B's memory
- Efficiency: Only used memory needs physical backing
- Sharing: Same physical page mapped into multiple processes (shared libraries,
mmap) - Simplification: Each program can be compiled for a fixed base address
The contract: The OS promises each process a contiguous virtual address space. The hardware (MMU) translates virtual addresses to physical addresses on every access.
Module 03: Virtual Address
A virtual address is what your program sees. On x86-64 with 4-level paging:
┌─────────┬─────────┬─────────┬─────────┬───────────────┐
│ PML4 │ PDPT │ PD │ PT │ Offset │
│ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │
└─────────┴─────────┴─────────┴─────────┴───────────────┘
47-39 38-30 29-21 20-12 11-0
48-bit virtual address space: 256 TB per process (128 TB user, 128 TB kernel).
Module 04: Physical Address
A physical address points to an actual location in RAM.
Physical Address = Physical Frame Number (PFN) × Page Size + Offset
The offset (bottom 12 bits) is identical in virtual and physical addresses. Only the page number needs translation.
Module 05: MMU (Memory Management Unit)
The MMU is a hardware component inside the CPU that performs address translation.
On every memory access:
- CPU issues a virtual address
- MMU splits it into VPN (Virtual Page Number) + Offset
- MMU looks up the physical frame in the page table
- MMU concatenates Physical Frame Number + Offset
- Physical address sent to memory controller
Without MMU: Every program would need to know physical addresses and avoid stepping on other programs.
Module 06: Page Tables
Page tables are multi-level hierarchical data structures stored in physical memory.
x86-64 4-level paging:
| Level | Name | Entries | Size |
|---|---|---|---|
| L4 | PML4 (Page Map Level 4) | 512 | 4 KB |
| L3 | PDPT (Page Directory Pointer Table) | 512 | 4 KB |
| L2 | PD (Page Directory) | 512 | 4 KB |
| L1 | PT (Page Table) | 512 | 4 KB |
Translation process:
CR3 → PML4 base address
PML4[VPN4] → PDPT base
PDPT[VPN3] → PD base
PD[VPN2] → PT base
PT[VPN1] → Page Frame + Flags
Page Table Entry (PTE) flags:
P(Present) — Page is in physical memoryR/W— Read/Write permissionU/S— User/Supervisor (kernel-only if clear)A(Accessed) — Set by CPU on read/writeD(Dirty) — Set by CPU on writeNX— No-Execute (bit 63, XD bit)
Module 07: Page Directory
The page directory (PD) is the L2 level of the page table hierarchy. Each entry points to a page table (PT) or can directly map a 2 MB huge page (if the PS bit is set).
5-level paging (newer CPUs): Adds PML5 for 57-bit virtual addresses (128 PB address space).
Module 08: TLB (Translation Lookaside Buffer)
The TLB is a hardware cache of recent virtual-to-physical translations.
| TLB Type | Entries | Associativity | What It Caches |
|---|---|---|---|
| L1 dTLB | 64 | 4-way | Data page translations |
| L1 iTLB | 128 | 8-way | Instruction page translations |
| L2 sTLB | 1536+ | 12-way | Shared, larger translations |
TLB miss: If the translation is not in the TLB, the MMU must walk the page table in memory — a 4-level walk can take 20+ memory accesses!
TLB shootdown: When the OS changes a page table entry, it must invalidate the TLB entry on all CPUs (using INVLPG or INVPCID).
Module 09: Page Faults
A page fault is an exception triggered when the MMU cannot translate a virtual address.
Types of page faults:
| Type | Cause | Handler Action |
|---|---|---|
| Major fault | Page not in RAM | Load from disk (swap/page file) |
| Minor fault | Page in RAM but not mapped | Map the page, update PTE |
| Protection fault | Access violation (write to RO, user to kernel) | Send SIGSEGV |
Page fault handler flow:
- CPU saves state, jumps to page fault handler
- Handler checks if address is valid (
do_page_fault()) - If valid but not present: allocate frame, fill from disk or zero
- If valid but protection issue: check COW, fix permissions
- If invalid: deliver
SIGSEGVto process
Module 10: Demand Paging
Demand paging means pages are allocated only when accessed, not at process start.
How it works:
exec()maps the program but marks all pages as not present- First access to a page triggers a page fault
- OS allocates a physical frame, loads the page from disk
- PTE is updated, process resumes
Benefits:
- Faster program startup
- Only used memory consumes RAM
- Shared libraries loaded on first use
Module 11: Copy-on-Write (COW)
COW is an optimization for fork():
- Parent forks child
- Child gets a copy of parent's page tables
- All pages marked read-only in both processes
- Either process writes → page fault → OS copies the page
- Each process gets its own writable copy of that page only
Result: fork() is fast because no physical copying happens until necessary.
Module 12: mmap()
mmap() maps files or anonymous memory into the virtual address space.
1// Anonymous mapping (like malloc, but page-aligned) 2void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE, 3 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 4 5// File mapping 6int fd = open("file.dat", O_RDONLY); 7void *addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
Behind the scenes:
mmap()creates VMA (Virtual Memory Area) structures- No physical pages allocated yet
- First access → page fault → frame allocated, file page loaded (or zeroed)
Module 13: brk()
brk() and sbrk() adjust the program break — the end of the data segment (heap).
Low Addresses High Addresses
┌─────────┬─────────┬─────────┬──────────────────────────┐
│ Text │ Data │ BSS │ Heap → │
└─────────┴─────────┴─────────┴──────────────────────────┘
↑
brk point
brk(addr)— Set break to absolute addresssbrk(increment)— Move break by increment
Limitation: The heap is a single contiguous region. Modern allocators use mmap() for large allocations to avoid heap fragmentation.
Module 14: malloc Internals
malloc is a user-space allocator layered on top of brk()/mmap():
glibc ptmalloc2 structure:
- Arena: A heap region (one per thread, plus a main arena)
- Chunk: Header + user data + optional footer
- Bins: Free lists organized by size
- Fast bins: 8-128 bytes, single-linked, LIFO, no coalescing
- Small bins: < 1024 bytes, double-linked, exact size
- Large bins: > 1024 bytes, double-linked, best-fit
- Unsorted bin: Recently freed chunks, first search target
Chunk metadata:
[prev_size | size | A | M | P | ... user data ... | size ]
A(Arenas bit): Main arena vs. thread arenaM(Mmapped bit): Allocated viammap()(not heap)P(Prev inuse bit): Previous chunk is allocated
Module 15: Kernel Allocator
The kernel cannot use malloc() — it has its own allocators:
| Function | Purpose |
|---|---|
kmalloc(size, flags) | General allocation, physically contiguous |
kzalloc(size, flags) | kmalloc + zero initialization |
kfree(ptr) | Free kmalloc memory |
krealloc(ptr, size, flags) | Resize allocation |
GFP flags:
GFP_KERNEL— Standard, may sleepGFP_ATOMIC— Interrupt-safe, never sleeps (uses emergency pools)GFP_DMA— Allocate from DMA-able zone
Module 16: Buddy Allocator
The buddy system is the kernel's page allocator. It manages physical pages in power-of-2 blocks.
How it works:
- Memory divided into blocks of size
2^npages - Allocation: Find smallest block that fits. Split larger blocks if needed.
- Freeing: Check if "buddy" (adjacent block of same size) is free. If so, coalesce into larger block.
Benefits: Fast allocation, easy coalescing, low external fragmentation. Drawback: Internal fragmentation (you might allocate 8 pages for a 5-page request).
Module 17: Slab / SLUB Allocator
The slab allocator sits on top of the buddy allocator and provides object-sized allocations.
How it works:
- Create caches for commonly used object types (
task_struct,inode,dentry) - Each cache contains slabs — one or more pages divided into objects
- Allocation: Return a free object from a slab
- Freeing: Mark object free, return to slab
SLUB (the modern replacement for SLAB):
- Simpler, better debugging, per-CPU caches
- No complex coloring or chain management
Benefits: No internal fragmentation for objects, fast (no splitting), cache-friendly (same-type objects clustered).
Module 18: Huge Pages
Standard pages are 4 KB. Huge pages are 2 MB or 1 GB.
Benefits:
- Fewer TLB entries needed (1 entry for 2 MB vs. 512 entries)
- Reduced page table walk overhead
- Better performance for large working sets (databases, HPC)
Usage:
1# Transparent Huge Pages (THP) — automatic 2echo always > /sys/kernel/mm/transparent_hugepage/enabled 3 4# Explicit huge pages 5echo 1024 > /proc/sys/vm/nr_hugepages 6mmap(..., MAP_HUGETLB, ...);
Tradeoff: Internal fragmentation. A 2 MB page allocated for 1 byte wastes ~2 MB.
Module 19: NUMA (Non-Uniform Memory Access)
In multi-socket systems, each CPU has local memory (fast) and remote memory (slow, accessed via interconnect).
Socket 0 Socket 1
┌─────────┐ ┌─────────┐
│ CPU 0 │──QPI/Infinity──│ CPU 1 │
│ CPU 1 │ Fabric │ CPU 2 │
└────┬────┘ └────┬────┘
│ Local RAM │ Local RAM
└─ Fast (~80ns) └─ Fast (~80ns)
└─ Remote access ──────────── Slow (~120ns+)
NUMA-aware allocation:
numactl --membind=0 ./program— Force allocation on node 0libnumaAPI for explicit control- Linux kernel's NUMA balancer migrates pages toward the CPU that accesses them
Module 20: Memory-Mapped Files
Memory-mapped files allow a file to be accessed as if it were an array in memory.
1int fd = open("data.bin", O_RDWR); 2void *addr = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 3 4// Now you can do: 5int value = ((int*)addr)[42]; // Read 43rd integer 6((int*)addr)[42] = 100; // Write back (if MAP_SHARED)
Page cache integration: The mapped pages are the same pages in the kernel's page cache. Multiple processes mapping the same file share physical pages.
Write-back: Modified pages are marked dirty. The kernel flushes them to disk asynchronously (or on msync()).
Core Architecture: The Full Translation Path
CPU
│
▼
Virtual Address (e.g., 0x7FFF_1234_5000)
│
├─ VPN (Virtual Page Number) ─────┐
│ │
▼ │
MMU │
│ │
▼ │
TLB Lookup? ──Yes?──► Physical Frame Number
│ │
No │
│ │
▼ │
Page Table Walk (4 levels) │
│ │
▼ │
PTE Found? ──Yes?──► Update TLB ──┘
│ │
No │
│ │
▼ │
Page Fault Handler │
│ │
├─ Not Present? ──► Allocate frame / Load from disk
│
├─ Protection Violation? ──► SIGSEGV
│
└─ COW? ──► Copy page, update PTEs
│
▼
Physical Address (e.g., 0x0000_8ABC_5000)
│
▼
RAM (DRAM)
🛠 Project: Virtual Memory Simulator
This C program simulates the entire virtual-to-physical translation pipeline: virtual address → page number → page table → TLB → physical frame → physical memory.
Features:
- Multi-level page table simulation
- TLB with LRU replacement
- Page fault handling with demand paging
- Copy-on-Write demonstration
- Physical frame allocator (bitmap)
- Page replacement (LRU)
Download: vm_simulator.c
Now let me compile the complete tutorial with the AI connection and all download links:
🤖 AI Connection: From Virtual Memory to GPU Tensors
The same memory principles you just learned power modern AI infrastructure:
CPU RAM
↓
PCIe Bus (16-32 GB/s, high latency ~1µs)
↓
GPU VRAM (HBM3e / GDDR6X)
↓
GPU Memory Hierarchy
├─ L2 Cache (shared across SMs)
├─ Shared Memory / L1 (per SM, ~100KB, software-managed)
└─ Registers (per thread, ~256 each)
↓
Tensor Allocation
├─ Contiguous chunks for weights
├─ Strided layouts for activations
└─ Paged attention for KV cache
↓
KV Cache (Key-Value Cache for Transformers)
├─ Grows with sequence length
├─ Often offloaded to CPU RAM (offload)
└─ Quantized to INT8/INT4 to save memory
Why this matters:
| Concept | CPU/OS | GPU/AI |
|---|---|---|
| Virtual Memory | Page tables + MMU | CUDA Unified Memory (UM) |
| TLB | Hardware cache of translations | GPU TLB (simpler, per-SM) |
| Page Fault | Demand paging from disk | CUDA UM page faults over PCIe |
| COW | fork() optimization | Model sharding across GPUs |
| NUMA | CPU socket-local memory | GPU-to-GPU NVLink topology |
| Huge Pages | 2MB/1GB for large datasets | Tensor memory alignment (128B) |
| Slab Allocator | Kernel object caches | CUDA memory pools (cudaMallocAsync) |
| Page Replacement | LRU / Clock algorithm | KV cache eviction strategies |
The KV Cache Problem: In transformer inference, the KV cache stores attention keys and values for all previous tokens. For a 70B model with 8192 context length:
- KV cache size: ~70B × 2 × 8192 × 2 bytes ≈ 2.3 GB per sequence
- With batch size 32: 73 GB — exceeds most single GPU VRAM
Solutions that reuse your memory knowledge:
- Paging: Split KV cache into blocks (like OS pages), allocate on demand
- Offloading: Move cold KV blocks to CPU RAM (like swap)
- Quantization: Reduce precision (FP16 → INT8 → INT4) — like compression
- NUMA-aware placement: Place tensors on the GPU that will access them
🎓 Summary: The Memory Management Mindset
| Module | Key Takeaway |
|---|---|
| Physical Memory | DRAM frames, banks, channels — finite and shared |
| Virtual Memory | Illusion of infinite, private, contiguous address space |
| Virtual Address | Split into VPN + offset; VPN translated, offset preserved |
| Physical Address | PFN × page_size + offset; points to actual RAM |
| MMU | Hardware translator; walks page tables on every access |
| Page Tables | Multi-level hierarchy (PML4→PDPT→PD→PT); stored in RAM |
| Page Directory | L2 table; can map 2MB huge pages directly |
| TLB | Hardware cache of translations; miss = expensive page walk |
| Page Faults | Major (disk), minor (map), protection (SIGSEGV) |
| Demand Paging | Allocate on first access, not at load time |
| Copy-on-Write | Share pages on fork; copy only on write |
| mmap() | Map files or anonymous memory; creates VMAs |
| brk() | Adjust heap break; contiguous but limited |
| malloc | User-space allocator on top of brk/mmap; fast bins, coalescing |
| kmalloc | Kernel allocator; GFP flags control behavior |
| Buddy Allocator | Power-of-2 blocks; fast alloc/free with coalescing |
Download the Virtual Memory Simulator
Complete C code with all demos:
Build and run:
1gcc -O2 -o vm_simulator vm_simulator.c 2./vm_simulator
What you'll see:
- Demand paging in action (page faults on first access)
- TLB hits vs. misses
fork()+ Copy-on-Write demonstration- Permission violation (simulated SIGSEGV)
- TLB LRU replacement behavior
- Physical memory allocation statistics
"Memory management is the art of illusion. The OS gives each process the fantasy of owning the entire machine, while the hardware quietly translates every fantasy into physical reality. Master this illusion, and you master the machine."
Master virtual memory, and you understand not just operating systems, but also GPU programming, database internals, distributed systems, and the memory bottlenecks that define modern AI infrastructure.