added physical memory manager

This commit is contained in:
2026-07-03 11:43:14 +01:00
parent cb2f49cb48
commit 6312e84262
4 changed files with 80 additions and 19 deletions
+25 -9
View File
@@ -32,10 +32,11 @@ Defined in `src/root.zig`, the shared loader↔kernel contract:
```zig ```zig
pub const MemoryKind = enum(u32) { pub const MemoryKind = enum(u32) {
usable, // free RAM the kernel may allocate 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 reclaimable, // usable once boot-time structures are done with
acpi_tables, // parse, then reclaim acpi_tables, // parse, then reclaim
acpi_nvs, // preserve across sleep acpi_nvs, // preserve across sleep
mmio, // device registers / reserved address space — not RAM at all
}; };
pub const MemoryRegion = extern struct { pub const MemoryRegion = extern struct {
@@ -70,12 +71,20 @@ pub const BootInfo = extern struct {
Two functions in `src/efi.zig`, called from `exitBootServices`: 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 → `conventional_memory → usable`; `boot_services_code`/`boot_services_data →
reclaimable` (free once we've exited); `acpi_reclaim_memory → acpi_tables`; reclaimable` (free once we've exited); `acpi_reclaim_memory → acpi_tables`;
`acpi_memory_nvs → acpi_nvs`; **everything else → reserved** (the safe default). `acpi_memory_nvs → acpi_nvs`; **everything else → reserved** (the safe default).
Our own `loader_data` — the kernel image and these buffers — falls into 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. `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 - **`convertMemoryMap`** walks the UEFI descriptors (striding by
`descriptor_size`, *not* `@sizeOf`), classifies each, and writes danos `descriptor_size`, *not* `@sizeOf`), classifies each, and writes danos
`MemoryRegion`s into an output buffer, coalescing adjacent same-kind regions. `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 `kmain` summarises the map to prove the handoff works. Booted in QEMU with
the handoff works. Booted in QEMU with 128 MiB, it reports something like: 128 MiB, it reports:
``` ```
mem regions: 35 danos: physical memory
usable RAM : 77 MiB 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 The `usable` figure is only ~77 of ~127 MiB because most of the rest is
`reserved` span that is mostly MMIO address space, not RAM. Those figures summing `reclaimable` boot-services memory — real RAM we'll take back once we implement
back to ~128 MiB is the sanity check that nothing was dropped. 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 ## How Raspberry Pi will fit
+13 -7
View File
@@ -262,7 +262,7 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
const d: *const uefi.tables.MemoryDescriptor = const d: *const uefi.tables.MemoryDescriptor =
@ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size)); @ptrCast(@alignCast(map.ptr + i * map.info.descriptor_size));
if (d.number_of_pages == 0) continue; 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. // Coalesce with the previous region if it's the same kind and contiguous.
if (count > 0) { if (count > 0) {
@@ -284,16 +284,22 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
return .{ .regions = @intFromPtr(regions), .len = count }; return .{ .regions = @intFromPtr(regions), .len = count };
} }
/// Map a UEFI memory type to danos's neutral kind. Anything we don't explicitly /// Map a UEFI descriptor to danos's neutral kind. A region that isn't
/// recognise is treated as `reserved` — the safe default. Our own loader data /// writeback-cacheable (`wb`) isn't backed by real RAM — it's device registers or
/// (the kernel image, these buffers) is LoaderData, which falls here too and so /// a reserved address-space window (e.g. PCIe config space) — so it's `mmio`
/// stays reserved until the kernel decides to reclaim it. /// regardless of type. UEFI overloads `reserved_memory_type` for both reserved RAM
fn classify(t: uefi.tables.MemoryType) danos.MemoryKind { /// and such holes, and the cache attribute is what actually tells them apart.
return switch (t) { /// 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, .conventional_memory => .usable,
.boot_services_code, .boot_services_data => .reclaimable, .boot_services_code, .boot_services_data => .reclaimable,
.acpi_reclaim_memory => .acpi_tables, .acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs, .acpi_memory_nvs => .acpi_nvs,
.memory_mapped_io, .memory_mapped_io_port_space => .mmio,
else => .reserved, else => .reserved,
}; };
} }
+38 -3
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. // 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]; const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(boot_info.memory_map.regions))[0..boot_info.memory_map.len];
var usable_pages: u64 = 0; 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| { 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: // Bring up the physical frame allocator over that map, and prove it works:
// allocate three frames, then hand them back. // allocate three frames, then hand them back.
@@ -66,6 +96,11 @@ fn kmain(boot_info: *const BootInfo) noreturn {
arch.halt(); 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 /// Freestanding has no OS to receive a panic. Print it to the console (if it is
/// up yet) in red, then halt. /// up yet) in red, then halt.
pub const panic = std.debug.FullPanic(struct { pub const panic = std.debug.FullPanic(struct {
+4
View File
@@ -52,6 +52,10 @@ pub const MemoryKind = enum(u32) {
acpi_tables, acpi_tables,
/// ACPI non-volatile storage: preserve across sleep, do not allocate. /// ACPI non-volatile storage: preserve across sleep, do not allocate.
acpi_nvs, 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 /// One contiguous span of physical memory. Because danos defines this layout