mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-25 16:52:29 +00:00
feat(sched): Weave tiered scheduler with real kernel-thread context switching
Replace the state-relabel-only round-robin with the SUB-OS "Weave" scheduler backed by a genuine callee-saved context switch. Threads now actually run on their own kernel stacks instead of merely having their state field rewritten. What was implemented: - sub_ctx_switch(): per-arch assembly (arch/x86_64/cpu/switch.asm, arch/aarch64/cpu/switch.S, arch/armv8i/cpu/switch.S) that saves/restores the callee-saved register set (+ flags on x86_64) on each thread's own stack, plus a first-run trampoline (sub_thread_trampoline) that launches a freshly forged thread frame with entry/arg in callee-saved registers. - task_forge_frame(): builds the initial saved-context frame per arch so the very first switch into a new thread lands on the trampoline; task_t gains a pointer-sized ctx_sp so the same struct works on 64- and 32-bit. - Weave policy (kernel/sched.c): WEAVE_NLANES priority lanes, always dispatch the lowest non-empty lane round-robin; a task that yields voluntarily is woven UP a lane (interactive), one preempted after burning its quantum is woven DOWN (CPU-bound). Cooperative switching (sched_yield / task_exit) is always live; timer-driven preemption is gated behind sched_set_preempt() and left disarmed for now. - task_exit() now switches to the next runnable thread via sched_exit_current() instead of spinning/halting. - Boot-time weave_selftest() (x86_64) and sched_dump() diagnostics. How it was verified: - Builds clean for all three targets: x86_64 (CROSS_COMPILE=x86_64-linux-gnu-, RUSTC="rustc --target x86_64-unknown-none"), aarch64, armv8i. No new warnings; the new x86_64 asm carries a .note.GNU-stack section. - Booted x86_64 under QEMU (TCG): the self-test runs three cooperative kernel threads to completion on separate stacks with the expected 1->2->3 round-robin interleave, reports "20 real context switches, 3/3 workers exited", returns cleanly to the boot thread, and boot proceeds normally to ring-3 init and the shell. No fault/panic. Left unverified: - The aarch64/armv8i sub_ctx_switch frames build and link but are not yet boot-tested on their QEMU targets (tracked in docs/AUTODEV_ROADMAP.md). - Timer preemption path is implemented but disarmed; exited kernel threads currently leak their stack (reaper is next in the backlog). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Jk6K69yGS7zYgvoWd6nJxQ
This commit is contained in:
10 files changed
+570
-58
No files matched your search
@@ -248,6 +248,14 @@ static void subos_modular_core_boot(void) {
|
||||
ktest_register_builtin_suites();
|
||||
printk(ANSI_BRIGHT_GREEN "OK\n" ANSI_RESET);
|
||||
|
||||
#if defined(__x86_64__)
|
||||
/* Prove real kernel-thread context switching before hardware interrupts
|
||||
* are enabled: the Weave scheduler drives a batch of cooperative threads
|
||||
* to completion on their own stacks and returns cleanly to this boot
|
||||
* thread. Interrupts are still masked here, so the run is deterministic. */
|
||||
weave_selftest();
|
||||
#endif
|
||||
|
||||
// Hardware Interrupts
|
||||
printk(KERN_INFO "[14/14] Enabling Hardware Interrupts... ");
|
||||
arch_enable_interrupts();
|
||||
|
||||
+241
-39
@@ -1,67 +1,269 @@
|
||||
#include <kernel/sched.h>
|
||||
#include <kernel/task.h>
|
||||
#include <kernel/sync.h>
|
||||
#include <arch/x86_64/pit.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <arch/arch.h>
|
||||
|
||||
static struct list_head ready_queue;
|
||||
static spinlock_t sched_lock = SPINLOCK_INIT;
|
||||
/*
|
||||
* SUB-OS "Weave" scheduler
|
||||
* ========================
|
||||
* See include/kernel/sched.h for the design overview. In short: WEAVE_NLANES
|
||||
* priority lanes, always dispatch the lowest non-empty lane round-robin, and
|
||||
* let a task's lane float based on how it gives up the CPU -- volunteers are
|
||||
* woven up (toward lower latency), quantum-burners are woven down.
|
||||
*
|
||||
* The switch itself is real: sub_ctx_switch() saves and restores callee-saved
|
||||
* state on each thread's own kernel stack. weave_current always names the
|
||||
* thread physically executing; the invariant is maintained by having the
|
||||
* switcher publish the target as current *before* the register swap.
|
||||
*/
|
||||
|
||||
extern void task_init_idle(void);
|
||||
extern void sub_ctx_switch(void** save_sp, void* load_sp);
|
||||
|
||||
/* Quantum (in ticks) granted when a task (re)enters each lane. Lower lanes are
|
||||
* for interactive work: short quanta, quick rotation. Higher lanes are for
|
||||
* CPU-bound work: longer quanta, fewer switches. */
|
||||
static const uint32_t weave_quantum[WEAVE_NLANES] = { 2, 4, 8, 16 };
|
||||
|
||||
static struct list_head weave_lane[WEAVE_NLANES];
|
||||
static task_t* weave_current = NULL;
|
||||
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 */
|
||||
|
||||
/* ---- lane primitives (call with weave_lock held) ------------------------ */
|
||||
|
||||
static void weave_enqueue(task_t* t) {
|
||||
if (t->weave_lane >= WEAVE_NLANES)
|
||||
t->weave_lane = WEAVE_NLANES - 1;
|
||||
list_add_tail(&t->list, &weave_lane[t->weave_lane]);
|
||||
weave_nready++;
|
||||
}
|
||||
|
||||
static task_t* weave_pick(void) {
|
||||
for (int l = 0; l < WEAVE_NLANES; l++) {
|
||||
if (!list_empty(&weave_lane[l]))
|
||||
return list_entry(weave_lane[l].next, task_t, list);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Publish `next` as the running thread and swap register/stack context.
|
||||
* Returns (into the *previous* thread) only when it is later switched back in.
|
||||
* Must be called WITHOUT weave_lock held -- the switched-in thread will resume
|
||||
* wherever it last called into the switch and expects the lock free. */
|
||||
static void weave_switch_to(task_t* next) {
|
||||
task_t* prev = weave_current;
|
||||
if (next == prev)
|
||||
return;
|
||||
|
||||
next->state = TASK_STATE_RUNNING;
|
||||
next->sched_runs++;
|
||||
weave_switches++;
|
||||
weave_current = next;
|
||||
task_set_current(next);
|
||||
|
||||
sub_ctx_switch(&prev->ctx_sp, next->ctx_sp);
|
||||
/* control resumes here as `prev` on some later switch-in */
|
||||
}
|
||||
|
||||
/* ---- public API --------------------------------------------------------- */
|
||||
|
||||
void sched_init(void) {
|
||||
INIT_LIST_HEAD(&ready_queue);
|
||||
task_init_idle();
|
||||
for (int l = 0; l < WEAVE_NLANES; l++)
|
||||
INIT_LIST_HEAD(&weave_lane[l]);
|
||||
weave_nready = 0;
|
||||
weave_switches = 0;
|
||||
weave_preempt = false;
|
||||
|
||||
task_init_idle(); /* sets current_task = swapper/0 */
|
||||
weave_current = task_current();
|
||||
weave_current->weave_lane = 0;
|
||||
weave_current->weave_credit = weave_quantum[0];
|
||||
weave_current->weave_on = 0; /* idle isn't part of the rotation yet */
|
||||
}
|
||||
|
||||
void sched_add_task(task_t* task) {
|
||||
if (!task) return;
|
||||
spin_lock(&sched_lock);
|
||||
list_add_tail(&task->list, &ready_queue);
|
||||
spin_unlock(&sched_lock);
|
||||
spin_lock(&weave_lock);
|
||||
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);
|
||||
}
|
||||
|
||||
/* A node is currently threaded into a lane iff its links are non-NULL and do
|
||||
* not point back at itself (list_del() NULLs them; INIT_LIST_HEAD() self-links
|
||||
* an idle node). Only such a node may be unlinked. */
|
||||
static bool weave_linked(task_t* t) {
|
||||
return t->list.next != NULL && t->list.next != &t->list;
|
||||
}
|
||||
|
||||
void sched_remove_task(task_t* task) {
|
||||
if (!task) return;
|
||||
spin_lock(&sched_lock);
|
||||
list_del(&task->list);
|
||||
spin_unlock(&sched_lock);
|
||||
spin_lock(&weave_lock);
|
||||
if (weave_linked(task)) {
|
||||
list_del(&task->list);
|
||||
if (weave_nready) weave_nready--;
|
||||
}
|
||||
task->weave_on = 0;
|
||||
spin_unlock(&weave_lock);
|
||||
}
|
||||
|
||||
void sched_join(void) {
|
||||
if (weave_current) weave_current->weave_on = 1;
|
||||
}
|
||||
|
||||
void sched_leave(void) {
|
||||
if (weave_current) weave_current->weave_on = 0;
|
||||
}
|
||||
|
||||
void sched_set_preempt(bool armed) {
|
||||
weave_preempt = armed;
|
||||
}
|
||||
|
||||
/*
|
||||
* Common rotation core. `voluntary` distinguishes a cooperative yield (the
|
||||
* caller is rewarded, woven up a lane) from an involuntary/preemptive switch
|
||||
* (the caller is woven down). Picks the next thread, requeues the caller if it
|
||||
* is still a participant, and performs the switch.
|
||||
*/
|
||||
static void weave_rotate(bool voluntary) {
|
||||
spin_lock(&weave_lock);
|
||||
|
||||
task_t* cur = weave_current;
|
||||
task_t* next = weave_pick();
|
||||
if (!next) {
|
||||
spin_unlock(&weave_lock);
|
||||
return; /* nobody else runnable: keep going */
|
||||
}
|
||||
list_del(&next->list);
|
||||
if (weave_nready) weave_nready--;
|
||||
|
||||
if (cur && cur->weave_on &&
|
||||
(cur->state == TASK_STATE_RUNNING || cur->state == TASK_STATE_READY)) {
|
||||
if (voluntary) {
|
||||
if (cur->weave_lane > 0) cur->weave_lane--; /* weave up */
|
||||
} else {
|
||||
if (cur->weave_lane < WEAVE_NLANES - 1) cur->weave_lane++; /* down */
|
||||
}
|
||||
cur->weave_credit = weave_quantum[cur->weave_lane];
|
||||
cur->state = TASK_STATE_READY;
|
||||
weave_enqueue(cur);
|
||||
}
|
||||
|
||||
next->weave_credit = weave_quantum[next->weave_lane];
|
||||
spin_unlock(&weave_lock);
|
||||
|
||||
weave_switch_to(next);
|
||||
}
|
||||
|
||||
void sched_yield(void) {
|
||||
// Cooperative yield
|
||||
sched_schedule();
|
||||
}
|
||||
|
||||
void sched_tick(void) {
|
||||
// Timer tick scheduler hook
|
||||
task_t* curr = task_current();
|
||||
if (curr && curr->pid != 0) {
|
||||
if (--curr->time_slice <= 0) {
|
||||
curr->time_slice = 10;
|
||||
sched_schedule();
|
||||
}
|
||||
}
|
||||
weave_rotate(true);
|
||||
}
|
||||
|
||||
void sched_schedule(void) {
|
||||
spin_lock(&sched_lock);
|
||||
weave_rotate(false);
|
||||
}
|
||||
|
||||
if (list_empty(&ready_queue)) {
|
||||
spin_unlock(&sched_lock);
|
||||
void sched_tick(void) {
|
||||
task_t* cur = weave_current;
|
||||
if (!cur || cur->pid == 0)
|
||||
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 */
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
spin_lock(&weave_lock);
|
||||
task_t* next = weave_pick();
|
||||
if (next) {
|
||||
list_del(&next->list);
|
||||
if (weave_nready) weave_nready--;
|
||||
}
|
||||
spin_unlock(&weave_lock);
|
||||
|
||||
if (!next)
|
||||
return;
|
||||
|
||||
void* graveyard; /* dead context's SP goes nowhere */
|
||||
next->state = TASK_STATE_RUNNING;
|
||||
next->sched_runs++;
|
||||
next->weave_credit = weave_quantum[next->weave_lane];
|
||||
weave_switches++;
|
||||
weave_current = next;
|
||||
task_set_current(next);
|
||||
sub_ctx_switch(&graveyard, next->ctx_sp);
|
||||
/* unreachable */
|
||||
}
|
||||
|
||||
/* ---- diagnostics -------------------------------------------------------- */
|
||||
|
||||
void sched_dump(void) {
|
||||
spin_lock(&weave_lock);
|
||||
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 : "?",
|
||||
weave_current ? (int)weave_current->pid : -1,
|
||||
weave_current ? (unsigned)weave_current->weave_lane : 0,
|
||||
(unsigned)weave_switches, (unsigned)weave_nready);
|
||||
for (int l = 0; l < WEAVE_NLANES; l++) {
|
||||
printk(" lane %d:", l);
|
||||
task_t* pos;
|
||||
int n = 0;
|
||||
list_for_each_entry(pos, &weave_lane[l], list) {
|
||||
printk(" %s", pos->name);
|
||||
n++;
|
||||
}
|
||||
if (!n) printk(" (empty)");
|
||||
printk("\n");
|
||||
}
|
||||
spin_unlock(&weave_lock);
|
||||
}
|
||||
|
||||
task_t* next = list_entry(ready_queue.next, task_t, list);
|
||||
list_del(&next->list);
|
||||
list_add_tail(&next->list, &ready_queue); // Move to back of round-robin queue
|
||||
/* ---- boot-time context-switch self-test --------------------------------- */
|
||||
|
||||
task_t* curr = task_current();
|
||||
if (curr != next) {
|
||||
// Context switch logic
|
||||
next->state = TASK_STATE_RUNNING;
|
||||
if (curr->state == TASK_STATE_RUNNING) {
|
||||
curr->state = TASK_STATE_READY;
|
||||
}
|
||||
static volatile int weave_test_exits = 0;
|
||||
|
||||
static void weave_worker(void* arg) {
|
||||
long id = (long)(uintptr_t)arg;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
task_t* self = task_current();
|
||||
printk(" [weave] worker %d slice %d (lane %u, runs %u)\n",
|
||||
(int)id, i, (unsigned)self->weave_lane, (unsigned)self->sched_runs);
|
||||
sched_yield(); /* cooperatively hand off the CPU */
|
||||
}
|
||||
printk(" [weave] worker %d done\n", (int)id);
|
||||
weave_test_exits++;
|
||||
/* fall off the end -> trampoline -> task_exit -> switch away, never return */
|
||||
}
|
||||
|
||||
void weave_selftest(void) {
|
||||
printk(ANSI_BRIGHT_CYAN
|
||||
"WEAVE: cooperative context-switch self-test\n" ANSI_RESET);
|
||||
|
||||
uint64_t before = weave_switches;
|
||||
weave_test_exits = 0;
|
||||
|
||||
task_create("weave-a", weave_worker, (void*)(uintptr_t)1, 0);
|
||||
task_create("weave-b", weave_worker, (void*)(uintptr_t)2, 0);
|
||||
task_create("weave-c", weave_worker, (void*)(uintptr_t)3, 0);
|
||||
|
||||
/* Join the rotation so exiting workers can switch back to us, then drive
|
||||
* the whole batch to completion by repeatedly yielding. */
|
||||
sched_join();
|
||||
while (weave_test_exits < 3)
|
||||
sched_yield();
|
||||
sched_leave();
|
||||
|
||||
spin_unlock(&sched_lock);
|
||||
printk(ANSI_BRIGHT_GREEN
|
||||
"WEAVE: self-test OK -- %u real context switches, 3/3 workers exited\n"
|
||||
ANSI_RESET, (unsigned)(weave_switches - before));
|
||||
}
|
||||
+57
-15
@@ -10,11 +10,56 @@ static task_t* current_task = NULL;
|
||||
static task_t kernel_idle_task;
|
||||
static LIST_HEAD(global_tasks);
|
||||
|
||||
static void task_wrapper(task_entry_fn_t entry, void* arg) {
|
||||
if (entry) {
|
||||
entry(arg);
|
||||
}
|
||||
task_exit(0);
|
||||
/* Per-arch context switch primitives (see arch/<arch>/cpu/switch.*). */
|
||||
extern void sub_ctx_switch(void** save_sp, void* load_sp);
|
||||
extern void sub_thread_trampoline(void);
|
||||
|
||||
/* Retire the running thread and switch to the next runnable one (sched.c). */
|
||||
extern void sched_exit_current(void);
|
||||
|
||||
/*
|
||||
* Forge the initial saved-context frame for a brand-new thread so that the
|
||||
* first switch into it lands on sub_thread_trampoline with the entry function
|
||||
* and its argument sitting in the callee-saved registers the trampoline reads.
|
||||
* The exact word layout mirrors the pop/ldp/pop sequence in each arch's
|
||||
* sub_ctx_switch, so the two must be kept in lockstep.
|
||||
*/
|
||||
static void* task_forge_frame(uint64_t stack_base, size_t stack_size,
|
||||
task_entry_fn_t entry, void* arg) {
|
||||
#if defined(__x86_64__)
|
||||
uint64_t top = (stack_base + stack_size) & ~0xFULL; /* 16-byte aligned */
|
||||
uint64_t* sp = (uint64_t*)top;
|
||||
*(--sp) = (uint64_t)&sub_thread_trampoline; /* ret target */
|
||||
*(--sp) = 0; /* rbp */
|
||||
*(--sp) = 0; /* rbx */
|
||||
*(--sp) = (uint64_t)entry; /* r12 -> entry */
|
||||
*(--sp) = (uint64_t)arg; /* r13 -> arg */
|
||||
*(--sp) = 0; /* r14 */
|
||||
*(--sp) = 0; /* r15 */
|
||||
*(--sp) = 0x2; /* rflags, IF clear */
|
||||
return (void*)sp;
|
||||
#elif defined(__aarch64__) || defined(__arm64__)
|
||||
uint64_t top = (stack_base + stack_size) & ~0xFULL;
|
||||
uint64_t* sp = (uint64_t*)top;
|
||||
sp -= 12; /* x19..x30 */
|
||||
sp[0] = (uint64_t)entry; /* x19 -> entry */
|
||||
sp[1] = (uint64_t)arg; /* x20 -> arg */
|
||||
for (int i = 2; i <= 10; i++) sp[i] = 0; /* x21..x28, x29(fp) */
|
||||
sp[11] = (uint64_t)&sub_thread_trampoline; /* x30(lr) -> trampoline */
|
||||
return (void*)sp;
|
||||
#elif defined(__arm__)
|
||||
uint32_t top = (uint32_t)((stack_base + stack_size) & ~0x7ULL);
|
||||
uint32_t* sp = (uint32_t*)top;
|
||||
sp -= 9; /* r4..r11, lr */
|
||||
sp[0] = (uint32_t)(uintptr_t)entry; /* r4 -> entry */
|
||||
sp[1] = (uint32_t)(uintptr_t)arg; /* r5 -> arg */
|
||||
for (int i = 2; i <= 7; i++) sp[i] = 0; /* r6..r11 */
|
||||
sp[8] = (uint32_t)(uintptr_t)&sub_thread_trampoline; /* lr */
|
||||
return (void*)sp;
|
||||
#else
|
||||
(void)stack_base; (void)stack_size; (void)entry; (void)arg;
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
task_t* task_create(const char* name, task_entry_fn_t entry, void* arg, int priority) {
|
||||
@@ -37,14 +82,10 @@ task_t* task_create(const char* name, task_entry_fn_t entry, void* arg, int prio
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint64_t* stack_top = (uint64_t*)(task->stack_base + task->stack_size);
|
||||
|
||||
// Set up initial stack frame for AMD64
|
||||
*(--stack_top) = (uint64_t)arg; // RDI (arg)
|
||||
*(--stack_top) = (uint64_t)entry; // RSI (entry function)
|
||||
*(--stack_top) = (uint64_t)task_wrapper; // RIP
|
||||
|
||||
task->rsp = (uint64_t)stack_top;
|
||||
// Forge the initial saved-register frame so the first context switch into
|
||||
// this thread lands on the trampoline with entry/arg in place.
|
||||
task->ctx_sp = task_forge_frame(task->stack_base, task->stack_size, entry, arg);
|
||||
task->rsp = (uint64_t)(uintptr_t)task->ctx_sp;
|
||||
|
||||
INIT_LIST_HEAD(&task->list);
|
||||
INIT_LIST_HEAD(&task->all_list);
|
||||
@@ -59,8 +100,9 @@ void task_exit(int exit_code) {
|
||||
if (current_task && current_task->pid != 0) {
|
||||
current_task->exit_code = exit_code;
|
||||
current_task->state = TASK_STATE_DEAD;
|
||||
sched_remove_task(current_task);
|
||||
sched_yield();
|
||||
current_task->weave_on = 0;
|
||||
sched_exit_current(); // switches to the next runnable thread; only
|
||||
// falls through here if nothing else is runnable
|
||||
}
|
||||
while (1) {
|
||||
arch_halt();
|
||||
|
||||
Reference in new issue
Block a user