The source layout now mirrors the runtime filesystem hierarchy
(docs/danos-file-system-hierarchy-FSH.md): what lives under system/ in the
source is what a running danos represents under /system. Each service and
driver is a sub-project directory that is its own Zig module — cross-project
references go by module name, never by a path into another project's files.
Moves (all git mv, history preserved):
- src/ -> system/ (danos internals; the self-representation)
root.zig -> danos.zig (the kernel<->user contract module)
kernel/arch/ -> kernel/architecture/ (arch -> architecture)
device/ -> devices/ (what /system/devices reflects)
boot/ -> /boot (the loaders, top level)
- sbin/ -> split by role:
init, vfs -> system/services/<name>/<name>.zig
hpetd, busd -> system/drivers/<name>/<name>.zig
vfs-test -> system/services/vfs/vfs-test.zig (inside the vfs project)
- lib/ -> library/runtime/ (room for other libraries beside runtime)
The VFS wire protocol becomes its own module, system/services/vfs/protocol.zig
("vfs-protocol"): the vfs sub-project exposes its interface, and the runtime's
file layer imports it by name. First instance of the "protocol module" pattern
(docs/driver-model.md); usb/block will expose theirs the same way.
Also: fix a naming-standard violation in the protocol — Op -> Operation (and
req -> request, _pad -> _padding). Docs updated: /system/services added to the
FHS doc, a repository-layout section added to the docs index, and stale source
paths swept across comments and docs.
Runtime boot paths are unchanged (the bootloader still loads /sbin/init);
aligning the runtime filesystem to the FHS is a separate follow-up. Suite 35/35
plus host tests green.
3.7 KiB
The kernel heap
The frame allocator hands out fixed 4 KiB physical frames;
the VMM maps pages into virtual addresses. The kernel heap sits on
top of both to provide what the rest of the kernel actually wants: alloc(n) /
free(p) for arbitrary byte sizes. It's the first real consumer of map(), and
the thing that unlocks dynamic data structures — lists, hash maps, driver state,
eventually a process table.
It's generic kernel code (system/kernel/heap.zig): the allocator logic is
architecture-neutral, using arch.mapPage and the frame allocator underneath.
A growable free-list allocator
The algorithm is a classic first-fit free list:
- The heap owns a virtual region. Free space is tracked as an address-ordered
singly linked list of free blocks; each block begins with a 16-byte header
(
size, and anextlink used while free). - alloc(n) walks the list for the first block big enough. If the block is much larger it's split — the front becomes the allocation, the remainder stays free. If nothing fits, the heap grows (below) and the search retries.
- free(p) finds the block header just before
pand inserts it back into the list, coalescing with the physically adjacent free blocks on either side so the space can be reused as one region rather than fragmenting away.
Allocations are 16-byte aligned; larger alignments aren't supported yet (the
std.mem.Allocator alloc returns null for them).
Growing on demand
The heap lives in the higher half of the address space (virtual base
0xFFFF_8000_0000_0000) — unmapped, well clear of the identity-mapped low half,
and leaving the low half free for a future user address space. (That base is
x86_64-canonical; another architecture would pick its own.)
When the free list can't satisfy a request, grow extends the mapped region: it
pulls fresh frames from the frame allocator and maps each
onto the end of the heap, then adds the new span as a free block (coalescing with
the current tail). So the heap starts at one page and expands page-by-page as
demand requires, up to a cap. This is exactly what the VMM's on-demand map was
built for.
A std.mem.Allocator
The heap is exposed as a std.mem.Allocator (heap.allocator()), Zig's
standard allocator interface. That's a deliberate multiplier: it means the whole of
Zig's standard library — ArrayList, AutoHashMap, std.fmt.allocPrint, and the
rest — works directly on the kernel heap, no bespoke containers required.
Verifying it
The heap test (see testing.md) exercises the allocator end to end:
[PASS] alloc 4096 bytes
[PASS] heap memory is writable and reads back
[PASS] freed block is reused <- free list + coalescing works
[PASS] many allocations (heap growth) stay valid <- grow() maps fresh frames
[PASS] std.ArrayList on the kernel heap <- std containers work on it
The "freed block is reused" check (free then re-alloc returns the same address) is
the proof that free and the free list actually work, not just alloc; "heap growth"
forces allocation past the initial page so grow/map runs; and the ArrayList
check is the std-integration payoff.
What's next (not done here)
- Thread/interrupt safety. The heap assumes a single caller — no lock yet.
It's safe now (nothing allocates from interrupt handlers), but threads or an
allocating IRQ handler will need a lock (or
cliaround the critical section). - Larger alignments than 16 (for page-aligned buffers, DMA regions).
resize/remapin place, so growing anArrayListneedn't always copy.- Reclaiming empty tail pages back to the frame allocator when the heap shrinks.