From 8f05ea44f69cb796e7b1f581ba1b8045c98d615b Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 17:38:57 +0200 Subject: rsnix: preemptive kernel tasks (ADR 0003) — sched, IST fault stacks, 176-byte frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sched: 16-slot static table, shell=task 0, idle=task 1, spawn with guard page + 8 KiB stack, RR on the 100 Hz tick, int 0x60 yield, sleep_ms, lazy reap, jobs/bg/sleep/kill shell commands - gdt: own GDT mirroring UEFI selectors + TSS with IST1/IST2 (guard-page overflows die gracefully instead of triple-faulting) - interrupts: isr_dispatch returns next frame in rax; IntFrame grows rsp/ss fields — QEMU's 64-bit iretq always pops all five slots from a 176-byte frame, so synthetic task frames must carry rsp=top+ss (a 160-byte frame made the first iretq pop rsp/ss from the unmapped page beyond the frame) - panic handler: task panics abort the task, shell keeps running - main: sched::init moved after page allocator + adopted tables (the idle's stack+guard need both; it was silently not spawning) --- CONTEXT.md | 45 ++++ Cargo.lock | 6 +- Cargo.toml | 2 +- rsdos/src/main.rs | 46 ++++ rsnix/src/gdt.rs | 242 +++++++++++++++++++++ rsnix/src/interrupts.rs | 139 ++++++++++--- rsnix/src/klog.rs | 17 +- rsnix/src/lib.rs | 2 + rsnix/src/sched.rs | 544 ++++++++++++++++++++++++++++++++++++++++++++++++ rsnix/src/serial.rs | 35 +++- shell/src/lib.rs | 155 +++++++++++++- 11 files changed, 1180 insertions(+), 53 deletions(-) create mode 100644 rsnix/src/gdt.rs create mode 100644 rsnix/src/sched.rs diff --git a/CONTEXT.md b/CONTEXT.md index 924854c..112b8de 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -359,6 +359,51 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: also untestable in QEMU. The disk arrival/removal path (PxSSTS based) is verified on real hardware instead. +✅ **Preemptive kernel tasks (ADR 0003)** — round-robin multitasking + on the 100 Hz PIT tick, one shared identity-mapped address space: + +- `sched`: 16-slot static task table (no ISR-time allocation); task 0 + = shell (boot stack), task 1 = idle `hlt` loop, tasks 2+ spawned + via `sched::spawn(entry, name)` with 3 pages each (1 unmapped + guard + 8 KiB stack); preemptive RR with `int $0x60` software + yield; `sleep_ms`/`yield_now`; lazy reaping of Done slots. +- `gdt`: kernel-owned GDT mirroring UEFI's selector layout (no + far-jump; CS/DS/SS keep working) plus a TSS with IST1 (page + fault) and IST2 (double fault) stacks — a stack overflow faults + inside the guard page, the CPU pushes the fault frame onto the + IST stack, and the task dies gracefully instead of triple- + faulting the machine (the ADR's guard-page promise, honored). +- `interrupts`: `isr_dispatch` returns the next frame pointer in + rax; asm does `mov rsp, rax` → pop → iretq. **QEMU quirk that + bit us**: QEMU's 64-bit interrupt entry always pushes + SS/RSP/RFLAGS/CS/RIP (40 bytes) and iretq always pops all five + (its same-privilege branch is never taken for 64-bit iretq), so + interrupt frames are 176 bytes and the synthetic task frames + must carry rsp=stack-top + ss — a 160-byte frame made the first + iretq pop rsp/ss from the unmapped page beyond the frame (task + started with rsp=0, or the iretq itself page-faulted). Real + hardware pushes only 24 bytes at same privilege, so the extra + fields are inert there; the synthetic frames set them + explicitly either way. +- Panic handling: a task panic (or guard-page overflow) is caught, + logged as `sched: task N "name" panicked: …`, the task is marked + Done, and the scheduler switches away — the shell keeps running. + Shell/idle/ISR/pre-scheduler panics still halt the kernel. +- Shell: `bg ` (spinner/ticker/counter demo + deliberate + `boom` panic + `overflow` recursion), `jobs` (state/ticks/ + panic message), `sleep ` (blocks only the shell), `kill + `. Only the shell touches disk/fs/tty/vt; background tasks + are headless (sleep + serial/klog only). +- Boot order note: `sched::init` must run after the page allocator + and adopted page tables (the idle's stack + guard need both) — + it was originally before them, silently dropping the idle task. + + Verified in QEMU (9-step script): demo interleaving at expected + cadences, sleep keeps background tasks logging, jobs states, + graceful `boom` panic with corpse in jobs, kill stops a task, and + `bg overflow` dies on the guard page with the machine intact; full + regression (rescan/til/klog/grammar/pager) re-passes. + ## Known issues / perf notes - **Scroll cost** — scrolling still shifts the whole framebuffer (a diff --git a/Cargo.lock b/Cargo.lock index 7b1ced6..2931bd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "rsdos" -version = "0.2.0" +version = "0.3.0" dependencies = [ "rsnix", "shell", @@ -19,11 +19,11 @@ dependencies = [ [[package]] name = "rsnix" -version = "0.2.0" +version = "0.3.0" [[package]] name = "shell" -version = "0.2.0" +version = "0.3.0" dependencies = [ "rsdosedit", "rsnix", diff --git a/Cargo.toml b/Cargo.toml index eb42b42..aab42e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["rsnix", "shell", "rsdos", "rsdosedit"] [workspace.package] -version = "0.2.0" +version = "0.3.0" edition = "2024" # no_std UEFI kernel: no unwinder available, so panic must abort. diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index a528810..7ca031c 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs @@ -8,9 +8,44 @@ use rsnix::uefi::*; // --------------------------------------------------------------------------- // Panic handler // --------------------------------------------------------------------------- + +/// Small fmt::Write over a byte slice (no allocation, bounded). +struct PanicBuf<'a> { + b: &'a mut [u8], + n: usize, +} + +impl core::fmt::Write for PanicBuf<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let m = s.len().min(self.b.len() - self.n); + self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]); + self.n += m; + Ok(()) + } +} + #[panic_handler] fn panic(info: &PanicInfo) -> ! { use core::fmt::Write; + + // A background task (id >= 2) panicked: die gracefully — log it, + // mark the task done, switch away. The shell keeps running and + // `jobs` shows the corpse. Panics in the shell, idle, ISR context, + // or before the scheduler exists still halt the whole system. + if rsnix::sched::is_ready() && !rsnix::sched::in_isr() && rsnix::sched::current() >= 2 { + let mut buf = [0u8; 192]; + let n = { + let mut w = PanicBuf { b: &mut buf, n: 0 }; + let _ = write!(&mut w, "{}", info.message()); + if let Some(loc) = info.location() { + let _ = write!(&mut w, " at {}:{}", loc.file(), loc.line()); + } + w.n + }; + let msg = core::str::from_utf8(&buf[..n]).unwrap_or("task panic"); + rsnix::sched::abort_current(msg); + } + rsnix::serial::write_str("\r\n*** PANIC ***\r\n"); let _ = write!(&mut rsnix::serial::SerialWriter, "{}\r\n", info.message()); if let Some(loc) = info.location() { @@ -114,6 +149,11 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut rsnix::serial_println!("serial: COM1 115200 8N1"); rsnix::ps2::init(); rsnix::serial_println!("ps2: keyboard initialized"); + if rsnix::gdt::init() { + rsnix::serial_println!("gdt: own GDT + TSS (IST: page-fault/double-fault)"); + } else { + rsnix::serial_println!("gdt: kept UEFI GDT (IST fault stacks off)"); + } rsnix::interrupts::init(); rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); @@ -151,6 +191,12 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut rsnix::serial_println!("pg: self-test FAILED"); } + // Scheduler init needs the page allocator (idle task's stack + guard + // page) and the adopted page tables, so it runs after the memory + // subsystem is fully up. The tick (interrupts::init) is already + // live, which is what `sleep`/wake scheduling depends on. + rsnix::sched::init(); + let t = rsnix::rtc::now(); rsnix::serial_println!( "rtc: {}-{:02}-{:02} {:02}:{:02}:{:02}", diff --git a/rsnix/src/gdt.rs b/rsnix/src/gdt.rs new file mode 100644 index 0000000..514ed2e --- /dev/null +++ b/rsnix/src/gdt.rs @@ -0,0 +1,242 @@ +// --------------------------------------------------------------------------- +// GDT + TSS — kernel-owned segment tables and interrupt stack tables (IST). +// +// The kernel boots on UEFI's GDT. We take over with our own GDT that +// mirrors the current selector layout (flat ring-0 code/data descriptors +// at the same indices UEFI chose), so CS/DS/SS/ES keep working without +// any far-jump or segment-reload gymnastics. On top of that we add a +// TSS with two interrupt stack tables: +// +// - IST1: page-fault (vector 14) stack. A stack-overflowing task +// faults inside its unmapped guard page; the CPU pushes the fault +// frame onto this dedicated stack instead of the faulting (also +// unmapped) stack, so we can kill the task gracefully instead of +// double/triple-faulting into a machine reset. +// - IST2: double-fault (vector 8) stack. If anything else goes wrong +// during exception handling we land here and halt cleanly. +// +// We never change privilege level, so the TSS's RSP0..2 are unused — +// only the IST pointers matter. +// --------------------------------------------------------------------------- +use core::arch::asm; + +const GDT_ENTRIES: usize = 64; +const TSS_INDEX: usize = GDT_ENTRIES - 1; // 63 +const TSS_SEL: u16 = (TSS_INDEX << 3) as u16; // 0x1F8 + +/// 8-byte segment descriptor. +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct GdtEntry { + limit_lo: u16, + base_lo: u16, + base_mid: u8, + access: u8, + flags_limit: u8, // limit 19:16 | flags nibble (G D/B L AVL) + base_hi: u8, +} + +impl GdtEntry { + const fn null() -> Self { + Self { + limit_lo: 0, + base_lo: 0, + base_mid: 0, + access: 0, + flags_limit: 0, + base_hi: 0, + } + } + /// Flat ring-0 64-bit code segment (present, exec/read, L=1). + const fn code() -> Self { + Self { + limit_lo: 0xFFFF, + base_lo: 0, + base_mid: 0, + access: 0x9A, + flags_limit: 0xAF, // G=1, D/B=0, L=1, AVL=0; limit 0xF + base_hi: 0, + } + } + /// Flat ring-0 data segment (present, read/write). + const fn data() -> Self { + Self { + limit_lo: 0xFFFF, + base_lo: 0, + base_mid: 0, + access: 0x92, + flags_limit: 0xCF, // G=1, D/B=1, L=0, AVL=0; limit 0xF + base_hi: 0, + } + } +} + +/// 64-bit TSS: only the IST pointers are used (no privilege changes). +#[repr(C, packed)] +#[derive(Clone, Copy)] +struct Tss { + _res1: u32, + rsp0: u64, + rsp1: u64, + rsp2: u64, + _res2: u64, + ist1: u64, + ist2: u64, + ist3: u64, + ist4: u64, + ist5: u64, + ist6: u64, + ist7: u64, + _res3: u64, + _res4: u16, + iomap_base: u16, +} + +const TSS_SIZE: usize = 104; + +impl Tss { + const fn zero() -> Self { + Self { + _res1: 0, + rsp0: 0, + rsp1: 0, + rsp2: 0, + _res2: 0, + ist1: 0, + ist2: 0, + ist3: 0, + ist4: 0, + ist5: 0, + ist6: 0, + ist7: 0, + _res3: 0, + _res4: 0, + iomap_base: 0, + } + } +} + +/// 4 KiB page-aligned block for the IST stacks. +#[repr(align(4096))] +struct Page([u8; 4096]); + +static mut PF_STACK: Page = Page([0; 4096]); +static mut DF_STACK: Page = Page([0; 4096]); +static mut TSS: Tss = Tss::zero(); +// GDT slots are 8 bytes except the TSS descriptor at index 63, which is +// 16 bytes — total 64*8 + 16. +static mut GDT: [u8; GDT_ENTRIES * 8 + 16] = [0; GDT_ENTRIES * 8 + 16]; +static mut GDTR: [u16; 5] = [0; 5]; + +unsafe fn read_seg(which: u8) -> u16 { + // `which` picks the segment register to read (0=cs, 1=ds, 2=ss, ...). + let v: u64; + unsafe { + match which { + 0 => asm!("mov {0}, cs", out(reg) v, options(nostack, preserves_flags)), + 1 => asm!("mov {0}, ds", out(reg) v, options(nostack, preserves_flags)), + 2 => asm!("mov {0}, ss", out(reg) v, options(nostack, preserves_flags)), + 3 => asm!("mov {0}, es", out(reg) v, options(nostack, preserves_flags)), + 4 => asm!("mov {0}, fs", out(reg) v, options(nostack, preserves_flags)), + _ => asm!("mov {0}, gs", out(reg) v, options(nostack, preserves_flags)), + } + } + v as u16 +} + +/// Install the kernel's own GDT (mirroring UEFI's selectors) and a TSS +/// with IST stacks for page-fault and double-fault handling. +/// +/// Returns false if UEFI's selector layout cannot be mirrored (any +/// nonzero selector at an index >= TSS_INDEX) — then we keep UEFI's GDT +/// and the IST-based fault stacks stay off (guard pages still exist but +/// an overflow would fault while pushing the fault frame → machine +/// reset instead of a graceful task death). +/// +/// Must run with interrupts disabled and before `interrupts::init` (the +/// IDT's vector-8/14 entries carry the IST indices). +pub fn init() -> bool { + unsafe { + asm!("cli", options(nostack)); + + let cs = read_seg(0); + let ds = read_seg(1); + let ss = read_seg(2); + let es = read_seg(3); + let fs = read_seg(4); + let gs = read_seg(5); + let sels = [cs, ds, ss, es, fs, gs]; + + // Every nonzero selector must land on a descriptor slot below + // TSS_INDEX (63 holds the 16-byte TSS descriptor). + for &s in &sels { + if s != 0 && (s >> 3) as usize >= TSS_INDEX { + return false; + } + } + + let gdt = core::ptr::addr_of_mut!(GDT[0]); + let write_entry = |idx: usize, e: GdtEntry| { + let p = gdt.add(idx * 8) as *mut GdtEntry; + core::ptr::write_unaligned(p, e); + }; + + write_entry(0, GdtEntry::null()); + write_entry((cs >> 3) as usize, GdtEntry::code()); + let cs_idx = (cs >> 3) as usize; + for &s in &sels { + let idx = (s >> 3) as usize; + if s != 0 && idx != 0 && idx != cs_idx { + write_entry(idx, GdtEntry::data()); + } + } + + // TSS descriptor (16 bytes) at index 63. + let tss_base = core::ptr::addr_of_mut!(TSS) as usize as u64; + TSS.ist1 = core::ptr::addr_of_mut!(PF_STACK.0[0]) as usize as u64 + 4096; + TSS.ist2 = core::ptr::addr_of_mut!(DF_STACK.0[0]) as usize as u64 + 4096; + TSS.iomap_base = TSS_SIZE as u16; // no I/O bitmap + + let mut td = [0u8; 16]; + td[0..2].copy_from_slice(&((TSS_SIZE - 1) as u16).to_le_bytes()); + td[2..4].copy_from_slice(&(tss_base as u16).to_le_bytes()); + td[4] = (tss_base >> 16) as u8; + td[5] = 0x89; // present, available 64-bit TSS + td[6] = 0x00; // limit 19:16 + flags = 0 + td[7] = (tss_base >> 24) as u8; + td[8..12].copy_from_slice(&((tss_base >> 32) as u32).to_le_bytes()); + // td[12..16] reserved — zero. + core::ptr::copy_nonoverlapping(td.as_ptr(), gdt.add(TSS_INDEX * 8), 16); + + // GDTR: limit = 63*8 + 16 - 1 = 519. + GDTR = [ + (TSS_INDEX * 8 + 16 - 1) as u16, + gdt as u16, + (gdt as u64 >> 16) as u16, + (gdt as u64 >> 32) as u16, + (gdt as u64 >> 48) as u16, + ]; + asm!("lgdt [{}]", in(reg) core::ptr::addr_of!(GDTR), options(nostack, preserves_flags)); + asm!("ltr ax", in("ax") TSS_SEL, options(nostack, preserves_flags)); + + // Re-load the data segments so the CPU re-validates them against + // the new GDT (CS cannot be reloaded by mov; its descriptor is + // unchanged at the same index). + if ds != 0 { + asm!("mov ds, ax", in("ax") ds, options(nostack, preserves_flags)); + } + if ss != 0 { + asm!("mov ss, ax", in("ax") ss, options(nostack, preserves_flags)); + } + if es != 0 { + asm!("mov es, ax", in("ax") es, options(nostack, preserves_flags)); + } + if fs != 0 { + asm!("mov fs, ax", in("ax") fs, options(nostack, preserves_flags)); + } + if gs != 0 { + asm!("mov gs, ax", in("ax") gs, options(nostack, preserves_flags)); + } + true + } +} diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index c176276..6abd54d 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs @@ -22,14 +22,14 @@ struct IdtEntry { } /// Fill one IDT entry at the given offset (in bytes). -unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8) { +unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8, ist: u8) { unsafe { let off = (vec as usize) * 16; let p = idt.add(off) as *mut IdtEntry; p.write(IdtEntry { offset_lo: handler as u16, selector, - ist: 0, + ist, flags: flags | 0x80, // bit 7 = present offset_mid: (handler >> 16) as u16, offset_hi: (handler >> 32) as u32, @@ -213,6 +213,19 @@ static KEYBUF: KeyBuf = KeyBuf::new(); core::arch::global_asm!( // ISR stubs — push vector number + error code, jump to common. + // Exceptions that push their own error code (8, 14) skip the push-0. + ".globl isr_stub_08", + ".p2align 4", + "isr_stub_08:", + " push 8", // vector (CPU already pushed the error code) + " jmp isr_common", + "", + ".globl isr_stub_14", + ".p2align 4", + "isr_stub_14:", + " push 14", + " jmp isr_common", + "", ".globl isr_stub_32", ".p2align 4", "isr_stub_32:", @@ -227,6 +240,15 @@ core::arch::global_asm!( " push 33", " jmp isr_common", "", + // Software yield (vector 0x60 — outside the PIC's 32-47 range, which + // is why 0x40 was rejected: 0x40 = 64 decimal = slave IRQ 8 slot). + ".globl isr_stub_60", + ".p2align 4", + "isr_stub_60:", + " push 0", + " push 0x60", + " jmp isr_common", + "", // Catch-all for unexpected interrupts. ".globl isr_stub_spurious", ".p2align 4", @@ -235,7 +257,8 @@ core::arch::global_asm!( " push 0xFF", " jmp isr_common", "", - // Common handler — save all registers, call into Rust, restore, iretq. + // Common handler — save all registers, call into Rust (which returns + // the next task's frame pointer in rax), switch rsp, restore, iretq. "isr_common:", " push rax", " push rcx", @@ -255,6 +278,7 @@ core::arch::global_asm!( "", " mov rcx, rsp", // first arg (rcx = MS x64 ABI) " call isr_dispatch", + " mov rsp, rax", // context switch: rax = next task's frame ptr "", " pop r15", " pop r14", @@ -277,37 +301,49 @@ core::arch::global_asm!( /// Saved register frame (as pushed by `isr_common`). #[repr(C)] -struct IntFrame { - r15: u64, - r14: u64, - r13: u64, - r12: u64, - r11: u64, - r10: u64, - r9: u64, - r8: u64, - rdi: u64, - rsi: u64, - rbp: u64, - rbx: u64, - rdx: u64, - rcx: u64, - rax: u64, - vector: u64, - error: u64, - rip: u64, - cs: u64, - rflags: u64, +pub(crate) struct IntFrame { + pub(crate) r15: u64, + pub(crate) r14: u64, + pub(crate) r13: u64, + pub(crate) r12: u64, + pub(crate) r11: u64, + pub(crate) r10: u64, + pub(crate) r9: u64, + pub(crate) r8: u64, + pub(crate) rdi: u64, + pub(crate) rsi: u64, + pub(crate) rbp: u64, + pub(crate) rbx: u64, + pub(crate) rdx: u64, + pub(crate) rcx: u64, + pub(crate) rax: u64, + pub(crate) vector: u64, + pub(crate) error: u64, + pub(crate) rip: u64, + pub(crate) cs: u64, + pub(crate) rflags: u64, + // QEMU's 64-bit interrupt entry ALWAYS pushes SS/RSP/RFLAGS/CS/RIP + // (40 bytes) and iretq always pops all five, so the tail must be + // 40 bytes — these two fields are the CPU-pushed RSP/SS. Synthetic + // task frames set them to the task's initial stack top and the + // kernel SS so the first iretq lands with a valid stack. + pub(crate) rsp: u64, + pub(crate) ss: u64, } -/// Called from the asm common handler — dispatches to the right Rust handler. +/// Called from the asm common handler — dispatches to the right Rust +/// handler and returns the frame pointer the asm should switch to (the +/// next task's, or the same one when no switch happens). #[unsafe(no_mangle)] -unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { - unsafe { +unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { + let fp = frame as *const IntFrame as usize; + crate::sched::set_in_isr(true); + let next = unsafe { match frame.vector { 32 => { TICKS.fetch_add(1, Ordering::Relaxed); eoi(0); + crate::sched::on_tick(fp) } 33 => { let byte: u8; @@ -316,10 +352,24 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { // Push scancode to ring buffer (called with interrupts disabled). KEYBUF.push(byte); eoi(1); + fp + } + 8 => { + crate::serial_println!("double fault — halting"); + halt() } - _ => {} + 14 => { + let cr2: u64; + core::arch::asm!("mov {}, cr2", out(reg) cr2, + options(nostack, preserves_flags)); + crate::sched::page_fault(cr2, fp) + } + 0x60 => crate::sched::on_yield(fp), + _ => fp, } - } + }; + crate::sched::set_in_isr(false); + next } // --------------------------------------------------------------------------- @@ -350,12 +400,25 @@ pub fn init() { // Fill all 256 entries with the spurious handler. for vec in 0..=255u8 { - idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8); + idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8, 0); } // Override vectors for interrupt we actually handle. - idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8); - idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8); + idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); + idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); + // Yield (software interrupt). + idt_set_entry( + idt_ptr, + 0x60, + isr_stub_60 as *const () as u64, + cs, + 0x0Eu8, + 0, + ); + // Double fault and page fault run on dedicated IST stacks (see + // gdt.rs) so a task fault can't corrupt the faulting task's stack. + idt_set_entry(idt_ptr, 8, isr_stub_08 as *const () as u64, cs, 0x0Eu8, 2); + idt_set_entry(idt_ptr, 14, isr_stub_14 as *const () as u64, cs, 0x0Eu8, 1); // Build the IDTR pseudo-descriptor. IDTR = [ @@ -389,11 +452,23 @@ pub fn init() { } } +/// Halt the CPU forever with interrupts disabled. +pub fn halt() -> ! { + loop { + unsafe { + core::arch::asm!("cli; hlt", options(nostack, preserves_flags)); + } + } +} + // --------------------------------------------------------------------------- // Extern symbols from global_asm! // --------------------------------------------------------------------------- unsafe extern "C" { + fn isr_stub_08(); + fn isr_stub_14(); fn isr_stub_32(); fn isr_stub_33(); + fn isr_stub_60(); fn isr_stub_spurious(); } diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs index 0c7dc06..5da5094 100644 --- a/rsnix/src/klog.rs +++ b/rsnix/src/klog.rs @@ -79,16 +79,19 @@ pub fn append(line: &str, mut ms: u64) { r.seq += 1; } -/// Append the formatted output of a `serial_println!` — the macro hook. -/// `fmt::Write` may deliver the line in several chunks, so accumulate -/// into one line buffer first, then append the whole line. The -/// timestamp is taken when the line is emitted (monotonic ms since -/// boot, from the PIT tick + live-counter interpolation). -pub fn append_fmt(args: core::fmt::Arguments<'_>) { - let ms = crate::interrupts::uptime_ms(); +/// Format one serial line, write it to the serial port, and append it +/// to the ring — all under the scheduler lock, so a line from one task +/// never interleaves with another task's line (line atomicity). +/// `serial_println!` funnels through here. +pub fn emit(args: core::fmt::Arguments<'_>) { let mut line = LineBuf::new(); let _ = core::fmt::write(&mut line, args); + let ms = crate::interrupts::uptime_ms(); + crate::sched::lock(); + crate::serial::write_raw(line.as_str()); + crate::serial::write_raw("\r\n"); append(line.as_str(), ms); + crate::sched::unlock(); } struct LineBuf { diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index a2d41da..08ac6c2 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs @@ -6,6 +6,7 @@ pub mod ahci; pub mod fat; pub mod font; pub mod fs; +pub mod gdt; pub mod gop; pub mod interrupts; pub mod klog; @@ -15,6 +16,7 @@ pub mod pci; pub mod power; pub mod ps2; pub mod rtc; +pub mod sched; pub mod serial; pub mod strbuf; pub mod tty; diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs new file mode 100644 index 0000000..4b10503 --- /dev/null +++ b/rsnix/src/sched.rs @@ -0,0 +1,544 @@ +// --------------------------------------------------------------------------- +// 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 = 3; // 1 guard page + 8 KiB stack +const NAME_MAX: usize = 24; +const PANIC_MAX: usize = 64; + +#[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, +} + +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, +}; + +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); + +// --------------------------------------------------------------------------- +// 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() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { + return cur; + } + unsafe { + TASKS[CURRENT].saved_rsp = cur; + TASKS[CURRENT].ticks += 1; + let now = crate::interrupts::uptime_ms(); + for t in 0..MAX_TASKS { + if TASKS[t].state == TaskState::Sleeping && TASKS[t].wake_at <= now { + TASKS[t].state = TaskState::Ready; + } + } + pick_next(cur) + } +} + +/// 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, + }; + } + 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(); +} + +/// 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. +pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize { + if !is_ready() || unsafe { CURRENT } <= IDLE_TID { + crate::serial_println!("pgfault: kernel page fault at {:#x} — halting", cr2); + crate::interrupts::halt(); + } + 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 = FmtBuf { b: &mut buf, n: 0 }; + 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.n + }; + 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", + i, + name_str(i), + state, + t.ticks + ); + 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, + }; + 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)); + } + } +} + +struct FmtBuf<'a> { + b: &'a mut [u8], + n: usize, +} + +impl fmt::Write for FmtBuf<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + let m = s.len().min(self.b.len() - self.n); + self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]); + self.n += m; + Ok(()) + } +} diff --git a/rsnix/src/serial.rs b/rsnix/src/serial.rs index 484a114..2267225 100644 --- a/rsnix/src/serial.rs +++ b/rsnix/src/serial.rs @@ -60,8 +60,17 @@ pub fn try_get_byte() -> Option { } } -/// Write a string to serial. +/// Write a string to serial, holding the scheduler lock for the whole +/// write so a line can't be interleaved with another task's output. pub fn write_str(s: &str) { + crate::sched::lock(); + write_raw(s); + crate::sched::unlock(); +} + +/// Raw serial write — no scheduler lock. Callers must hold the lock +/// (klog::emit does) or be in a context where preemption is impossible. +pub fn write_raw(s: &str) { for &b in s.as_bytes() { if b == b'\n' { put_byte(b'\r'); @@ -70,6 +79,16 @@ pub fn write_str(s: &str) { } } +/// One scheduler-locked call: the line plus its CRLF, atomic with +/// respect to context switches (used by the shell's term_println, where +/// the line and its newline must not be split by a tick). +pub fn write_line(s: &str) { + crate::sched::lock(); + write_raw(s); + write_raw("\r\n"); + crate::sched::unlock(); +} + // --------------------------------------------------------------------------- // fmt::Write implementation // --------------------------------------------------------------------------- @@ -96,17 +115,17 @@ macro_rules! serial_print { /// here, so the ring captures the whole boot sequence for free. /// User-facing shell output must NOT use this macro (shell output would /// pollute the boot log); it writes to the serial port directly. +/// +/// The whole line is formatted, written, and logged under one scheduler +/// lock (klog::emit), so lines from different tasks never interleave. #[macro_export] macro_rules! serial_println { () => { - $crate::serial::write_str("\r\n"); - $crate::klog::append_fmt(core::format_args!("")); + $crate::klog::emit(core::format_args!("")) + }; + ($($arg:tt)*) => { + $crate::klog::emit(core::format_args!($($arg)*)) }; - ($($arg:tt)*) => {{ - $crate::serial_print!($($arg)*); - $crate::serial::write_str("\r\n"); - $crate::klog::append_fmt(core::format_args!($($arg)*)); - }}; } // --------------------------------------------------------------------------- diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 28eb737..97fa6c2 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -83,9 +83,11 @@ fn term_print(s: &str) { } fn term_println(s: &str) { - term_print(s); + gop_print!("{}", s); gop_println!(); - rsnix::serial::write_str("\r\n"); + // One locked call (line + CRLF) so a tick can't split the pair and + // let a background task's serial line interleave mid-line. + rsnix::serial::write_line(s); } fn term_err(msg: &str) { @@ -696,6 +698,11 @@ write `{} TIL `", "touch" => cmd_touch(&parsed), "rm" => cmd_rm(&parsed), "edit" => cmd_edit(&parsed), + "bg" => cmd_bg(&parsed), + "jobs" => cmd_jobs(&parsed), + "sleep" => cmd_sleep(&parsed), + "kill" => cmd_kill(&parsed), + "demo" => cmd_demo(&parsed), _ => term_err(&alloc::format!("unknown command: {}", cmd)), } } @@ -718,6 +725,11 @@ fn cmd_help(_p: &Parsed) { date — show current date/time (RTC) pci — list PCI devices rescan — re-scan for hotplugged/removed disks (also runs automatically) + bg — spawn a background task (spinner, ticker, counter, boom, overflow) + jobs — list tasks (id, name, state, ticks) + sleep — block the shell for ms; background tasks keep running + kill — stop a background task (id from jobs) + demo — spawn spinner + ticker + counter ls — list a directory cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) klog — kernel boot log (cat /dev/klog; WRITING persists it) @@ -1562,6 +1574,145 @@ fn cmd_rescan(_p: &Parsed) { } } +// --------------------------------------------------------------------------- +// Multitasking: bg / jobs / sleep / kill / demo +// +// Background tasks are headless: they may sleep, yield, and log to +// serial/klog only. Only the shell touches the disk, filesystem, and +// terminal (the ADR 0003 invariant). +// --------------------------------------------------------------------------- + +fn cmd_bg(p: &Parsed) { + let Some(name) = p.args.first() else { + term_err("bg: needs a task name — spinner, ticker, counter, boom, overflow"); + return; + }; + let entry: fn() = match name.as_str() { + "spinner" => task_spinner, + "ticker" => task_ticker, + "counter" => task_counter, + "boom" => task_boom, + "overflow" => task_overflow, + _ => { + term_err(&alloc::format!( + "bg: unknown task '{}' (spinner, ticker, counter, boom, overflow)", + name + )); + return; + } + }; + match rsnix::sched::spawn(entry, name) { + Some(id) => term_println(&alloc::format!("bg: spawned task {} ({})", id, name)), + None => term_err("bg: task table full"), + } +} + +fn cmd_jobs(p: &Parsed) { + let mut pipe = Pipeline::new(&p.filters, &p.output); + let lines = rsnix::sched::jobs(); + if lines.is_empty() { + let _ = pipe.line("jobs: no tasks"); + } + for l in lines { + if !pipe.line(&l) { + break; + } + } + pipe.end(); +} + +fn cmd_sleep(p: &Parsed) { + let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { + Some(m) if m > 0 => m, + _ => { + term_err("sleep: needs a duration in ms (e.g. sleep 2000)"); + return; + } + }; + term_println(&alloc::format!( + "sleep: blocking shell for {} ms — background tasks keep running", + ms + )); + rsnix::sched::sleep_ms(ms); + term_println(&alloc::format!("sleep: woke after {} ms", ms)); +} + +fn cmd_kill(p: &Parsed) { + let id: usize = match p.args.first().and_then(|s| s.parse().ok()) { + Some(id) => id, + None => { + term_err("kill: needs a task id (see jobs)"); + return; + } + }; + match rsnix::sched::kill(id) { + Ok(()) => term_println(&alloc::format!("kill: task {} marked done", id)), + Err(e) => term_err(&alloc::format!("kill: {}", e)), + } +} + +fn cmd_demo(_p: &Parsed) { + let mut n = 0; + for (entry, name) in [ + (task_spinner as fn(), "spinner"), + (task_ticker as fn(), "ticker"), + (task_counter as fn(), "counter"), + ] { + if rsnix::sched::spawn(entry, name).is_some() { + n += 1; + } + } + term_println(&alloc::format!( + "demo: spawned {} task(s) — spinner 250ms, ticker 500ms, counter 1000ms (try jobs)", + n + )); +} + +// --- Demo task bodies (headless: sleep + serial/klog only) --- + +fn task_spinner() { + let mut n: u64 = 0; + loop { + rsnix::serial_println!("spinner: {}", n); + n += 1; + rsnix::sched::sleep_ms(250); + } +} + +fn task_ticker() { + let mut n: u64 = 0; + loop { + rsnix::serial_println!("ticker: {}", n); + n += 1; + rsnix::sched::sleep_ms(500); + } +} + +fn task_counter() { + let mut n: u64 = 0; + loop { + rsnix::serial_println!("counter: {}", n); + n += 1; + rsnix::sched::sleep_ms(1000); + } +} + +fn task_boom() { + panic!("boom: deliberate task panic"); +} + +/// Recurses until the guard page faults — the task should die with a +/// "stack overflow" panic while the shell keeps running. +fn task_overflow() { + #[allow(unconditional_recursion)] + fn recurse(d: u64) { + let keep = [0u8; 512]; + core::hint::black_box(&keep); + recurse(d + 1); + } + recurse(0); +} + fn cmd_pci(p: &Parsed) { let devices = rsnix::pci::probe(); let mut pipe = Pipeline::new(&p.filters, &p.output); -- cgit v1.2.3