# 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.