allocate AP kernel/IST stacks at bring-up, not statically
Hoist max_cpus into danos (root.zig), raise it to 128, and stop reserving a static [max_cpus][16 KiB] IST array. Only the BSP's IST stack is static (needed before the allocator); each AP's is heap-allocated when it comes online. Shrinks the kernel from ~1.1 MB to ~139 KiB in ReleaseSmall.
This commit is contained in:
parent
26ac97df31
commit
1b47de5058
|
|
@ -203,6 +203,13 @@ next lands.
|
||||||
[scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity`
|
[scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity`
|
||||||
test confirms a pinned task never migrates. This is the mechanism the fault-on-AP
|
test confirms a pinned task never migrates. This is the mechanism the fault-on-AP
|
||||||
test rides on, and the *explicit-affinity* real-time-predictable model.
|
test rides on, and the *explicit-affinity* real-time-predictable model.
|
||||||
|
- **Right-sized footprint** — the per-CPU ceiling (`danos.max_cpus`, one constant
|
||||||
|
shared by discovery, the scheduler, and the per-core GDT/TSS) is generous (128), but
|
||||||
|
the *large* per-core resources — the kernel and IST (double-fault) stacks — are
|
||||||
|
**heap-allocated at bring-up**, only for cores that actually come online. Only the
|
||||||
|
BSP's IST stack is static, because it must exist before the frame allocator does.
|
||||||
|
This kept the kernel image small (a static `[128][16 KiB]` IST array would have been
|
||||||
|
2 MiB of `.bss`); it's a few tens of KiB instead.
|
||||||
- **`single_threaded` off** — the kernel was built `single_threaded = true`, which
|
- **`single_threaded` off** — the kernel was built `single_threaded = true`, which
|
||||||
compiles `std.atomic` down to plain non-atomic ops. Harmless on one core, but it
|
compiles `std.atomic` down to plain non-atomic ops. Harmless on one core, but it
|
||||||
quietly breaks the big kernel lock across cores; it's now `false`.
|
quietly breaks the big kernel lock across cores; it's now `false`.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
//! the `Hal.mapMmio` callback the caller supplies (the arch VMM's map primitive).
|
//! the `Hal.mapMmio` callback the caller supplies (the arch VMM's map primitive).
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const danos = @import("danos");
|
||||||
const device = @import("device.zig");
|
const device = @import("device.zig");
|
||||||
const aml = @import("aml/aml.zig");
|
const aml = @import("aml/aml.zig");
|
||||||
const DeviceTree = device.DeviceTree;
|
const DeviceTree = device.DeviceTree;
|
||||||
|
|
@ -115,7 +116,7 @@ pub const CpuInfo = struct {
|
||||||
dropped: usize = 0,
|
dropped: usize = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const max_cpus = 64;
|
const max_cpus = danos.max_cpus;
|
||||||
|
|
||||||
/// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs.
|
/// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs.
|
||||||
pub var cpu_info: CpuInfo = .{};
|
pub var cpu_info: CpuInfo = .{};
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,14 @@ pub fn setSecondaryEntry(entry: *const fn () callconv(.c) noreturn) void {
|
||||||
smp.setSecondaryEntry(entry);
|
smp.setSecondaryEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bytes the kernel should allocate for an AP's IST (double-fault) stack, and where
|
||||||
|
/// to record its top before waking the core. The stack is heap-allocated per online
|
||||||
|
/// AP (the BSP's is static — it's needed before the allocator exists). See tss.zig.
|
||||||
|
pub const ist_stack_size = tss.ist_stack_size;
|
||||||
|
pub fn setApIstStack(cpu: usize, top: usize) void {
|
||||||
|
tss.setApIstStack(cpu, top);
|
||||||
|
}
|
||||||
|
|
||||||
/// Test hook: force the next `n` AP wake attempts to fail, so the retry path can be
|
/// Test hook: force the next `n` AP wake attempts to fail, so the retry path can be
|
||||||
/// exercised deterministically (see the smp-retry test). No effect when `n` is 0.
|
/// exercised deterministically (see the smp-retry test). No effect when `n` is 0.
|
||||||
pub fn testFailNextWakes(n: u32) void {
|
pub fn testFailNextWakes(n: u32) void {
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@
|
||||||
//! tss.zig). Two cores can't share one TSS descriptor slot, so each core gets its
|
//! tss.zig). Two cores can't share one TSS descriptor slot, so each core gets its
|
||||||
//! own copy of the table with its own TSS descriptor. Slot 0 is the BSP.
|
//! own copy of the table with its own TSS descriptor. Slot 0 is the BSP.
|
||||||
|
|
||||||
|
const danos = @import("danos");
|
||||||
|
|
||||||
/// Selectors into the table (index * 8). Same on every core's GDT.
|
/// Selectors into the table (index * 8). Same on every core's GDT.
|
||||||
pub const kernel_code = 0x08;
|
pub const kernel_code = 0x08;
|
||||||
pub const kernel_data = 0x10;
|
pub const kernel_data = 0x10;
|
||||||
pub const tss_selector = 0x18;
|
pub const tss_selector = 0x18;
|
||||||
|
|
||||||
const max_cpus = 64; // matches the scheduler / discovery pool
|
const max_cpus = danos.max_cpus;
|
||||||
const entries = 5; // null, code, data, TSS-low, TSS-high
|
const entries = 5; // null, code, data, TSS-low, TSS-high
|
||||||
|
|
||||||
/// The shared descriptors (slots 0-2); slots 3-4 hold this core's TSS descriptor,
|
/// The shared descriptors (slots 0-2); slots 3-4 hold this core's TSS descriptor,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
//! once can't share one fault stack. So the TSS and its IST stack are per-core,
|
//! once can't share one fault stack. So the TSS and its IST stack are per-core,
|
||||||
//! indexed by CPU number; slot 0 is the BSP.
|
//! indexed by CPU number; slot 0 is the BSP.
|
||||||
|
|
||||||
|
const danos = @import("danos");
|
||||||
const gdt = @import("gdt.zig");
|
const gdt = @import("gdt.zig");
|
||||||
|
|
||||||
/// x86_64 TSS. `packed` because several 64-bit fields sit at 4-byte-unaligned
|
/// x86_64 TSS. `packed` because several 64-bit fields sit at 4-byte-unaligned
|
||||||
|
|
@ -34,24 +35,35 @@ const Tss = packed struct {
|
||||||
/// The IST slot (1-based, as the IDT gate encodes it) used for critical faults.
|
/// The IST slot (1-based, as the IDT gate encodes it) used for critical faults.
|
||||||
pub const double_fault_ist = 1;
|
pub const double_fault_ist = 1;
|
||||||
|
|
||||||
const max_cpus = 64; // matches gdt.zig / the scheduler
|
const max_cpus = danos.max_cpus;
|
||||||
const ist_stack_size = 16 * 1024;
|
pub const ist_stack_size = 16 * 1024;
|
||||||
|
|
||||||
/// One TSS per core, and one IST1 stack per core. Static, so they need no allocator
|
/// One TSS per core (small — kept static). The IST stacks are 16 KiB each, so only
|
||||||
/// and are always valid. (max_cpus × 16 KiB of BSS for the IST stacks.)
|
/// the **BSP's** is static: it must exist before the frame allocator does, to catch a
|
||||||
|
/// fault during early boot. Each **AP** gets a heap-allocated IST stack at bring-up
|
||||||
|
/// (after the heap is up), the top of which the BSP records here before waking it —
|
||||||
|
/// so we reserve big stacks only for cores that actually come online.
|
||||||
var tss_table = [_]Tss{.{}} ** max_cpus;
|
var tss_table = [_]Tss{.{}} ** max_cpus;
|
||||||
var ist_stacks: [max_cpus][ist_stack_size]u8 align(16) = undefined;
|
var bsp_ist_stack: [ist_stack_size]u8 align(16) = undefined;
|
||||||
|
var ap_ist_top = [_]usize{0} ** max_cpus; // per-AP IST stack top (0 = BSP / not set)
|
||||||
|
|
||||||
/// Loads the task register with the TSS selector. Defined in isr.s.
|
/// Loads the task register with the TSS selector. Defined in isr.s.
|
||||||
extern fn load_tr(selector: u16) callconv(.c) void;
|
extern fn load_tr(selector: u16) callconv(.c) void;
|
||||||
|
|
||||||
/// Set up core `cpu`'s TSS: point IST1 at that core's stack, install the TSS
|
/// Record the top of the IST stack the kernel allocated for AP `cpu`. Called on the
|
||||||
|
/// BSP before waking that core; read by the core's own `setupThisCpu`.
|
||||||
|
pub fn setApIstStack(cpu: usize, top: usize) void {
|
||||||
|
ap_ist_top[cpu] = top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up core `cpu`'s TSS: point IST1 at its stack (the BSP's static one for core 0,
|
||||||
|
/// the allocated one recorded via `setApIstStack` for an AP), install the TSS
|
||||||
/// descriptor into that core's GDT, and load it into the task register. Requires the
|
/// descriptor into that core's GDT, and load it into the task register. Requires the
|
||||||
/// core's GDT to already be loaded (gdt.loadOnThisCpu first).
|
/// core's GDT to already be loaded (gdt.loadOnThisCpu first).
|
||||||
pub fn setupThisCpu(cpu: usize) void {
|
pub fn setupThisCpu(cpu: usize) void {
|
||||||
const t = &tss_table[cpu];
|
const t = &tss_table[cpu];
|
||||||
t.* = .{};
|
t.* = .{};
|
||||||
t.ist1 = @intFromPtr(&ist_stacks[cpu]) + ist_stack_size; // stacks grow down
|
t.ist1 = if (cpu == 0) @intFromPtr(&bsp_ist_stack) + ist_stack_size else ap_ist_top[cpu];
|
||||||
t.iomap_base = @sizeOf(Tss); // == limit: no I/O permission bitmap
|
t.iomap_base = @sizeOf(Tss); // == limit: no I/O permission bitmap
|
||||||
gdt.setTssFor(cpu, @intFromPtr(t), @sizeOf(Tss) - 1);
|
gdt.setTssFor(cpu, @intFromPtr(t), @sizeOf(Tss) - 1);
|
||||||
load_tr(gdt.tss_selector);
|
load_tr(gdt.tss_selector);
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,13 @@ fn bringUpSecondaries() void {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
const stack_top = (@intFromPtr(stack.ptr) + stack.len) & ~@as(usize, 15);
|
const stack_top = (@intFromPtr(stack.ptr) + stack.len) & ~@as(usize, 15);
|
||||||
|
// This core's IST (double-fault) stack — allocated only now that the core is
|
||||||
|
// real, rather than reserved statically for every possible core.
|
||||||
|
const ist = heap.allocator().alloc(u8, arch.ist_stack_size) catch {
|
||||||
|
log.print(" cpu apic_id {d}: no IST stack; skipped\n", .{core.apic_id});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
arch.setApIstStack(index, (@intFromPtr(ist.ptr) + ist.len) & ~@as(usize, 15));
|
||||||
const pc = scheduler.prepareSecondary(index, core.apic_id);
|
const pc = scheduler.prepareSecondary(index, core.apic_id);
|
||||||
var attempt: u32 = 1;
|
var attempt: u32 = 1;
|
||||||
while (attempt <= max_wake_attempts) : (attempt += 1) {
|
while (attempt <= max_wake_attempts) : (attempt += 1) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
//! shared queues.
|
//! shared queues.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const danos = @import("danos");
|
||||||
const arch = @import("arch");
|
const arch = @import("arch");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("heap.zig");
|
||||||
const sync = @import("sync.zig");
|
const sync = @import("sync.zig");
|
||||||
|
|
@ -68,7 +69,7 @@ pub const PerCpu = struct {
|
||||||
pinned_bitmap: u8 = 0,
|
pinned_bitmap: u8 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const max_cpus = 64; // matches the discovery pool (src/device/acpi.zig)
|
const max_cpus = danos.max_cpus;
|
||||||
var cpus = [_]PerCpu{.{}} ** max_cpus;
|
var cpus = [_]PerCpu{.{}} ** max_cpus;
|
||||||
|
|
||||||
/// This core's per-CPU state, via the arch layer's GS-base pointer. Valid only once
|
/// This core's per-CPU state, via the arch layer's GS-base pointer. Valid only once
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,14 @@ pub const Framebuffer = extern struct {
|
||||||
/// targets so far.
|
/// targets so far.
|
||||||
pub const page_size = 4096;
|
pub const page_size = 4096;
|
||||||
|
|
||||||
|
/// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping
|
||||||
|
/// arrays (discovery pool, scheduler state, per-core GDT/TSS). A generous headroom:
|
||||||
|
/// these structs are small (a few hundred bytes each), and the *large* per-core
|
||||||
|
/// resources (IST/kernel stacks) are allocated at bring-up, only for cores that
|
||||||
|
/// actually come online — so this ceiling costs little. A machine with more logical
|
||||||
|
/// CPUs than this has its surplus reported and left parked (see acpi `cpusDropped`).
|
||||||
|
pub const max_cpus = 128;
|
||||||
|
|
||||||
/// danos's own classification of a span of physical memory — deliberately not
|
/// danos's own classification of a span of physical memory — deliberately not
|
||||||
/// UEFI's vocabulary. Each boot path (UEFI now, device tree later) translates its
|
/// UEFI's vocabulary. Each boot path (UEFI now, device tree later) translates its
|
||||||
/// native memory description into these kinds, so the kernel never learns what
|
/// native memory description into these kinds, so the kernel never learns what
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue