summaryrefslogtreecommitdiffstats
path: root/rsnix/src/sched.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--rsnix/src/sched.rs544
1 files changed, 544 insertions, 0 deletions
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}