OPERATING SYSTEMS / 5. MEMORY MANAGEMENT
Memory Management — Virtual Memory & Paging
How the OS gives every process its own private universe of memory
EXPLANATION
Memory management is one of the OS's most critical jobs. It must create the illusion that each process has a huge private address space while actually sharing limited physical RAM among all processes. Virtual Memory — the fundamental abstraction: Every process runs in its own virtual address space. On a 64-bit Linux system, each process sees addresses from 0 to 2^48-1 (256 TB). Of course, physical RAM is much smaller (16 GB). Virtual memory works because: ① Most of a process's address space is empty (not backed by physical RAM) ② Only actively used pages are in physical RAM at any time ③ The hardware MMU (Memory Management Unit) translates virtual → physical for every memory access Pages and Page Tables: • Memory is divided into 4 KB pages (page = smallest unit of memory management) • The page table is a data structure the kernel maintains for each process. It maps virtual page numbers → physical page frame numbers • The MMU uses the page table automatically on every memory access (it's in hardware, fast) • TLB (Translation Lookaside Buffer): hardware cache for recent virtual→physical translations. Speeds up page table lookups enormously What happens when you access an unmapped address: ① CPU tries to access virtual address 0x7fff00001234 ② MMU looks up page table → entry not present → PAGE FAULT exception ③ Kernel's page fault handler runs (interrupt) ④ Kernel allocates a physical page frame ⑤ Kernel updates page table: virtual page 0x7fff00001 → physical frame 0x3A00 ⑥ Kernel returns → CPU retries the access → MMU finds mapping → success Demand Paging: pages are only brought into RAM when first accessed. When you exec() a program, the kernel doesn't load the entire binary. Just the first page of code is mapped. As execution proceeds, more pages get faulted in. This makes programs start fast. Swapping: when physical RAM is full, the kernel can evict cold pages to disk (swap space). Next access → page fault → kernel reads page back from disk → very slow. "Swap storm" = thrashing = system becomes unresponsive. Linux uses LRU-like eviction. The malloc() journey in C (same for Python's memory allocator): ① brk() or mmap() syscall → kernel maps pages into process's virtual address space ② malloc returns a pointer to that virtual address ③ On first write → page fault → kernel allocates physical RAM ④ free() → marks memory as available for future malloc() calls (doesn't return to kernel immediately) ⑤ Eventually: munmap() → kernel unmaps virtual pages, returns physical frames to free pool Memory layout of a process (from low to high addresses): • 0x0: NULL (unmapped, accessing it = segfault) • .text: the program's machine code (read-only) • .data: initialized global variables • .bss: uninitialized global variables (zero-filled) • Heap: grows upward (malloc() territory) • Memory maps: shared libraries, mmap'd files • Stack: grows downward (local variables, function call frames) • Kernel space: inaccessible from user space
DIAGRAM
VIRTUAL → PHYSICAL TRANSLATION:
Virtual Address: 0x7fff00001234
┌─────────────────────────────────────────────────┐
│ 48-bit virtual address: │
│ [PML4 idx][PDP idx][PD idx][PT idx][Offset] │
│ 9 bits 9 bits 9 bits 9 bits 12 bits │
└─────────────────────────────────────────────────┘
↓ MMU walks 4-level page table
Physical Address: 0x3A001234
PROCESS MEMORY LAYOUT (typical 64-bit Linux):
High ┌───────────────────────┐ 0x7FFFFFFF...
│ Kernel Space │ (invisible to user)
├───────────────────────┤
│ Stack │ ← grows ↓ (local vars)
│ ↓ │
│ [unmapped] │
│ ↑ │
│ Heap │ ← grows ↑ (malloc)
├───────────────────────┤
│ Shared Libraries │ (libc.so, libpython.so)
├───────────────────────┤
│ .bss (zero data) │
│ .data (globals) │
│ .text (code) │
Low └───────────────────────┘ 0x400000CODE