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 --- docs/adr/0003-preemptive-kernel-tasks.md | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/adr/0003-preemptive-kernel-tasks.md (limited to 'docs/adr') 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