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:
Claude committed 2026-08-25 08:16:37 +00:00
1 parent ecdc5be435
commit a14a7c79b0
10 files changed
+570 -58

No files matched your search

+5 -1
View File
@@ -37,6 +37,10 @@
- Once the VBE adapter is in graphics mode the VGA text buffer stops being scanned out, so the console would be invisible on the display even though it still reaches serial. fbcon renders the console into the framebuffer instead, the way Linux fbcon does.
- **VT100 subset**: SGR colours, cursor addressing, relative moves, erase-display and erase-line, save/restore — enough for full-screen curses-style programs such as the **nano** editor to render correctly.
- Presents a fixed **80x25** grid (matching `TTY_WIDTH`/`TTY_HEIGHT`, which the userland assumes) with the glyph cell scaled to fill whatever resolution is active. `video=WIDTHxHEIGHT` on the kernel command line selects the mode, defaulting to `1280x720`.
- 🧶 **Weave Scheduler & Real Context Switching (`kernel/sched.c`, `arch/*/cpu/switch.*`)**:
- **Genuine multitasking, not a state relabel**: `sub_ctx_switch()` is per-architecture assembly (x86_64/aarch64/armv8i) that saves and restores the callee-saved register set plus flags on each thread's own kernel stack, with a first-run trampoline that launches a freshly forged thread frame.
- **Tiered rotation with behavioural weaving**: runnable threads thread through `WEAVE_NLANES` priority lanes; the dispatcher always pulls the lowest non-empty lane round-robin. A thread that yields voluntarily is *woven up* toward low latency, one that burns its whole quantum and is preempted is *woven down* toward throughput — the lane floats on observed behaviour, not a static nice value.
- **Boot-time self-test**: before hardware interrupts come up, the Weave scheduler drives three cooperative kernel threads to completion on separate stacks (verified under QEMU: correct round-robin interleave, clean return to the boot thread) — proof the switch is real.
- 🧵 **Kernel Concurrency Core (`kernel/wait.c`, `kernel/futex.c`, `kernel/rcu.c`)**:
- **Wait Queues & Completions**: Sleeper parking with timed waits, one-shot completion barriers, and a static entry pool so a blocking path never re-enters the allocator (`waitinfo`).
- **Futex Hash Table**: 32 buckets with value re-check on the slow path, requeue support, and an `fmutex_t` sleeping mutex built on the uncontended-fastpath protocol (`futexinfo`).
@@ -104,7 +108,7 @@
- **X.509 Cryptographic Keyring** with signature verification.
- In-Kernel **eBPF Register Virtual Machine** and verifier.
- **io_uring** lockless asynchronous Submission/Completion ring buffers.
- Preemptive multi-tasking scheduler with Round-Robin quantum and spinlock synchronization.
- Weave tiered-rotation scheduler with real per-arch context switching and spinlock synchronization (see the Weave Scheduler entry above).
- Systemd-style unit manager (`systemctl`), cron background scheduler (`crond`), and RFC 5424 Syslog engine.
- 🧰 **LazyBox Userland Suite (85+ Linux, Rust, C++ & SUB-Lang Applets)**:
- Interactive shell with history, quote-aware tokenization, tab autocompletion, ANSI cursor editing, and script runner (`sh`).
+49
View File
@@ -0,0 +1,49 @@
/*
* SUB-OS kernel context switch (AArch64)
* --------------------------------------
* void sub_ctx_switch(void** save_sp, void* load_sp)
* x0 = save_sp, x1 = load_sp
*
* Saves the AAPCS64 callee-saved registers (x19-x28) plus the frame pointer
* (x29) and link register (x30) of the outgoing thread, records its stack
* pointer through *save_sp, then adopts the incoming thread's stack and
* restores the mirror image. Returns into whoever is switched in.
*
* NOTE: implemented for parity with x86_64 and to keep the shared scheduler
* building on this target; not yet exercised on hardware/QEMU for aarch64.
*/
.text
.globl sub_ctx_switch
.globl sub_thread_trampoline
sub_ctx_switch:
sub sp, sp, #(12 * 8)
stp x19, x20, [sp, #0]
stp x21, x22, [sp, #16]
stp x23, x24, [sp, #32]
stp x25, x26, [sp, #48]
stp x27, x28, [sp, #64]
stp x29, x30, [sp, #80] /* fp, lr */
mov x2, sp
str x2, [x0] /* *save_sp = outgoing SP */
mov sp, x1 /* adopt incoming SP */
ldp x19, x20, [sp, #0]
ldp x21, x22, [sp, #16]
ldp x23, x24, [sp, #32]
ldp x25, x26, [sp, #48]
ldp x27, x28, [sp, #64]
ldp x29, x30, [sp, #80]
add sp, sp, #(12 * 8)
ret
/* Fresh-thread trampoline: x19 = entry, x20 = argument (forged by task_create). */
sub_thread_trampoline:
mov x0, x20 /* arg */
blr x19 /* entry(arg) */
mov x0, #0
bl task_exit
1: wfi
b 1b
+34
View File
@@ -0,0 +1,34 @@
/*
* SUB-OS kernel context switch (ARMv8i / AArch32)
* -----------------------------------------------
* void sub_ctx_switch(void** save_sp, void* load_sp)
* r0 = save_sp, r1 = load_sp
*
* Saves the AAPCS callee-saved registers (r4-r11) plus the link register of
* the outgoing thread, records its stack pointer through *save_sp, then adopts
* the incoming thread's stack and restores the mirror image.
*
* NOTE: implemented for parity with x86_64 and to keep the shared scheduler
* building on this target; not yet exercised on hardware/QEMU for armv8i.
*/
.text
.syntax unified
.arm
.globl sub_ctx_switch
.globl sub_thread_trampoline
sub_ctx_switch:
push {r4-r11, lr}
str sp, [r0] @ *save_sp = outgoing SP
mov sp, r1 @ adopt incoming SP
pop {r4-r11, lr}
bx lr
/* Fresh-thread trampoline: r4 = entry, r5 = argument (forged by task_create). */
sub_thread_trampoline:
mov r0, r5 @ arg
blx r4 @ entry(arg)
mov r0, #0
bl task_exit
1: b 1b
+55
View File
@@ -0,0 +1,55 @@
[BITS 64]
default rel
; SUB-OS kernel context switch (x86_64)
; -------------------------------------
; sub_ctx_switch(void** save_sp /* rdi */, void* load_sp /* rsi */)
;
; Saves the SysV callee-saved registers plus RFLAGS of the outgoing thread onto
; its own stack, stashes the resulting stack pointer through *save_sp, then
; loads the incoming thread's stack pointer and unwinds the mirror image. From
; C this looks like an ordinary call that "returns" only when this thread is
; later switched back in. Caller-saved registers are the C caller's problem, as
; usual, so nothing else needs preserving here.
global sub_ctx_switch
global sub_thread_trampoline
extern task_exit
section .text
sub_ctx_switch:
push rbp
push rbx
push r12
push r13
push r14
push r15
pushfq
mov [rdi], rsp ; *save_sp = outgoing stack pointer
mov rsp, rsi ; adopt the incoming stack
popfq
pop r15
pop r14
pop r13
pop r12
pop rbx
pop rbp
ret
; First-run trampoline. task_create() forges an initial frame so that the very
; first switch into a fresh thread lands here with r12 = entry, r13 = argument.
; We call the entry function, and if it ever returns we retire the thread.
sub_thread_trampoline:
mov rdi, r13 ; arg
call r12 ; entry(arg)
xor rdi, rdi ; void entry -> exit code 0
call task_exit
.hang:
hlt
jmp .hang
section .note.GNU-stack noalloc noexec nowrite progbits
+67
View File
@@ -0,0 +1,67 @@
# SUB-OS Autonomous Development Roadmap
This file is the cross-run memory for the continuous-development effort. Each
run: read it, pick the single highest-value NEXT item achievable in one
session, implement + verify it, then move it to DONE with a one-line result and
refine the backlog.
## Known-good build invocations (this host)
- **Toolchain setup:** `apt-get install -y nasm gcc-aarch64-linux-gnu
binutils-aarch64-linux-gnu gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf
qemu-system-x86 qemu-system-arm`; for Rust: `rustup target add x86_64-unknown-none`.
- **x86_64:** `make x86_64_defconfig && make CROSS_COMPILE=x86_64-linux-gnu-
CXX=x86_64-linux-gnu-g++ RUSTC="rustc --target x86_64-unknown-none"`
- **aarch64:** `make aarch64_defconfig && make`
- **armv8i:** `make armv8i_defconfig && make`
- **Boot (x86_64, headless):** `qemu-system-x86_64 -drive format=raw,file=SUB-OS.img
-m 256M -netdev user,id=net0 -device e1000,netdev=net0 -serial stdio -display none`
(slow under TCG; the shell waits on input, so a timeout at the prompt is normal).
## DONE
- **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`,
per-arch assembly for x86_64/aarch64/armv8i) and a first-run trampoline.
Threads run on their own kernel stacks; a task's lane floats on behaviour
(voluntary yield → woven up, quantum-burn → woven down). Verified on x86_64
under QEMU: a boot-time self-test runs 3 cooperative kernel threads to
completion (20 real context switches, correct 1→2→3 round-robin interleave,
clean return to the boot thread, then boot proceeds to ring-3 init + shell).
ARM switch asm builds on both targets but is not yet boot-tested.
## 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
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
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
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,
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
the Weave lanes per-CPU with work-stealing between them.
7. **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
- `.S` files are run through cpp: no nested `/* ... */` comments.
- `printk`/vsprintf supports only `c s d i u o x X p %` — no length modifiers
(`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.
- During `subos_modular_core_boot()` step 13, hardware interrupts are still
masked (enabled at step 14), so cooperative in-boot tests are deterministic.
+40 -3
View File
@@ -3,11 +3,48 @@
#include "task.h"
/*
* SUB-OS "Weave" scheduler
* ------------------------
* A distinctive tiered-rotation scheduler rather than a plain run queue. Every
* runnable task threads through one of WEAVE_NLANES priority lanes. The
* dispatcher always pulls from the lowest-numbered non-empty lane and rotates
* round-robin inside it, so latency-sensitive work stays near the front.
*
* The lane a task sits in is not fixed: a task that gives up the CPU
* voluntarily (sched_yield) is treated as interactive and woven UP a lane,
* while a task that burns its whole quantum and is preempted (sched_schedule)
* is woven DOWN. This lets throughput-bound threads and interactive threads
* coexist without either starving, and it is driven entirely by observed
* behaviour rather than a static nice value.
*
* The mechanism underneath is a real callee-saved context switch
* (sub_ctx_switch, per-arch assembly); this is genuine multitasking, not a
* state relabel.
*/
#define WEAVE_NLANES 4
void sched_init(void);
void sched_yield(void);
void sched_schedule(void);
void sched_tick(void);
void sched_yield(void); /* voluntary: rewards the caller (woven up) */
void sched_schedule(void); /* involuntary/preemptive: caller woven down */
void sched_tick(void); /* timer hook: charges the running quantum */
void sched_add_task(task_t* task);
void sched_remove_task(task_t* task);
/* Let the currently-running context join or leave the run rotation. The kernel
* idle/boot thread uses these to briefly participate while it drives a batch
* of kernel threads to completion. */
void sched_join(void);
void sched_leave(void);
/* Arm or disarm preemptive switching from the timer tick. Cooperative
* switching (sched_yield / task_exit) always works; preemption is opt-in so
* early boot can run to completion deterministically. */
void sched_set_preempt(bool armed);
/* Diagnostics + the boot-time context-switch self-test. */
void sched_dump(void);
void weave_selftest(void);
#endif // _KERNEL_SCHED_H
+14
View File
@@ -26,6 +26,20 @@ typedef struct task_struct {
int priority;
int time_slice;
int exit_code;
/* --- SUB-OS "Weave" scheduler context ---------------------------------
* ctx_sp is the saved stack pointer handed to sub_ctx_switch(); it is
* pointer-sized so the same struct works on 64- and 32-bit targets.
* A task lives in exactly one lane while it is runnable; weave_credit is
* the quantum it has left in that lane, and weave_on marks it as an active
* participant in the run rotation (so a bare kernel idle task that merely
* calls sched_yield() is never enqueued). */
void* ctx_sp;
uint32_t weave_lane;
uint32_t weave_credit;
uint64_t sched_runs;
uint8_t weave_on;
struct list_head list;
struct list_head all_list;
} task_t;
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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();