commit - a9f11a50ab3f672b1b1a7799b89ef63e2eff0e26
commit + e1940d574f26f4e261757461655d7290fe1ff4e9
blob - /dev/null
blob + 16ccc96fd3c91895a5303ebb2fd9b5c25f47fad2 (mode 644)
--- /dev/null
+++ src/sched.rs
+// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et :
+//
+// Copyright (c) 2025-2026 Murilo Ijanc' <murilo@ijanc.org>
+//
+// 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<Vec<Task>>,
+ current: UnsafeCell<usize>,
+ tick: UnsafeCell<u64>,
+}
+
+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
+// vim: set tw=79 cc=80 ts=4 sw=4 sts=4 et :
+//
+// Copyright (c) 2025-2026 Murilo Ijanc' <murilo@ijanc.org>
+//
+// 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]);
+ }
+}