diff options
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 17 | ||||
| -rw-r--r-- | docs/adr/0003-preemptive-kernel-tasks.md | 97 |
2 files changed, 113 insertions, 1 deletions
| @@ -32,7 +32,9 @@ structs are hand-defined (not from `uefi-rs` crate) to stay lightweight. | |||
| 32 | 32 | ||
| 33 | Long mode mandates paging (`CR0.PG = 1`), so we accept that tax but keep it | 33 | Long mode mandates paging (`CR0.PG = 1`), so we accept that tax but keep it |
| 34 | simple. Identity-mapped 4-level tables, no higher-half, no demand paging, | 34 | simple. Identity-mapped 4-level tables, no higher-half, no demand paging, |
| 35 | no guard pages. `paging.rs` provides identity map/unmap/query helpers. At | 35 | no guard pages except task stacks — each task's stack gets one unmapped |
| 36 | guard page below it, and a page-fault handler turns an overflow into a | ||
| 37 | task panic. `paging.rs` provides identity map/unmap/query helpers. At | ||
| 36 | boot the kernel runs on UEFI's tables; `adopt_tables()` rebuilds that map | 38 | boot the kernel runs on UEFI's tables; `adopt_tables()` rebuilds that map |
| 37 | into the kernel's own tables and switches CR3 — needed because UEFI maps its | 39 | into the kernel's own tables and switches CR3 — needed because UEFI maps its |
| 38 | page-table pages read-only, so the firmware tree can't be modified in place. | 40 | page-table pages read-only, so the firmware tree can't be modified in place. |
| @@ -80,6 +82,19 @@ linear frame buffer is system memory that we own once boot services are gone. | |||
| 80 | Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table. | 82 | Scancode set 1, US QWERTY layout. Hardcoded scancode→ASCII table. |
| 81 | Other layouts can be added later by swapping the translation table. | 83 | Other layouts can be added later by swapping the translation table. |
| 82 | 84 | ||
| 85 | ## Task | ||
| 86 | |||
| 87 | A unit of concurrent execution in the kernel: its own kernel stack | ||
| 88 | (with a guard page below it), scheduler bookkeeping, and no | ||
| 89 | address-space separation — all tasks share the one identity-mapped | ||
| 90 | space and the kernel's facilities (TempleOS-style, not a process). | ||
| 91 | Tasks are preemptively round-robin-scheduled on the 100 Hz PIT tick; | ||
| 92 | the shell is task 0 and an idle `hlt` task is the always-ready | ||
| 93 | fallback. A task may sleep, yield, and log to serial/klog; only the | ||
| 94 | shell touches the disk, filesystem, and terminal. A panicking or | ||
| 95 | stack-overflowing task dies gracefully (logged, marked done, slot | ||
| 96 | reaped) without taking the kernel down. | ||
| 97 | |||
| 83 | ## Development loop | 98 | ## Development loop |
| 84 | 99 | ||
| 85 | Standalone project — no integration with the RNG initramfs build system. | 100 | Standalone project — no integration with the RNG initramfs build system. |
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. | ||
