Memory Management
Memory management is the subsystem responsible for allocating physical random-access memory (RAM) to executing processes, protecting process boundaries, and managing storage hierarchies. Operating systems resolve the physical limitations of hardware RAM through Virtual Memory, providing each user-space process with the abstraction of a private, contiguous, and massive address space.
The Problem: Fragmentation and Protection
Early operating systems allocated physical memory contiguously. A program was loaded as a single, uninterrupted block of physical addresses. This method introduced two major vulnerabilities:
- External Fragmentation: As programs were loaded and terminated, free memory became broken into small, non-contiguous gaps. If a new program required 10 MB of memory, it could not load if the largest single free block was only 8 MB, even if the total free space across all gaps exceeded 50 MB.
- Lack of Protection: Without hardware-enforced boundaries, buggy or malicious programs could write to arbitrary physical memory addresses, corrupting the memory of other applications or the kernel itself.
In systems without hardware memory protection, a simple pointer arithmetic error can compromise the entire OS:
/* unprotected_memory.c - Pointer arithmetic in non-protected memory systems (e.g. MS-DOS) */
#include <stdio.h>
void simulate_unprotected_write(void) {
// In systems without protection, any memory location is directly writeable.
// Specifying an arbitrary physical address (e.g., interrupt vector table at 0x0000)
volatile int *kernel_memory = (volatile int *)0x0000;
// An application bug (or exploit) could directly overwrite kernel code
*kernel_memory = 0xDEADBEEF;
}
The Solution: Virtual Memory
To resolve these issues, modern operating systems decouple the Logical Addresses referenced by compiler binaries from the Physical Addresses on the RAM bus.
The translation is handled by the Memory Management Unit (MMU), a hardware component embedded within the CPU that references kernel-maintained page tables to map virtual locations to physical RAM dynamically.
The following C program prints a local variable’s address. Concurrently executing multiple instances of this program shows that they access the same virtual address, yet their data remains isolated:
/* virtual_address_example.c - Printing virtual addresses of variables */
#include <stdio.h>
#include <unistd.h>
int global_variable = 42;
int main(void) {
printf("Virtual Address of global_variable: %p\n", (void *)&global_variable);
printf("Process PID: %d\n", getpid());
// Running multiple instances concurrently will show the same virtual address,
// but the OS maps them to distinct physical RAM frames.
return 0;
}
Paging: The Modern Approach
Modern operating systems divide physical and virtual memory into fixed-size blocks:
- Pages: Virtual memory blocks (typically 4 KB on standard architectures).
- Frames: Physical RAM blocks of the identical size.
For each process, the kernel maintains a Page Table mapping page indexes to their corresponding physical frames.
A 32-bit virtual address is decomposed by the MMU into a Page Number (high bits) and an Offset (low bits) to identify the target byte within the physical frame:
/* address_decomposition.c - Decomposing a 32-bit virtual address with 4KB pages */
#include <stdio.h>
#include <stdint.h>
#define PAGE_SIZE 4096 // 4KB
void decompose_address(uint32_t virtual_address) {
// 4KB page size requires 12 bits for offset (2^12 = 4096)
uint32_t page_number = virtual_address >> 12; // Shift right by 12 bits
uint32_t offset = virtual_address & 0xFFF; // Mask the lower 12 bits (0xFFF = 4095)
printf("Virtual Address: 0x%08X\n", virtual_address);
printf("Page Number: 0x%X (%u)\n", page_number, page_number);
printf("Page Offset: 0x%X (%u)\n", offset, offset);
}
This fixed-size division ensures:
- Zero External Fragmentation: Because any virtual page fits any free physical frame, memory allocation does not require contiguous RAM blocks.
- Process Isolation: Process A’s page table points to Frame 100, while Process B’s page table points to Frame 200 for the same virtual address, preventing cross-process read/write operations.
- Shared Memory: Shared resources (like the C library
libc.so) map to the same physical frames in read-only mode, reducing RAM footprints.
Paging Out and Swapping
When the total virtual memory demand exceeds the physical RAM capacity, the operating system manages page allocation dynamically:
- Page Fault: When a thread references a virtual page whose present bit is cleared (not in physical RAM), the MMU triggers a Page Fault hardware exception.
- Swapping (Eviction): The kernel page-fault handler intercepts the exception, identifies an inactive frame using an eviction algorithm (like Least Recently Used), and writes its contents to secondary storage (a swap file or partition).
- Loading: The kernel reads the requested page from the storage device into the vacated physical frame.
- Resuming: The kernel updates the process page table, sets the present bit, and instructs the CPU to re-execute the interrupted instruction.
Swap space status and memory utilization can be inspected using terminal commands on Linux:
# Example: Display swap space allocation and current utilization on Linux
swapon --show
The terminal reports the active swap partition details:
NAME TYPE SIZE USED PRIO
/dev/sda2 partition 8G 2G -2
Segmentation (The Historical Rival)
Unlike paging’s fixed-size structure, Segmentation divides memory into variable-sized logical segments reflecting compiler sections (e.g., Code, Data, Stack, Heap).
In legacy x86 architectures, segmentation calculates physical memory addresses by adding the segment base address to an instruction offset. The following x86 Assembly code demonstrates this segment-base calculation:
; Example: Segmentation register access in x86 Assembly (16-bit Real Mode)
mov ax, 0x1000 ; Load segment base address into general register
mov ds, ax ; Move base address to Data Segment register (DS)
mov bx, 0x0020 ; Set offset register
mov cx, [ds:bx] ; Access physical address: (0x1000 * 16) + 0x0020 = 0x10020
- Advantage: Aligns with logical compiler output, allowing distinct permissions (e.g., executing code, reading data) per segment.
- Disadvantage: Suffers from external fragmentation due to variable block sizes.
- Modern Implementations: Most 64-bit systems configure a flat model where segmentation registers are set to base address
0, applying all memory protection and isolation through page-level descriptors.
Memory Protection and Security
Memory management subsystems implement page-level hardware flags to secure executing processes:
- NX Bit (No-eXecute / Execute-Disable): Marks memory pages containing user data (like the stack or heap) as non-executable. This blocks security exploits (like stack-based buffer overflows) from executing shellcode injected into data inputs.
- ASLR (Address Space Layout Randomization): Randomizes the starting addresses of key memory segments (stack, heap, shared libraries) at process execution, making memory offsets unpredictable for attackers.
On protected systems, attempting to write to read-only regions (such as the text/code segment) causes a hardware trap, causing the OS to terminate the program:
/* sigsegv_example.c - Triggering a page protection fault (Segmentation Fault) */
#include <stdio.h>
void trigger_write_violation(void) {
// String literals are compiled into the read-only data (.rodata) page segment
char *read_only_string = "LibreUni";
// Modifying read-only memory causes a page protection hardware exception.
// The OS handles this exception by sending a SIGSEGV signal to the process.
read_only_string[0] = 'X';
}
Performance: The TLB
Translating every memory access through hierarchical page tables stored in RAM requires multiple memory reads (page table walks), slowing down the CPU. Hardware architectures solve this using the Translation Lookaside Buffer (TLB), a high-speed associative hardware cache in the MMU that stores the most recent virtual-to-physical address mappings.
Benchmarks accessing memory contiguously vs non-contiguously highlight the difference in TLB performance:
/* tlb_stride_benchmark.c - Demonstrating stride effects on memory performance */
#define MATRIX_SIZE 2048
int matrix[MATRIX_SIZE][MATRIX_SIZE];
void access_row_major(void) {
// High spatial locality: sequential cache lines and pages are accessed.
// Minimizes page table walks by generating frequent TLB hits.
for (int i = 0; i < MATRIX_SIZE; i++) {
for (int j = 0; j < MATRIX_SIZE; j++) {
matrix[i][j] = 1;
}
}
}
void access_column_major(void) {
// Large stride (2048 * 4 bytes = 8KB): hops across virtual pages.
// Causes frequent TLB cache thrashing, leading to constant TLB misses.
for (int j = 0; j < MATRIX_SIZE; j++) {
for (int i = 0; i < MATRIX_SIZE; i++) {
matrix[i][j] = 1;
}
}
}
Interactive Practice: Memory Management Essentials
To demonstrate your understanding of paging, virtual address space translation, and page tables, complete the practice quiz below.
Why does a page-based virtual memory system eliminate external fragmentation?
References & Further Reading
For an example of detailed documentation on page tables, VM, and memory mapping, refer to the resources below:
- Silberschatz, A., Galvin, P. B., & Gagne, G. (2018). Operating System Concepts (10th ed.). Wiley.
- Tanenbaum, A. S., & Bos, H. (2014). Modern Operating Systems (4th ed.). Pearson.
- Memory management (CC-BY-SA 4.0)
- Memory management unit (CC-BY-SA 4.0)