- Home
- Kernel Internals
- Memory
- Allocators
Allocators¶
There are three distinct allocators in the kernel, each serving a different purpose and lifetime.
1. Kernel Linked-List Heap (mem/heap.rs)¶
Region: __heap_start – __heap_end (64 KiB, defined in linker.ld, lives in the kernel binary after .bss)
Purpose: General kernel-internal allocations during early init (tested in init/heap.rs). This is the original, hand-written heap; it is present but seldom used now that most kernel subsystems use static arrays.
Block Layout¶
Each allocation is preceded by an inline HeapNode header (4 pointer-sized fields, ~32 bytes on x86-64):
struct HeapNode {
size: usize, // data bytes available after this header
status: HeapNodeStatus, // Free | Used | Unknown
previous: *mut HeapNode, // linked-list predecessor (null for first)
next: *mut HeapNode, // linked-list successor (null for last)
}
At init(), a single free node spanning the entire heap is written at __heap_start. HEAP_PTR tracks the "current" node for scanning.
Allocation (alloc)¶
- Walk the list from
HEAP_PTRlooking for a free node large enough foralloc_size. - If the found node is significantly larger,
split()it: the left node gets exactlyalloc_sizebytes (minimumMIN_HEAP_NODE_SIZE = 16), the right node gets the remainder. Both are marked Free; the caller immediately marks the left one Used. - Zero the data region of the allocated node.
- Return the address of the
HeapNodeheader (callers treat this as the usable address — note: the returned pointer is the header itself, not the data after it).
Deallocation (free)¶
- Mark the node at
vaddras Free. - Call
merge(), which walks backwards until it finds a free node and merges it with its free successor by summing sizes and updating the next/prev pointers.
Limitations¶
- The allocator has bugs in its
merge/allocwalk logic (it can loop indefinitely and has anOOMfallback counter). It should be considered legacy. - No thread safety — no lock is held during allocation.
- Minimum heap node size is
MIN_HEAP_NODE_SIZE = 16bytes; smaller requests are rounded up. - Total heap size: 64 KiB, fixed at compile time.
2. Kernel Rust Global Allocator — Bump Allocator (mem/bump.rs)¶
Region: Configured at runtime via BumpAllocator::init(heap_start, heap_size).
Purpose: Satisfies Rust's #[global_allocator] trait so that Box, Vec, and other alloc crate types can be used inside the kernel. Currently initialised by init_heap_allocator() in init/heap.rs, which points it at the same __heap_start / __heap_end region.
Algorithm¶
next ← heap_start (AtomicUsize, SeqCst)
alloc(layout):
aligned = (next + align - 1) & !(align - 1)
new_next = aligned + size
if new_next > heap_end: return null
next.store(new_next)
return aligned
dealloc(_ptr, _layout):
/* no-op */
Allocation is a single atomic fetch-and-add (effectively). Deallocation does nothing — freed memory is not reclaimed.
Properties¶
| Property | Value |
|---|---|
| Thread safety | Yes (SeqCst atomic) |
| Deallocation | No (bump; memory is permanent) |
| Fragmentation | None (monotone) |
| Max allocation | Heap size − alignment padding |
The bump allocator is appropriate for the kernel because most kernel-level Rust allocations are long-lived static structures. Any allocation that the kernel makes via Box::new or similar is effectively leaked for the kernel's lifetime.
3. Userland Heap — Free-List Allocator (mem/uheap.rs)¶
Region: 0xC00_000 – 0xFFF_FFF (4 MiB, virtual == physical, identity-mapped)
Purpose: Dynamic heap for userland processes. Exposed as syscalls 0x0a (malloc), 0x0b (realloc), 0x0f (free). All processes share one physical heap — there is no per-process isolation, but every block records its owner and is reclaimed when the owner dies.
Mapping: uheap::init() is called from init_processes() after save_kernel_cr3(). It sets P2[6] (0xC00_000) and P2[7] (0xE00_000) to USER+WRITE 2 MiB huge pages in the current page table. Because create_user_page_table clones the kernel P2, every subsequent user process inherits these entries automatically.
Block Layout (in-band, 8-byte header)¶
┌─────────────────┬─────────────────┐
│ data_size: u32 │ flags: u32 │ ← 8-byte header
└─────────────────┴─────────────────┘
│ data region │ ← data_size bytes
└────────────────────────────────────┘
flags bits |
Meaning |
|---|---|
| 0 | 1 = free, 0 = used |
| 8–15 | owning scheduler slot + 1 (0 = untagged) |
The syscall layer reads the caller's slot (scheduler::get_current_pid()) before entering the heap and passes it as owner, so the scheduler lock is never taken underneath the heap lock. NO_OWNER (0xFF) marks a block that belongs to no process and is never swept. realloc keeps the owner a block was allocated with.
At init(), a single free block spanning HEAP_SIZE − 8 bytes is written at HEAP_START.
Growing: the extension¶
One-time shared-heap growth. Mapping the extension into every live process keeps pointers returned by the allocator usable across existing tasks.
When an allocation (or a realloc) finds no room in the 4 MiB, the heap grows once, and the allocation is tried again:
- Size: an eighth of the usable RAM the Multiboot2 map reported (
boot::total_ram_bytes), in whole 2 MiB pages, at least 2 MiB — 32 MiB with 256 MiB of RAM, 256 MiB with 2 GiB. - Place:
0xA00_0000(160 MiB), or the first 2 MiB boundary past the tar archive (ustar::archive_base/len) when the archive is in the way. The whole range has to lie inside one usable region of the memory map (boot::usable_covers), below 1 GiB, the part of the identity map processes copy, and clear of the RAM disk (memdisk::TMP.overlaps), which on a machine of a few hundred MiB sits just above where the extension would reach; if it does not, the size is halved down to 2 MiB before giving up. A machine without that RAM keeps the 4 MiB, and says so once on the serial log. - Mapping:
pages::set_user_rangemakes those P2 entries USER+WRITE in the kernel's P2 and in every live process's copy of it. Each process has its own P2 (seecreate_user_page_table), so patching the kernel's alone would reach only processes started later;pages.rskeeps a registry of the live copies for this. The copy and its registration happen with interrupts off, as does the patching, so a process being created either copies the patched table or is registered in time to be patched. - Blocks: the extension is a second region with its own chain of the same 8-byte headers.
alloc,free,realloc,coalesce, the owner sweep andstatswalk both regions;uheap::extension()says where it is anduheap::contains(addr, len)whether a buffer lies in either. - Syscalls:
user_room/user_buf_okinabi/syscall.rsaccept a buffer in the extension like one in the 4 MiB, so heap memory from either can be handed to any syscall.
The heap shell command prints the extension when there is one. ktest fills the 4 MiB and checks the growth (test_heap_grow).
Allocation (malloc)¶
- Acquire
LOCK(see Locking) and drain any pending owner sweeps. - Linear scan from
HEAP_START: skip blocks whereflags & FLAG_FREE == 0ordata_size < size. - On finding a suitable free block:
- If
remainder = data_size − size ≥ HDR + MIN_SPLIT (16): split — write a new free header ataddr + HDR + size, mark the found block used withdata_size = size. - Otherwise: use the whole block (no split), mark used.
- Zero the data region.
- Return
addr + HDR(pointer to data, not header).
All sizes are rounded up to 8-byte alignment (align8) before the scan.
Reallocation (realloc)¶
Tries three strategies in order, under the lock:
- Shrink / same size: If
new_size ≤ old_size, optionally split the tail into a new free block if the remainder is large enough. Return the same pointer. - In-place expansion: If the immediately adjacent next block is free and
old_size + HDR + next_size ≥ new_size, absorb it. Split the remainder if large enough. Return the same pointer. - Allocate + copy + free:
alloc_inner(new_size),copy_nonoverlapping,set_free(old_hdr),coalesce(). Returns the new pointer (old pointer is invalid after this).
Special cases: ptr == 0 → malloc(new_size); new_size == 0 → free(ptr).
Deallocation (free)¶
- Validate
ptr ∈ [HEAP_START + HDR, HEAP_END). - Acquire
LOCK. set_free(ptr − HDR): writeFLAG_FREEto the header flags field.coalesce(): single linear pass that merges any pair of adjacent free blocks by summing[size + HDR + next_size]into the left block's header.
Reclaiming a dead process's blocks (free_owned)¶
When the scheduler kills a process or marks it crashed, it calls free_owned(slot) after releasing its own lock. The slot's bit is first published in PENDING; if the heap lock can be taken, every used block tagged with that slot is freed and coalesced, otherwise the next malloc performs the sweep. A busy lock therefore delays reclamation instead of leaking the blocks.
Locking¶
static LOCK: Mutex<()> (spin). All public functions acquire it through lock_heap():
- The lock is spun for with interrupts on, so its holder can still be scheduled and release it.
- Once taken, interrupts are turned off for as long as it is held, so the holder cannot be preempted — and then killed — inside the allocator.
- The holder's slot is recorded in
LOCK_HOLDER(lock_holder()reads it without the lock). - On release the lock is dropped first, then the holder is forgotten, then interrupts are restored.
If a process dies holding the lock anyway, free_owned takes it back (reclaim_from) before sweeping, since a killed or crashed process never returns to the allocator.
Statistics¶
stats() reports used and free bytes, the largest free block (what decides whether a large malloc can succeed), block counts, and used bytes per owning slot. The shell's heap command prints it, or the slot holding the lock when the heap is busy.
Constants¶
| Constant | Value | Description |
|---|---|---|
HEAP_START |
0xC00_000 |
First byte of the heap region |
HEAP_END |
0x1000_000 |
One past the last byte (exclusive) |
HDR |
8 |
Header size in bytes |
FLAG_FREE |
1 |
Free flag in the flags field |
MIN_SPLIT |
16 |
Minimum data size for a remainder block to be split off |
Pointer Validation in Syscalls¶
Syscall handlers accept buffers wholly inside the image and initial stacks
(0x400_000..0xA00_000), the initial shared heap or its mapped extension.
Buffers may not cross gaps between regions. See
Pointer Arguments. Heap
memory can be passed directly to 0x10 print or 0x17 blit.
The malloc/realloc/free syscalls themselves (0x0a, 0x0b, 0x0f) do not check pointers against the userland range — uheap::free validates against its active heap regions instead.
Related documentation: Memory Overview, Kernel Internals, Memory Syscalls, C++ Heap.