Built heap allocation

This commit is contained in:
2026-07-03 13:41:55 +01:00
parent 20b4661ff3
commit 269729f2f2
7 changed files with 334 additions and 5 deletions
+174
View File
@@ -0,0 +1,174 @@
//! The kernel heap: dynamic allocation for the kernel.
//!
//! Where the frame allocator ([pmm]) hands out fixed 4 KiB physical frames, the
//! heap hands out arbitrary byte-sized blocks from a virtual region, growing on
//! demand by mapping fresh frames into it (arch.mapPage) — the first real user of
//! the VMM (see docs/paging.md).
//!
//! The algorithm is a first-fit free list: an address-ordered singly linked list
//! of free blocks, split on allocation and coalesced with neighbours on free. It
//! is exposed as a std.mem.Allocator, so the kernel can use std containers.
//!
//! Not yet concurrency-safe: it assumes a single caller and no allocation from
//! interrupt handlers (ours don't). A lock comes with threads/SMP.
const std = @import("std");
const danos = @import("danos");
const arch = @import("arch");
const pmm = @import("pmm.zig");
const page_size = danos.page_size;
/// Virtual base of the heap: the start of the higher half, which is unmapped and
/// well clear of the identity-mapped low half. (Canonical on x86_64; an arch that
/// splits the address space differently would choose its own.)
const heap_base: usize = 0xFFFF_8000_0000_0000;
/// Cap on heap growth for now.
const heap_max: usize = 64 * 1024 * 1024;
/// A block header, placed at the start of every block. While the block is free it
/// also links into the free list via `next`.
const Block = extern struct {
size: usize, // total block size in bytes, including this header; a multiple of 16
next: ?*Block, // free-list link (only meaningful while free)
};
const header_size = @sizeOf(Block); // 16
const min_block = header_size + 16; // smallest block worth splitting off
var free_list: ?*Block = null;
var heap_end: usize = heap_base; // [heap_base, heap_end) is currently mapped
fn alignUp(value: usize, alignment: usize) usize {
return (value + alignment - 1) & ~(alignment - 1);
}
fn payloadOf(block: *Block) [*]u8 {
return @ptrFromInt(@intFromPtr(block) + header_size);
}
/// Bring the heap up with an initial mapped region.
pub fn init() void {
free_list = null;
heap_end = heap_base;
_ = grow(page_size);
}
/// Map more pages onto the end of the heap and add them as a free block. Returns
/// false if out of heap virtual space or out of physical frames.
fn grow(min_bytes: usize) bool {
const start = heap_end;
const bytes = alignUp(min_bytes, page_size);
if (start + bytes > heap_base + heap_max) return false;
var virt = start;
while (virt < start + bytes) : (virt += page_size) {
const frame = pmm.alloc() orelse return false;
arch.mapPage(virt, frame, true);
}
heap_end = start + bytes;
const block: *Block = @ptrFromInt(start);
block.size = bytes;
insertFree(block); // coalesces with the previous tail block if adjacent
return true;
}
/// Insert a block into the address-ordered free list, coalescing with the
/// physically adjacent free blocks on either side.
fn insertFree(block: *Block) void {
var prev: ?*Block = null;
var cur = free_list;
while (cur) |c| : (cur = c.next) {
if (@intFromPtr(c) > @intFromPtr(block)) break;
prev = c;
}
block.next = cur;
if (prev) |p| p.next = block else free_list = block;
// Merge forward into `cur` if they're contiguous.
if (cur) |c| {
if (@intFromPtr(block) + block.size == @intFromPtr(c)) {
block.size += c.size;
block.next = c.next;
}
}
// Merge `prev` forward into `block` if they're contiguous.
if (prev) |p| {
if (@intFromPtr(p) + p.size == @intFromPtr(block)) {
p.size += block.size;
p.next = block.next;
}
}
}
/// Allocate `len` bytes (16-byte aligned), or null if out of memory.
fn rawAlloc(len: usize) ?[*]u8 {
const need = alignUp(header_size + len, 16);
var attempts: u32 = 0;
while (attempts < 2) : (attempts += 1) {
var prev: ?*Block = null;
var cur = free_list;
while (cur) |block| : ({
prev = block;
cur = block.next;
}) {
if (block.size < need) continue;
if (block.size >= need + min_block) {
// Split: carve `need` off the front, leave the rest free.
const rest: *Block = @ptrFromInt(@intFromPtr(block) + need);
rest.size = block.size - need;
rest.next = block.next;
if (prev) |p| p.next = rest else free_list = rest;
block.size = need;
} else {
// Take the whole block.
if (prev) |p| p.next = block.next else free_list = block.next;
}
return payloadOf(block);
}
// Nothing fit: grow and try once more.
if (!grow(need)) return null;
}
return null;
}
fn rawFree(ptr: [*]u8) void {
const block: *Block = @ptrFromInt(@intFromPtr(ptr) - header_size);
insertFree(block);
}
// --- std.mem.Allocator interface -----------------------------------------
pub fn allocator() std.mem.Allocator {
return .{ .ptr = undefined, .vtable = &vtable };
}
const vtable = std.mem.Allocator.VTable{
.alloc = allocImpl,
.resize = resizeImpl,
.remap = remapImpl,
.free = freeImpl,
};
fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 {
// Blocks are 16-byte aligned; larger alignments aren't supported yet.
if (alignment.toByteUnits() > 16) return null;
return rawAlloc(len);
}
fn resizeImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) bool {
return false; // no in-place resize; the caller reallocates
}
fn remapImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 {
return null;
}
fn freeImpl(_: *anyopaque, memory: []u8, _: std.mem.Alignment, _: usize) void {
rawFree(memory.ptr);
}
+6 -1
View File
@@ -3,6 +3,7 @@ const danos = @import("danos");
const arch = @import("arch");
const console = @import("console.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const tests = @import("tests.zig");
const build_options = @import("build_options");
const BootInfo = danos.BootInfo;
@@ -92,10 +93,14 @@ fn kmain(boot_info: *const BootInfo) noreturn {
con.print(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()});
con.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_info.kernel_segment_count});
// Bring up the kernel heap (dynamic allocation), built on the VMM.
heap.init();
con.write("\ndanos: kernel heap online\n");
// Start the timer and unmask interrupts — the kernel now has a heartbeat.
arch.startTimer();
arch.enableInterrupts();
con.write("\ndanos: timer interrupts enabled\n");
con.write("danos: timer interrupts enabled\n");
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
// Normal builds fall through to the idle halt.
+66
View File
@@ -13,6 +13,7 @@ const std = @import("std");
const danos = @import("danos");
const arch = @import("arch");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
/// Formatted write straight to serial, independent of the framebuffer console.
fn log(comptime fmt: []const u8, args: anytype) void {
@@ -50,6 +51,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
timer();
} else if (eql(case, "vmm")) {
vmm();
} else if (eql(case, "heap")) {
heapTest();
} else if (eql(case, "fault-ud")) {
faultInvalidOpcode();
} else if (eql(case, "fault-pf")) {
@@ -138,6 +141,69 @@ fn vmm() void {
result();
}
/// Exercise the kernel heap: basic alloc/write/free, reuse, growth beyond the
/// initial region, and a std container backed by it.
fn heapTest() void {
log("DANOS-TEST-BEGIN: heap\n", .{});
const a = heap.allocator();
// Allocate, write a pattern, read it back, free.
const buf = a.alloc(u8, 4096) catch null;
check("alloc 4096 bytes", buf != null);
if (buf) |b| {
@memset(b, 0xAB);
check("heap memory is writable and reads back", b[0] == 0xAB and b[4095] == 0xAB);
a.free(b);
}
// Freeing then re-allocating the same size should reuse the block.
const p1 = a.alloc(u64, 8) catch null;
const addr1 = if (p1) |p| @intFromPtr(p.ptr) else 0;
if (p1) |p| a.free(p);
const p2 = a.alloc(u64, 8) catch null;
const addr2 = if (p2) |p| @intFromPtr(p.ptr) else 0;
check("freed block is reused", addr1 != 0 and addr1 == addr2);
if (p2) |p| a.free(p);
// Force growth past the initial page and check every block is usable.
var blocks: [64]?[]u8 = .{null} ** 64;
var ok = true;
for (&blocks, 0..) |*slot, i| {
const b = a.alloc(u8, 4096) catch null;
slot.* = b;
if (b) |bb| @memset(bb, @intCast(i & 0xff)) else {
ok = false;
}
}
for (blocks, 0..) |slot, i| {
if (slot) |bb| {
if (bb[0] != @as(u8, @intCast(i & 0xff)) or bb[4095] != @as(u8, @intCast(i & 0xff))) ok = false;
}
}
check("many allocations (heap growth) stay valid", ok);
for (blocks) |slot| {
if (slot) |bb| a.free(bb);
}
// A std container backed by the kernel heap.
var list: std.ArrayList(u32) = .empty;
var sum: u64 = 0;
var expected: u64 = 0;
var i: u32 = 0;
var list_ok = true;
while (i < 1000) : (i += 1) {
list.append(a, i) catch {
list_ok = false;
};
expected += i;
}
for (list.items) |v| sum += v;
list.deinit(a);
check("std.ArrayList on the kernel heap", list_ok and sum == expected);
result();
}
fn faultInvalidOpcode() void {
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
asm volatile ("ud2");