Commit Diff


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' <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
@@ -0,0 +1,175 @@
+// 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]);
+    }
+}