The convention that tunables live in system/parameters.zig with their reasoning attached predates this and got 2% compliance — 5 of 235. A convention with no teeth is how a bare `const maximum_devices = 64` reached an AMD desktop and cost it USB and storage. This is the same rule with a gate behind it. tools/check-bounds.py finds every bound-shaped declaration — a `maximum_*` const with a literal value, or a type with a literal array length — and requires the five-field block above it: what it counts, who decides its size, what it protects, what happens at the limit, and how anyone finds out. The at-limit vocabulary is closed: refuse, degrade, truncate, grow. There is deliberately no way to spell "silent", no way to spell "drop", and nothing meaning "allow", so the behaviours that did the damage cannot be written down. Truncation is legal only carrying a marker the reader can see, which is why klog_maximum_message qualifies and a USB descriptor cut at 512 bytes does not. An array length that names a declared bound is not itself a bound; only literal lengths are flagged, which pushes ceilings toward having names. The 273 that predate the rule are allowlisted so this lands without a tree-wide sweep in front of it, and that list may only shrink: declaring a bound means deleting its line, and the check fails on a stale entry too. Nothing may be added. Wired into `zig build test` and available alone as `zig build bounds`. Not in the default build — it reads the whole tree, and a red bounds check should not stop you booting a kernel. Five are now declared rather than allowlisted. Writing them out is its own argument: maximum_devices reads "protects: nothing — this is a sizing guess about someone else's computer", and maximum_tasks now carries the fact that it has been raised twice, each time by something that outgrew it. Verified the gate refuses an undeclared bound, a declared one using forbidden vocabulary, and an allowlist entry that has since been declared. Suite 115/115.
65 lines
3.6 KiB
Zig
65 lines
3.6 KiB
Zig
//! 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 [[boot-handoff]] to what
|
|
//! it actually is — the loader↔kernel handoff *contract* — with tunables living here.
|
|
//!
|
|
//! **Kernel only, and deliberately so.** This file exists because tunables were
|
|
//! crowding the loader↔kernel contract they were split out of; it is not a registry for
|
|
//! the whole system. A driver's ring size belongs to that driver, a protocol's payload
|
|
//! cap to that protocol. How a ceiling is *declared*, wherever it lives, is
|
|
//! docs/os-development/bounds.md — a shape, not a shared list.
|
|
|
|
/// 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.
|
|
///
|
|
/// bound: logical CPUs the kernel tracks
|
|
/// decided-by: hardware
|
|
/// protects: the per-CPU bookkeeping arrays, which are sized at compile time
|
|
/// at-limit: degrade — the surplus cores are left parked, never brought online
|
|
/// observed-by: platform.cpusDropped() -> the WARNING at kernel.zig:281
|
|
pub const maximum_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. Sized
|
|
/// for the initial-ramdisk sweep (the bundled binaries spawned at once) plus the
|
|
/// device manager's supervised children with room to grow — at 16 the sweep
|
|
/// started failing spawns once the bundle passed a dozen binaries. Raised to 48
|
|
/// for the USB stack: the xHCI bus driver spawns a supervised class-driver instance
|
|
/// per matched interface (keyboard, mouse, mass storage), on top of the FAT and
|
|
/// block servers and the growing ramdisk bundle.
|
|
///
|
|
/// The history above is the argument against this number: it has been raised twice,
|
|
/// each time by a machine or a bundle that outgrew it, which is the pattern the
|
|
/// bounds rule exists to stop. It is `ours` only because the task table is static;
|
|
/// how many drivers a machine needs is decided by how much hardware it has.
|
|
///
|
|
/// bound: kernel threads alive at once — the static task-table size
|
|
/// decided-by: hardware
|
|
/// protects: the statically allocated task table
|
|
/// at-limit: refuse — spawn fails; a supervised driver is never started
|
|
/// observed-by: the spawning supervisor's own log line; see docs/bounds-track-plan.md
|
|
pub const maximum_tasks = 48;
|
|
|
|
/// Each task's kernel stack (also each AP's bring-up stack), in bytes.
|
|
pub const kernel_stack_size = 16 * 1024;
|
|
|
|
/// Each user process's stack, in pages (32 KiB). Mapped just below a fixed top;
|
|
/// the System V entry block (argc/argv) occupies the top of the highest page, and
|
|
/// the page below the mapping is left unmapped as a guard, so an overflow faults
|
|
/// (killing only that process) instead of silently corrupting the image.
|
|
pub const user_stack_pages = 8;
|
|
|
|
/// 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;
|