// --------------------------------------------------------------------------- // sched — preemptive round-robin kernel tasks. // // 16 static task slots. The 100 Hz PIT tick (vector 32) and the // software yield vector (0x60) go through `isr_common`, which saves the // running task's full IntFrame on its own stack and asks the scheduler // for the next frame pointer — the asm then moves rsp there and iretq's, // which *is* the context switch. // // - task 0: the shell (boot stack, never allocated by us) // - task 1: idle (hlt loop, always ready — the lowest-priority // fallback) // - tasks 2..: spawn'd, 8 KiB stack + one unmapped guard page below // // Preemption is disabled while the scheduler lock (PREEMPT_DEPTH) is // held: the tick/yield handlers bail out entirely when the depth is // > 0, so table mutations and klog/serial line writes are atomic with // respect to switching. The lock is a counter, not a mutex — re-entrant // by design (a panic mid-emit increments again; the panic path clears // it, because a dead task's lock is meaningless). // // Task page faults (vector 14) run on a dedicated IST stack (see gdt.rs) // so a stack overflow inside a guard page kills the task gracefully // instead of double-faulting the machine. // --------------------------------------------------------------------------- use core::arch::asm; use core::fmt; use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; extern crate alloc; use alloc::string::String; use alloc::vec::Vec; use crate::interrupts::IntFrame; use crate::memory; use crate::paging; pub const MAX_TASKS: usize = 16; pub const SHELL_TID: usize = 0; pub const IDLE_TID: usize = 1; const STACK_PAGES: usize = 9; // 1 guard page + 32 KiB stack — faults dump on the task's own stack const NAME_MAX: usize = 24; const PANIC_MAX: usize = 64; // --------------------------------------------------------------------------- // Inter-task message queues // --------------------------------------------------------------------------- /// Capacity of each task's message ring (power of two; full sends drop /// with a "queue full" error). Sized so input bursts (a 100 Hz mouse, /// key repeat) far outrun a blocked consumer: 16 × ~1.6 s at worst. const MSG_CAP: usize = 16; /// One inter-task message. Input events use the same decoded types the /// PS/2 drivers produce, so a window's input routing is "post what the /// driver would have delivered to the console". `User(u64)` is the /// generic escape hatch (window commands, app signals, …). #[derive(Clone, Copy, Debug)] pub enum TaskMsg { Key(crate::ps2::KeyEvent), Mouse(crate::ps2::MouseEvent), AppMouse(crate::ps2::AppMouseEvent), Focus(bool), Close, Timer, User(u64), } /// Decode a message for logging/display (no Debug dep in the shell task). pub fn msg_describe(msg: &TaskMsg) -> alloc::string::String { use crate::ps2::KeyEvent; match msg { TaskMsg::Key(k) => match k { KeyEvent::Char(c) => alloc::format!("Key('{}')", c), KeyEvent::Enter => "Key(Enter)".into(), KeyEvent::Backspace => "Key(Backspace)".into(), KeyEvent::Tab => "Key(Tab)".into(), KeyEvent::Escape => "Key(Escape)".into(), KeyEvent::Ctrl(c) => alloc::format!("Key(Ctrl-{})", c), KeyEvent::CtrlArrow(d) => alloc::format!("Key(CtrlArrow{})", d), KeyEvent::ArrowUp => "Key(Up)".into(), KeyEvent::ArrowDown => "Key(Down)".into(), KeyEvent::ArrowLeft => "Key(Left)".into(), KeyEvent::ArrowRight => "Key(Right)".into(), KeyEvent::Home => "Key(Home)".into(), KeyEvent::End => "Key(End)".into(), KeyEvent::PageUp => "Key(PgUp)".into(), KeyEvent::PageDown => "Key(PgDn)".into(), KeyEvent::Delete => "Key(Delete)".into(), KeyEvent::F(n) => alloc::format!("Key(F{})", n), KeyEvent::Unknown(b) => alloc::format!("Key(0x{:02x})", b), }, TaskMsg::Mouse(m) => alloc::format!( "Mouse({:+},{:+} {} {})", m.dx, m.dy, if m.left { "L" } else { "-" }, if m.right { "R" } else { "-" } ), TaskMsg::AppMouse(m) => { alloc::format!("AppMouse({},{} {:?} b={:#x})", m.x, m.y, m.kind, m.buttons) } TaskMsg::Focus(f) => alloc::format!("Focus({})", f), TaskMsg::Close => "Close".into(), TaskMsg::Timer => "Timer".into(), TaskMsg::User(n) => alloc::format!("User({})", n), } } #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TaskState { Ready, Sleeping, Blocked, Done, } struct Task { name: [u8; NAME_MAX], name_len: u8, state: TaskState, /// Frame pointer to restore on switch-in (set at spawn; stored on /// every switch-away). saved_rsp: usize, /// Physical base of the stack allocation (0 = none, e.g. the shell). stack: usize, /// Guard page address (unmapped; 0 = none). guard: usize, wake_at: u64, ticks: u64, panic: [u8; PANIC_MAX], panic_len: u8, /// Per-task message ring (see `TaskMsg`). Accessed by the owning /// task (pop) and by any poster (push) — both under the scheduler /// lock, so `send` is callable from ISR or task context. msg: [TaskMsg; MSG_CAP], msg_head: u8, msg_tail: u8, msg_count: u8, } const TASK_NONE: Task = Task { name: [0; NAME_MAX], name_len: 0, state: TaskState::Done, saved_rsp: 0, stack: 0, guard: 0, wake_at: 0, ticks: 0, panic: [0; PANIC_MAX], panic_len: 0, msg: [TaskMsg::User(0); MSG_CAP], msg_head: 0, msg_tail: 0, msg_count: 0, }; static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS]; static mut CURRENT: usize = SHELL_TID; static mut KERNEL_CS: u16 = 0; static mut KERNEL_SS: u16 = 0; static SCHED_READY: AtomicBool = AtomicBool::new(false); static IN_ISR: AtomicBool = AtomicBool::new(false); static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); // --------------------------------------------------------------------------- // Watchdog — turn wedged states into visible panics instead of silent // freezes. Real hardware has no serial console, so the panic handler // renders to the framebuffer and writes the crash log; a freeze that would // otherwise look like a dead machine becomes a reported panic. // // The watchdog only runs when the PIT tick is firing (interrupts on). A // hang with interrupts disabled is invisible to it, so driver waits must // stay bounded (timeout loops) — enforced at the driver level. // --------------------------------------------------------------------------- /// Consecutive ticks where the preemption lock was held (depth > 0). The /// tick cannot switch tasks while the lock is held, so a hold this long is /// a deadlock (a driver wedged inside the serial/klog lock, ...). static STALL_TICKS: AtomicU64 = AtomicU64::new(0); const WATCHDOG_LOCK_TICKS: u64 = 500; // 5 s at 100 Hz /// Per-task "same interrupted frame" counter for spin detection. struct SpinWatch { last_rsp: usize, same: u32, } const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0, }; static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; /// A spinning background task is interrupted at roughly half the tick rate /// (it alternates with idle), so 1500 hits is ~30 s of wall time. const WATCHDOG_SPIN_TICKS: u32 = 1500; /// Boot-phase threshold for task 0 (the shell): before the REPL is live, /// task 0 runs init code where a fixed frame means a driver/controller /// hang — panic after ~15 s wall. The boot must finish well inside this. const WATCHDOG_BOOT_SPIN_TICKS: u32 = 750; /// True once the shell REPL is live (set by main.rs before shell::run()). /// The shell legitimately blocks on its message queue once live; /// before that, task 0 running boot code must not sit in a fixed frame. static SHELL_RUNNING: AtomicBool = AtomicBool::new(false); pub fn mark_shell_running() { SHELL_RUNNING.store(true, Ordering::Relaxed); } // --------------------------------------------------------------------------- // Preemption lock // --------------------------------------------------------------------------- /// Disable preemption (re-entrant counter). The tick/yield handlers /// skip all scheduling work while the depth is > 0. pub fn lock() { PREEMPT_DEPTH.fetch_add(1, Ordering::Relaxed); } /// Re-enable preemption (must pair with `lock`). pub fn unlock() { PREEMPT_DEPTH.fetch_sub(1, Ordering::Relaxed); } pub fn in_isr() -> bool { IN_ISR.load(Ordering::Relaxed) } pub fn set_in_isr(v: bool) { IN_ISR.store(v, Ordering::Relaxed); } pub fn is_ready() -> bool { SCHED_READY.load(Ordering::Relaxed) } pub fn current() -> usize { unsafe { CURRENT } } // --------------------------------------------------------------------------- // Tick / yield entry points (called from the ISR dispatch) // --------------------------------------------------------------------------- /// Timer tick: wake sleepers, account ticks, pick the next task. /// Receives the interrupted task's frame pointer; returns the frame /// pointer the asm should switch to (the same one when no switch). pub fn on_tick(cur: usize) -> usize { if !is_ready() { return cur; } unsafe { // Watchdog: the tick fired, so the CPU is alive. A state we cannot // leave is a hang, not a sleep — surface it as a visible panic. if PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { let n = STALL_TICKS.fetch_add(1, Ordering::Relaxed) + 1; if n >= WATCHDOG_LOCK_TICKS { watchdog_panic(cur, "preemption lock held"); } return cur; } STALL_TICKS.store(0, Ordering::Relaxed); TASKS[CURRENT].saved_rsp = cur; TASKS[CURRENT].ticks += 1; // Spin detection: a task whose interrupted frame never moves // (identical RSP on every tick) is stuck in a hot loop — no yield, // no sleep, no block. The shell (id 0) legitimately blocks on // recv_blocking with a fixed frame, and idle (id 1) hlt's; both // exempt. let id = CURRENT; let w = &mut SPIN_WATCH[id]; if w.last_rsp == cur { w.same += 1; // Background tasks (id >= 2): spinning is never legitimate. // Task 0 (shell): exempt once the REPL is live (recv_blocking // parks with a fixed frame); during the boot phase a fixed // frame means usb/ehci init is wedged on real hardware. let threshold = if id == SHELL_TID && !SHELL_RUNNING.load(Ordering::Relaxed) { WATCHDOG_BOOT_SPIN_TICKS } else if id >= 2 { WATCHDOG_SPIN_TICKS } else { u32::MAX // idle hlt's with a fixed frame forever — fine }; if w.same >= threshold { watchdog_panic(cur, "task spinning without progress"); } } else { w.last_rsp = cur; w.same = 1; } let now = crate::interrupts::uptime_ms(); for t in 0..MAX_TASKS { match TASKS[t].state { TaskState::Sleeping if TASKS[t].wake_at <= now => { TASKS[t].state = TaskState::Ready; } // Safety net for the recv race: a task that blocked // *after* its message was queued (or whose poster never // got to wake it) wakes on the next tick as long as its // queue is non-empty. TaskState::Blocked if TASKS[t].msg_count > 0 => { TASKS[t].state = TaskState::Ready; } _ => {} } } pick_next(cur) } } /// Render a watchdog verdict through the normal panic path (framebuffer + /// serial + crash log) and halt. Runs in tick/ISR context: `cur` is the /// interrupted task's IntFrame, so the reported RIP pinpoints the hang. fn watchdog_panic(cur: usize, why: &str) -> ! { let id = unsafe { CURRENT }; let name = name_str(id); let rip = unsafe { (cur as *const IntFrame).read().rip }; let up = crate::interrupts::uptime_ms(); if id == SHELL_TID { // Boot-phase hang: report the last completed boot stage so the // wedged init step is identifiable without serial. let stage = crate::gop::last_boot_stage(); panic!( "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms) boot-stage: {}", why, id, name, rip, up, stage ); } panic!( "watchdog: {} — task {} \"{}\" rip={:#x} (uptime {} ms)", why, id, name, rip, up ); } /// Software yield (vector 0x60). pub fn on_yield(cur: usize) -> usize { if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { return cur; } unsafe { TASKS[CURRENT].saved_rsp = cur; pick_next(cur) } } /// Round-robin over Ready tasks, skipping the idle task unless nothing /// else can run. Returns the next frame pointer (or `cur` to stay). unsafe fn pick_next(cur: usize) -> usize { unsafe { for i in 1..MAX_TASKS { let idx = (CURRENT + i) % MAX_TASKS; if idx == IDLE_TID { continue; } if TASKS[idx].state == TaskState::Ready { CURRENT = idx; return TASKS[idx].saved_rsp; } } if CURRENT != IDLE_TID && TASKS[IDLE_TID].state == TaskState::Ready { CURRENT = IDLE_TID; return TASKS[IDLE_TID].saved_rsp; } cur } } // --------------------------------------------------------------------------- // Spawn / lifecycle // --------------------------------------------------------------------------- /// Spawn a kernel task running `entry` on its own 8 KiB stack with a /// guard page below it. Returns the task id, or None when the table is /// full (Done slots are reaped first). `entry` runs inside a /// trampoline, so returning from it exits the task cleanly. pub fn spawn(entry: fn(), name: &str) -> Option { lock(); let id = unsafe { // Reap a Done task with a stack, or take a never-used slot. let mut slot = None; for i in 2..MAX_TASKS { if TASKS[i].state == TaskState::Done && TASKS[i].stack != 0 { reap(i); slot = Some(i); break; } if TASKS[i].state == TaskState::Done && TASKS[i].stack == 0 && TASKS[i].name_len == 0 { slot = Some(i); break; } } slot? }; let ok = spawn_at(id, entry, name); unlock(); if ok { Some(id) } else { None } } unsafe fn reap(id: usize) { unsafe { let t = &mut TASKS[id]; if t.stack != 0 { // Restore the guard page's mapping so the pages are reusable. paging::map_page(t.stack, paging::PAGE_WRITABLE); memory::free_pages(t.stack, STACK_PAGES); } *t = TASK_NONE; } } fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { let Some(base) = memory::alloc_pages(STACK_PAGES) else { return false; }; // The first page is the guard — unmap it so any stack overflow // faults there (caught on the vector-14 IST stack). if !paging::unmap_page(base) { memory::free_pages(base, STACK_PAGES); return false; } let top = base + STACK_PAGES * memory::PAGE_SIZE; let frame = (top - core::mem::size_of::()) as *mut IntFrame; let mut name_buf = [0u8; NAME_MAX]; let n = name.len().min(NAME_MAX); name_buf[..n].copy_from_slice(&name.as_bytes()[..n]); unsafe { core::ptr::write( frame, IntFrame { r15: 0, r14: 0, r13: 0, r12: 0, r11: 0, r10: 0, r9: 0, r8: 0, rdi: 0, rsi: 0, rbp: 0, rbx: 0, rdx: 0, rcx: entry as usize as u64, // MS x64 ABI: first arg rax: 0, vector: 0, error: 0, rip: trampoline as *const () as u64, cs: KERNEL_CS as u64, rflags: 0x202, // IF | reserved bit 1 // QEMU's iretq always pops RSP/SS from the frame tail. // rsp = the stack top so the first iretq lands with a // valid stack pointer (the trampoline then descends from // it normally); ss mirrors the kernel data selector. rsp: top as u64, ss: KERNEL_SS as u64, }, ); TASKS[id] = Task { name: name_buf, name_len: n as u8, state: TaskState::Ready, saved_rsp: frame as usize, stack: base, guard: base, wake_at: 0, ticks: 0, panic: [0; PANIC_MAX], panic_len: 0, msg: [TaskMsg::User(0); MSG_CAP], msg_head: 0, msg_tail: 0, msg_count: 0, }; } true } /// Entry trampoline: run the task body, then exit the task when it /// returns. Runs with the synthetic frame's registers (rcx = entry). unsafe extern "C" fn trampoline(entry: usize) -> ! { let f: fn() = unsafe { core::mem::transmute(entry as *const ()) }; f(); task_exit() } fn task_exit() -> ! { lock(); unsafe { TASKS[CURRENT].state = TaskState::Done; } unlock(); yield_int(); unreachable!() } /// Block the current task for `ms` milliseconds. The shell uses this /// for `sleep`; background tasks keep running meanwhile. pub fn sleep_ms(ms: u64) { lock(); unsafe { TASKS[CURRENT].state = TaskState::Sleeping; TASKS[CURRENT].wake_at = crate::interrupts::uptime_ms() + ms; } unlock(); yield_int(); } /// Cooperatively yield the rest of the current quantum. pub fn yield_now() { yield_int(); } // --------------------------------------------------------------------------- // Message passing // --------------------------------------------------------------------------- /// Post a message to a task's queue and wake it. Safe from any context: /// a task (e.g. a window manager routing input), the shell, or an ISR — /// the scheduler lock makes the queue mutation atomic w.r.t. switching. /// Returns an error when the task is gone or its queue is full. pub fn send(id: usize, msg: TaskMsg) -> Result<(), &'static str> { if id >= MAX_TASKS { return Err("no such task"); } lock(); let r = unsafe { let t = &TASKS[id]; if t.name_len == 0 || t.state == TaskState::Done { Err("no such task") } else if push_msg(id, msg) { if TASKS[id].state == TaskState::Blocked { TASKS[id].state = TaskState::Ready; } Ok(()) } else { Err("queue full") } }; unlock(); r } /// Pop the oldest pending message of the current task, if any. pub fn recv() -> Option { lock(); let m = unsafe { pop_msg(CURRENT) }; unlock(); m } /// Block until a message arrives, then return it. Wake is two-fold: /// `send` flips a Blocked task straight to Ready, and the tick scan /// (see `on_tick`) catches the race where the task blocked after the /// message was already queued. pub fn recv_blocking() -> TaskMsg { loop { if let Some(m) = recv() { return m; } lock(); unsafe { TASKS[CURRENT].state = TaskState::Blocked; } unlock(); yield_int(); } } /// Push onto the task's ring (caller holds the scheduler lock). unsafe fn push_msg(id: usize, msg: TaskMsg) -> bool { unsafe { let t = &mut TASKS[id]; if t.msg_count as usize >= MSG_CAP { return false; } t.msg[t.msg_tail as usize] = msg; t.msg_tail = (t.msg_tail + 1) & (MSG_CAP as u8 - 1); t.msg_count += 1; true } } /// Pop from the current task's ring (caller holds the scheduler lock). unsafe fn pop_msg(id: usize) -> Option { unsafe { let t = &mut TASKS[id]; if t.msg_count == 0 { return None; } let m = t.msg[t.msg_head as usize]; t.msg_head = (t.msg_head + 1) & (MSG_CAP as u8 - 1); t.msg_count -= 1; Some(m) } } /// Pending message count of a task (for `jobs`). pub fn pending_msgs(id: usize) -> usize { lock(); let n = unsafe { TASKS[id].msg_count as usize }; unlock(); n } /// Is the task slot live (spawned and not Done)? Used by the shell's /// `mouse ` routing check and by the window manager later. pub fn is_alive(id: usize) -> bool { if id >= MAX_TASKS { return false; } lock(); let alive = unsafe { TASKS[id].name_len != 0 && TASKS[id].state != TaskState::Done }; unlock(); alive } /// Software-interrupt the scheduler (vector 0x60). Net stack usage is /// zero (the frame is popped by iretq), so no `nostack` — the compiler /// must not use the red zone around the interrupt. #[inline(never)] fn yield_int() { unsafe { asm!("int 0x60"); } } // --------------------------------------------------------------------------- // Task panics and faults // --------------------------------------------------------------------------- /// Kill the current task with a panic message: logs it, marks the task /// Done, drops its scheduler lock (a dead task's lock is meaningless), /// and switches away forever. Called by the panic hook (task context) /// and by the page-fault handler (IST stack, ISR context). pub fn abort_current(msg: &str) -> ! { unsafe { let id = CURRENT; let n = msg.len().min(PANIC_MAX); TASKS[id].panic[..n].copy_from_slice(&msg.as_bytes()[..n]); TASKS[id].panic_len = n as u8; TASKS[id].state = TaskState::Done; crate::serial_println!( "sched: task {} \"{}\" panicked: {} (rsp_saved={:#x} stack={:#x} guard={:#x})", id, name_str(id), msg, TASKS[id].saved_rsp, TASKS[id].stack, TASKS[id].guard ); PREEMPT_DEPTH.store(0, Ordering::Relaxed); } yield_int(); unreachable!() } /// Vector 14 (page fault) handler — runs on the dedicated IST stack. /// A fault in a real task (id >= 2) kills the task gracefully; anything /// else (shell, idle, pre-scheduler) is a kernel bug: halt. /// Dump a fatal exception to serial *and* the screen (raw, bypassing the /// vt), then halt the shell or abort the faulting background task. /// /// Real hardware has no serial console, so the screen dump is what lets /// a user report a crash precisely — vector, fault address, RIP and task. /// Runs with interrupts disabled (we're inside the ISR). /// /// Heap-free by construction: the dump's own `alloc::format!` used to /// allocate from the heap, and when the underlying fault *was* heap /// corruption the reporter itself died (#PF → #DF → triple fault → /// silent reboot). Everything here formats into stack buffers, and the /// ISR `in_dump` guard halts instead of recursing if even that faults. #[allow(clippy::needless_return)] pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFrame) -> ! { crate::interrupts::enter_dump(); let vector = frame.vector; let error = frame.error; let rip = frame.rip; let rflags = frame.rflags; let rsp = frame.rsp; let id = unsafe { CURRENT }; let name = name_str(id); let in_kernel = unsafe { CURRENT } <= IDLE_TID; let img = crate::memory::kernel_image_range(); // Return-address filter: `.text` only (code addresses). Falls // back to the full image range if the PE headers are unreadable. let filter = crate::memory::text_range(); // Backtrace: candidate return addresses on the faulting stack. // 1) Walk the rbp chain (functions that keep frame pointers). // 2) Fill any remaining slots with a raw scan of the stack above // the interrupted rsp — robust when rbp is stale, since rustc // eliminates frame pointers in many functions. Both filters // accept only addresses inside the kernel image range; the // range is `(start, end)`, so the second element is used as-is. let mut bt: [u64; 12] = [0; 12]; let mut bn = 0usize; unsafe { let mut fp = frame.rbp; let mut steps = 0; while bn < bt.len() && steps < 64 { if fp == 0 || fp < frame.rsp.saturating_sub(0x8000) || fp > frame.rsp.saturating_add(0x100000) { break; } let ra = *(fp as *const u64).add(1); // [rbp+8] = return address if let Some((start, end)) = filter && ra >= start as u64 && ra < end as u64 { bt[bn] = ra; bn += 1; } let nfp = *(fp as *const u64); if nfp <= fp { break; // chain must grow upward } fp = nfp; steps += 1; } // Raw scan: 32 KiB above the interrupted rsp. let mut p = frame.rsp; let limit = frame.rsp.saturating_add(0x8000); while p + 8 <= limit && bn < bt.len() { let v = *(p as *const u64); if let Some((start, end)) = filter && v >= start as u64 && v < end as u64 { let mut dup = false; for i in 0..bn { if bt[i] == v { dup = true; break; } } if !dup { bt[bn] = v; bn += 1; } } p += 8; } } // Serial: a full, parseable line (stack-formatted). let mut buf = [0u8; 176]; let n = { let mut w = crate::strbuf::FmtBuf::new(&mut buf); let _ = fmt::write( &mut w, core::format_args!( "!!! {} vec={} err={:#x} rip={:#x} cr2={:#x} rsp={:#x} rbp={:#x} rflags={:#x} task={} \"{}\" img={:#x}", kind, vector, error, rip, cr2, rsp, frame.rbp, rflags, id, name, img.map(|(b, _)| b).unwrap_or(0) ), ); w.len() }; let line = core::str::from_utf8(&buf[..n]).unwrap_or("!!! dump"); crate::serial::write_str(line); crate::serial::write_str("\r\n!!! halting (shell task fault — machine stops)\r\n"); if bn > 0 { let mut bb = [0u8; 256]; let bbn = { let mut w = crate::strbuf::FmtBuf::new(&mut bb); let mut first = true; for i in 0..bn { if !first { let _ = fmt::write(&mut w, core::format_args!(" ")); } first = false; let _ = fmt::write(&mut w, core::format_args!("{:#x}", bt[i])); } w.len() }; let bt_line = core::str::from_utf8(&bb[..bbn]).unwrap_or("?"); crate::serial::write_str("bt="); crate::serial::write_str(bt_line); crate::serial::write_str("\r\n"); } // Screen: white-on-red band via the clamped raw dump path (heap-free). let mut b1 = [0u8; 72]; let mut b2 = [0u8; 72]; let mut b3 = [0u8; 96]; let n1 = { let mut w = crate::strbuf::FmtBuf::new(&mut b1); let _ = fmt::write( &mut w, core::format_args!("!!! {} vec={} err={:#x}", kind, vector, error), ); w.len() }; let n2 = { let mut w = crate::strbuf::FmtBuf::new(&mut b2); let _ = fmt::write(&mut w, core::format_args!("rip={:#x} cr2={:#x}", rip, cr2)); w.len() }; let n3 = { let mut w = crate::strbuf::FmtBuf::new(&mut b3); let _ = fmt::write( &mut w, core::format_args!( "rsp={:#x} task={} {} img={:#x}", rsp, id, name, img.map(|(b, _)| b).unwrap_or(0) ), ); w.len() }; let mut b4 = [0u8; 216]; let n4 = { let mut w = crate::strbuf::FmtBuf::new(&mut b4); if bn > 0 { let _ = fmt::write(&mut w, core::format_args!("bt=")); let mut first = true; for i in 0..bn { if !first { let _ = fmt::write(&mut w, core::format_args!(" ")); } first = false; let _ = fmt::write(&mut w, core::format_args!("{:#x}", bt[i])); } } else { let _ = fmt::write(&mut w, core::format_args!("bt=none")); } w.len() }; let l1 = core::str::from_utf8(&b1[..n1]).unwrap_or("!!!"); let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or("bt=?"); // Register dump — the faulting register state, so a real-hardware // repro can be diagnosed without serial. Two lines on screen (the // band is clamped), full set in crash.log via `lines`. let mut r1 = [0u8; 160]; let rn1 = { let mut w = crate::strbuf::FmtBuf::new(&mut r1); let _ = fmt::write( &mut w, core::format_args!( "rax={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x}", frame.rax, frame.rcx, frame.rdx, frame.rsi, frame.rdi ), ); w.len() }; let mut r2 = [0u8; 160]; let rn2 = { let mut w = crate::strbuf::FmtBuf::new(&mut r2); let _ = fmt::write( &mut w, core::format_args!( "rbx={:#x} r8={:#x} r9={:#x} r10={:#x} r11={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x}", frame.rbx, frame.r8, frame.r9, frame.r10, frame.r11, frame.r12, frame.r13, frame.r14, frame.r15 ), ); w.len() }; let l5 = core::str::from_utf8(&r1[..rn1]).unwrap_or(""); let l6 = core::str::from_utf8(&r2[..rn2]).unwrap_or(""); crate::serial::write_raw(l5); crate::serial::write_raw("\r\n"); crate::serial::write_raw(l6); crate::serial::write_raw("\r\n"); let lines = [l1, l2, l3, l4, l5, l6]; crate::gop::dump_lines(&lines); // Best-effort: persist the report to the disk so it survives the // reboot on real hardware (no serial console). Any fault here is // caught by the in_dump guard and halts. crate::fs::write_crash_log(&lines); if in_kernel { crate::interrupts::halt() } else { let mut mb = [0u8; 48]; let mn = { let mut w = crate::strbuf::FmtBuf::new(&mut mb); let _ = fmt::write(&mut w, core::format_args!("{} vec={}", kind, vector)); w.len() }; abort_current(core::str::from_utf8(&mb[..mn]).unwrap_or("fault")) } } pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize { if !is_ready() || unsafe { CURRENT } <= IDLE_TID { let frame = unsafe { &*(frame_ptr as *const crate::interrupts::IntFrame) }; fault_dump("page fault", cr2, frame) } let g = unsafe { TASKS[CURRENT].guard }; let in_guard = g != 0 && cr2 >= g as u64 && cr2 < (g + memory::PAGE_SIZE) as u64; let rip = unsafe { (frame_ptr as *const IntFrame).read().rip }; let mut buf = [0u8; 96]; let n = { let mut f = crate::strbuf::FmtBuf::new(&mut buf); let _ = fmt::write( &mut f, core::format_args!( "{} at {:#x} (rip={:#x})", if in_guard { "stack overflow (guard page)" } else { "page fault" }, cr2, rip ), ); f.len() }; abort_current(core::str::from_utf8(&buf[..n]).unwrap_or("task fault")) } // --------------------------------------------------------------------------- // Introspection (shell: jobs / kill) // --------------------------------------------------------------------------- /// Snapshot the task table for `jobs` — one line per used slot. pub fn jobs() -> Vec { let mut v = Vec::new(); lock(); unsafe { for i in 0..MAX_TASKS { let t = &TASKS[i]; if t.name_len == 0 { continue; // never used } let state = match t.state { TaskState::Ready => "ready", TaskState::Sleeping => "sleeping", TaskState::Blocked => "blocked", TaskState::Done => { if t.panic_len > 0 { "done (panicked)" } else { "done" } } }; let mut line = alloc::format!( " {:>2} {:<12} {:<15} {:>6} ticks q={}", i, name_str(i), state, t.ticks, t.msg_count ); if t.panic_len > 0 { let m = core::str::from_utf8(&t.panic[..t.panic_len as usize]).unwrap_or("?"); line.push_str(" "); line.push_str(m); } v.push(line); } } unlock(); v } /// Mark a task Done so it stops running and gets reaped at the next /// spawn. The shell (0) and idle (1) are protected. pub fn kill(id: usize) -> Result<(), &'static str> { if id == SHELL_TID { return Err("cannot kill the shell (task 0)"); } if id == IDLE_TID { return Err("cannot kill the idle task (task 1)"); } if id >= MAX_TASKS { return Err("no such task"); } lock(); let alive = unsafe { let t = &TASKS[id]; t.name_len != 0 && t.state != TaskState::Done }; if !alive { unlock(); return Err("no such task"); } unsafe { TASKS[id].state = TaskState::Done; } unlock(); Ok(()) } pub fn name_str(id: usize) -> &'static str { unsafe { let t = &TASKS[id]; if t.name_len == 0 { return ""; } core::str::from_utf8(&t.name[..t.name_len as usize]).unwrap_or("") } } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- /// Set up the task table: task 0 is the shell (boot stack), task 1 is /// the idle `hlt` fallback. Called after `interrupts::init` (the tick /// must be live so `sleep`/wake scheduling works). pub fn init() { unsafe { let cs64: u64; asm!("mov {0}, cs", out(reg) cs64, options(nostack, preserves_flags)); KERNEL_CS = cs64 as u16; let ss64: u64; asm!("mov {0}, ss", out(reg) ss64, options(nostack, preserves_flags)); KERNEL_SS = ss64 as u16; TASKS[SHELL_TID] = Task { name: { let mut n = [0u8; 24]; n[..5].copy_from_slice(b"shell"); n }, name_len: 5, state: TaskState::Ready, saved_rsp: 0, // captured on the first switch-away stack: 0, guard: 0, wake_at: 0, ticks: 0, panic: [0; PANIC_MAX], panic_len: 0, msg: [TaskMsg::User(0); MSG_CAP], msg_head: 0, msg_tail: 0, msg_count: 0, }; spawn_at(IDLE_TID, idle_main, "idle"); SCHED_READY.store(true, Ordering::Relaxed); } crate::serial_println!( "sched: ready — {} slots, shell=task 0, idle=task 1", MAX_TASKS ); crate::serial_println!( "sched: trampoline={:#x} idle={:#x} emit={:#x}", trampoline as *const () as usize, idle_main as *const () as usize, crate::klog::emit as *const () as usize ); } fn idle_main() { loop { unsafe { asm!("hlt", options(nostack, preserves_flags)); } } }