Memory Architecture

From OSDev Wiki
Jump to navigation Jump to search

Memory architecture describes the hardware and software mechanisms used to organize, address, translate, protect, cache, and access memory in a computer system.

For operating-system development, memory architecture matters because the kernel must convert the hardware-visible memory layout into safe and useful abstractions. These abstractions commonly include a physical page-frame allocator, kernel and user virtual address spaces, protected mappings, shared memory, and DMA buffers.

Memory architecture should not be confused with memory management. Memory architecture describes the mechanisms and address spaces provided by the machine, while memory management describes the policies and algorithms an operating system builds on top of them.

Overview

A processor does not simply read and write a block of memory.

Depending on the architecture and execution mode, a memory access may involve several mechanisms:

  • an instruction-fetch or data-access path;
  • segmentation or another architecture-specific address-generation stage;
  • page-table translation and permission checks;
  • a translation lookaside buffer;
  • one or more CPU caches;
  • a memory controller, interconnect, or device bus;
  • physical RAM, firmware-reserved memory, or a memory-mapped device.

The exact path depends on the architecture.

During early boot, a kernel may run with little or no memory protection. It must then discover the platform memory map, reserve memory already in use, initialize a physical memory allocator, construct page tables, enable or replace address translation, and establish protected kernel and user mappings.

Instruction and data organization

Computer systems are often described using one of three broad instruction/data memory organizations: Von Neumann, Harvard, and Modified Harvard.

Von Neumann architecture

In a Von Neumann architecture, instructions and data are stored in the same memory system and are addressed through the same memory space. Code is data: the bytes that encode instructions can, in principle, be read or written like other memory.

This model is close to the programmer visible model used by many general purpose systems. Executable files are loaded into memory, the processor fetches instructions from memory, and ordinary load/store instructions access data from the same general address space.

For OS development, this model explains why a kernel can load executable code, relocate it, copy it, map it into an address space, or mark it executable or non executable using page permissions.

Harvard architecture

In a Harvard architecture, instruction memory and data memory are separate. Instruction fetches and data accesses use distinct storage or distinct address spaces.

This separation is common in some microcontrollers and embedded systems. It can simplify certain hardware designs and allow instruction fetches and data accesses to occur independently. However, it can also make some operations less direct, such as treating program code as ordinary data.

For OS development, pure Harvard systems require special attention when loading code, modifying code, or implementing executable memory. A mechanism may be required to copy data into instruction memory or to synchronize instruction and data views.

Modified Harvard architecture

A Modified Harvard architecture combines features of both approaches. A common modern design has separate instruction and data caches near the CPU, while still presenting a mostly unified memory model to software.

For example, a processor may have separate L1 instruction and data caches, but both caches ultimately refer to the same physical memory. This can improve performance while preserving the convenience of a unified address space.

Modified Harvard design should not be confused with virtual memory. Split instruction and data caches concern how memory is fetched and cached. Virtual memory, demand paging, and protection are mainly provided by address translation hardware such as an MMU.

Address Spaces

An address space is the set of addresses meaningful to some component of the system. A modern OS often has to reason about several address spaces at once.

Physical address space

The physical address space is the address space used by the processor, memory controller, and platform hardware after any virtual-to-physical translation has taken place. Physical addresses may refer to RAM or to platform-defined regions such as device memory, firmware data, and reserved areas.

Physical address space must not be confused with installed RAM. A machine may support a larger physical address space than the amount of RAM installed, and some ranges inside that space may not be usable memory. For example, on x86 PCs, memory above the first megabyte is not guaranteed to be contiguous, RAM may be remapped above 4 GiB, and some physical address ranges may be assigned to memory-mapped devices or firmware data structures.

Address space width

The size of the physical address space is limited by the number of physical address bits supported by the processor. This limit is independent of the amount of RAM installed.

For example, 32-bit x86 without Physical Address Extension normally uses a 32-bit physical address space. With PAE, x86 can address physical memory beyond 4 GiB. In x86-64 long mode, the architectural physical address space can be up to 52 bits, although individual processors may implement fewer physical address bits.

Physical address width matters because it limits which physical frame numbers can appear in page-table entries and which physical ranges can be covered by identity mappings or direct physical memory mappings. It can also affect device access: some devices can only perform DMA to a smaller address range than the CPU can address, requiring low memory allocations, bounce buffers, or an IOMMU.

