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  └───────────────────────┘ 0x400000

CODE

PYTHON
1import ctypes
2import sys
3import gc
4import tracemalloc
5
6# ── Memory addresses in Python ─────────────────────────
7print("=== Object Addresses ===")
8a = [1, 2, 3]
9b = "hello"
10c = 42
11
12print(f"List at virtual address: {id(a):#018x}")
13print(f"String at virtual address: {id(b):#018x}")
14print(f"Int at virtual address: {id(c):#018x}")
15
16# id() IS the virtual address in CPython
17# The OS and MMU translate this to a physical address transparently
18
19# ── Object sizes ───────────────────────────────────────
20print("
21=== Object Sizes (bytes) ===")
22print(f"int(0): {sys.getsizeof(0)}")
23print(f"int(2^100): {sys.getsizeof(2**100)}")
24print(f"list[]: {sys.getsizeof([])}")
25print(f"list[1..10]: {sys.getsizeof(list(range(10)))}")
26print(f"dict{{}}: {sys.getsizeof({})}")
27print(f"str 'a': {sys.getsizeof('a')}")
28print(f"str 'hello': {sys.getsizeof('hello world')}")
29
30# ── Memory profiling ───────────────────────────────────
31tracemalloc.start()
32
33snapshot1 = tracemalloc.take_snapshot()
34
35# Do some allocations
36data = [list(range(1000)) for _ in range(1000)]
37
38snapshot2 = tracemalloc.take_snapshot()
39top_stats = snapshot2.compare_to(snapshot1, "lineno")
40
41print("
42=== Top memory allocations ===")
43for stat in top_stats[:5]:
44 print(f" {stat}")
45
46del data
47gc.collect()
48
49# ── Stack vs Heap in Python ───────────────────────────
50def demonstrate_stack():
51 local_var = 42 # on the stack (well, Python frame)
52 another = [1, 2, 3] # list object on heap, reference on frame
53 # local_var and another disappear when function returns
54 return id(another)
55
56heap_obj_addr = demonstrate_stack()
57print(f"
58Stack frame gone, but can we access {heap_obj_addr:#x}?")
59# The list might have been garbage collected now
60
61# ── mmap — directly mapping memory ────────────────────
62import mmap, os, tempfile
63
64# Create a memory-mapped file
65with tempfile.NamedTemporaryFile(delete=False) as f:
66 f.write(b"Hello, memory-mapped world!" + b"" * 100)
67 fname = f.name
68
69with open(fname, "r+b") as f:
70 mm = mmap.mmap(f.fileno(), 0) # map entire file into memory
71 print(f"
72mmap: {mm.read(27).decode()}")
73 mm.seek(0)
74 mm.write(b"Modified! ")
75 mm.close()
76
77os.unlink(fname)
← PREV4. Processes & ThreadsNEXT →6. File Systems & I/O