diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 16:19:45 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 16:19:45 +0200 |
| commit | ce43cb9554d443f12af886ee035a73bf9b322ac5 (patch) | |
| tree | 4611a08b2596ab49c25793f20e126b66fea26169 /docs | |
| parent | rsnix: formatting (diff) | |
| download | rs-dos-ce43cb9554d443f12af886ee035a73bf9b322ac5.tar.gz rs-dos-ce43cb9554d443f12af886ee035a73bf9b322ac5.zip | |
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
Diffstat (limited to '')
| -rw-r--r-- | docs/adr/0003-preemptive-kernel-tasks.md | 97 |
1 files changed, 97 insertions, 0 deletions
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 @@ | |||
| 1 | # ADR 0003 — Preemptive Kernel Tasks | ||
| 2 | |||
| 3 | ## Status | ||
| 4 | |||
| 5 | Accepted. | ||
| 6 | |||
| 7 | ## Context | ||
| 8 | |||
| 9 | The kernel is single-threaded and polling-based: a shell loop drives | ||
| 10 | everything, drivers poll (AHCI has no interrupts), and the only | ||
| 11 | preemption source is a 100 Hz PIT tick whose ISR currently just counts | ||
| 12 | and acknowledges. The interrupt path already saves a complete register | ||
| 13 | frame (`isr_common` pushes all GPRs + the iretq frame onto the | ||
| 14 | interrupted stack), so switching tasks is "return a different stack | ||
| 15 | pointer from the ISR dispatch" — a small asm change, not new machinery. | ||
| 16 | |||
| 17 | The destination is a windowed GUI running multiple applications at | ||
| 18 | once, which requires concurrent execution and, eventually, per-task | ||
| 19 | input/output channels. The immediate goal is the foundation: a | ||
| 20 | preemptive multitasking core that the GUI will be built on. | ||
| 21 | |||
| 22 | Two constraints shape the design. First, all kernel driver state is | ||
| 23 | single-user by construction: one shared AHCI DMA buffer plus a global | ||
| 24 | "active disk" selector, a per-volume cached FAT with a global active | ||
| 25 | volume, the VFS node table, and the shell's tty/vt terminal. Two tasks | ||
| 26 | doing disk or filesystem work concurrently would corrupt each other. | ||
| 27 | Second, there is no address-space separation and no user/kernel | ||
| 28 | boundary — the kernel is one identity-mapped space (TempleOS-style), so | ||
| 29 | "tasks" (not processes) sharing every kernel facility is the honest | ||
| 30 | model. | ||
| 31 | |||
| 32 | ## Decision | ||
| 33 | |||
| 34 | Preemptive, round-robin **kernel tasks** in the one shared address | ||
| 35 | space, preempted on the 100 Hz PIT tick: | ||
| 36 | |||
| 37 | - **Scheduler** — a fixed 16-slot static task table (the timer ISR | ||
| 38 | never allocates), round-robin across Ready tasks, one class, 10 ms | ||
| 39 | quantum. States: `ready`, `sleeping` (wake-at timestamp from | ||
| 40 | `uptime_ms`), `blocked`, `done`. The shell is task 0; a kernel idle | ||
| 41 | task (`hlt` loop) is the always-ready fallback. Reaping is lazy: | ||
| 42 | Done tasks' stacks are freed and slots reused at the next `spawn` (and | ||
| 43 | `jobs` sweeps). | ||
| 44 | - **Context switch** — the timer ISR saves the running task's frame on | ||
| 45 | its own stack, stores the frame pointer in the task's slot, picks the | ||
| 46 | next Ready task, and returns its frame pointer; the asm moves `rsp` | ||
| 47 | there and iretq's. Yield and sleep use a software interrupt (vector | ||
| 48 | 0x40) through the same machinery. | ||
| 49 | - **Stacks** — each task gets 8 KiB from the page allocator plus one | ||
| 50 | **guard page** mapped below it (the paging code already maps/unmaps). | ||
| 51 | A new page-fault handler (vector 14) reads CR2; a fault inside a | ||
| 52 | task's guard page is a "stack overflow" task panic, any other fault | ||
| 53 | halts the kernel with the address. | ||
| 54 | - **Spawn surface** — `sched::spawn(entry, name)` is general (any plain | ||
| 55 | `fn`), but the shell only offers safe builtins: `bg <name>`, | ||
| 56 | `jobs`, `sleep <ms>`, `kill <id>`, `demo`. First-slice tasks are | ||
| 57 | headless: they may sleep, yield, and log to serial/klog — nothing | ||
| 58 | else. Invariant: **only the shell task touches the disk, | ||
| 59 | filesystem, and terminal**; the scheduler does not enforce this (a | ||
| 60 | general `spawn` cannot), it is a documented boundary. | ||
| 61 | - **Line atomicity** — a scheduler lock (preempt-disable counter, "no | ||
| 62 | switching while held") is held by `klog::append_fmt` and | ||
| 63 | `serial::write_str` so lines from different tasks never interleave | ||
| 64 | mid-line. The timer ISR skips switching while the lock is held. | ||
| 65 | - **Graceful task panic** — the panic handler becomes task-aware: if | ||
| 66 | the scheduler is live and the panic is in a task (id ≥ 1), it logs | ||
| 67 | `sched: task N "name" panicked: <msg>`, marks the task Done, clears | ||
| 68 | the scheduler lock, and switches away via vector 0x40 — the shell | ||
| 69 | keeps running and `jobs` shows the corpse. Panics in ISR context, | ||
| 70 | before the scheduler exists, or in the shell task itself still halt. | ||
| 71 | - **Shell surface** — `bg <name>` spawns a builtin (`spinner`, | ||
| 72 | `ticker`, `counter`, plus `boom` which panics on purpose), `jobs` | ||
| 73 | lists id/name/state, `sleep <ms>` blocks the shell task (background | ||
| 74 | tasks keep running — the visible proof of preemption), `kill <id>` | ||
| 75 | marks a task Done, `demo` spawns a showcase set at different sleep | ||
| 76 | rates. | ||
| 77 | |||
| 78 | Explicitly **not** in this ADR: backgrounding arbitrary shell commands | ||
| 79 | (`cmd &`), per-task I/O channels, task isolation, priorities, and the | ||
| 80 | GUI itself. The scheduler's blocking/wake primitives are designed so | ||
| 81 | the GUI's per-task windows and input queues can be built on them later. | ||
| 82 | |||
| 83 | ## Consequences | ||
| 84 | |||
| 85 | - Concurrent disk/fs/tty/vt access remains unsupported: any second task | ||
| 86 | doing I/O needs per-task DMA buffers and real mutexes, which is | ||
| 87 | future work (the GUI-era step). | ||
| 88 | - Task stacks overflow into a clean, labeled task death instead of | ||
| 89 | silent corruption; the glossary's "no guard pages" claim now reads | ||
| 90 | "except task stacks". | ||
| 91 | - A buggy background task cannot take the kernel down (panic or stack | ||
| 92 | overflow), which the GUI will depend on; a buggy shell still can. | ||
| 93 | - The serial log becomes interleaved across tasks — expected, and the | ||
| 94 | scheduler lock keeps it line-clean. | ||
| 95 | - klog timestamps make task activity (`[ssss.mmm]`) traceable alongside | ||
| 96 | boot messages; rescan events, panic messages, and task logs all share | ||
| 97 | the ring. | ||
