summaryrefslogtreecommitdiffstats
path: root/rsnix
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 17:38:57 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 17:38:57 +0200
commit8f05ea44f69cb796e7b1f581ba1b8045c98d615b (patch)
tree3049f0a5fc33dd08f94af0e6493c588b2a4818b9 /rsnix
parentCONTEXT.md: versioning policy (a.b.c — overhaul/feature/fix) (diff)
downloadrs-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--rsnix/src/gdt.rs242
-rw-r--r--rsnix/src/interrupts.rs139
-rw-r--r--rsnix/src/klog.rs17
-rw-r--r--rsnix/src/lib.rs2
-rw-r--r--rsnix/src/sched.rs544
-rw-r--r--rsnix/src/serial.rs35
6 files changed, 932 insertions, 47 deletions
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// ---------------------------------------------------------------------------
21use core::arch::asm;
22
23const GDT_ENTRIES: usize = 64;
24const TSS_INDEX: usize = GDT_ENTRIES - 1; // 63
25const TSS_SEL: u16 = (TSS_INDEX << 3) as u16; // 0x1F8
26
27/// 8-byte segment descriptor.
28#[repr(C, packed)]
29#[derive(Clone, Copy)]
30struct 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
39impl 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)]
77struct 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
95const TSS_SIZE: usize = 104;
96
97impl 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))]
121struct Page([u8; 4096]);
122
123static mut PF_STACK: Page = Page([0; 4096]);
124static mut DF_STACK: Page = Page([0; 4096]);
125static 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.
128static mut GDT: [u8; GDT_ENTRIES * 8 + 16] = [0; GDT_ENTRIES * 8 + 16];
129static mut GDTR: [u16; 5] = [0; 5];
130
131unsafe 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).
158pub 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).
25unsafe fn idt_set_entry(idt: *mut u8, vec: u8, handler: u64, selector: u16, flags: u8) { 25unsafe 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
214core::arch::global_asm!( 214core::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)]
280struct IntFrame { 304pub(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)]
305unsafe extern "C" fn isr_dispatch(frame: &IntFrame) { 338unsafe 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.
456pub 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// ---------------------------------------------------------------------------
395unsafe extern "C" { 467unsafe 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). 86pub fn emit(args: core::fmt::Arguments<'_>) {
87pub 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
94struct LineBuf { 97struct 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;
6pub mod fat; 6pub mod fat;
7pub mod font; 7pub mod font;
8pub mod fs; 8pub mod fs;
9pub mod gdt;
9pub mod gop; 10pub mod gop;
10pub mod interrupts; 11pub mod interrupts;
11pub mod klog; 12pub mod klog;
@@ -15,6 +16,7 @@ pub mod pci;
15pub mod power; 16pub mod power;
16pub mod ps2; 17pub mod ps2;
17pub mod rtc; 18pub mod rtc;
19pub mod sched;
18pub mod serial; 20pub mod serial;
19pub mod strbuf; 21pub mod strbuf;
20pub mod tty; 22pub 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// ---------------------------------------------------------------------------
26use core::arch::asm;
27use core::fmt;
28use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
29
30extern crate alloc;
31use alloc::string::String;
32use alloc::vec::Vec;
33
34use crate::interrupts::IntFrame;
35use crate::memory;
36use crate::paging;
37
38pub const MAX_TASKS: usize = 16;
39pub const SHELL_TID: usize = 0;
40pub const IDLE_TID: usize = 1;
41const STACK_PAGES: usize = 3; // 1 guard page + 8 KiB stack
42const NAME_MAX: usize = 24;
43const PANIC_MAX: usize = 64;
44
45#[derive(Clone, Copy, PartialEq, Eq, Debug)]
46pub enum TaskState {
47 Ready,
48 Sleeping,
49 Blocked,
50 Done,
51}
52
53struct 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
70const 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
83static mut TASKS: [Task; MAX_TASKS] = [TASK_NONE; MAX_TASKS];
84static mut CURRENT: usize = SHELL_TID;
85static mut KERNEL_CS: u16 = 0;
86static mut KERNEL_SS: u16 = 0;
87
88static SCHED_READY: AtomicBool = AtomicBool::new(false);
89static IN_ISR: AtomicBool = AtomicBool::new(false);
90static 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.
98pub fn lock() {
99 PREEMPT_DEPTH.fetch_add(1, Ordering::Relaxed);
100}
101
102/// Re-enable preemption (must pair with `lock`).
103pub fn unlock() {
104 PREEMPT_DEPTH.fetch_sub(1, Ordering::Relaxed);
105}
106
107pub fn in_isr() -> bool {
108 IN_ISR.load(Ordering::Relaxed)
109}
110pub fn set_in_isr(v: bool) {
111 IN_ISR.store(v, Ordering::Relaxed);
112}
113pub fn is_ready() -> bool {
114 SCHED_READY.load(Ordering::Relaxed)
115}
116pub 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).
127pub 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).
145pub 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).
157unsafe 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.
185pub 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
212unsafe 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
224fn 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).
289unsafe extern "C" fn trampoline(entry: usize) -> ! {
290 let f: fn() = unsafe { core::mem::transmute(entry as *const ()) };
291 f();
292 task_exit()
293}
294
295fn 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.
307pub 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.
318pub 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)]
326fn 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).
340pub 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.
365pub 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.
399pub 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.
441pub 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
467pub 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).
484pub 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
524fn idle_main() {
525 loop {
526 unsafe {
527 asm!("hlt", options(nostack, preserves_flags));
528 }
529 }
530}
531
532struct FmtBuf<'a> {
533 b: &'a mut [u8],
534 n: usize,
535}
536
537impl 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.
64pub fn write_str(s: &str) { 65pub 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.
73pub 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).
85pub 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]
100macro_rules! serial_println { 122macro_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// ---------------------------------------------------------------------------