added physical memory manager

This commit is contained in:
Daniel Samson 2026-07-03 11:24:26 +01:00
parent cb2f49cb48
commit 6312e84262
4 changed files with 80 additions and 19 deletions

View File

@ -32,10 +32,11 @@ Defined in `src/root.zig`, the shared loader↔kernel contract:
```zig
pub const MemoryKind = enum(u32) {
usable, // free RAM the kernel may allocate
reserved, // firmware / MMIO / kernel image — never hand out
reserved, // firmware / kernel image — real RAM, but never hand out
reclaimable, // usable once boot-time structures are done with
acpi_tables, // parse, then reclaim
acpi_nvs, // preserve across sleep
mmio, // device registers / reserved address space — not RAM at all
};
pub const MemoryRegion = extern struct {
@ -70,12 +71,20 @@ pub const BootInfo = extern struct {
Two functions in `src/efi.zig`, called from `exitBootServices`:
- **`classify`** maps each UEFI memory type to a `MemoryKind`:
- **`classify`** maps each UEFI descriptor to a `MemoryKind`:
`conventional_memory → usable`; `boot_services_code`/`boot_services_data →
reclaimable` (free once we've exited); `acpi_reclaim_memory → acpi_tables`;
`acpi_memory_nvs → acpi_nvs`; **everything else → reserved** (the safe default).
Our own `loader_data` — the kernel image and these buffers — falls into
`reserved`, so it won't be handed out until the kernel deliberately reclaims it.
One subtlety: **a region that isn't writeback-cacheable (the descriptor's `wb`
attribute) is classified `mmio` regardless of type.** UEFI overloads
`reserved_memory_type` for both reserved RAM *and* reserved address-space windows
(PCIe config space, device BARs); the cache attribute is what actually tells them
apart, since only real RAM is writeback-cacheable. Without this, a QEMU q35 guest
reports ~12 GiB of "reserved" that is really a PCIe address hole near the 1 TB
mark — not memory at all.
- **`convertMemoryMap`** walks the UEFI descriptors (striding by
`descriptor_size`, *not* `@sizeOf`), classifies each, and writes danos
`MemoryRegion`s into an output buffer, coalescing adjacent same-kind regions.
@ -111,17 +120,24 @@ for (regions) |r| {
}
```
Today `kmain` just prints the region count and total usable RAM — enough to prove
the handoff works. Booted in QEMU with 128 MiB, it reports something like:
`kmain` summarises the map to prove the handoff works. Booted in QEMU with
128 MiB, it reports:
```
mem regions: 35
usable RAM : 77 MiB
danos: physical memory
total RAM : 0.12 GiB (127 MiB) - RAM the firmware reported
usable : 77 MiB - free now; owned by the frame allocator
reclaimable: 44 MiB - UEFI boot-services memory, free after exit
reserved : 6 MiB - kernel image, ACPI, runtime services
regions : 35 - entries in the firmware memory map
```
with the balance being `reclaimable` boot-services memory (~44 MiB) and a large
`reserved` span that is mostly MMIO address space, not RAM. Those figures summing
back to ~128 MiB is the sanity check that nothing was dropped.
The `usable` figure is only ~77 of ~127 MiB because most of the rest is
`reclaimable` boot-services memory — real RAM we'll take back once we implement
reclaiming, not memory that's gone. `total` counts only writeback-cacheable RAM,
so the ~12 GiB PCIe address hole is excluded (it's `mmio`), and the three RAM
categories summing back to the firmware's total is the sanity check that nothing
was dropped.
## How Raspberry Pi will fit

View File

@ -262,7 +262,7 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
const d: *const uefi.tables.MemoryDescriptor =
@ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size));
if (d.number_of_pages == 0) continue;
const kind = classify(d.@"type");
const kind = classify(d);
// Coalesce with the previous region if it's the same kind and contiguous.
if (count > 0) {
@ -284,16 +284,22 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
return .{ .regions = @intFromPtr(regions), .len = count };
}
/// Map a UEFI memory type to danos's neutral kind. Anything we don't explicitly
/// recognise is treated as `reserved` the safe default. Our own loader data
/// (the kernel image, these buffers) is LoaderData, which falls here too and so
/// stays reserved until the kernel decides to reclaim it.
fn classify(t: uefi.tables.MemoryType) danos.MemoryKind {
return switch (t) {
/// Map a UEFI descriptor to danos's neutral kind. A region that isn't
/// writeback-cacheable (`wb`) isn't backed by real RAM it's device registers or
/// a reserved address-space window (e.g. PCIe config space) so it's `mmio`
/// regardless of type. UEFI overloads `reserved_memory_type` for both reserved RAM
/// and such holes, and the cache attribute is what actually tells them apart.
/// Among RAM regions, anything we don't recognise is `reserved` the safe
/// default; our own LoaderData (kernel image, these buffers) lands there too and
/// stays reserved until the kernel reclaims it.
fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind {
if (!d.attribute.wb) return .mmio;
return switch (d.@"type") {
.conventional_memory => .usable,
.boot_services_code, .boot_services_data => .reclaimable,
.acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs,
.memory_mapped_io, .memory_mapped_io_port_space => .mmio,
else => .reserved,
};
}

View File

@ -40,11 +40,41 @@ fn kmain(boot_info: *const BootInfo) noreturn {
// own MemoryRegion, so this is a plain slice no firmware layout in sight.
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(boot_info.memory_map.regions))[0..boot_info.memory_map.len];
var usable_pages: u64 = 0;
var reclaim_pages: u64 = 0;
var reserved_pages: u64 = 0; // reserved RAM only MMIO is device space, not RAM
for (regions) |r| {
if (r.kind == .usable) usable_pages += r.pages;
switch (r.kind) {
.usable => usable_pages += r.pages,
.reclaimable => reclaim_pages += r.pages,
.reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages,
.mmio => {},
}
}
con.print(" mem regions: {d}\n", .{regions.len});
con.print(" usable RAM : {d} MiB\n", .{usable_pages * danos.page_size / (1024 * 1024)});
const total_pages = usable_pages + reclaim_pages + reserved_pages;
const total_bytes = total_pages * danos.page_size;
const gib = 1 << 30;
con.write("\ndanos: physical memory\n");
con.print(" total RAM : {d}.{d:0>2} GiB ({d} MiB) - RAM the firmware reported\n", .{ total_bytes / gib, (total_bytes % gib) * 100 / gib, mib(total_pages) });
con.print(" usable : {d} MiB - free now; owned by the frame allocator\n", .{mib(usable_pages)});
con.print(" reclaimable: {d} MiB - UEFI boot-services memory, free after exit\n", .{mib(reclaim_pages)});
con.print(" reserved : {d} MiB - kernel image, ACPI, runtime services\n", .{mib(reserved_pages)});
con.print(" regions : {d} - entries in the firmware memory map\n", .{regions.len});
// Bring up the physical frame allocator over that map, and prove it works:
// allocate three frames, then hand them back.
pmm.init(boot_info.memory_map);
const s = pmm.stats();
con.print("\ndanos: frame allocator online\n", .{});
con.print(" free frames: {d} ({d} MiB)\n", .{ s.free_frames, mib(s.free_frames) });
const f0 = pmm.alloc();
const f1 = pmm.alloc();
const f2 = pmm.alloc();
con.print(" alloc x3 : 0x{x} 0x{x} 0x{x}\n", .{ f0 orelse 0, f1 orelse 0, f2 orelse 0 });
if (f0) |p| pmm.free(p);
if (f1) |p| pmm.free(p);
if (f2) |p| pmm.free(p);
con.print(" after free : {d} frames free\n", .{pmm.stats().free_frames});
// Bring up the physical frame allocator over that map, and prove it works:
// allocate three frames, then hand them back.
@ -66,6 +96,11 @@ fn kmain(boot_info: *const BootInfo) noreturn {
arch.halt();
}
/// Frames (4 KiB pages) to whole MiB.
fn mib(pages: u64) u64 {
return pages * danos.page_size / (1024 * 1024);
}
/// Freestanding has no OS to receive a panic. Print it to the console (if it is
/// up yet) in red, then halt.
pub const panic = std.debug.FullPanic(struct {

View File

@ -52,6 +52,10 @@ pub const MemoryKind = enum(u32) {
acpi_tables,
/// ACPI non-volatile storage: preserve across sleep, do not allocate.
acpi_nvs,
/// Not backed by RAM: memory-mapped device registers or a reserved
/// address-space window (e.g. PCIe config space). Kept distinct from
/// `reserved` so RAM accounting doesn't count device address space.
mmio,
};
/// One contiguous span of physical memory. Because danos defines this layout