commit e1940d574f26f4e261757461655d7290fe1ff4e9 from: Murilo Ijanc date: Thu Mar 5 22:09:55 2026 UTC add task context switch and round-robin scheduler commit - a9f11a50ab3f672b1b1a7799b89ef63e2eff0e26 commit + e1940d574f26f4e261757461655d7290fe1ff4e9 blob - /dev/null blob + 16ccc96fd3c91895a5303ebb2fd9b5c25f47fad2 (mode 644) --- /dev/null +++ src/sched.rs @@ -0,0 +1,193 @@ +// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et : +// +// Copyright (c) 2025-2026 Murilo Ijanc' +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +use core::cell::UnsafeCell; + +use alloc::vec::Vec; + +use crate::serial; +use crate::task::{self, Context, Task, TaskState}; + +/// Global scheduler state. +/// Safety: only accessed with interrupts disabled +/// (single-core, cli/sti around access). +struct Scheduler { + tasks: UnsafeCell>, + current: UnsafeCell, + tick: UnsafeCell, +} + +unsafe impl Sync for Scheduler {} + +static SCHED: Scheduler = Scheduler { + tasks: UnsafeCell::new(Vec::new()), + current: UnsafeCell::new(0), + tick: UnsafeCell::new(0), +}; + +/// Initialize the scheduler with the bootstrap task (kmain). +pub fn init() { + unsafe { + let tasks = &mut *SCHED.tasks.get(); + tasks.push(Task::bootstrap()); // id 0: kmain/idle + } +} + +/// Spawn a new task. Returns the task id. +pub fn spawn(entry: fn() -> !) -> u64 { + unsafe { + let tasks = &mut *SCHED.tasks.get(); + let t = Task::new(entry); + let id = t.id; + tasks.push(t); + id + } +} + +/// Called from the timer IRQ handler. Increments tick +/// counter and performs a preemptive context switch if +/// there is another Ready task. +pub fn timer_tick() { + unsafe { + let tick = &mut *SCHED.tick.get(); + *tick += 1; + + if *tick % 100 == 0 { + serial::print("tick: "); + print_num(*tick); + serial::print("\n"); + } + + schedule(); + } +} + +/// Cooperative yield: current task voluntarily gives up CPU. +pub fn yield_now() { + unsafe { + schedule(); + } +} + +/// Mark the current task as dead and switch away. +pub fn exit() -> ! { + unsafe { + let tasks = &mut *SCHED.tasks.get(); + let current = *SCHED.current.get(); + tasks[current].state = TaskState::Dead; + schedule(); + } + // Should never return, but just in case + loop { + unsafe { core::arch::asm!("hlt"); } + } +} + +/// Get the current tick count. +pub fn ticks() -> u64 { + unsafe { *SCHED.tick.get() } +} + +/// Get the index of the currently running task. +pub fn current_task_index() -> usize { + unsafe { *SCHED.current.get() } +} + +/// Block the current task and schedule another. +pub fn block() { + unsafe { + let tasks = &mut *SCHED.tasks.get(); + let current = *SCHED.current.get(); + tasks[current].state = TaskState::Blocked; + schedule(); + } +} + +/// Unblock a task by index (set it to Ready). +pub fn unblock(task_index: usize) { + unsafe { + let tasks = &mut *SCHED.tasks.get(); + if task_index < tasks.len() + && tasks[task_index].state == TaskState::Blocked + { + tasks[task_index].state = TaskState::Ready; + } + } +} + +/// Core scheduling logic: pick next Ready task (round-robin) +/// and switch to it. +unsafe fn schedule() { + unsafe { + let tasks = &mut *SCHED.tasks.get(); + let current = &mut *SCHED.current.get(); + let n = tasks.len(); + + if n <= 1 { + return; + } + + // Find the next Ready task (round-robin) + let old_id = *current; + let mut next_id = old_id; + + for i in 1..n { + let candidate = (old_id + i) % n; + if tasks[candidate].state == TaskState::Ready { + next_id = candidate; + break; + } + } + + if next_id == old_id { + return; // no other task ready + } + + // Switch: only set old task to Ready if still Running + if tasks[old_id].state == TaskState::Running { + tasks[old_id].state = TaskState::Ready; + } + tasks[next_id].state = TaskState::Running; + *current = next_id; + + let old_ctx = + &mut tasks[old_id].ctx as *mut Context; + let new_ctx = + &tasks[next_id].ctx as *const Context; + + task::switch_context(old_ctx, new_ctx); + } +} + +fn print_num(val: u64) { + if val == 0 { + serial::putc(b'0'); + return; + } + let mut buf = [0u8; 20]; + let mut n = val; + let mut i = 0; + while n > 0 { + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + i += 1; + } + while i > 0 { + i -= 1; + serial::putc(buf[i]); + } +} blob - /dev/null blob + f1c2960be23d165db4898c64b6b1766543a0d331 (mode 644) --- /dev/null +++ src/task.rs @@ -0,0 +1,175 @@ +// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et : +// +// Copyright (c) 2025-2026 Murilo Ijanc' +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +use core::arch::naked_asm; + +use alloc::boxed::Box; + +use crate::serial; + +const STACK_SIZE: usize = 16 * 1024; // 16 KiB per task + +static mut NEXT_ID: u64 = 0; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TaskState { + Ready, + Running, + Blocked, + Dead, +} + +/// Saved CPU context for context switching. +/// Only callee-saved registers + RSP + RIP. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct Context { + pub rsp: u64, +} + +pub struct Task { + pub id: u64, + pub state: TaskState, + pub ctx: Context, + _stack: Box<[u8]>, // owned stack memory +} + +impl Task { + /// Create a new task that will start executing at `entry`. + pub fn new(entry: fn() -> !) -> Self { + let id = unsafe { + let id = NEXT_ID; + NEXT_ID += 1; + id + }; + + // Allocate stack + let stack = Box::new([0u8; STACK_SIZE]); + let stack_top = + stack.as_ptr() as u64 + STACK_SIZE as u64; + + // Prepare the stack as if switch_context had been + // called: push fake callee-saved regs + return addr. + // + // Stack layout (growing downward): + // [stack_top - 8] = entry (return address for ret) + // [stack_top - 16] = 0 (r15) + // [stack_top - 24] = 0 (r14) + // [stack_top - 32] = 0 (r13) + // [stack_top - 40] = 0 (r12) + // [stack_top - 48] = 0 (rbp) + // [stack_top - 56] = 0 (rbx) + // ← rsp points here + let sp = stack_top - 56; + unsafe { + // entry address (where `ret` jumps to) + *((stack_top - 8) as *mut u64) = entry as u64; + // callee-saved regs = 0 + *((stack_top - 16) as *mut u64) = 0; // r15 + *((stack_top - 24) as *mut u64) = 0; // r14 + *((stack_top - 32) as *mut u64) = 0; // r13 + *((stack_top - 40) as *mut u64) = 0; // r12 + *((stack_top - 48) as *mut u64) = 0; // rbp + *((stack_top - 56) as *mut u64) = 0; // rbx + } + + serial::print("task: created #"); + print_num(id); + serial::print("\n"); + + Task { + id, + state: TaskState::Ready, + ctx: Context { rsp: sp }, + _stack: stack, + } + } + + /// Create a "bootstrap" task representing the current + /// execution context (kmain). Its RSP will be filled in + /// by the first switch_context call. + pub fn bootstrap() -> Self { + let id = unsafe { + let id = NEXT_ID; + NEXT_ID += 1; + id + }; + + Task { + id, + state: TaskState::Running, + ctx: Context { rsp: 0 }, + _stack: Box::new([0u8; 0]), + } + } +} + +/// Switch from the current task to the next task. +/// Saves callee-saved registers on the current stack, +/// stores RSP in `old`, loads RSP from `new`, restores +/// callee-saved registers, and returns into the new task. +/// +/// # Safety +/// Both pointers must point to valid Context structs. +#[unsafe(no_mangle)] +#[unsafe(naked)] +pub unsafe extern "C" fn switch_context( + old: *mut Context, + new: *const Context, +) { + naked_asm!( + // Save callee-saved registers on current stack + "push rbx", + "push rbp", + "push r12", + "push r13", + "push r14", + "push r15", + // Save current RSP into old->rsp + "mov [rdi], rsp", + // Load new RSP from new->rsp + "mov rsp, [rsi]", + // Restore callee-saved registers from new stack + "pop r15", + "pop r14", + "pop r13", + "pop r12", + "pop rbp", + "pop rbx", + // Return into the new task + "ret", + ); +} + +fn print_num(val: u64) { + if val == 0 { + serial::putc(b'0'); + return; + } + let mut buf = [0u8; 20]; + let mut n = val; + let mut i = 0; + while n > 0 { + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + i += 1; + } + while i > 0 { + i -= 1; + serial::putc(buf[i]); + } +}