reclaiming uefi memory
This commit is contained in:
+18
-5
@@ -100,12 +100,25 @@ 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.
|
||||
- **Consumers**: the virtual memory manager / page tables and then the kernel heap
|
||||
will be the first real users, each asking `alloc()` for frames.
|
||||
- **Reclaiming `reclaimable`** (UEFI boot-services) memory, and eventually the
|
||||
`reserved` `loader_data` (kernel image, boot buffers) once nothing needs it —
|
||||
see the deferred list in [memory-map.md](memory-map.md).
|
||||
- **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.
|
||||
|
||||
+30
-22
@@ -32,8 +32,7 @@ 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 / kernel image — real RAM, but never hand out
|
||||
reclaimable, // usable once boot-time structures are done with
|
||||
reserved, // firmware / kernel image / boot stack — real RAM, never hand out
|
||||
acpi_tables, // parse, then reclaim
|
||||
acpi_nvs, // preserve across sleep
|
||||
mmio, // device registers / reserved address space — not RAM at all
|
||||
@@ -72,11 +71,19 @@ pub const BootInfo = extern struct {
|
||||
Two functions in `src/efi.zig`, called from `exitBootServices`:
|
||||
|
||||
- **`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.
|
||||
`conventional_memory` **and** `boot_services_code`/`boot_services_data → usable`;
|
||||
`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`.
|
||||
|
||||
Folding boot-services memory into `usable` is deliberate: we've already called
|
||||
ExitBootServices, so it's free RAM now, and doing the classification *here* (in
|
||||
the loader) means the kernel never learns about a UEFI-specific "reclaimable"
|
||||
state — it just sees usable RAM. The one catch is that our stack lives in
|
||||
boot-services memory and the kernel starts out running on it, so
|
||||
`convertMemoryMap` keeps the single region containing the current stack pointer
|
||||
`reserved`. All the boot-protocol knowledge stays on the loader side of the
|
||||
boundary; the kernel's frame allocator has no idea any of this happened.
|
||||
|
||||
One subtlety: **a region that isn't writeback-cacheable (the descriptor's `wb`
|
||||
attribute) is classified `mmio` regardless of type.** UEFI overloads
|
||||
@@ -126,18 +133,17 @@ for (regions) |r| {
|
||||
```
|
||||
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
|
||||
usable : 121 MiB - free RAM (incl. reclaimed boot-services memory)
|
||||
reserved : 6 MiB - kernel image, boot stack, ACPI, runtime services
|
||||
regions : 28 - entries in the firmware memory map
|
||||
```
|
||||
|
||||
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.
|
||||
`usable` is ~121 of ~127 MiB because the loader already folded the boot-services
|
||||
memory into it — so the frame allocator gets it all with no special step. The ~6 MiB
|
||||
`reserved` is the kernel image, the boot stack's region, ACPI, and runtime services.
|
||||
`total` counts only writeback-cacheable RAM, so the ~12 GiB PCIe address hole is
|
||||
excluded (it's `mmio`), and the RAM categories summing back to the firmware's total
|
||||
is the sanity check that nothing was dropped.
|
||||
|
||||
## How Raspberry Pi will fit
|
||||
|
||||
@@ -150,11 +156,13 @@ never knows the difference.
|
||||
## What's next
|
||||
|
||||
This page is plumbing plus classification only. The map's first consumer, the
|
||||
**physical frame allocator**, is built directly on the `usable` regions here —
|
||||
see [frame-allocator.md](frame-allocator.md). Still to come after that:
|
||||
**physical frame allocator**, is built directly on the `usable` regions here — which
|
||||
already include the reclaimed boot-services memory the loader folded in (see
|
||||
[frame-allocator.md](frame-allocator.md)). Still to come:
|
||||
|
||||
- Reclaiming `reclaimable` regions, and carefully freeing `reserved` `loader_data`
|
||||
(kernel image, these buffers) once the kernel is done reading them.
|
||||
- Paging / the kernel's own page tables, then a heap.
|
||||
- Freeing the `reserved` `loader_data` (these boot-time buffers) once the kernel is
|
||||
done reading the map.
|
||||
- Capturing the ACPI RSDP from the UEFI configuration table before exit (the same
|
||||
"grab it before ExitBootServices" pattern), for when ACPI parsing arrives.
|
||||
|
||||
See the roadmap in [efi.md](efi.md) for where this sits in the boot flow.
|
||||
|
||||
+17
-6
@@ -269,6 +269,12 @@ fn exitBootServices(bs: *uefi.tables.BootServices) !danos.MemoryMap {
|
||||
/// never sees UEFI's vocabulary — the same seam the framebuffer already uses.
|
||||
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
|
||||
const regions: [*]danos.MemoryRegion = @ptrCast(@alignCast(out.ptr));
|
||||
// We're about to call boot-services memory `usable`, but our own stack lives
|
||||
// in it and the kernel starts out running on it. Keep the region holding the
|
||||
// current stack pointer reserved so it's never handed out.
|
||||
const rsp = asm volatile ("mov %%rsp, %[out]"
|
||||
: [out] "=r" (-> usize),
|
||||
);
|
||||
var count: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < map.info.len) : (i += 1) {
|
||||
@@ -277,7 +283,10 @@ 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);
|
||||
var kind = classify(d);
|
||||
// The descriptor we're executing on stays reserved (see rsp above).
|
||||
const region_end = d.physical_start + d.number_of_pages * danos.page_size;
|
||||
if (kind == .usable and rsp >= d.physical_start and rsp < region_end) kind = .reserved;
|
||||
|
||||
// Coalesce with the previous region if it's the same kind and contiguous.
|
||||
if (count > 0) {
|
||||
@@ -304,14 +313,16 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
|
||||
/// 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.
|
||||
///
|
||||
/// Boot-services memory is folded straight into `usable`: we've already called
|
||||
/// ExitBootServices, so it's free RAM now — the kernel never needs to know it was
|
||||
/// ever the firmware's (the one live piece, our stack, is reserved by the caller).
|
||||
/// Anything unrecognised is `reserved` — the safe default; our own LoaderData (the
|
||||
/// kernel image and these buffers) lands there and stays reserved.
|
||||
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,
|
||||
.conventional_memory, .boot_services_code, .boot_services_data => .usable,
|
||||
.acpi_reclaim_memory => .acpi_tables,
|
||||
.acpi_memory_nvs => .acpi_nvs,
|
||||
.memory_mapped_io, .memory_mapped_io_port_space => .mmio,
|
||||
|
||||
+3
-6
@@ -52,25 +52,22 @@ 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| {
|
||||
switch (r.kind) {
|
||||
.usable => usable_pages += r.pages,
|
||||
.reclaimable => reclaim_pages += r.pages,
|
||||
.reserved, .acpi_tables, .acpi_nvs => reserved_pages += r.pages,
|
||||
.mmio => {},
|
||||
}
|
||||
}
|
||||
const total_pages = usable_pages + reclaim_pages + reserved_pages;
|
||||
const total_pages = usable_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(" usable : {d} MiB - free RAM (incl. reclaimed boot-services memory)\n", .{mib(usable_pages)});
|
||||
con.print(" reserved : {d} MiB - kernel image, boot stack, 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:
|
||||
|
||||
+5
-4
@@ -58,12 +58,13 @@ fn regions(map: danos.MemoryMap) []const danos.MemoryRegion {
|
||||
pub fn init(map: danos.MemoryMap) void {
|
||||
const regs = regions(map);
|
||||
|
||||
// 1. Size the bitmap to cover every frame up to the highest usable address.
|
||||
// Reserved/MMIO spans above that are simply outside the map and never
|
||||
// allocatable.
|
||||
// 1. Size the bitmap to cover every frame up to the highest RAM address —
|
||||
// including reserved RAM, so those frames are trackable (e.g. to free the
|
||||
// boot buffers later). Only MMIO (device address space) is excluded.
|
||||
// Everything starts unallocatable; usable regions are freed below.
|
||||
var highest: u64 = 0;
|
||||
for (regs) |r| {
|
||||
if (r.kind != .usable) continue;
|
||||
if (r.kind == .mmio) continue;
|
||||
const end = r.base + r.pages * page_size;
|
||||
if (end > highest) highest = end;
|
||||
}
|
||||
|
||||
+6
-5
@@ -41,13 +41,14 @@ pub const page_size = 4096;
|
||||
/// native memory description into these kinds, so the kernel never learns what
|
||||
/// booted it. [[arch]] keeps the same discipline for CPU code.
|
||||
pub const MemoryKind = enum(u32) {
|
||||
/// Free RAM the kernel may allocate.
|
||||
/// Free RAM the kernel may allocate. Each boot path folds its own transient
|
||||
/// memory into this once it's genuinely free (e.g. the UEFI loader classifies
|
||||
/// boot-services memory as usable after ExitBootServices), so the kernel never
|
||||
/// has to know about boot-protocol-specific "reclaimable" states.
|
||||
usable,
|
||||
/// Firmware, MMIO, the kernel image, our own boot buffers — never hand out.
|
||||
/// Firmware, MMIO, the kernel image, our own boot buffers, the boot stack —
|
||||
/// never hand out.
|
||||
reserved,
|
||||
/// Usable once the kernel is done with boot-time structures (e.g. UEFI boot
|
||||
/// services memory, which is free after ExitBootServices).
|
||||
reclaimable,
|
||||
/// ACPI tables: parse, then reclaim.
|
||||
acpi_tables,
|
||||
/// ACPI non-volatile storage: preserve across sleep, do not allocate.
|
||||
|
||||
Reference in New Issue
Block a user