Memory map

The kernel must not assume that every physical address is usable RAM. During boot, the bootloader or firmware provides a memory map describing which physical ranges are usable and which are reserved.

On BIOS-based x86 systems, the common mechanism is INT 15h, EAX=0xE820. On UEFI systems, the firmware provides a UEFI memory map. Boot protocols such as Multiboot, Limine, and BOOTBOOT may also pass a memory map to the kernel.

Only regions explicitly marked usable should be handed to the physical page-frame allocator. The kernel must also reserve memory occupied by the kernel image, boot modules, firmware tables, framebuffers, page tables, and other data structures created during boot.

Virtual address space

A virtual address space is the address space seen by software when address translation is enabled. When a program executes a load or store, the address in the instruction is a virtual address. The MMU translates that virtual address into a physical address before the access reaches memory or any memory-mapped device.

Virtual address spaces allow each process to have its own independent view of memory. Two processes may use the same virtual address while mapping it to entirely different physical pages. The kernel may also map the same physical page at multiple virtual addresses, share pages between processes, or leave ranges deliberately unmapped so that invalid accesses generate faults that the kernel can handle.

Virtual memory is a central concept that is used in many other computer system ideas.

Translation results are cached in a translation lookaside buffer. When the kernel modifies a mapping it must invalidate the relevant TLB entries, otherwise stale translations may be used. On SMP systems this requires notifying other CPUs as well.

Canonical addresses on x86-64

On x86-64, not every 64-bit value is a valid virtual address. With four-level paging, bits 63 through 48 must be copies of bit 47. With five-level paging, bits 63 through 57 must be copies of bit 56. Addresses satisfying this rule are called canonical addresses. Accessing a non-canonical address causes a general-protection fault.

This is why many higher-half kernels are placed near the top of the canonical address space, rather than in the middle of the 64-bit range.

Bus and DMA address space

A device does not necessarily see memory in the same way as the CPU. Devices that perform direct memory access use DMA addresses, which may be physical addresses or may be translated through an IOMMU.

Some devices can only address part of physical memory. For example, a device may be limited to 32-bit DMA addresses even when the CPU supports a larger physical address space. In that case, the kernel must allocate DMA buffers from a reachable range, use bounce buffers, or configure an IOMMU mapping.

This distinction is important because changing CPU page tables does not automatically change what a device can access. The CPU virtual address space, the physical address space, and a device DMA-visible address space are related but not identical.

I/O address spaces

Some architectures provide special address spaces for device I/O.

On x86, port-mapped I/O uses a separate I/O port address space accessed with instructions such as in and out. This is distinct from ordinary memory accesses.

Many modern devices instead use memory-mapped I/O. In MMIO, device registers appear inside the physical address space and are accessed with ordinary load and store instructions after the kernel maps the relevant physical range. MMIO regions usually require appropriate cache attributes, because device registers must not be cached like normal RAM.

Memory hierarchy

Real systems contain a hierarchy of storage with different sizes, speeds, and visibility:

  • CPU registers;
  • L1 instruction and data caches;
  • larger shared caches;
  • main memory;
  • persistent storage;
  • device-local memory.

The OS usually does not allocate registers or ordinary CPU cache lines directly, but it must still account for the hierarchy. This is because page size, alignment, cacheability, DMA requirements, and memory ordering can all affect correctness and performance.

For a simple kernel, the memory hierarchy can often be ignored at first. As the kernel grows, cache behavior becomes important for page-table updates, DMA buffers, memory-mapped device registers, atomic operations, and multiprocessing.

For device drivers and multiprocessing kernels, memory hierarchy details become especially important. Device memory may require uncacheable or write-combining mappings. DMA buffers may need alignment, cache flushing, or coherency management. Page-table updates may require TLB invalidation, and atomic operations may require memory-ordering guarantees.

Memory protection and translation

Segmentation

Main article: Segmentation

Segmentation divides an address space into regions described by segment bases, limits, types, and privilege rules.

On 32-bit x86, segmentation can participate directly in isolation and address translation. Many modern operating systems instead use a mostly flat segmentation model and rely on paging for fine-grained memory management.

