diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 17:38:57 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 17:38:57 +0200 |
| commit | 8f05ea44f69cb796e7b1f581ba1b8045c98d615b (patch) | |
| tree | 3049f0a5fc33dd08f94af0e6493c588b2a4818b9 | |
| parent | CONTEXT.md: versioning policy (a.b.c — overhaul/feature/fix) (diff) | |
| download | rs-dos-8f05ea44f69cb796e7b1f581ba1b8045c98d615b.tar.gz rs-dos-8f05ea44f69cb796e7b1f581ba1b8045c98d615b.zip | |
rsnix: preemptive kernel tasks (ADR 0003) — sched, IST fault stacks, 176-byte frames
- 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)
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 45 | ||||
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 46 | ||||
| -rw-r--r-- | rsnix/src/gdt.rs | 242 | ||||
| -rw-r--r-- | rsnix/src/interrupts.rs | 139 | ||||
| -rw-r--r-- | rsnix/src/klog.rs | 17 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 544 | ||||
| -rw-r--r-- | rsnix/src/serial.rs | 35 | ||||
| -rw-r--r-- | shell/src/lib.rs | 155 |
11 files changed, 1180 insertions, 53 deletions
| @@ -359,6 +359,51 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 359 | also untestable in QEMU. The disk arrival/removal path (PxSSTS | 359 | also untestable in QEMU. The disk arrival/removal path (PxSSTS |
| 360 | based) is verified on real hardware instead. | 360 | based) is verified on real hardware instead. |
| 361 | 361 | ||
| 362 | ✅ **Preemptive kernel tasks (ADR 0003)** — round-robin multitasking | ||
| 363 | on the 100 Hz PIT tick, one shared identity-mapped address space: | ||
| 364 | |||
| 365 | - `sched`: 16-slot static task table (no ISR-time allocation); task 0 | ||
| 366 | = shell (boot stack), task 1 = idle `hlt` loop, tasks 2+ spawned | ||
| 367 | via `sched::spawn(entry, name)` with 3 pages each (1 unmapped | ||
| 368 | guard + 8 KiB stack); preemptive RR with `int $0x60` software | ||
| 369 | yield; `sleep_ms`/`yield_now`; lazy reaping of Done slots. | ||
| 370 | - `gdt`: kernel-owned GDT mirroring UEFI's selector layout (no | ||
| 371 | far-jump; CS/DS/SS keep working) plus a TSS with IST1 (page | ||
| 372 | fault) and IST2 (double fault) stacks — a stack overflow faults | ||
| 373 | inside the guard page, the CPU pushes the fault frame onto the | ||
| 374 | IST stack, and the task dies gracefully instead of triple- | ||
| 375 | faulting the machine (the ADR's guard-page promise, honored). | ||
| 376 | - `interrupts`: `isr_dispatch` returns the next frame pointer in | ||
| 377 | rax; asm does `mov rsp, rax` → pop → iretq. **QEMU quirk that | ||
| 378 | bit us**: QEMU's 64-bit interrupt entry always pushes | ||
| 379 | SS/RSP/RFLAGS/CS/RIP (40 bytes) and iretq always pops all five | ||
| 380 | (its same-privilege branch is never taken for 64-bit iretq), so | ||
| 381 | interrupt frames are 176 bytes and the synthetic task frames | ||
| 382 | must carry rsp=stack-top + ss — a 160-byte frame made the first | ||
| 383 | iretq pop rsp/ss from the unmapped page beyond the frame (task | ||
| 384 | started with rsp=0, or the iretq itself page-faulted). Real | ||
| 385 | hardware pushes only 24 bytes at same privilege, so the extra | ||
| 386 | fields are inert there; the synthetic frames set them | ||
| 387 | explicitly either way. | ||
| 388 | - Panic handling: a task panic (or guard-page overflow) is caught, | ||
| 389 | logged as `sched: task N "name" panicked: …`, the task is marked | ||
| 390 | Done, and the scheduler switches away — the shell keeps running. | ||
| 391 | Shell/idle/ISR/pre-scheduler panics still halt the kernel. | ||
| 392 | - Shell: `bg <builtin>` (spinner/ticker/counter demo + deliberate | ||
| 393 | `boom` panic + `overflow` recursion), `jobs` (state/ticks/ | ||
| 394 | panic message), `sleep <ms>` (blocks only the shell), `kill | ||
| 395 | <id>`. Only the shell touches disk/fs/tty/vt; background tasks | ||
| 396 | are headless (sleep + serial/klog only). | ||
| 397 | - Boot order note: `sched::init` must run after the page allocator | ||
| 398 | and adopted page tables (the idle's stack + guard need both) — | ||
| 399 | it was originally before them, silently dropping the idle task. | ||
| 400 | |||
| 401 | Verified in QEMU (9-step script): demo interleaving at expected | ||
| 402 | cadences, sleep keeps background tasks logging, jobs states, | ||
| 403 | graceful `boom` panic with corpse in jobs, kill stops a task, and | ||
| 404 | `bg overflow` dies on the guard page with the machine intact; full | ||
| 405 | regression (rescan/til/klog/grammar/pager) re-passes. | ||
| 406 | |||
| 362 | ## Known issues / perf notes | 407 | ## Known issues / perf notes |
| 363 | 408 | ||
| 364 | - **Scroll cost** — scrolling still shifts the whole framebuffer (a | 409 | - **Scroll cost** — scrolling still shifts the whole framebuffer (a |
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.2.0" | 7 | version = "0.3.0" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.2.0" | 22 | version = "0.3.0" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.2.0" | 26 | version = "0.3.0" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.2.0" | 6 | version = "0.3.0" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # 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::*; | |||
| 8 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
| 9 | // Panic handler | 9 | // Panic handler |
| 10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
| 11 | |||
| 12 | /// Small fmt::Write over a byte slice (no allocation, bounded). | ||
| 13 | struct PanicBuf<'a> { | ||
| 14 | b: &'a mut [u8], | ||
| 15 | n: usize, | ||
| 16 | } | ||
| 17 | |||
| 18 | impl core::fmt::Write for PanicBuf<'_> { | ||
| 19 | fn write_str(&mut self, s: &str) -> core::fmt::Result { | ||
| 20 | let m = s.len().min(self.b.len() - self.n); | ||
| 21 | self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]); | ||
| 22 | self.n += m; | ||
| 23 | Ok(()) | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 11 | #[panic_handler] | 27 | #[panic_handler] |
| 12 | fn panic(info: &PanicInfo) -> ! { | 28 | fn panic(info: &PanicInfo) -> ! { |
| 13 | use core::fmt::Write; | 29 | use core::fmt::Write; |
| 30 | |||
| 31 | // A background task (id >= 2) panicked: die gracefully — log it, | ||
| 32 | // mark the task done, switch away. The shell keeps running and | ||
| 33 | // `jobs` shows the corpse. Panics in the shell, idle, ISR context, | ||
| 34 | // or before the scheduler exists still halt the whole system. | ||
| 35 | if rsnix::sched::is_ready() && !rsnix::sched::in_isr() && rsnix::sched::current() >= 2 { | ||
| 36 | let mut buf = [0u8; 192]; | ||
| 37 | let n = { | ||
| 38 | let mut w = PanicBuf { b: &mut buf, n: 0 }; | ||
| 39 | let _ = write!(&mut w, "{}", info.message()); | ||
| 40 | if let Some(loc) = info.location() { | ||
| 41 | let _ = write!(&mut w, " at {}:{}", loc.file(), loc.line()); | ||
| 42 | } | ||
| 43 | w.n | ||
| 44 | }; | ||
| 45 | let msg = core::str::from_utf8(&buf[..n]).unwrap_or("task panic"); | ||
| 46 | rsnix::sched::abort_current(msg); | ||
| 47 | } | ||
| 48 | |||
| 14 | rsnix::serial::write_str("\r\n*** PANIC ***\r\n"); | 49 | rsnix::serial::write_str("\r\n*** PANIC ***\r\n"); |
| 15 | let _ = write!(&mut rsnix::serial::SerialWriter, "{}\r\n", info.message()); | 50 | let _ = write!(&mut rsnix::serial::SerialWriter, "{}\r\n", info.message()); |
| 16 | if let Some(loc) = info.location() { | 51 | if let Some(loc) = info.location() { |
| @@ -114,6 +149,11 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 114 | rsnix::serial_println!("serial: COM1 115200 8N1"); | 149 | rsnix::serial_println!("serial: COM1 115200 8N1"); |
| 115 | rsnix::ps2::init(); | 150 | rsnix::ps2::init(); |
| 116 | rsnix::serial_println!("ps2: keyboard initialized"); | 151 | rsnix::serial_println!("ps2: keyboard initialized"); |
| 152 | if rsnix::gdt::init() { | ||
| 153 | rsnix::serial_println!("gdt: own GDT + TSS (IST: page-fault/double-fault)"); | ||
| 154 | } else { | ||
| 155 | rsnix::serial_println!("gdt: kept UEFI GDT (IST fault stacks off)"); | ||
| 156 | } | ||
| 117 | rsnix::interrupts::init(); | 157 | rsnix::interrupts::init(); |
| 118 | rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); | 158 | rsnix::serial_println!("intr: IDT loaded, PIC remapped, timer ~100 Hz"); |
| 119 | 159 | ||
| @@ -151,6 +191,12 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 151 | rsnix::serial_println!("pg: self-test FAILED"); | 191 | rsnix::serial_println!("pg: self-test FAILED"); |
| 152 | } | 192 | } |
| 153 | 193 | ||
| 194 | // Scheduler init needs the page allocator (idle task's stack + guard | ||
| 195 | // page) and the adopted page tables, so it runs after the memory | ||
| 196 | // subsystem is fully up. The tick (interrupts::init) is already | ||
| 197 | // live, which is what `sleep`/wake scheduling depends on. | ||
| 198 | rsnix::sched::init(); | ||
| 199 | |||
| 154 | let t = rsnix::rtc::now(); | 200 | let t = rsnix::rtc::now(); |
| 155 | rsnix::serial_println!( | 201 | rsnix::serial_println!( |
| 156 | "rtc: {}-{:02}-{:02} {:02}:{:02}:{:02}", | 202 | "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 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // GDT + TSS — kernel-owned segment tables and interrupt stack tables (IST). | ||
| 3 | // | ||
| 4 | // The kernel boots on UEFI's GDT. We take over with our own GDT that | ||
| 5 | // mirrors the current selector layout (flat ring-0 code/data descriptors | ||
| 6 | // at the same indices UEFI chose), so CS/DS/SS/ES keep working without | ||
| 7 | // any far-jump or segment-reload gymnastics. On top of that we add a | ||
| 8 | // TSS with two interrupt stack tables: | ||
| 9 | // | ||
| 10 | // - IST1: page-fault (vector 14) stack. A stack-overflowing task | ||
| 11 | // faults inside its unmapped guard page; the CPU pushes the fault | ||
| 12 | // frame onto this dedicated stack instead of the faulting (also | ||
| 13 | // unmapped) stack, so we can kill the task gracefully instead of | ||
| 14 | // double/triple-faulting into a machine reset. | ||
| 15 | // - IST2: double-fault (vector 8) stack. If anything else goes wrong | ||
| 16 | // during exception handling we land here and halt cleanly. | ||
| 17 | // | ||
| 18 | // We never change privilege level, so the TSS's RSP0..2 are unused — | ||
| 19 | // only the IST pointers matter. | ||
| 20 | // --------------------------------------------------------------------------- | ||
| 21 | use core::arch::asm; | ||
| 22 | |||
| 23 | const GDT_ENTRIES: usize = 64; | ||
| 24 | const TSS_INDEX: usize = GDT_ENTRIES - 1; // 63 | ||
| 25 | const TSS_SEL: u16 = (TSS_INDEX << 3) as u16; // 0x1F8 | ||
| 26 | |||
| 27 | /// 8-byte segment descriptor. | ||
| 28 | #[repr(C, packed)] | ||
| 29 | #[derive(Clone, Copy)] | ||
| 30 | struct GdtEntry { | ||
| 31 | limit_lo: u16, | ||
| 32 | base_lo: u16, | ||
| 33 | base_mid: u8, | ||
| 34 | access: u8, | ||
| 35 | flags_limit: u8, // limit 19:16 | flags nibble (G D/B L AVL) | ||
| 36 | base_hi: u8, | ||
| 37 | } | ||
| 38 | |||
| 39 | impl GdtEntry { | ||
| 40 | const fn null() -> Self { | ||
| 41 | Self { | ||
| 42 | limit_lo: 0, | ||
| 43 | base_lo: 0, | ||
| 44 | base_mid: 0, | ||
| 45 | access: 0, | ||
| 46 | flags_limit: 0, | ||
| 47 | base_hi: 0, | ||
| 48 | } | ||
| 49 | } | ||
| 50 | /// Flat ring-0 64-bit code segment (present, exec/read, L=1). | ||
| 51 | const fn code() -> Self { | ||
| 52 | Self { | ||
| 53 | limit_lo: 0xFFFF, | ||
| 54 | base_lo: 0, | ||
| 55 | base_mid: 0, | ||
| 56 | access: 0x9A, | ||
| 57 | flags_limit: 0xAF, // G=1, D/B=0, L=1, AVL=0; limit 0xF | ||
| 58 | base_hi: 0, | ||
| 59 | } | ||
| 60 | } | ||
| 61 | /// Flat ring-0 data segment (present, read/write). | ||
| 62 | const fn data() -> Self { | ||
| 63 | Self { | ||
| 64 | limit_lo: 0xFFFF, | ||
| 65 | base_lo: 0, | ||
| 66 | base_mid: 0, | ||
| 67 | access: 0x92, | ||
| 68 | flags_limit: 0xCF, // G=1, D/B=1, L=0, AVL=0; limit 0xF | ||
| 69 | base_hi: 0, | ||
| 70 | } | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | /// 64-bit TSS: only the IST pointers are used (no privilege changes). | ||
| 75 | #[repr(C, packed)] | ||
| 76 | #[derive(Clone, Copy)] | ||
| 77 | struct Tss { | ||
| 78 | _res1: u32, | ||
| 79 | rsp0: u64, | ||
| 80 | rsp1: u64, | ||
| 81 | rsp2: u64, | ||
| 82 | _res2: u64, | ||
| 83 | ist1: u64, | ||
| 84 | ist2: u64, | ||
| 85 | ist3: u64, | ||
| 86 | ist4: u64, | ||
| 87 | ist5: u64, | ||
| 88 | ist6: u64, | ||
| 89 | ist7: u64, | ||
| 90 | _res3: u64, | ||
| 91 | _res4: u16, | ||
| 92 | iomap_base: u16, | ||
| 93 | } | ||
| 94 | |||
| 95 | const TSS_SIZE: usize = 104; | ||
| 96 | |||
| 97 | impl Tss { | ||
| 98 | const fn zero() -> Self { | ||
| 99 | Self { | ||
| 100 | _res1: 0, | ||
| 101 | rsp0: 0, | ||
| 102 | rsp1: 0, | ||
| 103 | rsp2: 0, | ||
| 104 | _res2: 0, | ||
| 105 | ist1: 0, | ||
| 106 | ist2: 0, | ||
| 107 | ist3: 0, | ||
| 108 | ist4: 0, | ||
| 109 | ist5: 0, | ||
| 110 | ist6: 0, | ||
| 111 | ist7: 0, | ||
| 112 | _res3: 0, | ||
| 113 | _res4: 0, | ||
| 114 | iomap_base: 0, | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | /// 4 KiB page-aligned block for the IST stacks. | ||
| 120 | #[repr(align(4096))] | ||
| 121 | struct Page([u8; 4096]); | ||
| 122 | |||
| 123 | static mut PF_STACK: Page = Page([0; 4096]); | ||
| 124 | static mut DF_STACK: Page = Page([0; 4096]); | ||
| 125 | static mut TSS: Tss = Tss::zero(); | ||
| 126 | // GDT slots are 8 bytes except the TSS descriptor at index 63, which is | ||
| 127 | // 16 bytes — total 64*8 + 16. | ||
| 128 | static mut GDT: [u8; GDT_ENTRIES * 8 + 16] = [0; GDT_ENTRIES * 8 + 16]; | ||
| 129 | static mut GDTR: [u16; 5] = [0; 5]; | ||
| 130 | |||
| 131 | unsafe fn read_seg(which: u8) -> u16 { | ||
| 132 | // `which` picks the segment register to read (0=cs, 1=ds, 2=ss, ...). | ||
| 133 | let v: u64; | ||
| 134 | unsafe { | ||
| 135 | match which { | ||
| 136 | 0 => asm!("mov {0}, cs", out(reg) v, options(nostack, preserves_flags)), | ||
| 137 | 1 => asm!("mov {0}, ds", out(reg) v, options(nostack, preserves_flags)), | ||
| 138 | 2 => asm!("mov {0}, ss", out(reg) v, options(nostack, preserves_flags)), | ||
| 139 | 3 => asm!("mov {0}, es", out(reg) v, options(nostack, preserves_flags)), | ||
| 140 | 4 => asm!("mov {0}, fs", out(reg) v, options(nostack, preserves_flags)), | ||
| 141 | _ => asm!("mov {0}, gs", out(reg) v, options(nostack, preserves_flags)), | ||
| 142 | } | ||
| 143 | } | ||
| 144 | v as u16 | ||
| 145 | } | ||
| 146 | |||
| 147 | /// Install the kernel's own GDT (mirroring UEFI's selectors) and a TSS | ||
| 148 | /// with IST stacks for page-fault and double-fault handling. | ||
| 149 | /// | ||
| 150 | /// Returns false if UEFI's selector layout cannot be mirrored (any | ||
| 151 | /// nonzero selector at an index >= TSS_INDEX) — then we keep UEFI's GDT | ||
| 152 | /// and the IST-based fault stacks stay off (guard pages still exist but | ||
| 153 | /// an overflow would fault while pushing the fault frame → machine | ||
| 154 | /// reset instead of a graceful task death). | ||
| 155 | /// | ||
| 156 | /// Must run with interrupts disabled and before `interrupts::init` (the | ||
| 157 | /// IDT's vector-8/14 entries carry the IST indices). | ||
| 158 | pub fn init() -> bool { | ||
| 159 | unsafe { | ||
| 160 | asm!("cli", options(nostack)); | ||
| 161 | |||
| 162 | let cs = read_seg(0); | ||
| 163 | let ds = read_seg(1); | ||
| 164 | let ss = read_seg(2); | ||
| 165 | let es = read_seg(3); | ||
| 166 | let fs = read_seg(4); | ||
| 167 | let gs = read_seg(5); | ||
| 168 | let sels = [cs, ds, ss, es, fs, gs]; | ||
| 169 | |||
| 170 | // Every nonzero selector must land on a descriptor slot below | ||
| 171 | // TSS_INDEX (63 holds the 16-byte TSS descriptor). | ||
| 172 | for &s in &sels { | ||
| 173 | if s != 0 && (s >> 3) as usize >= TSS_INDEX { | ||
| 174 | return false; | ||
| 175 | } | ||
| 176 | } | ||
| 177 | |||
| 178 | let gdt = core::ptr::addr_of_mut!(GDT[0]); | ||
| 179 | let write_entry = |idx: usize, e: GdtEntry| { | ||
| 180 | let p = gdt.add(idx * 8) as *mut GdtEntry; | ||
| 181 | core::ptr::write_unaligned(p, e); | ||
| 182 | }; | ||
| 183 | |||
| 184 | write_entry(0, GdtEntry::null()); | ||
| 185 | write_entry((cs >> 3) as usize, GdtEntry::code()); | ||
| 186 | let cs_idx = (cs >> 3) as usize; | ||
| 187 | for &s in &sels { | ||
| 188 | let idx = (s >> 3) as usize; | ||
| 189 | if s != 0 && idx != 0 && idx != cs_idx { | ||
| 190 | write_entry(idx, GdtEntry::data()); | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | // TSS descriptor (16 bytes) at index 63. | ||
| 195 | let tss_base = core::ptr::addr_of_mut!(TSS) as usize as u64; | ||
| 196 | TSS.ist1 = core::ptr::addr_of_mut!(PF_STACK.0[0]) as usize as u64 + 4096; | ||
| 197 | TSS.ist2 = core::ptr::addr_of_mut!(DF_STACK.0[0]) as usize as u64 + 4096; | ||
| 198 | TSS.iomap_base = TSS_SIZE as u16; // no I/O bitmap | ||
| 199 | |||
| 200 | let mut td = [0u8; 16]; | ||
| 201 | td[0..2].copy_from_slice(&((TSS_SIZE - 1) as u16).to_le_bytes()); | ||
| 202 | td[2..4].copy_from_slice(&(tss_base as u16).to_le_bytes()); | ||
| 203 | td[4] = (tss_base >> 16) as u8; | ||
| 204 | td[5] = 0x89; // present, available 64-bit TSS | ||
| 205 | td[6] = 0x00; // limit 19:16 + flags = 0 | ||
| 206 | td[7] = (tss_base >> 24) as u8; | ||
| 207 | td[8..12].copy_from_slice(&((tss_base >> 32) as u32).to_le_bytes()); | ||
| 208 | // td[12..16] reserved — zero. | ||
| 209 | core::ptr::copy_nonoverlapping(td.as_ptr(), gdt.add(TSS_INDEX * 8), 16); | ||
| 210 | |||
| 211 | // GDTR: limit = 63*8 + 16 - 1 = 519. | ||
| 212 | GDTR = [ | ||
| 213 | (TSS_INDEX * 8 + 16 - 1) as u16, | ||
| 214 | gdt as u16, | ||
| 215 | (gdt as u64 >> 16) as u16, | ||
| 216 | (gdt as u64 >> 32) as u16, | ||
| 217 | (gdt as u64 >> 48) as u16, | ||
| 218 | ]; | ||
| 219 | asm!("lgdt [{}]", in(reg) core::ptr::addr_of!(GDTR), options(nostack, preserves_flags)); | ||
| 220 | asm!("ltr ax", in("ax") TSS_SEL, options(nostack, preserves_flags)); | ||
| 221 | |||
| 222 | // Re-load the data segments so the CPU re-validates them against | ||
| 223 | // the new GDT (CS cannot be reloaded by mov; its descriptor is | ||
| 224 | // unchanged at the same index). | ||
| 225 | if ds != 0 { | ||
| 226 | asm!("mov ds, ax", in("ax") ds, options(nostack, preserves_flags)); | ||
| 227 | } | ||
| 228 | if ss != 0 { | ||
| 229 | asm!("mov ss, ax", in("ax") ss, options(nostack, preserves_flags)); | ||
| 230 | } | ||
| 231 | if es != 0 { | ||
| 232 | asm!("mov es, ax", in("ax") es, options(nostack, preserves_flags)); | ||
| 233 | } | ||
| 234 | if fs != 0 { | ||
| 235 | asm!("mov fs, ax", in("ax") fs, options(nostack, preserves_flags)); | ||
| 236 | } | ||
| 237 | if gs != 0 { | ||
| 238 | asm!("mov gs, ax", in("ax") gs, options(nostack, preserves_flags)); | ||
| 239 | } | ||
| 240 | true | ||
| 241 | } | ||
| 242 | } | ||
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 { | |||
| 22 | } | 22 | } |
| 23 | 23 | ||
| 24 | /// Fill one IDT entry at the given offset (in bytes). | 24 | /// Fill one IDT entry at the given offset (in bytes). |
| 25 | unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8) { | 25 | unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8, ist: u8) { |
| 26 | unsafe { | 26 | unsafe { |
| 27 | let off = (vec as usize) * 16; | 27 | let off = (vec as usize) * 16; |
| 28 | let p = idt.add(off) as *mut IdtEntry; | 28 | let p = idt.add(off) as *mut IdtEntry; |
| 29 | p.write(IdtEntry { | 29 | p.write(IdtEntry { |
| 30 | offset_lo: handler as u16, | 30 | offset_lo: handler as u16, |
| 31 | selector, | 31 | selector, |
| 32 | ist: 0, | 32 | ist, |
| 33 | flags: flags | 0x80, // bit 7 = present | 33 | flags: flags | 0x80, // bit 7 = present |
| 34 | offset_mid: (handler >> 16) as u16, | 34 | offset_mid: (handler >> 16) as u16, |
| 35 | offset_hi: (handler >> 32) as u32, | 35 | offset_hi: (handler >> 32) as u32, |
| @@ -213,6 +213,19 @@ static KEYBUF: KeyBuf = KeyBuf::new(); | |||
| 213 | 213 | ||
| 214 | core::arch::global_asm!( | 214 | core::arch::global_asm!( |
| 215 | // ISR stubs — push vector number + error code, jump to common. | 215 | // ISR stubs — push vector number + error code, jump to common. |
| 216 | // Exceptions that push their own error code (8, 14) skip the push-0. | ||
| 217 | ".globl isr_stub_08", | ||
| 218 | ".p2align 4", | ||
| 219 | "isr_stub_08:", | ||
| 220 | " push 8", // vector (CPU already pushed the error code) | ||
| 221 | " jmp isr_common", | ||
| 222 | "", | ||
| 223 | ".globl isr_stub_14", | ||
| 224 | ".p2align 4", | ||
| 225 | "isr_stub_14:", | ||
| 226 | " push 14", | ||
| 227 | " jmp isr_common", | ||
| 228 | "", | ||
| 216 | ".globl isr_stub_32", | 229 | ".globl isr_stub_32", |
| 217 | ".p2align 4", | 230 | ".p2align 4", |
| 218 | "isr_stub_32:", | 231 | "isr_stub_32:", |
| @@ -227,6 +240,15 @@ core::arch::global_asm!( | |||
| 227 | " push 33", | 240 | " push 33", |
| 228 | " jmp isr_common", | 241 | " jmp isr_common", |
| 229 | "", | 242 | "", |
| 243 | // Software yield (vector 0x60 — outside the PIC's 32-47 range, which | ||
| 244 | // is why 0x40 was rejected: 0x40 = 64 decimal = slave IRQ 8 slot). | ||
| 245 | ".globl isr_stub_60", | ||
| 246 | ".p2align 4", | ||
| 247 | "isr_stub_60:", | ||
| 248 | " push 0", | ||
| 249 | " push 0x60", | ||
| 250 | " jmp isr_common", | ||
| 251 | "", | ||
| 230 | // Catch-all for unexpected interrupts. | 252 | // Catch-all for unexpected interrupts. |
| 231 | ".globl isr_stub_spurious", | 253 | ".globl isr_stub_spurious", |
| 232 | ".p2align 4", | 254 | ".p2align 4", |
| @@ -235,7 +257,8 @@ core::arch::global_asm!( | |||
| 235 | " push 0xFF", | 257 | " push 0xFF", |
| 236 | " jmp isr_common", | 258 | " jmp isr_common", |
| 237 | "", | 259 | "", |
| 238 | // Common handler — save all registers, call into Rust, restore, iretq. | 260 | // Common handler — save all registers, call into Rust (which returns |
| 261 | // the next task's frame pointer in rax), switch rsp, restore, iretq. | ||
| 239 | "isr_common:", | 262 | "isr_common:", |
| 240 | " push rax", | 263 | " push rax", |
| 241 | " push rcx", | 264 | " push rcx", |
| @@ -255,6 +278,7 @@ core::arch::global_asm!( | |||
| 255 | "", | 278 | "", |
| 256 | " mov rcx, rsp", // first arg (rcx = MS x64 ABI) | 279 | " mov rcx, rsp", // first arg (rcx = MS x64 ABI) |
| 257 | " call isr_dispatch", | 280 | " call isr_dispatch", |
| 281 | " mov rsp, rax", // context switch: rax = next task's frame ptr | ||
| 258 | "", | 282 | "", |
| 259 | " pop r15", | 283 | " pop r15", |
| 260 | " pop r14", | 284 | " pop r14", |
| @@ -277,37 +301,49 @@ core::arch::global_asm!( | |||
| 277 | 301 | ||
| 278 | /// Saved register frame (as pushed by `isr_common`). | 302 | /// Saved register frame (as pushed by `isr_common`). |
| 279 | #[repr(C)] | 303 | #[repr(C)] |
| 280 | struct IntFrame { | 304 | pub(crate) struct IntFrame { |
| 281 | r15: u64, | 305 | pub(crate) r15: u64, |
| 282 | r14: u64, | 306 | pub(crate) r14: u64, |
| 283 | r13: u64, | 307 | pub(crate) r13: u64, |
| 284 | r12: u64, | 308 | pub(crate) r12: u64, |
| 285 | r11: u64, | 309 | pub(crate) r11: u64, |
| 286 | r10: u64, | 310 | pub(crate) r10: u64, |
| 287 | r9: u64, | 311 | pub(crate) r9: u64, |
| 288 | r8: u64, | 312 | pub(crate) r8: u64, |
| 289 | rdi: u64, | 313 | pub(crate) rdi: u64, |
| 290 | rsi: u64, | 314 | pub(crate) rsi: u64, |
| 291 | rbp: u64, | 315 | pub(crate) rbp: u64, |
| 292 | rbx: u64, | 316 | pub(crate) rbx: u64, |
| 293 | rdx: u64, | 317 | pub(crate) rdx: u64, |
| 294 | rcx: u64, | 318 | pub(crate) rcx: u64, |
| 295 | rax: u64, | 319 | pub(crate) rax: u64, |
| 296 | vector: u64, | 320 | pub(crate) vector: u64, |
| 297 | error: u64, | 321 | pub(crate) error: u64, |
| 298 | rip: u64, | 322 | pub(crate) rip: u64, |
| 299 | cs: u64, | 323 | pub(crate) cs: u64, |
| 300 | rflags: u64, | 324 | pub(crate) rflags: u64, |
| 325 | // QEMU's 64-bit interrupt entry ALWAYS pushes SS/RSP/RFLAGS/CS/RIP | ||
| 326 | // (40 bytes) and iretq always pops all five, so the tail must be | ||
| 327 | // 40 bytes — these two fields are the CPU-pushed RSP/SS. Synthetic | ||
| 328 | // task frames set them to the task's initial stack top and the | ||
| 329 | // kernel SS so the first iretq lands with a valid stack. | ||
| 330 | pub(crate) rsp: u64, | ||
| 331 | pub(crate) ss: u64, | ||
| 301 | } | 332 | } |
| 302 | 333 | ||
| 303 | /// Called from the asm common handler — dispatches to the right Rust handler. | 334 | /// Called from the asm common handler — dispatches to the right Rust |
| 335 | /// handler and returns the frame pointer the asm should switch to (the | ||
| 336 | /// next task's, or the same one when no switch happens). | ||
| 304 | #[unsafe(no_mangle)] | 337 | #[unsafe(no_mangle)] |
| 305 | unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { | 338 | unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { |
| 306 | unsafe { | 339 | let fp = frame as *const IntFrame as usize; |
| 340 | crate::sched::set_in_isr(true); | ||
| 341 | let next = unsafe { | ||
| 307 | match frame.vector { | 342 | match frame.vector { |
| 308 | 32 => { | 343 | 32 => { |
| 309 | TICKS.fetch_add(1, Ordering::Relaxed); | 344 | TICKS.fetch_add(1, Ordering::Relaxed); |
| 310 | eoi(0); | 345 | eoi(0); |
| 346 | crate::sched::on_tick(fp) | ||
| 311 | } | 347 | } |
| 312 | 33 => { | 348 | 33 => { |
| 313 | let byte: u8; | 349 | let byte: u8; |
| @@ -316,10 +352,24 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { | |||
| 316 | // Push scancode to ring buffer (called with interrupts disabled). | 352 | // Push scancode to ring buffer (called with interrupts disabled). |
| 317 | KEYBUF.push(byte); | 353 | KEYBUF.push(byte); |
| 318 | eoi(1); | 354 | eoi(1); |
| 355 | fp | ||
| 356 | } | ||
| 357 | 8 => { | ||
| 358 | crate::serial_println!("double fault — halting"); | ||
| 359 | halt() | ||
| 319 | } | 360 | } |
| 320 | _ => {} | 361 | 14 => { |
| 362 | let cr2: u64; | ||
| 363 | core::arch::asm!("mov {}, cr2", out(reg) cr2, | ||
| 364 | options(nostack, preserves_flags)); | ||
| 365 | crate::sched::page_fault(cr2, fp) | ||
| 366 | } | ||
| 367 | 0x60 => crate::sched::on_yield(fp), | ||
| 368 | _ => fp, | ||
| 321 | } | 369 | } |
| 322 | } | 370 | }; |
| 371 | crate::sched::set_in_isr(false); | ||
| 372 | next | ||
| 323 | } | 373 | } |
| 324 | 374 | ||
| 325 | // --------------------------------------------------------------------------- | 375 | // --------------------------------------------------------------------------- |
| @@ -350,12 +400,25 @@ pub fn init() { | |||
| 350 | 400 | ||
| 351 | // Fill all 256 entries with the spurious handler. | 401 | // Fill all 256 entries with the spurious handler. |
| 352 | for vec in 0..=255u8 { | 402 | for vec in 0..=255u8 { |
| 353 | idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8); | 403 | idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8, 0); |
| 354 | } | 404 | } |
| 355 | 405 | ||
| 356 | // Override vectors for interrupt we actually handle. | 406 | // Override vectors for interrupt we actually handle. |
| 357 | idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8); | 407 | idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); |
| 358 | idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8); | 408 | idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); |
| 409 | // Yield (software interrupt). | ||
| 410 | idt_set_entry( | ||
| 411 | idt_ptr, | ||
| 412 | 0x60, | ||
| 413 | isr_stub_60 as *const () as u64, | ||
| 414 | cs, | ||
| 415 | 0x0Eu8, | ||
| 416 | 0, | ||
| 417 | ); | ||
| 418 | // Double fault and page fault run on dedicated IST stacks (see | ||
| 419 | // gdt.rs) so a task fault can't corrupt the faulting task's stack. | ||
| 420 | idt_set_entry(idt_ptr, 8, isr_stub_08 as *const () as u64, cs, 0x0Eu8, 2); | ||
| 421 | idt_set_entry(idt_ptr, 14, isr_stub_14 as *const () as u64, cs, 0x0Eu8, 1); | ||
| 359 | 422 | ||
| 360 | // Build the IDTR pseudo-descriptor. | 423 | // Build the IDTR pseudo-descriptor. |
| 361 | IDTR = [ | 424 | IDTR = [ |
| @@ -389,11 +452,23 @@ pub fn init() { | |||
| 389 | } | 452 | } |
| 390 | } | 453 | } |
| 391 | 454 | ||
| 455 | /// Halt the CPU forever with interrupts disabled. | ||
| 456 | pub fn halt() -> ! { | ||
| 457 | loop { | ||
| 458 | unsafe { | ||
| 459 | core::arch::asm!("cli; hlt", options(nostack, preserves_flags)); | ||
| 460 | } | ||
| 461 | } | ||
| 462 | } | ||
| 463 | |||
| 392 | // --------------------------------------------------------------------------- | 464 | // --------------------------------------------------------------------------- |
| 393 | // Extern symbols from global_asm! | 465 | // Extern symbols from global_asm! |
| 394 | // --------------------------------------------------------------------------- | 466 | // --------------------------------------------------------------------------- |
| 395 | unsafe extern "C" { | 467 | unsafe extern "C" { |
| 468 | fn isr_stub_08(); | ||
| 469 | fn isr_stub_14(); | ||
| 396 | fn isr_stub_32(); | 470 | fn isr_stub_32(); |
| 397 | fn isr_stub_33(); | 471 | fn isr_stub_33(); |
| 472 | fn isr_stub_60(); | ||
| 398 | fn isr_stub_spurious(); | 473 | fn isr_stub_spurious(); |
| 399 | } | 474 | } |
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) { | |||
| 79 | r.seq += 1; | 79 | r.seq += 1; |
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | /// Append the formatted output of a `serial_println!` — the macro hook. | 82 | /// Format one serial line, write it to the serial port, and append it |
| 83 | /// `fmt::Write` may deliver the line in several chunks, so accumulate | 83 | /// to the ring — all under the scheduler lock, so a line from one task |
| 84 | /// into one line buffer first, then append the whole line. The | 84 | /// never interleaves with another task's line (line atomicity). |
| 85 | /// timestamp is taken when the line is emitted (monotonic ms since | 85 | /// `serial_println!` funnels through here. |
| 86 | /// boot, from the PIT tick + live-counter interpolation). | 86 | pub fn emit(args: core::fmt::Arguments<'_>) { |
| 87 | pub fn append_fmt(args: core::fmt::Arguments<'_>) { | ||
| 88 | let ms = crate::interrupts::uptime_ms(); | ||
| 89 | let mut line = LineBuf::new(); | 87 | let mut line = LineBuf::new(); |
| 90 | let _ = core::fmt::write(&mut line, args); | 88 | let _ = core::fmt::write(&mut line, args); |
| 89 | let ms = crate::interrupts::uptime_ms(); | ||
| 90 | crate::sched::lock(); | ||
| 91 | crate::serial::write_raw(line.as_str()); | ||
| 92 | crate::serial::write_raw("\r\n"); | ||
| 91 | append(line.as_str(), ms); | 93 | append(line.as_str(), ms); |
| 94 | crate::sched::unlock(); | ||
| 92 | } | 95 | } |
| 93 | 96 | ||
| 94 | struct LineBuf { | 97 | 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; | |||
| 6 | pub mod fat; | 6 | pub mod fat; |
| 7 | pub mod font; | 7 | pub mod font; |
| 8 | pub mod fs; | 8 | pub mod fs; |
| 9 | pub mod gdt; | ||
| 9 | pub mod gop; | 10 | pub mod gop; |
| 10 | pub mod interrupts; | 11 | pub mod interrupts; |
| 11 | pub mod klog; | 12 | pub mod klog; |
| @@ -15,6 +16,7 @@ pub mod pci; | |||
| 15 | pub mod power; | 16 | pub mod power; |
| 16 | pub mod ps2; | 17 | pub mod ps2; |
| 17 | pub mod rtc; | 18 | pub mod rtc; |
| 19 | pub mod sched; | ||
| 18 | pub mod serial; | 20 | pub mod serial; |
| 19 | pub mod strbuf; | 21 | pub mod strbuf; |
| 20 | pub mod tty; | 22 | 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 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // sched — preemptive round-robin kernel tasks. | ||
| 3 | // | ||
| 4 | // 16 static task slots. The 100 Hz PIT tick (vector 32) and the | ||
| 5 | // software yield vector (0x60) go through `isr_common`, which saves the | ||
| 6 | // running task's full IntFrame on its own stack and asks the scheduler | ||
| 7 | // for the next frame pointer — the asm then moves rsp there and iretq's, | ||
| 8 | // which *is* the context switch. | ||
| 9 | // | ||
| 10 | // - task 0: the shell (boot stack, never allocated by us) | ||
| 11 | // - task 1: idle (hlt loop, always ready — the lowest-priority | ||
| 12 | // fallback) | ||
| 13 | // - tasks 2..: spawn'd, 8 KiB stack + one unmapped guard page below | ||
| 14 | // | ||
| 15 | // Preemption is disabled while the scheduler lock (PREEMPT_DEPTH) is | ||
| 16 | // held: the tick/yield handlers bail out entirely when the depth is | ||
| 17 | // > 0, so table mutations and klog/serial line writes are atomic with | ||
| 18 | // respect to switching. The lock is a counter, not a mutex — re-entrant | ||
| 19 | // by design (a panic mid-emit increments again; the panic path clears | ||
| 20 | // it, because a dead task's lock is meaningless). | ||
| 21 | // | ||
| 22 | // Task page faults (vector 14) run on a dedicated IST stack (see gdt.rs) | ||
| 23 | // so a stack overflow inside a guard page kills the task gracefully | ||
| 24 | // instead of double-faulting the machine. | ||
| 25 | // --------------------------------------------------------------------------- | ||
| 26 | use core::arch::asm; | ||
| 27 | use core::fmt; | ||
| 28 | use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; | ||
| 29 | |||
| 30 | extern crate alloc; | ||
| 31 | use alloc::string::String; | ||
| 32 | use alloc::vec::Vec; | ||
| 33 | |||
| 34 | use crate::interrupts::IntFrame; | ||
| 35 | use crate::memory; | ||
| 36 | use crate::paging; | ||
| 37 | |||
| 38 | pub const MAX_TASKS: usize = 16; | ||
| 39 | pub const SHELL_TID: usize = 0; | ||
| 40 | pub const IDLE_TID: usize = 1; | ||
| 41 | const STACK_PAGES: usize = 3; // 1 guard page + 8 KiB stack | ||
| 42 | const NAME_MAX: usize = 24; | ||
| 43 | const PANIC_MAX: usize = 64; | ||
| 44 | |||
| 45 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] | ||
| 46 | pub enum TaskState { | ||
| 47 | Ready, | ||
| 48 | Sleeping, | ||
| 49 | Blocked, | ||
| 50 | Done, | ||
| 51 | } | ||
| 52 | |||
| 53 | struct Task { | ||
| 54 | name: [u8; NAME_MAX], | ||
| 55 | name_len: u8, | ||
| 56 | state: TaskState, | ||
| 57 | /// Frame pointer to restore on switch-in (set at spawn; stored on | ||
| 58 | /// every switch-away). | ||
| 59 | saved_rsp: usize, | ||
| 60 | /// Physical base of the stack allocation (0 = none, e.g. the shell). | ||
| 61 | stack: usize, | ||
| 62 | /// Guard page address (unmapped; 0 = none). | ||
| 63 | guard: usize, | ||
| 64 | wake_at: u64, | ||
| 65 | ticks: u64, | ||
| 66 | panic: [u8; PANIC_MAX], | ||
| 67 | panic_len: u8, | ||
| 68 | } | ||
| 69 | |||
| 70 | const TASK_NONE: Task = Task { | ||
| 71 | name: [0; NAME_MAX], | ||
| 72 | name_len: 0, | ||
| 73 | state: TaskState::Done, | ||
| 74 | saved_rsp: 0, | ||
| 75 | stack: 0, | ||
| 76 | guard: 0, | ||
| 77 | wake_at: 0, | ||
| 78 | ticks: 0, | ||
| 79 | panic: [0; PANIC_MAX], | ||
| 80 | panic_len: 0, | ||
| 81 | }; | ||
| 82 | |||
| 83 | static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS]; | ||
| 84 | static mut CURRENT: usize = SHELL_TID; | ||
| 85 | static mut KERNEL_CS: u16 = 0; | ||
| 86 | static mut KERNEL_SS: u16 = 0; | ||
| 87 | |||
| 88 | static SCHED_READY: AtomicBool = AtomicBool::new(false); | ||
| 89 | static IN_ISR: AtomicBool = AtomicBool::new(false); | ||
| 90 | static PREEMPT_DEPTH: AtomicU64 = AtomicU64::new(0); | ||
| 91 | |||
| 92 | // --------------------------------------------------------------------------- | ||
| 93 | // Preemption lock | ||
| 94 | // --------------------------------------------------------------------------- | ||
| 95 | |||
| 96 | /// Disable preemption (re-entrant counter). The tick/yield handlers | ||
| 97 | /// skip all scheduling work while the depth is > 0. | ||
| 98 | pub fn lock() { | ||
| 99 | PREEMPT_DEPTH.fetch_add(1, Ordering::Relaxed); | ||
| 100 | } | ||
| 101 | |||
| 102 | /// Re-enable preemption (must pair with `lock`). | ||
| 103 | pub fn unlock() { | ||
| 104 | PREEMPT_DEPTH.fetch_sub(1, Ordering::Relaxed); | ||
| 105 | } | ||
| 106 | |||
| 107 | pub fn in_isr() -> bool { | ||
| 108 | IN_ISR.load(Ordering::Relaxed) | ||
| 109 | } | ||
| 110 | pub fn set_in_isr(v: bool) { | ||
| 111 | IN_ISR.store(v, Ordering::Relaxed); | ||
| 112 | } | ||
| 113 | pub fn is_ready() -> bool { | ||
| 114 | SCHED_READY.load(Ordering::Relaxed) | ||
| 115 | } | ||
| 116 | pub fn current() -> usize { | ||
| 117 | unsafe { CURRENT } | ||
| 118 | } | ||
| 119 | |||
| 120 | // --------------------------------------------------------------------------- | ||
| 121 | // Tick / yield entry points (called from the ISR dispatch) | ||
| 122 | // --------------------------------------------------------------------------- | ||
| 123 | |||
| 124 | /// Timer tick: wake sleepers, account ticks, pick the next task. | ||
| 125 | /// Receives the interrupted task's frame pointer; returns the frame | ||
| 126 | /// pointer the asm should switch to (the same one when no switch). | ||
| 127 | pub fn on_tick(cur: usize) -> usize { | ||
| 128 | if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { | ||
| 129 | return cur; | ||
| 130 | } | ||
| 131 | unsafe { | ||
| 132 | TASKS[CURRENT].saved_rsp = cur; | ||
| 133 | TASKS[CURRENT].ticks += 1; | ||
| 134 | let now = crate::interrupts::uptime_ms(); | ||
| 135 | for t in 0..MAX_TASKS { | ||
| 136 | if TASKS[t].state == TaskState::Sleeping && TASKS[t].wake_at <= now { | ||
| 137 | TASKS[t].state = TaskState::Ready; | ||
| 138 | } | ||
| 139 | } | ||
| 140 | pick_next(cur) | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | /// Software yield (vector 0x60). | ||
| 145 | pub fn on_yield(cur: usize) -> usize { | ||
| 146 | if !is_ready() || PREEMPT_DEPTH.load(Ordering::Relaxed) > 0 { | ||
| 147 | return cur; | ||
| 148 | } | ||
| 149 | unsafe { | ||
| 150 | TASKS[CURRENT].saved_rsp = cur; | ||
| 151 | pick_next(cur) | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | /// Round-robin over Ready tasks, skipping the idle task unless nothing | ||
| 156 | /// else can run. Returns the next frame pointer (or `cur` to stay). | ||
| 157 | unsafe fn pick_next(cur: usize) -> usize { | ||
| 158 | unsafe { | ||
| 159 | for i in 1..MAX_TASKS { | ||
| 160 | let idx = (CURRENT + i) % MAX_TASKS; | ||
| 161 | if idx == IDLE_TID { | ||
| 162 | continue; | ||
| 163 | } | ||
| 164 | if TASKS[idx].state == TaskState::Ready { | ||
| 165 | CURRENT = idx; | ||
| 166 | return TASKS[idx].saved_rsp; | ||
| 167 | } | ||
| 168 | } | ||
| 169 | if CURRENT != IDLE_TID && TASKS[IDLE_TID].state == TaskState::Ready { | ||
| 170 | CURRENT = IDLE_TID; | ||
| 171 | return TASKS[IDLE_TID].saved_rsp; | ||
| 172 | } | ||
| 173 | cur | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | // --------------------------------------------------------------------------- | ||
| 178 | // Spawn / lifecycle | ||
| 179 | // --------------------------------------------------------------------------- | ||
| 180 | |||
| 181 | /// Spawn a kernel task running `entry` on its own 8 KiB stack with a | ||
| 182 | /// guard page below it. Returns the task id, or None when the table is | ||
| 183 | /// full (Done slots are reaped first). `entry` runs inside a | ||
| 184 | /// trampoline, so returning from it exits the task cleanly. | ||
| 185 | pub fn spawn(entry: fn(), name: &str) -> Option<usize> { | ||
| 186 | lock(); | ||
| 187 | let id = unsafe { | ||
| 188 | // Reap a Done task with a stack, or take a never-used slot. | ||
| 189 | let mut slot = None; | ||
| 190 | for i in 2..MAX_TASKS { | ||
| 191 | if TASKS[i].state == TaskState::Done && TASKS[i].stack != 0 { | ||
| 192 | reap(i); | ||
| 193 | slot = Some(i); | ||
| 194 | break; | ||
| 195 | } | ||
| 196 | if TASKS[i].state == TaskState::Done && TASKS[i].stack == 0 && TASKS[i].name_len == 0 { | ||
| 197 | slot = Some(i); | ||
| 198 | break; | ||
| 199 | } | ||
| 200 | } | ||
| 201 | slot? | ||
| 202 | }; | ||
| 203 | let ok = spawn_at(id, entry, name); | ||
| 204 | unlock(); | ||
| 205 | if ok { | ||
| 206 | Some(id) | ||
| 207 | } else { | ||
| 208 | None | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | unsafe fn reap(id: usize) { | ||
| 213 | unsafe { | ||
| 214 | let t = &mut TASKS[id]; | ||
| 215 | if t.stack != 0 { | ||
| 216 | // Restore the guard page's mapping so the pages are reusable. | ||
| 217 | paging::map_page(t.stack, paging::PAGE_WRITABLE); | ||
| 218 | memory::free_pages(t.stack, STACK_PAGES); | ||
| 219 | } | ||
| 220 | *t = TASK_NONE; | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | fn spawn_at(id: usize, entry: fn(), name: &str) -> bool { | ||
| 225 | let Some(base) = memory::alloc_pages(STACK_PAGES) else { | ||
| 226 | return false; | ||
| 227 | }; | ||
| 228 | // The first page is the guard — unmap it so any stack overflow | ||
| 229 | // faults there (caught on the vector-14 IST stack). | ||
| 230 | if !paging::unmap_page(base) { | ||
| 231 | memory::free_pages(base, STACK_PAGES); | ||
| 232 | return false; | ||
| 233 | } | ||
| 234 | let top = base + STACK_PAGES * memory::PAGE_SIZE; | ||
| 235 | let frame = (top - core::mem::size_of::<IntFrame>()) as *mut IntFrame; | ||
| 236 | let mut name_buf = [0u8; NAME_MAX]; | ||
| 237 | let n = name.len().min(NAME_MAX); | ||
| 238 | name_buf[..n].copy_from_slice(&name.as_bytes()[..n]); | ||
| 239 | unsafe { | ||
| 240 | core::ptr::write( | ||
| 241 | frame, | ||
| 242 | IntFrame { | ||
| 243 | r15: 0, | ||
| 244 | r14: 0, | ||
| 245 | r13: 0, | ||
| 246 | r12: 0, | ||
| 247 | r11: 0, | ||
| 248 | r10: 0, | ||
| 249 | r9: 0, | ||
| 250 | r8: 0, | ||
| 251 | rdi: 0, | ||
| 252 | rsi: 0, | ||
| 253 | rbp: 0, | ||
| 254 | rbx: 0, | ||
| 255 | rdx: 0, | ||
| 256 | rcx: entry as usize as u64, // MS x64 ABI: first arg | ||
| 257 | rax: 0, | ||
| 258 | vector: 0, | ||
| 259 | error: 0, | ||
| 260 | rip: trampoline as *const () as u64, | ||
| 261 | cs: KERNEL_CS as u64, | ||
| 262 | rflags: 0x202, // IF | reserved bit 1 | ||
| 263 | // QEMU's iretq always pops RSP/SS from the frame tail. | ||
| 264 | // rsp = the stack top so the first iretq lands with a | ||
| 265 | // valid stack pointer (the trampoline then descends from | ||
| 266 | // it normally); ss mirrors the kernel data selector. | ||
| 267 | rsp: top as u64, | ||
| 268 | ss: KERNEL_SS as u64, | ||
| 269 | }, | ||
| 270 | ); | ||
| 271 | TASKS[id] = Task { | ||
| 272 | name: name_buf, | ||
| 273 | name_len: n as u8, | ||
| 274 | state: TaskState::Ready, | ||
| 275 | saved_rsp: frame as usize, | ||
| 276 | stack: base, | ||
| 277 | guard: base, | ||
| 278 | wake_at: 0, | ||
| 279 | ticks: 0, | ||
| 280 | panic: [0; PANIC_MAX], | ||
| 281 | panic_len: 0, | ||
| 282 | }; | ||
| 283 | } | ||
| 284 | true | ||
| 285 | } | ||
| 286 | |||
| 287 | /// Entry trampoline: run the task body, then exit the task when it | ||
| 288 | /// returns. Runs with the synthetic frame's registers (rcx = entry). | ||
| 289 | unsafe extern "C" fn trampoline(entry: usize) -> ! { | ||
| 290 | let f: fn() = unsafe { core::mem::transmute(entry as *const ()) }; | ||
| 291 | f(); | ||
| 292 | task_exit() | ||
| 293 | } | ||
| 294 | |||
| 295 | fn task_exit() -> ! { | ||
| 296 | lock(); | ||
| 297 | unsafe { | ||
| 298 | TASKS[CURRENT].state = TaskState::Done; | ||
| 299 | } | ||
| 300 | unlock(); | ||
| 301 | yield_int(); | ||
| 302 | unreachable!() | ||
| 303 | } | ||
| 304 | |||
| 305 | /// Block the current task for `ms` milliseconds. The shell uses this | ||
| 306 | /// for `sleep`; background tasks keep running meanwhile. | ||
| 307 | pub fn sleep_ms(ms: u64) { | ||
| 308 | lock(); | ||
| 309 | unsafe { | ||
| 310 | TASKS[CURRENT].state = TaskState::Sleeping; | ||
| 311 | TASKS[CURRENT].wake_at = crate::interrupts::uptime_ms() + ms; | ||
| 312 | } | ||
| 313 | unlock(); | ||
| 314 | yield_int(); | ||
| 315 | } | ||
| 316 | |||
| 317 | /// Cooperatively yield the rest of the current quantum. | ||
| 318 | pub fn yield_now() { | ||
| 319 | yield_int(); | ||
| 320 | } | ||
| 321 | |||
| 322 | /// Software-interrupt the scheduler (vector 0x60). Net stack usage is | ||
| 323 | /// zero (the frame is popped by iretq), so no `nostack` — the compiler | ||
| 324 | /// must not use the red zone around the interrupt. | ||
| 325 | #[inline(never)] | ||
| 326 | fn yield_int() { | ||
| 327 | unsafe { | ||
| 328 | asm!("int 0x60"); | ||
| 329 | } | ||
| 330 | } | ||
| 331 | |||
| 332 | // --------------------------------------------------------------------------- | ||
| 333 | // Task panics and faults | ||
| 334 | // --------------------------------------------------------------------------- | ||
| 335 | |||
| 336 | /// Kill the current task with a panic message: logs it, marks the task | ||
| 337 | /// Done, drops its scheduler lock (a dead task's lock is meaningless), | ||
| 338 | /// and switches away forever. Called by the panic hook (task context) | ||
| 339 | /// and by the page-fault handler (IST stack, ISR context). | ||
| 340 | pub fn abort_current(msg: &str) -> ! { | ||
| 341 | unsafe { | ||
| 342 | let id = CURRENT; | ||
| 343 | let n = msg.len().min(PANIC_MAX); | ||
| 344 | TASKS[id].panic[..n].copy_from_slice(&msg.as_bytes()[..n]); | ||
| 345 | TASKS[id].panic_len = n as u8; | ||
| 346 | TASKS[id].state = TaskState::Done; | ||
| 347 | crate::serial_println!( | ||
| 348 | "sched: task {} \"{}\" panicked: {} (rsp_saved={:#x} stack={:#x} guard={:#x})", | ||
| 349 | id, | ||
| 350 | name_str(id), | ||
| 351 | msg, | ||
| 352 | TASKS[id].saved_rsp, | ||
| 353 | TASKS[id].stack, | ||
| 354 | TASKS[id].guard | ||
| 355 | ); | ||
| 356 | PREEMPT_DEPTH.store(0, Ordering::Relaxed); | ||
| 357 | } | ||
| 358 | yield_int(); | ||
| 359 | unreachable!() | ||
| 360 | } | ||
| 361 | |||
| 362 | /// Vector 14 (page fault) handler — runs on the dedicated IST stack. | ||
| 363 | /// A fault in a real task (id >= 2) kills the task gracefully; anything | ||
| 364 | /// else (shell, idle, pre-scheduler) is a kernel bug: halt. | ||
| 365 | pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize { | ||
| 366 | if !is_ready() || unsafe { CURRENT } <= IDLE_TID { | ||
| 367 | crate::serial_println!("pgfault: kernel page fault at {:#x} — halting", cr2); | ||
| 368 | crate::interrupts::halt(); | ||
| 369 | } | ||
| 370 | let g = unsafe { TASKS[CURRENT].guard }; | ||
| 371 | let in_guard = g != 0 && cr2 >= g as u64 && cr2 < (g + memory::PAGE_SIZE) as u64; | ||
| 372 | let rip = unsafe { (frame_ptr as *const IntFrame).read().rip }; | ||
| 373 | let mut buf = [0u8; 96]; | ||
| 374 | let n = { | ||
| 375 | let mut f = FmtBuf { b: &mut buf, n: 0 }; | ||
| 376 | let _ = fmt::write( | ||
| 377 | &mut f, | ||
| 378 | core::format_args!( | ||
| 379 | "{} at {:#x} (rip={:#x})", | ||
| 380 | if in_guard { | ||
| 381 | "stack overflow (guard page)" | ||
| 382 | } else { | ||
| 383 | "page fault" | ||
| 384 | }, | ||
| 385 | cr2, | ||
| 386 | rip | ||
| 387 | ), | ||
| 388 | ); | ||
| 389 | f.n | ||
| 390 | }; | ||
| 391 | abort_current(core::str::from_utf8(&buf[..n]).unwrap_or("task fault")) | ||
| 392 | } | ||
| 393 | |||
| 394 | // --------------------------------------------------------------------------- | ||
| 395 | // Introspection (shell: jobs / kill) | ||
| 396 | // --------------------------------------------------------------------------- | ||
| 397 | |||
| 398 | /// Snapshot the task table for `jobs` — one line per used slot. | ||
| 399 | pub fn jobs() -> Vec<String> { | ||
| 400 | let mut v = Vec::new(); | ||
| 401 | lock(); | ||
| 402 | unsafe { | ||
| 403 | for i in 0..MAX_TASKS { | ||
| 404 | let t = &TASKS[i]; | ||
| 405 | if t.name_len == 0 { | ||
| 406 | continue; // never used | ||
| 407 | } | ||
| 408 | let state = match t.state { | ||
| 409 | TaskState::Ready => "ready", | ||
| 410 | TaskState::Sleeping => "sleeping", | ||
| 411 | TaskState::Blocked => "blocked", | ||
| 412 | TaskState::Done => { | ||
| 413 | if t.panic_len > 0 { | ||
| 414 | "done (panicked)" | ||
| 415 | } else { | ||
| 416 | "done" | ||
| 417 | } | ||
| 418 | } | ||
| 419 | }; | ||
| 420 | let mut line = alloc::format!( | ||
| 421 | " {:>2} {:<12} {:<15} {:>6} ticks", | ||
| 422 | i, | ||
| 423 | name_str(i), | ||
| 424 | state, | ||
| 425 | t.ticks | ||
| 426 | ); | ||
| 427 | if t.panic_len > 0 { | ||
| 428 | let m = core::str::from_utf8(&t.panic[..t.panic_len as usize]).unwrap_or("?"); | ||
| 429 | line.push_str(" "); | ||
| 430 | line.push_str(m); | ||
| 431 | } | ||
| 432 | v.push(line); | ||
| 433 | } | ||
| 434 | } | ||
| 435 | unlock(); | ||
| 436 | v | ||
| 437 | } | ||
| 438 | |||
| 439 | /// Mark a task Done so it stops running and gets reaped at the next | ||
| 440 | /// spawn. The shell (0) and idle (1) are protected. | ||
| 441 | pub fn kill(id: usize) -> Result<(), &'static str> { | ||
| 442 | if id == SHELL_TID { | ||
| 443 | return Err("cannot kill the shell (task 0)"); | ||
| 444 | } | ||
| 445 | if id == IDLE_TID { | ||
| 446 | return Err("cannot kill the idle task (task 1)"); | ||
| 447 | } | ||
| 448 | if id >= MAX_TASKS { | ||
| 449 | return Err("no such task"); | ||
| 450 | } | ||
| 451 | lock(); | ||
| 452 | let alive = unsafe { | ||
| 453 | let t = &TASKS[id]; | ||
| 454 | t.name_len != 0 && t.state != TaskState::Done | ||
| 455 | }; | ||
| 456 | if !alive { | ||
| 457 | unlock(); | ||
| 458 | return Err("no such task"); | ||
| 459 | } | ||
| 460 | unsafe { | ||
| 461 | TASKS[id].state = TaskState::Done; | ||
| 462 | } | ||
| 463 | unlock(); | ||
| 464 | Ok(()) | ||
| 465 | } | ||
| 466 | |||
| 467 | pub fn name_str(id: usize) -> &'static str { | ||
| 468 | unsafe { | ||
| 469 | let t = &TASKS[id]; | ||
| 470 | if t.name_len == 0 { | ||
| 471 | return ""; | ||
| 472 | } | ||
| 473 | core::str::from_utf8(&t.name[..t.name_len as usize]).unwrap_or("") | ||
| 474 | } | ||
| 475 | } | ||
| 476 | |||
| 477 | // --------------------------------------------------------------------------- | ||
| 478 | // Init | ||
| 479 | // --------------------------------------------------------------------------- | ||
| 480 | |||
| 481 | /// Set up the task table: task 0 is the shell (boot stack), task 1 is | ||
| 482 | /// the idle `hlt` fallback. Called after `interrupts::init` (the tick | ||
| 483 | /// must be live so `sleep`/wake scheduling works). | ||
| 484 | pub fn init() { | ||
| 485 | unsafe { | ||
| 486 | let cs64: u64; | ||
| 487 | asm!("mov {0}, cs", out(reg) cs64, options(nostack, preserves_flags)); | ||
| 488 | KERNEL_CS = cs64 as u16; | ||
| 489 | let ss64: u64; | ||
| 490 | asm!("mov {0}, ss", out(reg) ss64, options(nostack, preserves_flags)); | ||
| 491 | KERNEL_SS = ss64 as u16; | ||
| 492 | |||
| 493 | TASKS[SHELL_TID] = Task { | ||
| 494 | name: { | ||
| 495 | let mut n = [0u8; 24]; | ||
| 496 | n[..5].copy_from_slice(b"shell"); | ||
| 497 | n | ||
| 498 | }, | ||
| 499 | name_len: 5, | ||
| 500 | state: TaskState::Ready, | ||
| 501 | saved_rsp: 0, // captured on the first switch-away | ||
| 502 | stack: 0, | ||
| 503 | guard: 0, | ||
| 504 | wake_at: 0, | ||
| 505 | ticks: 0, | ||
| 506 | panic: [0; PANIC_MAX], | ||
| 507 | panic_len: 0, | ||
| 508 | }; | ||
| 509 | spawn_at(IDLE_TID, idle_main, "idle"); | ||
| 510 | SCHED_READY.store(true, Ordering::Relaxed); | ||
| 511 | } | ||
| 512 | crate::serial_println!( | ||
| 513 | "sched: ready — {} slots, shell=task 0, idle=task 1", | ||
| 514 | MAX_TASKS | ||
| 515 | ); | ||
| 516 | crate::serial_println!( | ||
| 517 | "sched: trampoline={:#x} idle={:#x} emit={:#x}", | ||
| 518 | trampoline as *const () as usize, | ||
| 519 | idle_main as *const () as usize, | ||
| 520 | crate::klog::emit as *const () as usize | ||
| 521 | ); | ||
| 522 | } | ||
| 523 | |||
| 524 | fn idle_main() { | ||
| 525 | loop { | ||
| 526 | unsafe { | ||
| 527 | asm!("hlt", options(nostack, preserves_flags)); | ||
| 528 | } | ||
| 529 | } | ||
| 530 | } | ||
| 531 | |||
| 532 | struct FmtBuf<'a> { | ||
| 533 | b: &'a mut [u8], | ||
| 534 | n: usize, | ||
| 535 | } | ||
| 536 | |||
| 537 | impl fmt::Write for FmtBuf<'_> { | ||
| 538 | fn write_str(&mut self, s: &str) -> fmt::Result { | ||
| 539 | let m = s.len().min(self.b.len() - self.n); | ||
| 540 | self.b[self.n..self.n + m].copy_from_slice(&s.as_bytes()[..m]); | ||
| 541 | self.n += m; | ||
| 542 | Ok(()) | ||
| 543 | } | ||
| 544 | } | ||
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<u8> { | |||
| 60 | } | 60 | } |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | /// Write a string to serial. | 63 | /// Write a string to serial, holding the scheduler lock for the whole |
| 64 | /// write so a line can't be interleaved with another task's output. | ||
| 64 | pub fn write_str(s: &str) { | 65 | pub fn write_str(s: &str) { |
| 66 | crate::sched::lock(); | ||
| 67 | write_raw(s); | ||
| 68 | crate::sched::unlock(); | ||
| 69 | } | ||
| 70 | |||
| 71 | /// Raw serial write — no scheduler lock. Callers must hold the lock | ||
| 72 | /// (klog::emit does) or be in a context where preemption is impossible. | ||
| 73 | pub fn write_raw(s: &str) { | ||
| 65 | for &b in s.as_bytes() { | 74 | for &b in s.as_bytes() { |
| 66 | if b == b'\n' { | 75 | if b == b'\n' { |
| 67 | put_byte(b'\r'); | 76 | put_byte(b'\r'); |
| @@ -70,6 +79,16 @@ pub fn write_str(s: &str) { | |||
| 70 | } | 79 | } |
| 71 | } | 80 | } |
| 72 | 81 | ||
| 82 | /// One scheduler-locked call: the line plus its CRLF, atomic with | ||
| 83 | /// respect to context switches (used by the shell's term_println, where | ||
| 84 | /// the line and its newline must not be split by a tick). | ||
| 85 | pub fn write_line(s: &str) { | ||
| 86 | crate::sched::lock(); | ||
| 87 | write_raw(s); | ||
| 88 | write_raw("\r\n"); | ||
| 89 | crate::sched::unlock(); | ||
| 90 | } | ||
| 91 | |||
| 73 | // --------------------------------------------------------------------------- | 92 | // --------------------------------------------------------------------------- |
| 74 | // fmt::Write implementation | 93 | // fmt::Write implementation |
| 75 | // --------------------------------------------------------------------------- | 94 | // --------------------------------------------------------------------------- |
| @@ -96,17 +115,17 @@ macro_rules! serial_print { | |||
| 96 | /// here, so the ring captures the whole boot sequence for free. | 115 | /// here, so the ring captures the whole boot sequence for free. |
| 97 | /// User-facing shell output must NOT use this macro (shell output would | 116 | /// User-facing shell output must NOT use this macro (shell output would |
| 98 | /// pollute the boot log); it writes to the serial port directly. | 117 | /// pollute the boot log); it writes to the serial port directly. |
| 118 | /// | ||
| 119 | /// The whole line is formatted, written, and logged under one scheduler | ||
| 120 | /// lock (klog::emit), so lines from different tasks never interleave. | ||
| 99 | #[macro_export] | 121 | #[macro_export] |
| 100 | macro_rules! serial_println { | 122 | macro_rules! serial_println { |
| 101 | () => { | 123 | () => { |
| 102 | $crate::serial::write_str("\r\n"); | 124 | $crate::klog::emit(core::format_args!("")) |
| 103 | $crate::klog::append_fmt(core::format_args!("")); | 125 | }; |
| 126 | ($($arg:tt)*) => { | ||
| 127 | $crate::klog::emit(core::format_args!($($arg)*)) | ||
| 104 | }; | 128 | }; |
| 105 | ($($arg:tt)*) => {{ | ||
| 106 | $crate::serial_print!($($arg)*); | ||
| 107 | $crate::serial::write_str("\r\n"); | ||
| 108 | $crate::klog::append_fmt(core::format_args!($($arg)*)); | ||
| 109 | }}; | ||
| 110 | } | 129 | } |
| 111 | 130 | ||
| 112 | // --------------------------------------------------------------------------- | 131 | // --------------------------------------------------------------------------- |
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) { | |||
| 83 | } | 83 | } |
| 84 | 84 | ||
| 85 | fn term_println(s: &str) { | 85 | fn term_println(s: &str) { |
| 86 | term_print(s); | 86 | gop_print!("{}", s); |
| 87 | gop_println!(); | 87 | gop_println!(); |
| 88 | rsnix::serial::write_str("\r\n"); | 88 | // One locked call (line + CRLF) so a tick can't split the pair and |
| 89 | // let a background task's serial line interleave mid-line. | ||
| 90 | rsnix::serial::write_line(s); | ||
| 89 | } | 91 | } |
| 90 | 92 | ||
| 91 | fn term_err(msg: &str) { | 93 | fn term_err(msg: &str) { |
| @@ -696,6 +698,11 @@ write `{} <path> TIL <marker>`", | |||
| 696 | "touch" => cmd_touch(&parsed), | 698 | "touch" => cmd_touch(&parsed), |
| 697 | "rm" => cmd_rm(&parsed), | 699 | "rm" => cmd_rm(&parsed), |
| 698 | "edit" => cmd_edit(&parsed), | 700 | "edit" => cmd_edit(&parsed), |
| 701 | "bg" => cmd_bg(&parsed), | ||
| 702 | "jobs" => cmd_jobs(&parsed), | ||
| 703 | "sleep" => cmd_sleep(&parsed), | ||
| 704 | "kill" => cmd_kill(&parsed), | ||
| 705 | "demo" => cmd_demo(&parsed), | ||
| 699 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), | 706 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), |
| 700 | } | 707 | } |
| 701 | } | 708 | } |
| @@ -718,6 +725,11 @@ fn cmd_help(_p: &Parsed) { | |||
| 718 | date — show current date/time (RTC) | 725 | date — show current date/time (RTC) |
| 719 | pci — list PCI devices | 726 | pci — list PCI devices |
| 720 | rescan — re-scan for hotplugged/removed disks (also runs automatically) | 727 | rescan — re-scan for hotplugged/removed disks (also runs automatically) |
| 728 | bg <name> — spawn a background task (spinner, ticker, counter, boom, overflow) | ||
| 729 | jobs — list tasks (id, name, state, ticks) | ||
| 730 | sleep <ms> — block the shell for ms; background tasks keep running | ||
| 731 | kill <id> — stop a background task (id from jobs) | ||
| 732 | demo — spawn spinner + ticker + counter | ||
| 721 | ls — list a directory | 733 | ls — list a directory |
| 722 | cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) | 734 | cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) |
| 723 | klog — kernel boot log (cat /dev/klog; WRITING <dst> persists it) | 735 | klog — kernel boot log (cat /dev/klog; WRITING <dst> persists it) |
| @@ -1562,6 +1574,145 @@ fn cmd_rescan(_p: &Parsed) { | |||
| 1562 | } | 1574 | } |
| 1563 | } | 1575 | } |
| 1564 | 1576 | ||
| 1577 | // --------------------------------------------------------------------------- | ||
| 1578 | // Multitasking: bg / jobs / sleep / kill / demo | ||
| 1579 | // | ||
| 1580 | // Background tasks are headless: they may sleep, yield, and log to | ||
| 1581 | // serial/klog only. Only the shell touches the disk, filesystem, and | ||
| 1582 | // terminal (the ADR 0003 invariant). | ||
| 1583 | // --------------------------------------------------------------------------- | ||
| 1584 | |||
| 1585 | fn cmd_bg(p: &Parsed) { | ||
| 1586 | let Some(name) = p.args.first() else { | ||
| 1587 | term_err("bg: needs a task name — spinner, ticker, counter, boom, overflow"); | ||
| 1588 | return; | ||
| 1589 | }; | ||
| 1590 | let entry: fn() = match name.as_str() { | ||
| 1591 | "spinner" => task_spinner, | ||
| 1592 | "ticker" => task_ticker, | ||
| 1593 | "counter" => task_counter, | ||
| 1594 | "boom" => task_boom, | ||
| 1595 | "overflow" => task_overflow, | ||
| 1596 | _ => { | ||
| 1597 | term_err(&alloc::format!( | ||
| 1598 | "bg: unknown task '{}' (spinner, ticker, counter, boom, overflow)", | ||
| 1599 | name | ||
| 1600 | )); | ||
| 1601 | return; | ||
| 1602 | } | ||
| 1603 | }; | ||
| 1604 | match rsnix::sched::spawn(entry, name) { | ||
| 1605 | Some(id) => term_println(&alloc::format!("bg: spawned task {} ({})", id, name)), | ||
| 1606 | None => term_err("bg: task table full"), | ||
| 1607 | } | ||
| 1608 | } | ||
| 1609 | |||
| 1610 | fn cmd_jobs(p: &Parsed) { | ||
| 1611 | let mut pipe = Pipeline::new(&p.filters, &p.output); | ||
| 1612 | let lines = rsnix::sched::jobs(); | ||
| 1613 | if lines.is_empty() { | ||
| 1614 | let _ = pipe.line("jobs: no tasks"); | ||
| 1615 | } | ||
| 1616 | for l in lines { | ||
| 1617 | if !pipe.line(&l) { | ||
| 1618 | break; | ||
| 1619 | } | ||
| 1620 | } | ||
| 1621 | pipe.end(); | ||
| 1622 | } | ||
| 1623 | |||
| 1624 | fn cmd_sleep(p: &Parsed) { | ||
| 1625 | let ms: u64 = match p.args.first().and_then(|s| s.parse().ok()) { | ||
| 1626 | Some(m) if m > 0 => m, | ||
| 1627 | _ => { | ||
| 1628 | term_err("sleep: needs a duration in ms (e.g. sleep 2000)"); | ||
| 1629 | return; | ||
| 1630 | } | ||
| 1631 | }; | ||
| 1632 | term_println(&alloc::format!( | ||
| 1633 | "sleep: blocking shell for {} ms — background tasks keep running", | ||
| 1634 | ms | ||
| 1635 | )); | ||
| 1636 | rsnix::sched::sleep_ms(ms); | ||
| 1637 | term_println(&alloc::format!("sleep: woke after {} ms", ms)); | ||
| 1638 | } | ||
| 1639 | |||
| 1640 | fn cmd_kill(p: &Parsed) { | ||
| 1641 | let id: usize = match p.args.first().and_then(|s| s.parse().ok()) { | ||
| 1642 | Some(id) => id, | ||
| 1643 | None => { | ||
| 1644 | term_err("kill: needs a task id (see jobs)"); | ||
| 1645 | return; | ||
| 1646 | } | ||
| 1647 | }; | ||
| 1648 | match rsnix::sched::kill(id) { | ||
| 1649 | Ok(()) => term_println(&alloc::format!("kill: task {} marked done", id)), | ||
| 1650 | Err(e) => term_err(&alloc::format!("kill: {}", e)), | ||
| 1651 | } | ||
| 1652 | } | ||
| 1653 | |||
| 1654 | fn cmd_demo(_p: &Parsed) { | ||
| 1655 | let mut n = 0; | ||
| 1656 | for (entry, name) in [ | ||
| 1657 | (task_spinner as fn(), "spinner"), | ||
| 1658 | (task_ticker as fn(), "ticker"), | ||
| 1659 | (task_counter as fn(), "counter"), | ||
| 1660 | ] { | ||
| 1661 | if rsnix::sched::spawn(entry, name).is_some() { | ||
| 1662 | n += 1; | ||
| 1663 | } | ||
| 1664 | } | ||
| 1665 | term_println(&alloc::format!( | ||
| 1666 | "demo: spawned {} task(s) — spinner 250ms, ticker 500ms, counter 1000ms (try jobs)", | ||
| 1667 | n | ||
| 1668 | )); | ||
| 1669 | } | ||
| 1670 | |||
| 1671 | // --- Demo task bodies (headless: sleep + serial/klog only) --- | ||
| 1672 | |||
| 1673 | fn task_spinner() { | ||
| 1674 | let mut n: u64 = 0; | ||
| 1675 | loop { | ||
| 1676 | rsnix::serial_println!("spinner: {}", n); | ||
| 1677 | n += 1; | ||
| 1678 | rsnix::sched::sleep_ms(250); | ||
| 1679 | } | ||
| 1680 | } | ||
| 1681 | |||
| 1682 | fn task_ticker() { | ||
| 1683 | let mut n: u64 = 0; | ||
| 1684 | loop { | ||
| 1685 | rsnix::serial_println!("ticker: {}", n); | ||
| 1686 | n += 1; | ||
| 1687 | rsnix::sched::sleep_ms(500); | ||
| 1688 | } | ||
| 1689 | } | ||
| 1690 | |||
| 1691 | fn task_counter() { | ||
| 1692 | let mut n: u64 = 0; | ||
| 1693 | loop { | ||
| 1694 | rsnix::serial_println!("counter: {}", n); | ||
| 1695 | n += 1; | ||
| 1696 | rsnix::sched::sleep_ms(1000); | ||
| 1697 | } | ||
| 1698 | } | ||
| 1699 | |||
| 1700 | fn task_boom() { | ||
| 1701 | panic!("boom: deliberate task panic"); | ||
| 1702 | } | ||
| 1703 | |||
| 1704 | /// Recurses until the guard page faults — the task should die with a | ||
| 1705 | /// "stack overflow" panic while the shell keeps running. | ||
| 1706 | fn task_overflow() { | ||
| 1707 | #[allow(unconditional_recursion)] | ||
| 1708 | fn recurse(d: u64) { | ||
| 1709 | let keep = [0u8; 512]; | ||
| 1710 | core::hint::black_box(&keep); | ||
| 1711 | recurse(d + 1); | ||
| 1712 | } | ||
| 1713 | recurse(0); | ||
| 1714 | } | ||
| 1715 | |||
| 1565 | fn cmd_pci(p: &Parsed) { | 1716 | fn cmd_pci(p: &Parsed) { |
| 1566 | let devices = rsnix::pci::probe(); | 1717 | let devices = rsnix::pci::probe(); |
| 1567 | let mut pipe = Pipeline::new(&p.filters, &p.output); | 1718 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
