summaryrefslogtreecommitdiff
path: root/na/kernel/sched_core.cc
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
commit1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (patch)
tree0f5e01cd7c7a05abb6b1c7472d7f00f49caf02cc /na/kernel/sched_core.cc
parent63dc025705f6533b91fb1a7d26e6ed864efd9900 (diff)
downloadlume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.gz
lume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.xz
feat: establish bootable new architecture checkpoint
Replace the compressed boot path with a relocatable higher-half RISC-V entry, expand memory and SMP support, and add the driver, TTY, VFS, exec, signal, syscall, initramfs, and self-test infrastructure needed for a bootable kernel checkpoint.
Diffstat (limited to 'na/kernel/sched_core.cc')
-rw-r--r--na/kernel/sched_core.cc306
1 files changed, 240 insertions, 66 deletions
diff --git a/na/kernel/sched_core.cc b/na/kernel/sched_core.cc
index 85248ef..c0e068e 100644
--- a/na/kernel/sched_core.cc
+++ b/na/kernel/sched_core.cc
@@ -13,11 +13,15 @@
#include <lume/sched_class.h>
#include <lume/task.h>
#include <lume/pmm.h>
+#include <lume/vmm.h>
#include <lume/addr.h>
#include <lume/new.h>
#include <lume/kprintf.h>
#include <lume/klog.h>
+#include <lume/smp.h>
#include <arch/cpu.h>
+#include <arch/mmu.h>
+#include <arch/trap.h>
#include <arch/context.h>
#include <kernel/sched_internal.h>
@@ -37,11 +41,15 @@ static const SchedClass* g_sched_class_head = &g_sched_class_o1;
static void idle_thread_func(void* /*arg*/)
{
while (true) {
+ if (g_rq[arch::cpu::id()].need_resched()) {
+ schedule();
+ }
arch::cpu::intr_on();
arch::cpu::halt_until_interrupt();
}
}
+
/* ========================================================================
* Idle class (always returns idle task)
* ======================================================================== */
@@ -108,6 +116,9 @@ static const char* idle_name_for_cpu(uint32 cpu)
* Kernel thread entry — MI-layer function called by MD trampoline
* ======================================================================== */
+/* Forward declaration — C++ linkage (task.cc) */
+void task_exit(int code);
+
extern "C" void kthread_entry(void (*entry)(void*), void* arg)
{
RunQueue* rq = this_rq();
@@ -117,74 +128,38 @@ extern "C" void kthread_entry(void (*entry)(void*), void* arg)
entry(arg);
- kprintf("[sched] kthread exited\n");
- while (true) {
- arch::cpu::halt_until_interrupt();
- }
+ /* Entry function returned — clean exit (release PID, stack, TCB) */
+ task_exit(0);
+ __builtin_unreachable();
}
/* ========================================================================
* fork_child_entry — called from ret_from_fork assembly
* ======================================================================== */
extern "C" void userret(arch::TrapFrame* tf);
+extern "C" void trap_vector();
extern "C" void fork_child_entry(arch::TrapFrame* tf)
{
RunQueue* rq = this_rq();
rq->lock().release();
- /* Do NOT enable interrupts here! userret expects them disabled. */
-
- /* Jump to user space */
+ /* Set stvec to trap_vector for user-mode traps, then return to user */
+ arch::trap::set_vector(reinterpret_cast<uint64>(trap_vector));
userret(tf);
__builtin_unreachable();
}
/* ========================================================================
- * Helper: create a TCB with a kernel stack
+ * alloc_task — canonical TCB allocator (defined in task.cc)
+ *
+ * Single point of initialization for TaskControlBlock. Ensures all
+ * fields are properly zeroed when the struct gains new members.
* ======================================================================== */
-static TaskControlBlock* create_tcb(const char* name, uint8 priority,
- uint8 flags, uint8 cpu)
-{
- auto* task = new TaskControlBlock;
- if (!task) return nullptr;
-
- task->context = {};
-
- Page* stack_page = pmm_alloc_pages(1);
- if (!stack_page) {
- delete task;
- return nullptr;
- }
- task->kstack_pa = page_to_pa(stack_page).raw;
- task->kstack_va = pa_to_va(phys_addr(task->kstack_pa)).raw;
-
- *reinterpret_cast<uint64*>(task->kstack_va) = kStackCanary;
-
- task->name = name;
- task->priority = priority;
- task->time_slice = priority_to_timeslice(priority);
- task->state = TaskState::Runnable;
- task->cpu_id = cpu;
- task->flags = flags;
- task->exit_code = 0;
- task->parent = nullptr;
- task->mm = nullptr;
- task->trapframe = nullptr;
-
- task->run_link.init();
- task->all_link.init();
- task->children.init();
- task->sibling.init();
- task->wait_chld.init("wait_chld");
-
- task->sched_class = &g_sched_class_o1;
- task->sched_entity = &task->run_link;
-
- return task;
-}
+extern TaskControlBlock* alloc_task(const char* name, uint8 priority,
+ uint8 flags, uint8 cpu);
/* ========================================================================
* Public API: current_task()
@@ -212,6 +187,10 @@ void sched_enqueue(TaskControlBlock* task)
}
task->sched_class->enqueue_task(rq, task);
+
+ if (rq->current() == rq->idle() || task->priority < rq->current()->priority) {
+ rq->set_need_resched(true);
+ }
}
void sched_dequeue(TaskControlBlock* task)
@@ -262,6 +241,52 @@ void schedule()
rq->set_current(next);
rq->clear_need_resched();
+ /* Switch address space (satp) if the new task has a different mm.
+ * Kernel threads (mm == nullptr) borrow the previous task's mm,
+ * so no satp switch is needed — this matches Linux's lazy TLB behavior. */
+ {
+ VmSpace* prev_mm = prev ? prev->mm : nullptr;
+ VmSpace* next_mm = next->mm;
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ if (next_mm && next_mm != prev_mm) {
+ /* Switching TO a user process with a different address space.
+ * Linux: switch_mm_irqs_off() in arch/riscv/mm/context.c
+ * sets satp and flushes TLB. */
+ arch::mmu::set_page_table(next_mm->root_pa);
+ arch::mmu::flush_tlb_all();
+ next_mm->cpu_mask.fetch_or(1ULL << cpu, __ATOMIC_RELAXED);
+
+ /* Critical: update TrapFrame's kernel_satp so that trap_vector.S
+ * restores the correct page table on next trap entry.
+ * Without this, a process migrated to a new CPU would have its
+ * trap entry use a stale satp from when trapframe_init_user()
+ * was called, pointing to whatever page table was active then
+ * on the ORIGINAL CPU. */
+ if (next->trapframe) {
+ next->trapframe->kernel_satp = arch::mmu::read_satp();
+ }
+ }
+
+ if (prev_mm && prev_mm != next_mm) {
+ /* Leaving prev's address space — clear our bit */
+ prev_mm->cpu_mask.fetch_and(~(1ULL << cpu), __ATOMIC_RELAXED);
+ }
+ }
+
+ /* FPU lazy save/restore.
+ * Save prev's FP registers only if they were modified (FS=Dirty).
+ * Restore next's FP state for user tasks; disable FPU for kthreads. */
+ if (arch::fpu_state_dirty(arch::fpu_get_fs())) {
+ fpu_save(&prev->fpstate);
+ }
+ if (next->mm != nullptr) {
+ arch::fpu_set_fs(arch::kSstatusFS_Clean);
+ fpu_restore(&next->fpstate);
+ } else {
+ arch::fpu_set_fs(arch::kSstatusFS_Off);
+ }
+
arch::context_switch(&prev->context, &next->context);
rq->lock().release();
@@ -274,24 +299,37 @@ void schedule()
void sched_tick()
{
RunQueue* rq = this_rq();
- LockGuard guard(rq->lock());
-
- TaskControlBlock* curr = rq->current();
- if (!curr) return;
+ {
+ LockGuard guard(rq->lock());
+ TaskControlBlock* curr = rq->current();
+ if (!curr) return;
+ curr->sched_class->task_tick(rq, curr);
+ }
- curr->sched_class->task_tick(rq, curr);
+ /* Periodic load balancing — runs without holding rq lock */
+ sched_balance();
}
/* ========================================================================
- * sched_check_preempt() — called before returning to user mode
+ * sched_check_preempt() — called from trap return path
+ *
+ * Only preempts if:
+ * 1. need_resched flag is set (a higher-priority task is ready)
+ * 2. preempt_count == 0 (no spinlocks held, safe to context switch)
* ======================================================================== */
void sched_check_preempt()
{
RunQueue* rq = this_rq();
- if (rq->need_resched()) {
- schedule();
+ if (!rq->need_resched()) return;
+
+ TaskControlBlock* curr = current_task();
+ if (curr && curr->preempt_count > 0) {
+ /* Cannot preempt — caller is in a critical section.
+ * The preemption will fire when the last lock is released. */
+ return;
}
+ schedule();
}
/* ========================================================================
@@ -328,32 +366,37 @@ void sched_init()
g_sched_class_head = &g_sched_class_o1;
- TaskControlBlock* idle0 = create_tcb(idle_name_for_cpu(0), kIdlePriority,
- TF_KTHREAD | TF_IDLE, 0);
- if (!idle0) kernel_panic("sched_init: failed to create idle/0");
+ uint64 bsp_cpu = arch::cpu::id();
+ uint8 bsp_cpu8 = static_cast<uint8>(bsp_cpu);
+
+ TaskControlBlock* idle0 = alloc_task(idle_name_for_cpu(static_cast<uint32>(bsp_cpu)),
+ kIdlePriority,
+ TF_KTHREAD | TF_IDLE, bsp_cpu8);
+ if (!idle0) kernel_panic("sched_init: failed to create idle/bsp");
idle0->pid = 0;
idle0->tgid = 0;
idle0->sched_class = &g_sched_class_idle;
idle0->sched_entity = nullptr;
- uint64 stack_top = idle0->kstack_va + kKernelStackSize;
+ uint64 stack_top = idle0->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
idle0->context = {};
idle0->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
idle0->context.sp = stack_top;
idle0->context.s[0] = reinterpret_cast<uint64>(idle_thread_func);
idle0->context.s[1] = 0;
- g_rq[0].set_idle(idle0);
+ g_rq[bsp_cpu].set_idle(idle0);
- TaskControlBlock* bsp_task = create_tcb("kernel_main", 0, TF_KTHREAD, 0);
+ TaskControlBlock* bsp_task = alloc_task("kernel_main", 0, TF_KTHREAD, bsp_cpu8);
if (!bsp_task) kernel_panic("sched_init: failed to create bsp_task");
bsp_task->pid = -1;
bsp_task->tgid = -1;
bsp_task->state = TaskState::Running;
- g_rq[0].set_current(bsp_task);
+ g_rq[bsp_cpu].set_current(bsp_task);
}
/* ========================================================================
@@ -366,7 +409,7 @@ void sched_init_ap()
const char* name = idle_name_for_cpu(static_cast<uint32>(cpu));
- TaskControlBlock* idle = create_tcb(name, kIdlePriority,
+ TaskControlBlock* idle = alloc_task(name, kIdlePriority,
TF_KTHREAD | TF_IDLE,
static_cast<uint8>(cpu));
if (!idle) kernel_panic("sched_init_ap: failed to create idle");
@@ -376,7 +419,8 @@ void sched_init_ap()
idle->sched_class = &g_sched_class_idle;
idle->sched_entity = nullptr;
- uint64 stack_top = idle->kstack_va + kKernelStackSize;
+ uint64 stack_top = idle->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
idle->context = {};
idle->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
idle->context.sp = stack_top;
@@ -385,7 +429,7 @@ void sched_init_ap()
g_rq[cpu].set_idle(idle);
- TaskControlBlock* ap_task = create_tcb("ap_boot", 0, TF_KTHREAD,
+ TaskControlBlock* ap_task = alloc_task("ap_boot", 0, TF_KTHREAD,
static_cast<uint8>(cpu));
if (!ap_task) kernel_panic("sched_init_ap: failed to create ap_task");
@@ -395,3 +439,133 @@ void sched_init_ap()
g_rq[cpu].set_current(ap_task);
}
+
+/* ========================================================================
+ * sched_nr_running() — runnable task count for a CPU
+ * ======================================================================== */
+
+uint32 sched_nr_running(uint32 cpu_id)
+{
+ if (cpu_id >= kMaxCpus) return 0;
+ return g_rq[cpu_id].o1().nr_running();
+}
+
+/* ========================================================================
+ * sched_migrate_task() — move a task between CPUs
+ *
+ * Locks are acquired in cpu_id order to prevent ABBA deadlock.
+ * The task must be Runnable (in a runqueue) and not currently running.
+ * ======================================================================== */
+
+int sched_migrate_task(TaskControlBlock* task, uint8 target_cpu)
+{
+ if (!task || target_cpu >= kMaxCpus) return -22; /* -EINVAL */
+
+ uint8 src_cpu = task->cpu_id;
+ if (src_cpu == target_cpu) return 0; /* already there */
+
+ /* Cannot migrate idle or currently-running tasks */
+ if (task->flags & TF_IDLE) return -22;
+
+ RunQueue* src_rq = rq_for_cpu(src_cpu);
+ RunQueue* dst_rq = rq_for_cpu(target_cpu);
+
+ /* Ordered lock acquisition: always lock lower cpu_id first */
+ if (src_cpu < target_cpu) {
+ src_rq->lock().acquire();
+ dst_rq->lock().acquire();
+ } else {
+ dst_rq->lock().acquire();
+ src_rq->lock().acquire();
+ }
+
+ int ret = -22;
+
+ /* Double-check: task must not be current on either CPU */
+ if (task != src_rq->current() && task->state == TaskState::Runnable) {
+ /* Dequeue from source */
+ task->sched_class->dequeue_task(src_rq, task);
+
+ /* Update cpu_id and enqueue on destination */
+ task->cpu_id = target_cpu;
+ task->sched_class->enqueue_task(dst_rq, task);
+
+ /* Signal target CPU to reschedule */
+ dst_rq->set_need_resched(true);
+ ret = 0;
+ }
+
+ /* Release in reverse order */
+ if (src_cpu < target_cpu) {
+ dst_rq->lock().release();
+ src_rq->lock().release();
+ } else {
+ src_rq->lock().release();
+ dst_rq->lock().release();
+ }
+
+ /* If migration succeeded, send reschedule IPI to target */
+ if (ret == 0) {
+ smp_send_reschedule(target_cpu);
+ }
+
+ return ret;
+}
+
+/* ========================================================================
+ * sched_balance() — periodic load balancer (pull model)
+ *
+ * Called from sched_tick() context. Looks at all other CPUs and if any
+ * has 2+ more runnable tasks than the local CPU, pulls one task.
+ * This is a simplified Linux-style load_balance().
+ * ======================================================================== */
+
+void sched_balance()
+{
+ uint32 this_cpu = static_cast<uint32>(arch::cpu::id());
+ uint32 local_load = g_rq[this_cpu].o1().nr_running();
+
+ /* Only balance if we're idle or lightly loaded */
+ if (local_load > 1) return;
+
+ uint32 busiest_cpu = this_cpu;
+ uint32 busiest_load = local_load;
+
+ /* Find the busiest CPU */
+ for (uint32 cpu = 0; cpu < kMaxCpus; cpu++) {
+ if (cpu == this_cpu) continue;
+ uint32 load = g_rq[cpu].o1().nr_running();
+ if (load > busiest_load) {
+ busiest_load = load;
+ busiest_cpu = cpu;
+ }
+ }
+
+ /* Only pull if imbalance is >= 2 */
+ if (busiest_cpu == this_cpu || busiest_load < local_load + 2) return;
+
+ /* Try to pull one task from the busiest CPU */
+ RunQueue* busy_rq = rq_for_cpu(busiest_cpu);
+
+ /* Lock ordering: lower cpu_id first */
+ RunQueue* first = (this_cpu < busiest_cpu)
+ ? &g_rq[this_cpu] : busy_rq;
+ RunQueue* second = (this_cpu < busiest_cpu)
+ ? busy_rq : &g_rq[this_cpu];
+
+ first->lock().acquire();
+ second->lock().acquire();
+
+ /* Re-check after acquiring locks */
+ TaskControlBlock* victim = busy_rq->o1().pick_next();
+ if (victim && victim != busy_rq->current()
+ && !(victim->flags & TF_IDLE)) {
+ victim->sched_class->dequeue_task(busy_rq, victim);
+ victim->cpu_id = static_cast<uint8>(this_cpu);
+ victim->sched_class->enqueue_task(&g_rq[this_cpu], victim);
+ g_rq[this_cpu].set_need_resched(true);
+ }
+
+ second->lock().release();
+ first->lock().release();
+}