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.
125 lines
6.5 KiB
Markdown
125 lines
6.5 KiB
Markdown
# The physical frame allocator
|
|
|
|
Once the kernel knows what RAM exists ([memory-map.md](memory-map.md)), it needs a
|
|
way to *hand out* that RAM: give me a free page of physical memory, and later,
|
|
here's one back. That's the **physical frame allocator** (a "physical memory
|
|
manager", hence `system/kernel/pmm.zig`). It deals only in fixed 4 KiB **frames** — the
|
|
natural unit because that's the granularity the CPU's paging hardware maps — and
|
|
it is the primitive everything above it stands on: page tables, the kernel heap,
|
|
per-process memory all ultimately ask the frame allocator for pages.
|
|
|
|
It's **generic kernel code**: it operates on the neutral `danos.MemoryRegion`
|
|
array, so there's no UEFI in it and nothing architecture-specific beyond the 4 KiB
|
|
page. (Contrast [arch.md](arch.md), which is where CPU-specific code lives.)
|
|
|
|
## Why a bitmap
|
|
|
|
There are a few classic designs; danos starts with the simplest that still
|
|
supports freeing:
|
|
|
|
- **Bitmap** (chosen): one bit per frame, `1 = used`, `0 = free`. Freeing is
|
|
trivial (clear a bit), it's very compact, and you can later extend it to
|
|
allocate *contiguous* runs by scanning for consecutive zero bits. Allocation is
|
|
a linear scan, but that's cheap and easy to reason about.
|
|
- **Intrusive free-list / stack**: store the "next free frame" pointer inside each
|
|
free frame; O(1) alloc and free. Elegant, but it can't satisfy contiguous
|
|
multi-frame requests and can't answer "is *this* frame free?".
|
|
- **Buddy allocator**: great for contiguous power-of-two blocks, but more
|
|
machinery than a first allocator needs.
|
|
|
|
Compactness matters less than clarity here, but it's a nice property: 128 MiB of
|
|
RAM is 32768 frames — a **4 KiB bitmap, a single frame**. Even 64 GiB needs only
|
|
2 MiB of bitmap.
|
|
|
|
## How it works
|
|
|
|
State lives in `system/kernel/pmm.zig`: the `bitmap` slice, `total_frames`, `used_frames`,
|
|
and a `next_hint` marking where the next allocation scan should start.
|
|
|
|
### init(map) — building it from the memory map
|
|
|
|
1. **Size it.** Find the highest address across all `usable` regions;
|
|
`total_frames = highest / page_size`. Reserved and MMIO spans above that
|
|
(remember the ~12 GiB of MMIO from [memory-map.md](memory-map.md)) sit *outside*
|
|
the bitmap and are simply never allocatable.
|
|
2. **Place it (the bootstrap).** The bitmap needs storage before an allocator
|
|
exists — a chicken-and-egg. Solution: pick the first `usable` region big enough
|
|
to hold the bitmap and put it there, addressing it directly as a pointer. That
|
|
last part relies on the firmware's **identity mapping** still being in effect
|
|
(physical address == virtual address), which holds until the kernel installs
|
|
its own page tables.
|
|
3. **Mark, then free.** Set the whole bitmap to `used` (`0xff`), then walk the
|
|
`usable` regions clearing their bits. Doing it in that direction means every
|
|
gap, reserved span, and hole is unallocatable *by default* — we only ever hand
|
|
back memory the firmware explicitly called usable.
|
|
4. **Take back the essentials.** Re-reserve the frames the bitmap itself occupies
|
|
(they're inside a usable region we just freed), plus **frame 0**, so an address
|
|
of `0` can keep meaning "no frame".
|
|
|
|
### alloc() → ?u64
|
|
|
|
Scan the bitmap from `next_hint` (wrapping once) for the first free bit, mark it
|
|
used, advance the hint, and return `frame * page_size`. Returns `null` when no
|
|
frame is free — genuine out-of-memory. The hint avoids rescanning the low,
|
|
long-since-allocated frames on every call.
|
|
|
|
### free(addr)
|
|
|
|
Clear the frame's bit and, if it's below `next_hint`, pull the hint back so the
|
|
reclaimed frame gets reused soon. Bogus or double frees (a frame already marked
|
|
free, or one out of range) are ignored rather than corrupting the used count.
|
|
|
|
## Correctness points worth remembering
|
|
|
|
- **Generic walk.** Because `MemoryRegion` is danos's own type, the map is a plain
|
|
slice — none of the variable descriptor-stride from the raw UEFI map.
|
|
- **Identity mapping assumption.** Placing the bitmap by physical address only
|
|
works while the firmware's identity map is live. When danos sets up its own
|
|
paging, the bitmap (and any other physical pointer) will need an explicit
|
|
mapping. This is a deliberate, documented dependency of this stage.
|
|
- **Frame 0 is reserved** so `0` stays a safe "none" sentinel — and the bitmap is
|
|
never placed there. (An early bug did exactly that: a `usable` region at physical
|
|
address 0 collided with a `0`-means-not-found sentinel and tripped a panic. The
|
|
fix was an optional plus starting the bitmap at least one page in.)
|
|
- **Everything non-usable is unallocatable by construction** — the "mark all used,
|
|
then free usable" order gives that for free, so the kernel image, the loader's
|
|
buffers, MMIO and firmware memory can never be handed out.
|
|
|
|
## Verifying it
|
|
|
|
`kmain` brings the allocator up and self-tests it. Booted in QEMU with 128 MiB:
|
|
|
|
```
|
|
danos: frame allocator online
|
|
free frames: 19751 (77 MiB) <- matches the map's 77 MiB usable
|
|
alloc x3 : 0x2000 0x3000 0x4000 <- frame 0 reserved, bitmap at 0x1000, so allocs start at 0x2000
|
|
after free : 19751 frames free <- three freed, count restored
|
|
```
|
|
|
|
The `free frames` MiB agreeing with the memory map's `usable RAM`, the three
|
|
distinct consecutive addresses, and the count returning to its start after freeing
|
|
are the three signals that init, alloc and free are all correct.
|
|
|
|
## Boot-services memory comes pre-reclaimed
|
|
|
|
The UEFI boot-services memory (~44 MiB) is defunct and free once
|
|
`ExitBootServices` runs, taking usable RAM from ~76 MiB up to ~121 MiB. The frame
|
|
allocator does **nothing special** to get it: the loader already classified it as
|
|
`usable` (see [memory-map.md](memory-map.md)), so it's just part of the `usable`
|
|
regions `init` frees. Keeping that boot-protocol knowledge on the loader side is
|
|
deliberate — the kernel has no notion of "reclaimable" or of UEFI at all.
|
|
|
|
The one live piece in that memory is the boot stack the kernel starts on; the loader
|
|
leaves the single region containing it `reserved`, so `init` won't hand it out. A
|
|
later step will move task 0 onto a kernel-owned stack, freeing that last ~1 MiB
|
|
region too (and giving user mode the clean stack it wants).
|
|
|
|
## What's next (not done here)
|
|
|
|
- **Contiguous allocation** — scan for N consecutive free bits — for callers that
|
|
need physically adjacent frames.
|
|
- **A kernel stack for task 0**, so the boot stack's region can be freed too (and
|
|
for the clean stack user mode wants).
|
|
- **Freeing the `reserved` `loader_data`** (the boot-time map buffers) once the
|
|
kernel is done reading the memory map.
|