Files
Claude a14a7c79b0 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
2026-08-25 08:16:37 +00:00

198 lines
7.1 KiB
C

#include <kernel/task.h>
#include <kernel/sched.h>
#include <kernel/printk.h>
#include <mm/kmalloc.h>
#include <lib/string.h>
#include <arch/arch.h>
static pid_t next_pid = 1;
static task_t* current_task = NULL;
static task_t kernel_idle_task;
static LIST_HEAD(global_tasks);
/* 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) {
task_t* task = (task_t*)kzalloc(sizeof(task_t));
if (!task) return NULL;
task->pid = next_pid++;
task->ppid = current_task ? current_task->pid : 0;
task->pgid = task->pid;
strncpy(task->name, name ? name : "task", sizeof(task->name) - 1);
task->state = TASK_STATE_READY;
task->priority = priority;
task->time_slice = 10; // 10 ticks = 100ms default
// Allocate 16 KB stack for thread
task->stack_size = 16384;
task->stack_base = (uint64_t)kmalloc(task->stack_size);
if (!task->stack_base) {
kfree(task);
return NULL;
}
// 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);
list_add_tail(&task->all_list, &global_tasks);
sched_add_task(task);
return task;
}
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;
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();
}
}
task_t* task_current(void) {
return current_task;
}
pid_t task_get_pid(void) {
return current_task ? current_task->pid : 0;
}
void task_set_current(task_t* task) {
current_task = task;
}
task_t* task_find_by_pid(pid_t pid) {
if (pid == 0) return &kernel_idle_task;
task_t* pos;
list_for_each_entry(pos, &global_tasks, all_list) {
if (pos->pid == pid) {
return pos;
}
}
return NULL;
}
int task_kill(pid_t pid, int sig) {
if (pid == 0) {
printk(ANSI_RED "Error: cannot signal swapper/0 (PID 0)\n" ANSI_RESET);
return -1;
}
task_t* t = task_find_by_pid(pid);
if (!t) {
printk(ANSI_RED "kill: (%d) - No such process\n" ANSI_RESET, pid);
return -1;
}
printk(ANSI_YELLOW "Signal %d sent to process '%s' (PID %d)\n" ANSI_RESET, sig, t->name, pid);
if (sig == 9 || sig == 15) { // SIGKILL / SIGTERM
t->state = TASK_STATE_DEAD;
sched_remove_task(t);
}
return 0;
}
static void print_process_tree_node(task_t* parent, int depth) {
for (int i = 0; i < depth; i++) {
printk(" │ ");
}
const char* state_col = (parent->state == TASK_STATE_RUNNING) ? ANSI_BRIGHT_GREEN :
(parent->state == TASK_STATE_READY) ? ANSI_BRIGHT_CYAN : ANSI_YELLOW;
printk(" ├─ " ANSI_BOLD "%s" ANSI_RESET " (pid: " ANSI_BRIGHT_YELLOW "%d" ANSI_RESET ", %s%s" ANSI_RESET ")\n",
parent->name, parent->pid, state_col,
parent->state == TASK_STATE_RUNNING ? "RUNNING" :
parent->state == TASK_STATE_READY ? "READY" :
parent->state == TASK_STATE_SLEEPING ? "SLEEP" : "BLOCKED");
task_t* pos;
list_for_each_entry(pos, &global_tasks, all_list) {
if (pos->ppid == parent->pid && pos != parent && pos->state != TASK_STATE_DEAD) {
print_process_tree_node(pos, depth + 1);
}
}
}
void task_dump_pstree(void) {
printk(ANSI_BRIGHT_CYAN "=== SUB-OS Process Tree Hierarchy (pstree) ===\n" ANSI_RESET);
printk(ANSI_BOLD "systemd/init (PID 1)\n" ANSI_RESET);
task_t* pos;
list_for_each_entry(pos, &global_tasks, all_list) {
if (pos->ppid <= 1 && pos->state != TASK_STATE_DEAD) {
print_process_tree_node(pos, 0);
}
}
}
void task_init_idle(void) {
memset(&kernel_idle_task, 0, sizeof(task_t));
kernel_idle_task.pid = 0;
kernel_idle_task.ppid = 0;
kernel_idle_task.pgid = 0;
strcpy(kernel_idle_task.name, "swapper/0");
kernel_idle_task.state = TASK_STATE_RUNNING;
INIT_LIST_HEAD(&kernel_idle_task.list);
INIT_LIST_HEAD(&kernel_idle_task.all_list);
INIT_LIST_HEAD(&global_tasks);
current_task = &kernel_idle_task;
}