summaryrefslogtreecommitdiffstats
path: root/docs/adr/0003-preemptive-kernel-tasks.md
blob: ac3802763881a348580f610f1eec5e65eab172bf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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 <name>`,
  `jobs`, `sleep <ms>`, `kill <id>`, `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: <msg>`, 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 <name>` spawns a builtin (`spinner`,
  `ticker`, `counter`, plus `boom` which panics on purpose), `jobs`
  lists id/name/state, `sleep <ms>` blocks the shell task (background
  tasks keep running — the visible proof of preemption), `kill <id>`
  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.