Loading learning content...
Memory management stands as one of the most critical responsibilities of any operating system. The choice between segmentation and paging—or understanding when to combine them—represents a fundamental architectural decision that shapes virtually every aspect of system behavior: from program loading and execution to protection enforcement and resource sharing.
These two schemes represent fundamentally different philosophies about how to organize and manage virtual memory:
Understanding the deep differences between these approaches is essential for any systems engineer, whether you're designing operating systems, optimizing application performance, or debugging memory-related issues in production systems.
By the end of this page, you will have a comprehensive understanding of how segmentation and paging differ across every significant dimension: fundamental philosophy, memory organization, address translation, fragmentation behavior, sharing mechanisms, protection models, and hardware requirements. This knowledge forms the foundation for understanding modern hybrid approaches used in real operating systems.
Before diving into technical comparison tables, we must understand the fundamentally different worldviews that gave rise to segmentation and paging.
Segmentation: The Programmer's Perspective
Segmentation emerged from the recognition that programs are not monolithic blocks of undifferentiated bytes. Instead, programs have natural logical divisions:
Segmentation makes these divisions explicit in the memory architecture. Each segment has its own base address and length, growing according to its logical needs. A stack segment grows downward; a heap segment grows upward. Segment sizes are determined by program requirements, not hardware constraints.
Paging: The Physical Reality Perspective
Paging emerged from a different concern: how do we efficiently allocate physical memory without the complexity of variable-sized regions?
In paging, both virtual and physical memory are divided into fixed-size blocks:
Typically, pages and frames are the same size (commonly 4KB on x86 systems, though larger "huge pages" exist). The mapping between pages and frames is completely arbitrary—any page can map to any frame. This eliminates external fragmentation entirely and simplifies memory allocation to a straightforward free-list management problem.
Segmentation appeared first historically (MULTICS in the 1960s) as a way to implement capability-based protection and program modularity. Paging emerged as hardware became more sophisticated and the need for efficient memory utilization grew. Modern systems often combine both—using segmentation for logical organization and paging for physical memory management.
The following comprehensive table provides a systematic comparison across all major dimensions of memory management. Each dimension represents a critical aspect that affects system design, performance, and functionality.
| Dimension | Segmentation | Paging |
|---|---|---|
| Unit of Division | Variable-sized segments | Fixed-sized pages/frames |
| Visibility to Programmer | Visible (logical structure) | Invisible (transparent) |
| Address Components | Segment number + offset | Page number + offset |
| Size Determined By | Program logic and data needs | Hardware architecture |
| Typical Unit Size | Bytes to megabytes (varies) | 4KB, 2MB, or 1GB (fixed) |
| External Fragmentation | Present (variable sizes) | Eliminated (fixed sizes) |
| Internal Fragmentation | Minimal (exact sizing) | Present (last page waste) |
| Memory Allocation | Complex (variable-size holes) | Simple (free frame list) |
| Address Translation | Segment table lookup + bounds check | Page table lookup |
| Translation Hardware | Comparators for bounds | Simple table indexing |
| Protection Granularity | Segment-level (logical) | Page-level (arbitrary) |
| Sharing Unit | Entire segment (meaningful unit) | Individual pages (arbitrary) |
| Table Entry Size | Base + limit + protection | Frame number + protection |
| Table Size Scaling | Proportional to segment count | Proportional to address space |
| Dynamic Growth | Natural (segments grow/shrink) | Complex (remap pages) |
| Relocation | Automatic via base register | Automatic via page table |
Each row in this table represents a distinct dimension of memory management. The "winner" in each dimension depends entirely on your system's requirements. Neither segmentation nor paging is universally superior—they make different tradeoffs that suit different use cases.
The most fundamental difference lies in how each scheme divides the address space.
Segmentation: Logical Divisions
In a segmented system, the address space is divided according to the logical structure of the program. Consider a typical C program:
1234567891011121314151617181920212223242526
// This program would typically have these segments:// Segment 0: Code segment (machine instructions)// Segment 1: Data segment (global/static variables)// Segment 2: Stack segment (local variables, call frames)// Segment 3: Heap segment (dynamically allocated memory)// Segment 4: Shared library segment (libc, etc.) #include <stdlib.h> int global_counter = 0; // Located in data segment (segment 1)static int static_data[1000]; // Located in data segment (segment 1) void recursive_function(int depth) { // Code in segment 0 int local_array[100]; // Stack allocation in segment 2 if (depth > 0) { recursive_function(depth - 1); }} int main() { // Code in segment 0 int* dynamic = malloc(1024); // Heap allocation in segment 3 recursive_function(10); free(dynamic); return 0;}Segmented Address Format:
┌─────────────────┬─────────────────────────────────┐
│ Segment Number │ Offset within Segment │
├─────────────────┼─────────────────────────────────┤
│ s bits │ d bits │
└─────────────────┴─────────────────────────────────┘
│ │
▼ ▼
Index into Displacement from
Segment Table segment base
The width of the offset field determines the maximum segment size. For example, if the offset is 16 bits, maximum segment size is 64KB. However, actual segments can be any size up to this maximum.
Paging: Fixed Divisions
In a paged system, the address space is divided into fixed-size pages without regard to program structure:
Paged Address Format:
┌─────────────────┬─────────────────────────────────┐
│ Page Number │ Page Offset │
├─────────────────┼─────────────────────────────────┤
│ p bits │ d bits │
└─────────────────┴─────────────────────────────────┘
│ │
▼ ▼
Index into Displacement from
Page Table frame base
The offset field is fixed based on page size. For 4KB pages, the offset is exactly 12 bits (2^12 = 4096). Unlike segmentation, there's no variability—every page is exactly the same size.
Visual Comparison of Address Space Layout:
Address translation—converting virtual addresses to physical addresses—works fundamentally differently in each scheme.
Segmentation Translation Process:
123456789101112131415161718192021222324252627282930
// Segmentation Address Translation Algorithmfunction translate_segmented_address(logical_address): // Extract components from logical address segment_number = extract_segment_number(logical_address) offset = extract_offset(logical_address) // Validate segment number if segment_number >= SEGMENT_TABLE_LENGTH: raise SEGMENTATION_FAULT("Invalid segment number") // Lookup segment table entry entry = segment_table[segment_number] // Check if segment is valid/present if not entry.valid: raise SEGMENTATION_FAULT("Segment not present") // CRITICAL: Bounds check - prevents buffer overflows if offset >= entry.limit: raise SEGMENTATION_FAULT("Offset exceeds segment limit") // Check protection bits against access type if access_type == WRITE and not entry.writable: raise PROTECTION_FAULT("Write to read-only segment") if access_type == EXECUTE and not entry.executable: raise PROTECTION_FAULT("Execute in non-executable segment") // Compute physical address physical_address = entry.base + offset return physical_addressPaging Translation Process:
123456789101112131415161718192021222324252627282930313233343536
// Paging Address Translation Algorithmfunction translate_paged_address(virtual_address): // Extract components from virtual address page_number = virtual_address >> PAGE_OFFSET_BITS // Upper bits page_offset = virtual_address & PAGE_OFFSET_MASK // Lower bits // Validate page number if page_number >= PAGE_TABLE_SIZE: raise PAGE_FAULT("Invalid page number") // Lookup page table entry pte = page_table[page_number] // Check valid/present bit if not pte.valid: raise PAGE_FAULT("Page not in physical memory") // Check protection bits if access_type == WRITE and not pte.writable: raise PROTECTION_FAULT("Write to read-only page") if access_type == EXECUTE and not pte.executable: raise PROTECTION_FAULT("Execute in non-executable page") // Note: NO bounds check needed - offset is fixed size // and always valid within page boundaries // Compute physical address by concatenation frame_number = pte.frame_number physical_address = (frame_number << PAGE_OFFSET_BITS) | page_offset // Update accessed/dirty bits for page replacement pte.accessed = true if access_type == WRITE: pte.dirty = true return physical_addressSegmentation requires an explicit bounds check (compare offset against limit) because segments have variable sizes. Paging does NOT require bounds checking—the fixed page size means any offset within the page offset bits is valid by definition. This makes paging faster in hardware but removes a layer of protection against buffer overflows.
The hardware required to implement each scheme differs significantly, affecting both cost and performance.
Segmentation Hardware:
Paging Hardware:
| Hardware Component | Segmentation | Paging |
|---|---|---|
| Arithmetic Units | Adder (base + offset) | Concatenation only |
| Comparison Units | Comparator (offset < limit) | None required |
| Table Entry Cache | Rarely used historically | TLB (essential) |
| Memory Accesses | 1-2 per translation | 1-4+ per translation (multi-level) |
| Hardware Complexity | Moderate | Lower (but TLB adds complexity) |
Without a TLB, paging would be prohibitively slow—every memory access would require additional memory accesses to walk the page table. The TLB caches recently used page table entries, achieving hit rates of 95-99% in typical workloads. This makes paging practical despite its table lookup overhead.
The structure of the mapping tables differs significantly between the two schemes.
Segment Table Entry (STE) Structure:
12345678910111213141516171819202122232425262728
// Segment Table Entry Structure// Total size: typically 64 bits (8 bytes) or larger struct SegmentTableEntry { // Core addressing fields uint32_t base; // Starting physical address (32-64 bits) uint32_t limit; // Segment size in bytes (allows variable sizes) // Protection and status bits uint8_t valid : 1; // Segment is present in memory uint8_t readable : 1; // Read access permitted uint8_t writable : 1; // Write access permitted uint8_t executable : 1; // Execute access permitted uint8_t accessed : 1; // Segment has been accessed uint8_t dirty : 1; // Segment has been modified uint8_t privilege : 2; // Ring level (0-3 for x86) // Additional metadata uint8_t type : 4; // Segment type (code, data, stack, etc.) uint8_t granularity: 1; // Limit in bytes (0) or 4KB units (1) uint8_t reserved : 5;}; // Example segment table for a simple process:// Entry 0: Code segment - base=0x00400000, limit=32768, R-X// Entry 1: Data segment - base=0x00410000, limit=65536, RW-// Entry 2: Stack segment - base=0x7FFF0000, limit=16384, RW-// Entry 3: Heap segment - base=0x00800000, limit=262144, RW-Page Table Entry (PTE) Structure:
12345678910111213141516171819202122232425262728293031323334
// Page Table Entry Structure (x86-64 example)// Total size: 64 bits (8 bytes) struct PageTableEntry { // Core addressing field uint64_t frame_number : 40; // Physical frame number (up to 1TB RAM) // Protection and status bits (standard in most architectures) uint64_t present : 1; // Page is in physical memory uint64_t read_write : 1; // 0=read-only, 1=read-write uint64_t user_super : 1; // 0=supervisor only, 1=user accessible uint64_t write_through : 1; // Write-through caching uint64_t cache_disable : 1; // Disable caching for this page uint64_t accessed : 1; // Page has been read uint64_t dirty : 1; // Page has been written uint64_t page_size : 1; // Large page (2MB/1GB) indicator uint64_t global : 1; // Don't invalidate on CR3 change // Available for OS use uint64_t available : 3; // OS-defined bits // x86-64 specific uint64_t reserved : 7; // Must be zero uint64_t no_execute : 1; // NX bit - prevent code execution}; // Note: Page table entries DON'T need a limit field!// All pages are exactly PAGE_SIZE bytes (e.g., 4096)//// Example 4KB page table:// Entry 0: frame=0x00000, present=1, RW=0, US=0 (kernel code)// Entry 1: frame=0x00001, present=1, RW=1, US=0 (kernel data)// Entry 2: frame=0x00123, present=1, RW=0, US=1 (user code)// Entry 3: frame=0x00456, present=1, RW=1, US=1 (user data)| Aspect | Segment Table Entry | Page Table Entry |
|---|---|---|
| Size Encoding | Required (limit field) | Not needed (fixed page size) |
| Entry Size | Larger (base + limit) | Smaller (frame only) |
| Number of Entries | Small (~6-16 per process) | Large (millions for 64-bit) |
| Total Table Size | Small (~128 bytes) | Huge (hierarchical needed) |
| Sparsity | Dense (all entries used) | Sparse (many holes) |
This comprehensive comparison reveals that segmentation and paging represent fundamentally different philosophies about memory organization—neither is universally superior.
You now understand the comprehensive differences between segmentation and paging. The following pages will dive deeper into specific aspects: fragmentation differences, sharing mechanisms, protection models, and guidance on when to use each approach—essential knowledge for any systems engineer.