In x86-64 long mode, most segment bases and limits are ignored, although FS and GS retain programmable bases and are commonly used for thread-local or per-CPU data.

Paging

Main article: Paging

Paging divides virtual and physical memory into fixed-size units called pages and page frames. Page tables describe how virtual pages map to physical frames and what operations are permitted.

Page-table entries commonly encode permissions and attributes such as:

  • present or valid;
  • readable or writable;
  • user-accessible or supervisor-only;
  • executable or non-executable;
  • accessed or dirty;
  • cache or memory type;
  • page size.

Paging allows an operating system to implement process isolation, demand paging, copy-on-write, shared memory, memory-mapped files, guard pages, kernel higher-half mappings, and sparse address spaces.

An invalid or disallowed access raises a page fault or architecture-equivalent exception. The kernel must determine whether the fault is recoverable, such as by allocating a page, or fatal to the current process.

Translation lookaside buffers

Main article: TLB

A translation lookaside buffer caches recent address translations and permission information. Without it, many memory accesses would require repeated page-table walks.

When the kernel changes or removes a mapping, it must invalidate any stale TLB entry that could still authorize or redirect an access. On a multiprocessor system, the same translation may be cached by several CPUs, requiring a TLB shootdown or an architecture-specific address-space synchronization mechanism.

The exact invalidation required depends on the change. Creating a previously absent mapping, tightening permissions, replacing a physical frame, and destroying an address space may have different rules on different architectures.

IOMMU

Main article: IOMMU

An input-output memory management unit translates device DMA addresses and can restrict which memory a device may access.

An IOMMU can provide:

  • DMA isolation;
  • device-specific address spaces;
  • access to high physical memory for limited devices;
  • scatter-gather mappings;
  • safer device assignment to virtual machines.

An IOMMU does not replace CPU page tables. It performs a separate translation for device-originated accesses, and the kernel must manage both sets of mappings consistently.

Memory types and ordering

Memory accesses are not always observed in program order, and not every physical range behaves like normal RAM.

Architectures provide memory types, barriers, fences, and atomic operations so software can control caching and ordering. These mechanisms are especially important for:

  • device registers;
  • shared data between CPUs;
  • lock-free data structures;
  • DMA buffers;
  • page-table updates;
  • self-modifying code.

The required rules are architecture-specific. A portable kernel abstraction must still eventually map each operation to the guarantees provided by the target architecture.

Typical kernel initialization sequence

A kernel commonly performs memory initialization in approximately the following order:

  1. Receive or locate the platform memory map.
  2. Identify the kernel image, modules, firmware structures, framebuffer, and other occupied regions.
  3. Establish a temporary or bootstrap allocator.
  4. Normalize the memory map and reserve every page already in use.
  5. Initialize the physical page-frame allocator.
  6. Construct the kernel's page tables.
  7. Map the kernel image with appropriate read, write, and execute permissions.
  8. Create mappings for physical memory, devices, and required firmware structures.
  9. Load or activate the new translation structures.
  10. Initialize the kernel virtual-memory allocator.
  11. Install page-fault handling.
  12. Create per-process address spaces and user mappings.
  13. Initialize DMA and IOMMU support as required by devices.

The exact order varies by architecture and boot protocol. In particular, enabling paging for the first time, replacing bootloader page tables, or leaving UEFI boot services may impose additional requirements.

Common mistakes

Frequent memory-architecture errors in early kernels include:

  • assuming installed RAM is contiguous;
  • treating the entire physical address space as usable RAM;
  • failing to reserve the kernel, modules, page tables, or framebuffer;
  • storing physical addresses in pointer types without an explicit mapping;
  • confusing virtual, physical, and DMA addresses;
  • using an address after the mapping that made it valid has been removed;
  • forgetting page alignment or integer-overflow checks;
  • modifying page tables without required TLB invalidation;
  • mapping writable data as executable;
  • mapping MMIO with ordinary write-back caching;
  • reclaiming bootloader or firmware memory while references to it remain;
  • assuming all CPUs automatically observe page-table or cache changes;
  • using the architectural maximum address width instead of the implemented width.

Clear naming helps prevent these errors. Kernels often use distinct types or naming conventions for virtual addresses, physical addresses, page-frame numbers, and DMA addresses.

See also