From ce43cb9554d443f12af886ee035a73bf9b322ac5 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 16:19:45 +0200 Subject: docs: ADR 0003 (preemptive kernel tasks) + task glossary terms - ADR 0003: preemptive round-robin kernel tasks on the 100 Hz tick, shared address space, guard pages + page-fault handler, graceful task panics, safe-builtin spawn surface, only-shell-touches-I/O invariant - CONTEXT.md: new 'Task' glossary entry; 'Paging strategy' now says guard pages exist on task stacks --- CONTEXT.md | 17 +++++- docs/adr/0003-preemptive-kernel-tasks.md | 97 ++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0003-preemptive-kernel-tasks.md diff --git a/CONTEXT.md b/CONTEXT.md index aa2392e..65475ce 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -32,7 +32,9 @@ structs are hand-defined (not from `uefi-rs` crate) to stay lightweight. Long mode mandates paging (`CR0.PG = 1`), so we accept that tax but keep it simple. Identity-mapped 4-level tables, no higher-half, no demand paging, -no guard pages. `paging.rs` provides identity map/unmap/query helpers. At +no guard pages except task stacks — each task's stack gets one unmapped +guard page below it, and a page-fault handler turns an overflow into a +task panic. `paging.rs` provides identity map/unmap/query helpers. At boot the kernel runs on UEFI's tables; `adopt_tables()` rebuilds that map into the kernel's own tables and switches CR3 — needed because UEFI maps its page-table pages read-only, so the firmware tree can't be modified in place. @@ -80,6 +82,19 @@ linear frame buffer is system memory that we own once boot services are gone. Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table. Other layouts can be added later by swapping the translation table. +## Task + +A unit of concurrent execution in the kernel: its own kernel stack +(with a guard page below it), scheduler bookkeeping, and no +address-space separation — all tasks share the one identity-mapped +space and the kernel's facilities (TempleOS-style, not a process). +Tasks are preemptively round-robin-scheduled on the 100 Hz PIT tick; +the shell is task 0 and an idle `hlt` task is the always-ready +fallback. A task may sleep, yield, and log to serial/klog; only the +shell touches the disk, filesystem, and terminal. A panicking or +stack-overflowing task dies gracefully (logged, marked done, slot +reaped) without taking the kernel down. + ## Development loop Standalone project — no integration with the RNG initramfs build system. diff --git a/docs/adr/0003-preemptive-kernel-tasks.md b/docs/adr/0003-preemptive-kernel-tasks.md new file mode 100644 index 0000000..ac38027 --- /dev/null +++ b/docs/adr/0003-preemptive-kernel-tasks.md @@ -0,0 +1,97 @@ +# ADR 0003 — Preemptive Kernel Tasks + +## Status + +Accepted. + +## Context + +The kernel is single-threaded and polling-based: a shell loop drives +everything, drivers poll (AHCI has no interrupts), and the only +preemption source is a 100 Hz PIT tick whose ISR currently just counts +and acknowledges. The interrupt path already saves a complete register +frame (`isr_common` pushes all GPRs + the iretq frame onto the +interrupted stack), so switching tasks is "return a different stack +pointer from the ISR dispatch" — a small asm change, not new machinery. + +The destination is a windowed GUI running multiple applications at +once, which requires concurrent execution and, eventually, per-task +input/output channels. The immediate goal is the foundation: a +preemptive multitasking core that the GUI will be built on. + +Two constraints shape the design. First, all kernel driver state is +single-user by construction: one shared AHCI DMA buffer plus a global +"active disk" selector, a per-volume cached FAT with a global active +volume, the VFS node table, and the shell's tty/vt terminal. Two tasks +doing disk or filesystem work concurrently would corrupt each other. +Second, there is no address-space separation and no user/kernel +boundary — the kernel is one identity-mapped space (TempleOS-style), so +"tasks" (not processes) sharing every kernel facility is the honest +model. + +## Decision + +Preemptive, round-robin **kernel tasks** in the one shared address +space, preempted on the 100 Hz PIT tick: + +- **Scheduler** — a fixed 16-slot static task table (the timer ISR + never allocates), round-robin across Ready tasks, one class, 10 ms + quantum. States: `ready`, `sleeping` (wake-at timestamp from + `uptime_ms`), `blocked`, `done`. The shell is task 0; a kernel idle + task (`hlt` loop) is the always-ready fallback. Reaping is lazy: + Done tasks' stacks are freed and slots reused at the next `spawn` (and + `jobs` sweeps). +- **Context switch** — the timer ISR saves the running task's frame on + its own stack, stores the frame pointer in the task's slot, picks the + next Ready task, and returns its frame pointer; the asm moves `rsp` + there and iretq's. Yield and sleep use a software interrupt (vector + 0x40) through the same machinery. +- **Stacks** — each task gets 8 KiB from the page allocator plus one + **guard page** mapped below it (the paging code already maps/unmaps). + A new page-fault handler (vector 14) reads CR2; a fault inside a + task's guard page is a "stack overflow" task panic, any other fault + halts the kernel with the address. +- **Spawn surface** — `sched::spawn(entry, name)` is general (any plain + `fn`), but the shell only offers safe builtins: `bg `, + `jobs`, `sleep `, `kill `, `demo`. First-slice tasks are + headless: they may sleep, yield, and log to serial/klog — nothing + else. Invariant: **only the shell task touches the disk, + filesystem, and terminal**; the scheduler does not enforce this (a + general `spawn` cannot), it is a documented boundary. +- **Line atomicity** — a scheduler lock (preempt-disable counter, "no + switching while held") is held by `klog::append_fmt` and + `serial::write_str` so lines from different tasks never interleave + mid-line. The timer ISR skips switching while the lock is held. +- **Graceful task panic** — the panic handler becomes task-aware: if + the scheduler is live and the panic is in a task (id ≥ 1), it logs + `sched: task N "name" panicked: `, marks the task Done, clears + the scheduler lock, and switches away via vector 0x40 — the shell + keeps running and `jobs` shows the corpse. Panics in ISR context, + before the scheduler exists, or in the shell task itself still halt. +- **Shell surface** — `bg ` spawns a builtin (`spinner`, + `ticker`, `counter`, plus `boom` which panics on purpose), `jobs` + lists id/name/state, `sleep ` blocks the shell task (background + tasks keep running — the visible proof of preemption), `kill ` + marks a task Done, `demo` spawns a showcase set at different sleep + rates. + +Explicitly **not** in this ADR: backgrounding arbitrary shell commands +(`cmd &`), per-task I/O channels, task isolation, priorities, and the +GUI itself. The scheduler's blocking/wake primitives are designed so +the GUI's per-task windows and input queues can be built on them later. + +## Consequences + +- Concurrent disk/fs/tty/vt access remains unsupported: any second task + doing I/O needs per-task DMA buffers and real mutexes, which is + future work (the GUI-era step). +- Task stacks overflow into a clean, labeled task death instead of + silent corruption; the glossary's "no guard pages" claim now reads + "except task stacks". +- A buggy background task cannot take the kernel down (panic or stack + overflow), which the GUI will depend on; a buggy shell still can. +- The serial log becomes interleaved across tasks — expected, and the + scheduler lock keeps it line-clean. +- klog timestamps make task activity (`[ssss.mmm]`) traceable alongside + boot messages; rescan events, panic messages, and task logs all share + the ring. -- cgit v1.2.3