From 76b578ac1f16c3fcb146920d481b7e9b2cc820b7 Mon Sep 17 00:00:00 2001 From: kernalix7 Date: Wed, 15 Jul 2026 15:53:08 +0900 Subject: [PATCH] feat(kernel,hal): preemptive kernel-thread scheduling on riscv64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings riscv64 to parity with aarch64: it now takes S-mode timer interrupts and preempts busy kernel threads. - boot.rs: replace the spin-stub trap vector with a real S-mode handler. It saves the caller-saved GPRs + sepc + sstatus into a 144-byte frame (sepc/sstatus stacked so a preemptive switch can't clobber them), calls riscv_handle_trap, restores, and srets. - irq.rs (new): riscv_handle_trap reads scause; on the S-mode timer interrupt (0x8000_0000_0000_0005) it re-arms the SBI one-shot (which also clears the pending STIP and keeps sie.STIE), charges a tick, and runs a guarded sched_yield_once. No GIC/EOI on riscv — the re-arm is the acknowledgement. - kthread.rs (new): mirrors the aarch64 pool; spawn_kthread seeds the 112-byte switch frame with entry at offset 0 (riscv switch_context does first, unlike aarch64's +8). - sched_glue.rs: real sched_yield_once (prepare_switch -> switch_context); read_cr3 reads satp. - main.rs: riscv cooperative + preemptive demos (busy C/D that never yield; the SBI timer alone rotates them). Threads set sstatus.SIE on entry (the interrupt-mask-inheritance lesson from aarch64). - timer.rs: drop the now-unused CLINT_MTIME const (read_mtime moved to rdtime in #145). Verified: 'qemu-system-riscv64 -M virt -bios default' runs the cooperative demo, then C and D interleave under the timer and print 'timer preemption verified', with no S-mode faults (remaining traps are OpenSBI HPM probing). x86_64/aarch64 unaffected. --- crates/hal/src/arch/riscv64/boot.rs | 55 +++- crates/hal/src/arch/riscv64/timer.rs | 3 - crates/kernel/src/arch/riscv64/irq.rs | 130 +++++++++ crates/kernel/src/arch/riscv64/kthread.rs | 262 ++++++++++++++++++ crates/kernel/src/arch/riscv64/mod.rs | 15 +- crates/kernel/src/arch/riscv64/sched_glue.rs | 76 ++++-- crates/kernel/src/main.rs | 272 +++++++++++++++++++ 7 files changed, 786 insertions(+), 27 deletions(-) create mode 100644 crates/kernel/src/arch/riscv64/irq.rs create mode 100644 crates/kernel/src/arch/riscv64/kthread.rs diff --git a/crates/hal/src/arch/riscv64/boot.rs b/crates/hal/src/arch/riscv64/boot.rs index c2cf93f..1785b17 100644 --- a/crates/hal/src/arch/riscv64/boot.rs +++ b/crates/hal/src/arch/riscv64/boot.rs @@ -83,12 +83,61 @@ core::arch::global_asm!( " mv a1, s1", " call kernel_main", " j .", // should never return - // ── Minimal trap vector stub ────────────────────────────────────────── - // Keeps the CPU from faulting before Rust installs proper handlers. + // ── S-mode trap vector (direct mode) ───────────────────────────────── + // On any trap the CPU jumps here with interrupts disabled (SIE→SPIE), + // sepc = trapped PC, scause/stval set, still on the kernel stack. Save + // the caller-saved GPRs + sepc + sstatus into a 144-byte frame (18×8, + // 16-byte aligned), call the Rust handler, restore, and `sret`. sepc and + // sstatus MUST be stacked because a preemptive switch inside the handler + // could otherwise clobber them before this thread is re-elected. + // Callee-saved regs (s0-s11), sp, gp, tp are preserved by the C handler. ".balign 4", ".global riscv_trap_vector", "riscv_trap_vector:", - " j riscv_trap_vector", // spin until Rust handler is set + " addi sp, sp, -144", + " sd ra, 0(sp)", + " sd t0, 8(sp)", + " sd t1, 16(sp)", + " sd t2, 24(sp)", + " sd a0, 32(sp)", + " sd a1, 40(sp)", + " sd a2, 48(sp)", + " sd a3, 56(sp)", + " sd a4, 64(sp)", + " sd a5, 72(sp)", + " sd a6, 80(sp)", + " sd a7, 88(sp)", + " sd t3, 96(sp)", + " sd t4, 104(sp)", + " sd t5, 112(sp)", + " sd t6, 120(sp)", + " csrr t0, sepc", + " sd t0, 128(sp)", + " csrr t0, sstatus", + " sd t0, 136(sp)", + " call riscv_handle_trap", + " ld t0, 136(sp)", + " csrw sstatus, t0", + " ld t0, 128(sp)", + " csrw sepc, t0", + " ld ra, 0(sp)", + " ld t0, 8(sp)", + " ld t1, 16(sp)", + " ld t2, 24(sp)", + " ld a0, 32(sp)", + " ld a1, 40(sp)", + " ld a2, 48(sp)", + " ld a3, 56(sp)", + " ld a4, 64(sp)", + " ld a5, 72(sp)", + " ld a6, 80(sp)", + " ld a7, 88(sp)", + " ld t3, 96(sp)", + " ld t4, 104(sp)", + " ld t5, 112(sp)", + " ld t6, 120(sp)", + " addi sp, sp, 144", + " sret", // ── Boot stack (64 KiB in BSS) ─────────────────────────────────────── ".section .bss.stack", ".balign 16", diff --git a/crates/hal/src/arch/riscv64/timer.rs b/crates/hal/src/arch/riscv64/timer.rs index 3acdeb1..c92d9df 100644 --- a/crates/hal/src/arch/riscv64/timer.rs +++ b/crates/hal/src/arch/riscv64/timer.rs @@ -18,9 +18,6 @@ use oncrix_lib::Result; /// CLINT MMIO base for QEMU virt machine. pub const CLINT_BASE: usize = 0x0200_0000; -/// CLINT mtime register offset (64-bit). -const CLINT_MTIME: usize = 0xBFF8; - /// SBI extension ID for the Timer extension. const SBI_EXT_TIMER: usize = 0x5449_4D45; /// SBI function ID: sbi_set_timer. diff --git a/crates/kernel/src/arch/riscv64/irq.rs b/crates/kernel/src/arch/riscv64/irq.rs new file mode 100644 index 0000000..e54fd14 --- /dev/null +++ b/crates/kernel/src/arch/riscv64/irq.rs @@ -0,0 +1,130 @@ +// Copyright 2026 ONCRIX Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! RISC-V 64-bit trap dispatch (timer tick + preemption). +//! +//! The HAL S-mode trap vector (installed via `stvec` in +//! `crates/hal/src/arch/riscv64/boot.rs`) saves the caller-saved GPRs plus +//! `sepc`/`sstatus`, then `call`s [`riscv_handle_trap`] for every trap. This +//! module owns the Rust-side handling: it reads `scause`, services the +//! supervisor-timer interrupt (re-arm + tick + preempt), and returns so the +//! trap vector can restore state and `sret`. +//! +//! Mirrors the aarch64 [`aarch64_handle_irq`](crate::arch::aarch64::irq) but +//! stays deliberately minimal: the riscv64 bring-up port runs kernel threads +//! only (no itimers/timerfd wiring yet), so the handler charges a tick and +//! attempts one cooperative context switch. +//! +//! Unlike the aarch64 GICv3 path there is **no** end-of-interrupt write: on +//! RISC-V, re-arming the SBI timer with a fresh future deadline is what clears +//! the pending supervisor-timer bit (`sip.STIP`). + +use oncrix_hal::arch::riscv64::ns16550::{NS16550_BASE, Ns16550}; +use oncrix_hal::arch::riscv64::timer::RiscvTimer; +use oncrix_hal::serial::SerialPort as _; +use oncrix_hal::timer::Timer; + +/// `scause` value for a supervisor-mode timer interrupt. +/// +/// Interrupts set the high bit (bit 63) of `scause`; the S-mode timer +/// interrupt code is 5, giving `0x8000_0000_0000_0005`. +const SUPERVISOR_TIMER_INTERRUPT: u64 = 0x8000_0000_0000_0005; + +/// Tick period for the one-shot timer re-arm (10 ms in nanoseconds). +const TICK_PERIOD_NS: u64 = 10_000_000; + +/// Number of timer interrupts announced on the console during bring-up. +/// +/// Bounded so the boot log shows interrupts are being delivered without +/// flooding. Accessed only from [`riscv_handle_trap`] (single-CPU trap +/// context, `sstatus.SIE = 0` on entry), so a plain `static mut` counter is +/// race-free. +static mut TICK_LOG_COUNT: u32 = 0; + +/// S-mode trap handler entry point. +/// +/// Called from the HAL trap vector (`call riscv_handle_trap`) after the +/// caller-saved GPRs plus `sepc`/`sstatus` have been stacked and with +/// supervisor interrupts masked by the hardware on trap entry +/// (`sstatus.SIE = 0`). Reads `scause`; on the supervisor-timer interrupt it +/// re-arms the SBI timer (which clears the pending bit and keeps `sie.STIE` +/// set), charges a tick to the current thread, and attempts one preemptive +/// kernel-thread switch. Any other trap cause returns immediately, letting the +/// vector `sret` back to the interrupted context. +#[unsafe(no_mangle)] +pub extern "C" fn riscv_handle_trap() { + let scause: u64; + // SAFETY: `csrr scause` is a side-effect-free read of the trap-cause CSR; + // `nomem`/`nostack` are correct for a pure CSR read. + unsafe { + core::arch::asm!("csrr {0}, scause", out(reg) scause, options(nomem, nostack)); + } + + // Only the supervisor-timer interrupt drives preemption during bring-up. + if scause != SUPERVISOR_TIMER_INTERRUPT { + return; + } + + // Re-arm the one-shot timer for the next 10 ms tick. RISC-V has no + // hardware periodic mode, so each tick is re-armed here; setting a fresh + // future deadline via SBI is also what clears the pending `sip.STIP` + // (there is no GIC-style EOI). `set_oneshot` additionally keeps + // `sie.STIE` enabled. + let mut timer = RiscvTimer::new(); + let ticks = timer.nanos_to_ticks(TICK_PERIOD_NS); + let _ = timer.set_oneshot(ticks); + + // Bring-up visibility: announce the first few timer interrupts so a QEMU + // boot log shows interrupts are actually being delivered and handled in + // S-mode. Bounded so it does not flood the console. + // SAFETY: single-CPU trap context (`sstatus.SIE = 0`); TICK_LOG_COUNT is + // only touched here, so the read-modify-write is race-free. + unsafe { + if TICK_LOG_COUNT < 3 { + TICK_LOG_COUNT += 1; + let mut serial = Ns16550::new(NS16550_BASE); + let _ = serial.write_str("[ONCRIX/riscv64] timer IRQ received (preemptive)\n"); + } + } + + // Charge one tick of CPU time to the currently running thread, before the + // switch, so it lands on the thread that consumed the slice. + // SAFETY: trap context with `sstatus.SIE = 0` on a single CPU — the + // scheduler is not concurrently mutated, so this `&mut` borrow is + // exclusive and is dropped before the switch below takes its own borrow. + unsafe { + #[allow(static_mut_refs)] + let sched = &mut crate::arch::riscv64::init::SCHEDULER; + if let Some(t) = sched.current_mut() { + t.charge_tick(); + } + } + + // Skip the switch when only the idle/current thread is runnable — + // `prepare_switch` would return `None` anyway, but the up-front check + // avoids touching the scheduler internals on every tick. + // SAFETY: single-CPU + `sstatus.SIE = 0` guarantees the scheduler is not + // concurrently mutated; this is a shared read-only borrow. + let should_switch = unsafe { + #[allow(static_mut_refs)] + let sched = &crate::arch::riscv64::init::SCHEDULER; + sched.ready_count() > 0 + }; + if !should_switch { + return; + } + + // Attempt one preemptive kernel-thread switch. This is acceptable from + // trap context during bring-up: the interrupted thread resumes when it is + // re-elected, at which point `switch_context` returns into this handler's + // epilogue and the trap vector's `sret` restores the preempted state + // (the vector stacked `sepc`/`sstatus` for exactly this reason). + // SAFETY: trap entry guarantees interrupts are masked (`sstatus.SIE = 0`) + // and we hold no scheduler borrow here; `sched_yield_once` documents both + // as its contract. Single-CPU, so the `&mut` borrow is exclusive. + unsafe { + #[allow(static_mut_refs)] + let sched = &mut crate::arch::riscv64::init::SCHEDULER; + let _ = crate::arch::riscv64::sched_glue::sched_yield_once(sched); + } +} diff --git a/crates/kernel/src/arch/riscv64/kthread.rs b/crates/kernel/src/arch/riscv64/kthread.rs new file mode 100644 index 0000000..9dbba2f --- /dev/null +++ b/crates/kernel/src/arch/riscv64/kthread.rs @@ -0,0 +1,262 @@ +// Copyright 2026 ONCRIX Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! RISC-V 64-bit kernel thread creation and management. +//! +//! Mirror of [`crate::arch::aarch64::kthread`] for the riscv64 port. +//! Provides the ability to spawn kernel threads with their own stacks +//! and CPU contexts. Kernel threads run in supervisor mode (S-mode, the +//! RISC-V analogue of Ring 0) and are used for background tasks (idle +//! loop, init, housekeeping). +//! +//! The distinguishing riscv64 detail is the **seeded switch frame**: +//! [`spawn_kthread`] writes a 112-byte frame at the top of the new +//! thread's stack laid out exactly the way +//! [`switch_context`](super::context::switch_context) pops it, so the +//! very first switch *into* the thread restores zeroed callee-saved +//! registers and `ret`s straight to the entry point. + +use super::context::CpuContext; +use oncrix_process::pid::{Pid, Tid, alloc_tid}; +use oncrix_process::thread::{Priority, Thread}; + +/// Kernel thread stack size (16 KiB per thread). +/// +/// Matches the x86_64 and aarch64 ports: 8 KiB is too small for threads +/// that nest function calls, 16 KiB matches the Linux kernel default. +const KTHREAD_STACK_SIZE: usize = 16384; + +/// Maximum number of kernel threads. +const MAX_KTHREADS: usize = 32; + +/// Size of the seeded [`switch_context`](super::context::switch_context) +/// restore frame, in bytes (13 × 8 = the `ra` + `s0`–`s11` callee-saved +/// set, rounded up to 112 for 16-byte alignment). A multiple of 16, so +/// subtracting it from a 16-aligned stack top keeps `sp` 16-aligned as +/// the RISC-V calling convention requires. +const SWITCH_FRAME_SIZE: u64 = 112; + +/// Number of 8-byte slots spanned by the seeded frame (112 / 8 = 14: +/// `ra` + `s0`–`s11` = 13 live slots plus one alignment-padding slot). +const SWITCH_FRAME_SLOTS: u64 = SWITCH_FRAME_SIZE / 8; + +/// 16-byte aligned stack buffer. +/// +/// The RISC-V calling convention requires `sp` to be 16-byte aligned at a +/// procedure-call boundary. Wrapping the stack array ensures the base +/// address (and therefore every 16-aligned offset) is compliant. +#[repr(C, align(16))] +#[derive(Clone, Copy)] +struct AlignedStack([u8; KTHREAD_STACK_SIZE]); + +impl AlignedStack { + const fn zero() -> Self { + Self([0; KTHREAD_STACK_SIZE]) + } +} + +/// Static pool of kernel thread stacks. +/// +/// Each thread gets a dedicated 16 KiB, 16-byte-aligned stack. Placed in +/// BSS so it does not bloat the kernel image. +static mut KTHREAD_STACKS: [AlignedStack; MAX_KTHREADS] = [AlignedStack::zero(); MAX_KTHREADS]; + +/// CPU contexts for kernel threads (parallel array with stacks). +static mut KTHREAD_CONTEXTS: [CpuContext; MAX_KTHREADS] = + [const { CpuContext::empty() }; MAX_KTHREADS]; + +/// Allocation bitmap: true = slot in use. +static mut KTHREAD_USED: [bool; MAX_KTHREADS] = [false; MAX_KTHREADS]; + +/// Execute a closure with supervisor interrupts disabled, restoring the +/// previous `sstatus.SIE` interrupt-enable state on return. +/// +/// RISC-V analogue of the aarch64 `DAIF` mask/restore helper: the current +/// `sstatus` is read while `SIE` (bit 1) is cleared in one atomic +/// `csrrci`, and on exit `SIE` is re-set only if it was set on entry — so +/// a caller that was already masked stays masked. +/// +/// # Safety +/// +/// The closure must not enable interrupts itself. +#[inline] +unsafe fn with_interrupts_disabled(f: F) -> R +where + F: FnOnce() -> R, +{ + /// `sstatus.SIE` — the global supervisor interrupt-enable bit. + const SSTATUS_SIE: usize = 1 << 1; + + let prev: usize; + // SAFETY: Critical-section entry — read the current `sstatus` into + // `prev` and clear `SIE` (immediate `0b10`) atomically. No `nomem`: + // toggling the global interrupt-enable is cli/sti-class and must act + // as a compiler barrier so the closure's memory ops are not reordered + // out of the critical section. `csrrci` has no memory/stack operand. + unsafe { + core::arch::asm!("csrrci {0}, sstatus, 0b10", out(reg) prev, options(nostack)); + } + let result = f(); + // Re-set `SIE` only if it was set on entry (`restore` is `SIE` or 0). + let restore = prev & SSTATUS_SIE; + // SAFETY: Restore the captured interrupt-enable state. No `nomem` — the + // enable is cli/sti-class and must fence the closure's memory ops. A + // `csrs` of 0 sets no bits, so a previously-masked caller stays masked. + unsafe { + core::arch::asm!("csrs sstatus, {0}", in(reg) restore, options(nostack)); + } + result +} + +/// Kernel thread descriptor returned after spawning. +#[derive(Debug, Clone, Copy)] +pub struct KernelThread { + /// Thread ID. + pub tid: Tid, + /// Slot index in the static pool. + pub slot: usize, +} + +/// Spawn a new kernel thread. +/// +/// `entry` is the function the thread will execute. It must be +/// `extern "C" fn() -> !` (never returns). +/// +/// A 112-byte [`switch_context`](super::context::switch_context) restore +/// frame is seeded at the top of the thread's stack so that the first +/// switch *into* the thread pops zeroed callee-saved registers and `ret`s +/// to `entry`. The parallel [`CpuContext`] in the static pool is +/// initialised to point `sp` at that frame; callers copy it into the +/// scheduler-owned [`Thread`] via [`kthread_context`]. +/// +/// Returns a [`KernelThread`] descriptor and a [`Thread`] for the +/// scheduler. +/// +/// # Safety +/// +/// `entry` must point to a valid kernel function that never returns. +pub unsafe fn spawn_kthread( + entry: extern "C" fn() -> !, + priority: Priority, +) -> oncrix_lib::Result<(KernelThread, Thread)> { + // All access to the static arrays is wrapped in a critical section + // (SIE-masked) to prevent races with trap handlers that might also + // inspect thread state. + + // Find a free slot. + // SAFETY: Interrupts are disabled for the duration of the search, + // preventing concurrent modification of the pool bitmap. + let slot = unsafe { + with_interrupts_disabled(|| { + let used_ptr = &raw mut KTHREAD_USED; + let mut found = None; + for (i, used) in (*used_ptr).iter_mut().enumerate() { + if !*used { + *used = true; + found = Some(i); + break; + } + } + found + }) + } + .ok_or(oncrix_lib::Error::OutOfMemory)?; + + // Compute the stack top. RISC-V stacks grow downward; the buffer is + // 16-byte aligned via `AlignedStack` and `KTHREAD_STACK_SIZE` is a + // multiple of 16, so `stack_top` is 16-aligned. + // SAFETY: Interrupts disabled; raw pointer to our own pool. + let stack_top = unsafe { + with_interrupts_disabled(|| { + let stacks_ptr = &raw const KTHREAD_STACKS; + let base = (*stacks_ptr)[slot].0.as_ptr(); + base as u64 + KTHREAD_STACK_SIZE as u64 + }) + }; + + // Seed the switch-context restore frame and the parallel CpuContext. + // + // `switch_context` restores the new thread with: + // ld sp, 0(a1) ; sp <- new.sp = frame_base + // ld ra, 0(sp) ; ra <- [frame_base + 0] (return address) + // ld s0, 8(sp) ; s0 <- [frame_base + 8] + // ... + // ld s11, 96(sp) ; s11 <- [frame_base + 96] + // addi sp, sp, 112 ; sp = frame_base + 112 = stack_top + // ret ; jump to ra + // + // So the only slot that matters is byte offset +0 (`ra` = the address + // `ret` jumps to). Every other slot is a callee-saved GPR / alignment + // pad we simply zero. + // + // SAFETY: Interrupts disabled; `frame_base .. frame_base + 112` lies + // inside this slot's 16 KiB stack buffer, and we write our own context + // array. + let frame_base = stack_top - SWITCH_FRAME_SIZE; + unsafe { + with_interrupts_disabled(|| { + // Zero all fourteen 8-byte slots of the frame. + for i in 0..SWITCH_FRAME_SLOTS { + let slot_ptr = (frame_base + i * 8) as *mut u64; + *slot_ptr = 0; + } + // `ra` (return address) lives at offset +0 — `switch_context` + // does `ld ra, 0(sp)` first. + let ra_ptr = frame_base as *mut u64; + *ra_ptr = entry as *const () as u64; + + let ctx_ptr = &raw mut KTHREAD_CONTEXTS; + let ctx = &mut (*ctx_ptr)[slot]; + *ctx = CpuContext::empty(); + // CpuContext.sp is byte offset 0 — the value switch_context + // loads into `sp` before popping the seeded frame. + ctx.sp = frame_base; + ctx.pc = entry as *const () as u64; + }); + } + + let tid = alloc_tid(); + let thread = Thread::new(tid, Pid::KERNEL, priority); + + let kt = KernelThread { tid, slot }; + + Ok((kt, thread)) +} + +/// Get a pointer to a kernel thread's CPU context. +/// +/// Callers copy the pointed-to [`CpuContext`] into the scheduler-owned +/// [`Thread`] (via [`Thread::set_cpu_context`]) so the scheduler's +/// context-switch datapath reads the seeded `sp`/`pc`. +/// +/// # Safety +/// +/// The slot must be a valid, in-use kernel thread slot. +pub unsafe fn kthread_context(slot: usize) -> *mut CpuContext { + // SAFETY: Interrupts disabled to prevent concurrent modification. + // The caller guarantees `slot` is valid. + unsafe { + with_interrupts_disabled(|| { + let ctx_ptr = &raw mut KTHREAD_CONTEXTS; + &raw mut (*ctx_ptr)[slot] + }) + } +} + +/// Free a kernel thread slot. +/// +/// # Safety +/// +/// The thread must no longer be scheduled or running. +pub unsafe fn free_kthread(slot: usize) { + if slot < MAX_KTHREADS { + // SAFETY: Interrupts disabled to prevent concurrent access; + // `slot` is bounds-checked above. + unsafe { + with_interrupts_disabled(|| { + let used_ptr = &raw mut KTHREAD_USED; + (*used_ptr)[slot] = false; + }); + } + } +} diff --git a/crates/kernel/src/arch/riscv64/mod.rs b/crates/kernel/src/arch/riscv64/mod.rs index 866bf39..3c78a76 100644 --- a/crates/kernel/src/arch/riscv64/mod.rs +++ b/crates/kernel/src/arch/riscv64/mod.rs @@ -8,15 +8,22 @@ //! GDT/IDT. Exception/interrupt routing is via the trap vector installed //! in boot.rs (HAL). //! -//! The `clone`, `context`, `init`, `init_embed`, `sched_glue`, and -//! `syscall_entry` submodules are riscv64 build stubs that mirror the -//! x86_64 public API so architecture-neutral kernel code type-checks; they -//! are not yet functional. +//! The `context`, `irq`, `kthread`, and `sched_glue` submodules are +//! functional for **kernel-thread preemptive scheduling** (the +//! [`switch_context`] primitive, a seeded-stack spawn path, and the +//! supervisor-timer trap handler). The `clone`, `init_embed`, and +//! `syscall_entry` submodules remain riscv64 build stubs that mirror the +//! x86_64 public API so architecture-neutral kernel code type-checks; the +//! userspace/U-mode transition they need is not written yet. +//! +//! [`switch_context`]: context::switch_context pub mod clone; pub mod context; pub mod init; pub mod init_embed; +pub mod irq; +pub mod kthread; pub mod sched_glue; pub mod syscall_entry; diff --git a/crates/kernel/src/arch/riscv64/sched_glue.rs b/crates/kernel/src/arch/riscv64/sched_glue.rs index 05a3920..27c141c 100644 --- a/crates/kernel/src/arch/riscv64/sched_glue.rs +++ b/crates/kernel/src/arch/riscv64/sched_glue.rs @@ -3,37 +3,79 @@ //! RISC-V 64-bit scheduler plumbing (context-switch driver). //! -//! riscv64 build stub — not yet functional. +//! Kernel-thread cooperative scheduling for the riscv64 bring-up port. +//! Unlike the x86_64 [`sched_glue`](crate::arch::x86_64::sched_glue), this +//! path deliberately does **no** userspace/trap-stack/`satp`/syscall-mirror +//! work: riscv64 kernel threads all run in supervisor mode on the identity- +//! mapped address space installed by the boot stub (`satp` is already +//! programmed for the Sv39 root), so a switch is just the callee-saved +//! register swap performed by +//! [`switch_context`](super::context::switch_context). //! -//! Mirrors the public API of [`crate::arch::x86_64::sched_glue`] so the -//! architecture-neutral `current`/fork glue type-checks on riscv64. Real -//! preemption requires the riscv64 [`switch_context`](super::context) -//! implementation, which is not written yet. +//! Per-process address spaces and the S-mode → U-mode transition needed for +//! *user* threads are still unimplemented; those hooks live in +//! [`crate::arch::riscv64::init`] as no-ops. use oncrix_process::context::Cr3Frame; use oncrix_process::scheduler::RoundRobinScheduler; -/// Attempt one round-robin preemption. +/// Attempt one round-robin, kernel-thread-only cooperative switch. /// -/// riscv64 build stub — not yet functional; always reports "no switch -/// happened" so callers fall through without touching CPU state. +/// Asks the scheduler for the next runnable thread via +/// [`RoundRobinScheduler::prepare_switch`]; if one exists, saves the +/// outgoing thread's callee-saved state and restores the incoming thread's +/// via [`switch_context`](super::context::switch_context). +/// +/// Returns `true` if a switch actually happened, `false` if no other ready +/// thread was available (or the scheduler handed back a null context +/// pointer). /// /// # Safety /// -/// Must be called with interrupts disabled. Currently a no-op that returns -/// `false`. -pub unsafe fn sched_yield_once(_sched: &mut RoundRobinScheduler) -> bool { - false +/// * Must be called with supervisor interrupts (`sstatus.SIE`) masked. +/// * `sched` must be the live kernel scheduler whose threads own the kernel +/// stacks referenced by the saved contexts. +/// * The outgoing thread must be reachable again via its own +/// [`CpuContext`](oncrix_process::context::CpuContext) once the switch +/// completes. +pub unsafe fn sched_yield_once(sched: &mut RoundRobinScheduler) -> bool { + let Some(t) = sched.prepare_switch() else { + return false; + }; + if t.prev_ctx.is_null() || t.next_ctx.is_null() { + return false; + } + // Kernel threads share the identity-mapped address space (`satp` is + // already installed by the boot stub), so there is no page-table or + // trap-stack work to do around the switch — just swap register state. + // + // SAFETY: `prev_ctx`/`next_ctx` are non-null `CpuContext` pointers owned + // by threads in `sched` (checked above); interrupts are masked per this + // function's contract; `switch_context` upholds the RISC-V saved-frame + // invariant documented on it. + unsafe { + super::context::switch_context(t.prev_ctx, t.next_ctx); + } + true } -/// Read the active address-space root. +/// Read the active address-space root (`satp`). /// -/// riscv64 build stub — not yet functional; returns [`Cr3Frame::NONE`]. On -/// riscv64 the real implementation would read the `satp` CSR. +/// The riscv64 analogue of reading `CR3`. Consumed by the architecture- +/// neutral fork dispatch; the riscv64 kernel-thread scheduler does not +/// otherwise need it (kernel threads keep the boot `satp`). /// /// # Safety /// -/// Safe to call from any ring-0 context. Currently returns a placeholder. +/// Safe to call from any supervisor-mode context; `csrr , satp` is a +/// privileged but side-effect-free CSR read. pub unsafe fn read_cr3() -> Cr3Frame { - Cr3Frame::NONE + let satp: u64; + // SAFETY: Reading `satp` is privileged but has no side effects; the + // supervisor-mode caller is legitimate by construction (kernel-only + // module). No memory operand, no stack use. + unsafe { + core::arch::asm!("csrr {0}, satp", out(reg) satp, options(nomem, nostack)); + } + Cr3Frame::new(satp) } diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index f6c0c5f..9057b12 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -173,6 +173,141 @@ extern "C" fn demo_preempt_d() -> ! { } } +/// RISC-V cooperative-scheduler bring-up demo: kernel thread A. +/// +/// Prints a marker proving it executed, then cooperatively yields back +/// to the scheduler. Never returns — parks in `wfi` if it is ever +/// resumed a second time. +#[cfg(target_arch = "riscv64")] +extern "C" fn demo_thread_a() -> ! { + let mut serial = Ns16550::new(NS16550_BASE); + let _ = serial.write_str("[ONCRIX/riscv64] cooperative scheduler: thread A ran\n"); + // Hand control back exactly once (interrupts still masked here), then + // park. It must NOT call yield_now again: once the preemptive phase + // enables SIE, sched_yield_once's interrupts-off contract would be + // violated. `wfi` with SIE set simply waits for the next tick. + // SAFETY: interrupts are masked during the cooperative hand-off and no + // scheduler borrow is held across the yield. + unsafe { + let _ = oncrix_kernel::current::yield_now(); + } + // When the preemptive phase later re-elects this thread, it resumes here + // from inside a timer-trap context, so it inherits sstatus.SIE = 0 + // (masked). Set SIE so the CPU can take the next tick and preempt us + // again; without this the wfi below would wait for an interrupt that can + // never arrive. + // SAFETY: kernel thread in S-mode; setting sstatus.SIE only enables IRQ + // delivery. No nomem: the SIE set is cli/sti-class and must act as a + // compiler barrier. + unsafe { + core::arch::asm!("csrsi sstatus, 0x2", options(nostack)); + } + loop { + // SAFETY: `wfi` parks the CPU until an interrupt; harmless in S-mode. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } +} + +/// RISC-V cooperative-scheduler bring-up demo: kernel thread B. +/// +/// Counterpart to [`demo_thread_a`]; prints its own marker then yields. +#[cfg(target_arch = "riscv64")] +extern "C" fn demo_thread_b() -> ! { + let mut serial = Ns16550::new(NS16550_BASE); + let _ = serial.write_str("[ONCRIX/riscv64] cooperative scheduler: thread B ran\n"); + // SAFETY: see `demo_thread_a` — single cooperative hand-off, then park. + unsafe { + let _ = oncrix_kernel::current::yield_now(); + } + // SAFETY: see `demo_thread_a` — set SIE so preemption can resume us. + unsafe { + core::arch::asm!("csrsi sstatus, 0x2", options(nostack)); + } + loop { + // SAFETY: `wfi` parks the CPU until an interrupt; harmless in S-mode. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } +} + +/// Number of times each preemptive busy thread has announced itself. +/// +/// Written by [`demo_preempt_c`]/[`demo_preempt_d`] and polled by the boot +/// thread. Because those threads NEVER yield, any progress by both of them +/// proves the SBI timer preempted one to run the other. +#[cfg(target_arch = "riscv64")] +static PREEMPT_C_HITS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); +#[cfg(target_arch = "riscv64")] +static PREEMPT_D_HITS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); + +/// Busy-spin duration between announcements (arbitrary, tuned so a 10 ms +/// timer tick lands mid-spin and forces a preemptive switch, while still +/// letting the boot thread be re-elected to report success promptly). +#[cfg(target_arch = "riscv64")] +const PREEMPT_SPIN: u32 = 4_000_000; + +/// RISC-V preemptive-scheduling demo: busy thread C. +/// +/// Unlike the cooperative demo threads, this NEVER yields — it only +/// busy-spins and prints. The only way control leaves it is a timer +/// interrupt preempting it (via `riscv_handle_trap` → `sched_yield_once`). +/// It runs with SIE set (enabled on entry), announces itself a bounded +/// number of times, then parks in `wfi`. +#[cfg(target_arch = "riscv64")] +extern "C" fn demo_preempt_c() -> ! { + use core::sync::atomic::Ordering; + // This thread is first entered from a timer-trap context (sstatus.SIE=0). + // Set SIE so the timer can preempt us mid-spin — the whole point of the + // demo. SAFETY: kernel thread in S-mode; setting sstatus.SIE only enables + // IRQ delivery. No nomem: the SIE set is cli/sti-class and must act as a + // compiler barrier. + unsafe { + core::arch::asm!("csrsi sstatus, 0x2", options(nostack)); + } + let mut serial = Ns16550::new(NS16550_BASE); + while PREEMPT_C_HITS.load(Ordering::Relaxed) < 3 { + // Busy work — deliberately no yield. A timer tick will preempt us. + for _ in 0..PREEMPT_SPIN { + core::hint::spin_loop(); + } + PREEMPT_C_HITS.fetch_add(1, Ordering::Relaxed); + let _ = serial.write_str("[ONCRIX/riscv64] preemptive: thread C scheduled\n"); + } + loop { + // SAFETY: `wfi` parks until an interrupt; harmless in S-mode. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } +} + +/// RISC-V preemptive-scheduling demo: busy thread D. See [`demo_preempt_c`]. +#[cfg(target_arch = "riscv64")] +extern "C" fn demo_preempt_d() -> ! { + use core::sync::atomic::Ordering; + // SAFETY: see `demo_preempt_c` — set SIE so the timer can preempt us. + unsafe { + core::arch::asm!("csrsi sstatus, 0x2", options(nostack)); + } + let mut serial = Ns16550::new(NS16550_BASE); + while PREEMPT_D_HITS.load(Ordering::Relaxed) < 3 { + for _ in 0..PREEMPT_SPIN { + core::hint::spin_loop(); + } + PREEMPT_D_HITS.fetch_add(1, Ordering::Relaxed); + let _ = serial.write_str("[ONCRIX/riscv64] preemptive: thread D scheduled\n"); + } + loop { + // SAFETY: `wfi` parks until an interrupt; harmless in S-mode. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } +} + /// Main kernel initialization sequence. /// /// Called from the 64-bit trampoline in `boot.S` after the 32-bit stub @@ -548,6 +683,143 @@ pub extern "C" fn kernel_main() -> ! { } let _ = serial.write_str("[ONCRIX/riscv64] All early initialization complete.\n"); + + // ─── Cooperative kernel-thread scheduling bring-up demo ─── + // + // Proves the riscv64 `switch_context` actually schedules real kernel + // threads at runtime. Sequence: + // 1. Mask sstatus.SIE — `switch_context`/`sched_yield_once` require + // interrupts masked, and a cooperative hand-off must not be + // preempted by the SBI timer. + // 2. Register the running boot context as a schedulable thread and + // promote it to Running, so `prepare_switch` has a slot to save + // the outgoing state into. + // 3. Spawn two kernel threads. `spawn_kthread` seeds each stack's + // 112-byte switch frame (ra = entry) plus a parallel + // `CpuContext`; copy that context into the scheduler-owned + // `Thread` before registering it. + // 4. Yield. Control flows boot → A → B → boot (round-robin), so + // both thread bodies print before we return here. + { + use oncrix_kernel::arch::init::SCHEDULER; + use oncrix_kernel::arch::riscv64::kthread::{kthread_context, spawn_kthread}; + use oncrix_kernel::current::{spawn_thread, yield_now}; + use oncrix_process::pid::{Pid, alloc_tid}; + use oncrix_process::thread::{Priority, Thread}; + + // Step 1: mask supervisor interrupts for the whole sequence. + // SAFETY: ring-0 (S-mode) boot context; masking interrupts is the + // documented precondition of the switch/yield primitives. No + // nomem: the SIE clear is cli/sti-class and must act as a compiler + // barrier. + unsafe { + core::arch::asm!("csrci sstatus, 0x2", options(nostack)); + } + + let _ = + serial.write_str("[ONCRIX/riscv64] cooperative scheduler: bring-up demo start\n"); + + // Step 2: register + promote the boot thread. + // SAFETY: single-threaded boot; SCHEDULER is not aliased here, and + // each access below is a distinct, non-overlapping borrow. + unsafe { + let boot = Thread::new(alloc_tid(), Pid::KERNEL, Priority::NORMAL); + let _ = spawn_thread(boot); + let sched = &raw mut SCHEDULER; + let _ = (*sched).schedule(); + } + + // Step 3: spawn the two demo kernel threads and copy each seeded + // context into the scheduler-owned `Thread`. + // SAFETY: entry fns are valid `extern "C" fn() -> !`; single CPU + // with interrupts masked keeps the pool + scheduler exclusive to + // this code. + unsafe { + if let Ok((kt, mut thread)) = spawn_kthread(demo_thread_a, Priority::NORMAL) { + thread.set_cpu_context(*kthread_context(kt.slot)); + let _ = spawn_thread(thread); + } + if let Ok((kt, mut thread)) = spawn_kthread(demo_thread_b, Priority::NORMAL) { + thread.set_cpu_context(*kthread_context(kt.slot)); + let _ = spawn_thread(thread); + } + } + + // Step 4: hand the CPU to the demo threads; returns once both A + // and B have run and yielded back to the boot thread. + // SAFETY: interrupts masked above; no scheduler borrow held. + unsafe { + let _ = yield_now(); + } + + let _ = serial.write_str( + "[ONCRIX/riscv64] cooperative scheduler: thread A/B ran, back on boot thread\n", + ); + } + + // Preemptive scheduling demo: spawn two BUSY kernel threads that never + // yield, then re-arm the timer and set sstatus.SIE. Because C and D + // only busy-spin, the sole way both make progress is the SBI timer + // preempting one to run the other (riscv_handle_trap → + // sched_yield_once). The boot thread polls their atomic counters and + // reports success once both have printed — proving preemption, not + // just that interrupts arrive. + { + use core::sync::atomic::Ordering; + use oncrix_hal::arch::riscv64::timer::RiscvTimer; + use oncrix_hal::timer::Timer; + use oncrix_kernel::arch::riscv64::kthread::{kthread_context, spawn_kthread}; + use oncrix_kernel::current::spawn_thread; + use oncrix_process::thread::Priority; + + // Spawn C and D while interrupts are still masked. + // SAFETY: single CPU, interrupts masked → pool + scheduler are + // exclusive to this code; entry fns are valid extern "C" fn()->!. + unsafe { + if let Ok((kt, mut t)) = spawn_kthread(demo_preempt_c, Priority::NORMAL) { + t.set_cpu_context(*kthread_context(kt.slot)); + let _ = spawn_thread(t); + } + if let Ok((kt, mut t)) = spawn_kthread(demo_preempt_d, Priority::NORMAL) { + t.set_cpu_context(*kthread_context(kt.slot)); + let _ = spawn_thread(t); + } + } + + // Re-arm the timer and set the global interrupt enable. From here + // the SBI timer rotates among the runnable threads. `set_oneshot` + // sets sie.STIE (the per-source enable); sstatus.SIE is the global + // gate. + // SAFETY: PLIC + SBI timer were initialized above; arming the + // timer and setting sstatus.SIE are the documented steps to enable + // interrupt delivery. No nomem on the SIE set: it is cli/sti-class + // and must act as a compiler barrier. + unsafe { + let mut timer = RiscvTimer::new(); + let ticks = timer.nanos_to_ticks(10_000_000); + let _ = timer.set_oneshot(ticks); + core::arch::asm!("csrsi sstatus, 0x2", options(nostack)); // set SIE + } + let _ = serial.write_str( + "[ONCRIX/riscv64] IRQs unmasked; preemptive scheduler armed (threads C, D).\n", + ); + + // Poll until both busy threads have run under preemption. Each + // timer tick that fires while the boot thread is current will + // preempt it into C or D; this loop re-checks after each wakeup. + while PREEMPT_C_HITS.load(Ordering::Relaxed) < 3 + || PREEMPT_D_HITS.load(Ordering::Relaxed) < 3 + { + // SAFETY: `wfi` parks until the next timer interrupt; harmless. + unsafe { + core::arch::asm!("wfi", options(nomem, nostack)); + } + } + let _ = serial.write_str( + "[ONCRIX/riscv64] preemptive: C and D both ran — timer preemption verified.\n", + ); + } + let _ = serial.write_str("[ONCRIX/riscv64] Entering halt loop.\n"); }