diff --git a/build.zig b/build.zig index 369e906..f177510 100644 --- a/build.zig +++ b/build.zig @@ -61,6 +61,13 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/root.zig"), }); + // Kernel tunables (max_cpus, stack sizes, tick rate). A dependency-free module of + // compile-time constants, imported wherever a knob is read; keeps the trade-offs + // in one place instead of scattered across the tree. See src/config.zig. + const config_mod = b.addModule("config", .{ + .root_source_file = b.path("src/config.zig"), + }); + // Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging). // The generic kernel imports this as "arch" and never names x86_64, so a new // architecture is a matter of pointing this module at a different directory. @@ -68,6 +75,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/kernel/arch/x86_64/cpu.zig"), .imports = &.{ .{ .name = "danos", .module = mod }, // paging uses the shared BootInfo/memory-map types + .{ .name = "config", .module = config_mod }, // max_cpus, ist_stack_size, timer_hz }, }); // CPU-exception stubs — real assembly, since they need cross-symbol @@ -86,6 +94,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/device/platform.zig"), .imports = &.{ .{ .name = "danos", .module = mod }, // BootInfo (carries the ACPI RSDP) + .{ .name = "config", .module = config_mod }, // max_cpus (the discovery pool) }, }); @@ -122,6 +131,7 @@ pub fn build(b: *std.Build) void { .{ .name = "danos", .module = mod }, .{ .name = "arch", .module = arch_mod }, .{ .name = "platform", .module = platform_mod }, + .{ .name = "config", .module = config_mod }, .{ .name = "build_options", .module = build_options_mod }, }, }), diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..843967c --- /dev/null +++ b/src/config.zig @@ -0,0 +1,30 @@ +//! Kernel tunables — the compile-time knobs, gathered in one place. +//! +//! These constants would otherwise be scattered across the files that use them, +//! hiding the trade-offs. Keeping them here makes them visible at a glance and gives +//! one spot to change them. They're plain `comptime` constants (zero runtime cost); +//! any one can later be promoted to a `-D` build option if a target needs to vary it +//! (see build.zig's `-Dtest-case` for the pattern). This keeps root.zig to what it +//! actually is — the bootloader↔kernel handoff *contract* — with tunables living here. + +/// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping +/// arrays (discovery pool, scheduler state, per-core GDT/TSS). Generous headroom: +/// those structs are small, and the *large* per-core resources (kernel and IST +/// stacks) are allocated at bring-up for cores that actually come online, so this +/// ceiling is cheap. A machine with more logical CPUs has its surplus reported and +/// left parked (see acpi `cpusDropped`). +pub const max_cpus = 128; + +/// Maximum tasks (kernel threads) alive at once — the static task-table size. Each +/// online core consumes one slot for its idle task, plus task 0 on the BSP. +pub const max_tasks = 16; + +/// Each task's kernel stack (also each AP's bring-up stack), in bytes. +pub const kernel_stack_size = 16 * 1024; + +/// Each core's IST (double-fault) stack, in bytes. The BSP's is static; an AP's is +/// heap-allocated at bring-up. +pub const ist_stack_size = 16 * 1024; + +/// Scheduler tick / preemption rate, in Hz (the timer's periodic frequency). +pub const timer_hz = 1000; diff --git a/src/device/acpi.zig b/src/device/acpi.zig index 5aba781..da1a84c 100644 --- a/src/device/acpi.zig +++ b/src/device/acpi.zig @@ -16,6 +16,7 @@ const std = @import("std"); const danos = @import("danos"); +const config = @import("config"); const device = @import("device.zig"); const aml = @import("aml/aml.zig"); const DeviceTree = device.DeviceTree; @@ -116,7 +117,7 @@ pub const CpuInfo = struct { dropped: usize = 0, }; -const max_cpus = danos.max_cpus; +const max_cpus = config.max_cpus; /// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs. pub var cpu_info: CpuInfo = .{}; diff --git a/src/kernel/arch/x86_64/cpu.zig b/src/kernel/arch/x86_64/cpu.zig index a6b2950..5677c86 100644 --- a/src/kernel/arch/x86_64/cpu.zig +++ b/src/kernel/arch/x86_64/cpu.zig @@ -5,6 +5,7 @@ //! (halt, the descriptor tables, later paging), and nothing generic. const danos = @import("danos"); +const config = @import("config"); const gdt = @import("gdt.zig"); const tss = @import("tss.zig"); const idt = @import("idt.zig"); @@ -148,8 +149,8 @@ pub fn pageExecutable(virt: u64) bool { return paging.isExecutable(virt); } -/// Kernel tick rate: 1000 Hz (1 ms), the scheduler's time quantum. -pub const timer_hz = 1000; +/// Kernel tick rate (the scheduler's time quantum), from config. +pub const timer_hz = config.timer_hz; /// The ACPI PM timer, as a calibration reference (re-exported for the config). pub const PmTimer = apic.PmTimer; diff --git a/src/kernel/arch/x86_64/gdt.zig b/src/kernel/arch/x86_64/gdt.zig index 9d45819..7b24f77 100644 --- a/src/kernel/arch/x86_64/gdt.zig +++ b/src/kernel/arch/x86_64/gdt.zig @@ -9,14 +9,14 @@ //! 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. -const danos = @import("danos"); +const config = @import("config"); /// Selectors into the table (index * 8). Same on every core's GDT. pub const kernel_code = 0x08; pub const kernel_data = 0x10; pub const tss_selector = 0x18; -const max_cpus = danos.max_cpus; +const max_cpus = config.max_cpus; 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, diff --git a/src/kernel/arch/x86_64/tss.zig b/src/kernel/arch/x86_64/tss.zig index 8237f0d..076db3a 100644 --- a/src/kernel/arch/x86_64/tss.zig +++ b/src/kernel/arch/x86_64/tss.zig @@ -9,7 +9,7 @@ //! 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. -const danos = @import("danos"); +const config = @import("config"); const gdt = @import("gdt.zig"); /// x86_64 TSS. `packed` because several 64-bit fields sit at 4-byte-unaligned @@ -35,8 +35,8 @@ const Tss = packed struct { /// The IST slot (1-based, as the IDT gate encodes it) used for critical faults. pub const double_fault_ist = 1; -const max_cpus = danos.max_cpus; -pub const ist_stack_size = 16 * 1024; +const max_cpus = config.max_cpus; +pub const ist_stack_size = config.ist_stack_size; /// One TSS per core (small — kept static). The IST stacks are 16 KiB each, so only /// the **BSP's** is static: it must exist before the frame allocator does, to catch a diff --git a/src/kernel/main.zig b/src/kernel/main.zig index babd1b7..ea284d3 100644 --- a/src/kernel/main.zig +++ b/src/kernel/main.zig @@ -1,5 +1,6 @@ const std = @import("std"); const danos = @import("danos"); +const config = @import("config"); const arch = @import("arch"); const console = @import("console.zig"); const log = @import("log.zig"); @@ -278,7 +279,7 @@ fn bringUpSecondaries() void { log.print("\ndanos: bringing up {d} application processor(s)\n", .{cores.len - 1}); const max_wake_attempts = 3; // a core that misses the first INIT-SIPI-SIPI gets retried for (cores[1..], 1..) |core, index| { - const stack = heap.allocator().alloc(u8, 16 * 1024) catch { + const stack = heap.allocator().alloc(u8, config.kernel_stack_size) catch { log.print(" cpu apic_id {d}: no stack; skipped\n", .{core.apic_id}); continue; }; diff --git a/src/kernel/scheduler.zig b/src/kernel/scheduler.zig index 2aab53d..aa03dbd 100644 --- a/src/kernel/scheduler.zig +++ b/src/kernel/scheduler.zig @@ -18,7 +18,7 @@ //! shared queues. const std = @import("std"); -const danos = @import("danos"); +const config = @import("config"); const arch = @import("arch"); const heap = @import("heap.zig"); const sync = @import("sync.zig"); @@ -27,8 +27,8 @@ const sync = @import("sync.zig"); pub const Priority = u3; const num_priorities = 8; -const stack_size = 16 * 1024; // each task's kernel stack is 16 KiB -const max_tasks = 16; // the maximum number of tasks alive at once is 16 in a static sized pool +const stack_size = config.kernel_stack_size; // each task's kernel stack +const max_tasks = config.max_tasks; // maximum tasks alive at once (static pool) const State = enum { free, ready, running, blocked }; @@ -69,7 +69,7 @@ pub const PerCpu = struct { pinned_bitmap: u8 = 0, }; -const max_cpus = danos.max_cpus; +const max_cpus = config.max_cpus; var cpus = [_]PerCpu{.{}} ** max_cpus; /// This core's per-CPU state, via the arch layer's GS-base pointer. Valid only once diff --git a/src/root.zig b/src/root.zig index a1d7b93..25924e0 100644 --- a/src/root.zig +++ b/src/root.zig @@ -45,14 +45,6 @@ pub const Framebuffer = extern struct { /// targets so far. 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 /// 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