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