Virtual Memory Management Subsystem
Subsystem: vm
The subsystem implements all necessary functionality for memory management within the system. It also provides implementations of various allocators and some APIs for working with page tables.
The system has some specific characteristics.
Mappings
-
Upper Half
This portion of virtual memory is reserved for kernel needs. The kernel code and data are all located in the upper half.
-
Lower Half
This is used for user space. The kernel does not store any data in this section.
Linear Memory Access Region (LMA)
This is an important feature of the kernel architecture.
The Linear Memory Access region is a large area of virtual memory in the upper half with a predefined address and size (specific to each platform), which is linearly mapped to physical pages starting from address 0x0. The region size should be large enough to cover the potential range of physical addresses. For the x86-64 architecture, it is 256 GiB
.
The main idea is the ability to access any physical address without the need for additional page mapping.
Mapping virtual to physical addresses is a costly operation, which may also require additional page table allocations.
The LMA region solves this problem, allowing significant optimization of memory handling within the kernel. The region is mapped only once during kernel initialization.
To convert any physical address to virtual and vice versa, you can use:
vm.getVirtLma(address: anytype) @TypeOf(address)
phys
: the physical address/pointer.
vm.getPhysLma(address: anytype) @TypeOf(address)
-
virt
: the virtual address/pointer.Only addresses obtained from
vm.getVirtLma
are allowed.
Memory Allocation
All memory allocation starts with physical page allocation, managed by vm.PageAllocator
.
Page Allocator vm.PageAllocator
note
The page allocator is thread-safe.
This allocator is fast and efficient, based on a buddy algorithm.
The main goal during its design was to achieve the highest performance and minimize memory overhead. Avoiding additional memory allocation for the allocator itself is impossible, but this allocation is a one-time event during system initialization and is minimized in size.
Due to the chosen algorithm, the implementation allows allocation of page quantities that are powers of two, designated as rank
.
The only drawback is the need to remember the size of the allocated region for subsequent freeing.
API
note
More detailed information is available in the code documentation.
vm.PageAllocator.alloc(rank: u32) ?usize
Allocates a linear block of physical pages of the specified rank (size).
Returns the physical address of the allocated pages, or null
if allocation fails.
rank
: Determines the number of pages as2^rank
.
vm.PageAllocator.free(base: usize, rank: u32) void
Frees a physical memory of the specified rank (size).
base
: Physical address of the first page in a linear block returned byvm.PageAllocator.alloc
.rank
: Determines the number of pages as2^rank
, must be the same as in thevm.PageAllocator.alloc
call.
Object Allocator vm.ObjectAllocator
note
The object allocator is not thread-safe. However, there is a simple wrapper, vm.SafeOma
, which includes utils.Spinlock
and is thread-safe.
tip
The allocator uses the LMA region for fast physical page mapping. Therefore, vm.getPhysLma
can safely be used for allocated objects.
The object allocator was introduced for very fast allocation of memory sections of the same size. It’s useful in cases of frequent allocation of identical structures, such as descriptors for processes, files, devices, and more.
This allocator is based on the allocation of arenas (i.e., large blocks of one or more pages), which are then divided into multiple objects. When freed, objects are added to a singly linked list, allowing immediate reallocation: complexity O(1)
.
Fragmentation is either non-existent or amounts to only a few bytes per arena, depending on the object size.
API
note
More detailed information is available in the code documentation.
vm.ObjectAllocator.init(T: type) vm.ObjectAllocator
Initializes an allocator for a specific object type.
Can be used at comptime
.
T
: The type of objects to allocate.
vm.ObjectAllocator.deinit(self) void
Deinitializes the allocator, freeing all allocated memory.
vm.ObjectAllocator.alloc(self, T: type) ?*T
Allocates memory for an object and casts it to a pointer of type T
.
Returns a pointer to the allocated object, or null
if allocation fails.
T
: type of the pointer.
vm.ObjectAllocator.free(self, obj_ptr: anytype) void
Frees the memory of an object. Invalid object pointers cause undefined behavior.
obj_ptr
: pointer to the object to free.
Universal Allocator vm.UniversalAllocator
note
The universal allocator is thread-safe.
tip
The allocator uses the LMA region for fast physical page mapping. Therefore, vm.getPhysLma
can safely be used for allocated sections.
The universal allocator allows allocation of memory sections of various sizes. It’s not as efficient as the page allocator or object allocator. However, there is often a need to allocate memory sections of unknown size, such as strings or other arrays. In these cases, this allocator is very useful.
It is based on multiple object allocators with predefined different sizes. When allocating memory, the size is rounded up to the nearest fit, which may cause some local fragmentation.
For large memory sections, it directly uses vm.PageAllocator
, allocating the required number of pages. Sizes are also rounded, so it is best to allocate blocks in powers of two, matching page sizes, to avoid fragmentation.
API
note
More detailed information about the allocator’s implementation and limitations is available in the code documentation.
The vm
subsystem provides convenient shortcuts for direct calls:
vm.malloc(size: usize) ?*anyopaque
vm.free(ptr: ?*anyopaque)
It also has a shortcut for allocating a specific object:
vm.alloc(T: type) ?*T
tip
When working with Zig’s standard library (std
), you can also use the ready-made std.mem.Allocator
implementation based on vm.UniversalAllocator
:
vm.std_allocator
Working with Page Tables
The subsystem aims to remove platform dependency. It provides a set of functions for interacting with page tables and mapping virtual to physical addresses.
vm.PageTable
represents the platform-dependent page table structure defined in arch.vm.PageTable
.
vm.allocPt() ?*vm.PageTable
Allocates a new page table and zeroes all entries.
Returns null
if allocation fails.
vm.newPt() ?*vm.PageTable
Allocates a new page table and maps all necessary kernel units.
Returns null
if allocation fails.
vm.freePt(pt: *vm.PageTable) void
Frees a page table.
vm.getPt() *vm.PageTable
Gets the current page table from a specific CPU register.
vm.setPt(pt: *const vm.PageTable) void
Sets the given page table to a specific CPU register.
vm.mmap(
virt: usize,
phys: usize,
pages: u32,
flags: vm.MapFlags,
page_table: *PageTable
) vm.Error!void
Maps a virtual memory range to a physical memory range.
Returns vm.Error
if mapping fails.
virt
: base virtual address to which physicall region must be mapped.phys
: region base physical address.pages
: number of pages to map.flags
: flags to specify (seevm.MapFlags
structure).page_table
: target page table.
vm.getPhys(address: anytype) ?@TypeOf(address)
Translates the virtual address into a physical address via the current page table.
Returns the physical address or null
if the address isn’t mapped.
vm.getPhysPt(address: anytype, pt: *const vm.PageTable) ?@TypeOf(address)
Translates the virtual address into a physical address via a specific page table.
Returns the physical address or null
if the address isn’t mapped.
More detailed information is provided in the code documentation.