@@ -28,42 +28,46 @@ impl InterruptIndex {
2828 }
2929}
3030
31- /// Incremented on every timer IRQ. The base a preemptive scheduler (next
32- /// phase) will tick against — nothing schedules off it yet .
31+ /// Incremented on every timer IRQ — also every real preemption tick, see
32+ /// `on_timer_tick` .
3333static TICKS : AtomicU64 = AtomicU64 :: new ( 0 ) ;
3434
3535pub fn ticks ( ) -> u64 {
3636 TICKS . load ( Ordering :: Relaxed )
3737}
3838
39- /// Cooperative-scheduler watchdog: **detection, not preemption.** There's
40- /// no timer-interrupt-driven context switch yet (see `scheduler.rs`'s
41- /// module doc comment) — a thread that never calls `yield_now()` still
42- /// can't be forcibly interrupted and moved aside. What this *does* do is
43- /// turn "the whole kernel silently hangs forever" into a loud, immediate,
44- /// diagnosable panic instead — the same "detect deterministically instead
45- /// of silently misbehaving" trade guard pages made for stack overflows
46- /// (see the top-level README). Not a substitute for real preemption, which
47- /// is what T1 Critical's <300ms MARSHAL real-time constraint (see README's
48- /// "Sandbox tiers") actually needs — this only guarantees a hang is *found*
49- /// quickly, not that the system keeps making progress through one.
39+ /// **Backstop, not the primary defense anymore.** Used to be the *only*
40+ /// thing standing between a thread that never calls `yield_now()` and the
41+ /// whole kernel silently hanging forever — see `scheduler.rs`'s module doc
42+ /// comment for the real fix now in place: every timer tick forces a
43+ /// reschedule regardless of what's running, so a thread that never
44+ /// cooperates no longer blocks anything (T1 Critical's <300ms MARSHAL
45+ /// real-time constraint — see README's "Sandbox tiers" — needed exactly
46+ /// this, not just faster detection). What this still catches: `reschedule`
47+ /// itself panicking, hanging, or somehow never getting called at all (a
48+ /// bug in the mechanism, not in a thread using it) — genuinely different
49+ /// failure classes than "a thread forgot to yield," which preemption now
50+ /// makes irrelevant on its own. `kernel/tests/watchdog.rs` was rewritten
51+ /// to prove *that* — real recovery, not just detection — once this landed.
5052///
51- /// Lock-free by necessity: this is checked from `timer_interrupt_handler`,
52- /// which can fire while any code — including code already holding
53- /// `scheduler::SCHEDULER`'s lock, mid-`yield_now()` — is running. Taking
54- /// that same lock here would deadlock the CPU against itself the moment an
55- /// interrupt landed inside its own critical section. Plain atomics avoid
56- /// that entirely, at the cost of not being able to inspect the run queue
57- /// (so this can't tell *which* thread is stuck, only that *something* has
58- /// gone too long without any thread cooperating).
53+ /// Lock-free by necessity: this is checked from `on_timer_tick`, which
54+ /// fires with `RFLAGS.IF=0` (an interrupt gate) — nothing else can be
55+ /// mid-`SCHEDULER.lock()` while this runs, so a plain atomic isn't required
56+ /// for *that* reason anymore, but stays lock-free anyway: reading it here
57+ /// must never itself be what causes a hang this is supposed to catch.
5958static LAST_YIELD_TICK : AtomicU64 = AtomicU64 :: new ( 0 ) ;
6059
61- /// Off until `scheduler::init()` arms it, and deliberately turned back off
62- /// before a ring 3 handoff (see `userspace::enter_usermode`'s call site in
63- /// `main.rs`) — ring 3 code doesn't call `yield_now()` at all yet (see
64- /// `userspace.rs`'s doc comment on why `SYS_YIELD` is meaningless there
65- /// today), so leaving the watchdog armed across that handoff would
66- /// eventually panic on perfectly intended behavior, not a real hang.
60+ /// Off until `scheduler::init()` arms it — stays armed permanently after
61+ /// that now (nothing disarms it anymore). Safe to leave on across a ring 3
62+ /// handoff, unlike before real preemption existed: back when `user_hello`
63+ /// was entered via a raw `userspace::enter_usermode` call invisible to the
64+ /// scheduler, its intentional forever-spin (never calling `yield_now()`)
65+ /// would eventually trip this watchdog on perfectly intended behavior, so
66+ /// `main.rs` disarmed it first. Now `user_hello` runs as a real, preemptible
67+ /// scheduler thread (`scheduler::spawn_ring3_shared`) — the timer keeps
68+ /// forcing a `reschedule` every tick regardless of whether it cooperates,
69+ /// which is exactly what keeps [`LAST_YIELD_TICK`] moving and this watchdog
70+ /// quiet.
6771static WATCHDOG_ARMED : AtomicBool = AtomicBool :: new ( false ) ;
6872
6973/// ~20 PIT ticks at the default (unreprogrammed) ~18.2 Hz rate this kernel
@@ -73,10 +77,10 @@ static WATCHDOG_ARMED: AtomicBool = AtomicBool::new(false);
7377/// noticing something's wrong, let alone a CI run timing out.
7478const WATCHDOG_THRESHOLD_TICKS : u64 = 20 ;
7579
76- /// Called from `scheduler::yield_now() ` on every call, whether or not it
77- /// actually switches to another thread — a system with only one runnable
78- /// thread that keeps calling `yield_now()` in its own loop is still
79- /// cooperating, even though no context switch happens .
80+ /// Called from `scheduler::reschedule ` on every call, whether triggered by
81+ /// a voluntary `yield_now()` or an involuntary timer tick, and whether or
82+ /// not it actually switches to another thread — any of those is proof the
83+ /// mechanism itself is still alive .
8084pub fn record_yield ( ) {
8185 LAST_YIELD_TICK . store ( ticks ( ) , Ordering :: Relaxed ) ;
8286}
@@ -86,10 +90,6 @@ pub fn arm_watchdog() {
8690 WATCHDOG_ARMED . store ( true , Ordering :: Relaxed ) ;
8791}
8892
89- pub fn disarm_watchdog ( ) {
90- WATCHDOG_ARMED . store ( false , Ordering :: Relaxed ) ;
91- }
92-
9393lazy_static ! {
9494 static ref IDT : InterruptDescriptorTable = {
9595 let mut idt = InterruptDescriptorTable :: new( ) ;
@@ -102,7 +102,17 @@ lazy_static! {
102102 . set_handler_fn( double_fault_handler)
103103 . set_stack_index( gdt:: DOUBLE_FAULT_IST_INDEX ) ;
104104 }
105- idt[ InterruptIndex :: Timer . as_u8( ) ] . set_handler_fn( timer_interrupt_handler) ;
105+ // `timer_entry` is naked, not `extern "x86-interrupt" fn` — it
106+ // needs to capture the *complete* register state (a
107+ // `scheduler::TrapFrame`) for real preemption to be able to resume
108+ // it later, which the typed interrupt-calling-convention ABI
109+ // doesn't expose. Same raw-address registration `syscall::entry`
110+ // already uses, below.
111+ unsafe {
112+ idt[ InterruptIndex :: Timer . as_u8( ) ]
113+ . set_handler_addr( x86_64:: VirtAddr :: new( timer_entry as * const ( ) as u64 ) )
114+ . set_present( true ) ;
115+ }
106116 // `crate::syscall::entry` is a naked function, not
107117 // `extern "x86-interrupt" fn` — it doesn't fit `set_handler_fn`'s
108118 // typed signature, hence the raw-address variant. Software
@@ -121,6 +131,19 @@ lazy_static! {
121131 // actually admit it.
122132 . set_privilege_level( x86_64:: PrivilegeLevel :: Ring3 ) ;
123133 }
134+ // `scheduler::reschedule_entry`, same reasoning as `timer_entry`
135+ // above (needs a full `TrapFrame`, not the typed ABI) — kernel-only
136+ // (default Ring0 DPL), unlike `syscall::VECTOR`: every caller,
137+ // including a ring 3 thread's `SYS_YIELD`, already reaches
138+ // `scheduler::yield_now()` from inside `syscall::dispatch`, i.e.
139+ // from ring 0.
140+ unsafe {
141+ idt[ crate :: scheduler:: RESCHEDULE_VECTOR ]
142+ . set_handler_addr( x86_64:: VirtAddr :: new(
143+ crate :: scheduler:: reschedule_entry as * const ( ) as u64 ,
144+ ) )
145+ . set_present( true ) ;
146+ }
124147 idt
125148 } ;
126149}
@@ -191,7 +214,77 @@ extern "x86-interrupt" fn general_protection_fault_handler(
191214 }
192215}
193216
194- extern "x86-interrupt" fn timer_interrupt_handler ( _stack_frame : InterruptStackFrame ) {
217+ /// Entry point installed at the timer IRQ vector. Naked, not
218+ /// `extern "x86-interrupt" fn` — see this module's IDT-registration comment
219+ /// on why real preemption needs the *complete* register state
220+ /// (`scheduler::TrapFrame`) a typed interrupt handler doesn't expose.
221+ /// Structurally identical to `scheduler::reschedule_entry` (same GPR
222+ /// push/pop sequence, same final `iretq`) — the only difference is what
223+ /// Rust function each calls in between, which is exactly the point: one
224+ /// mechanism, two triggers.
225+ ///
226+ /// # Safety
227+ /// Never call this directly — reached only by the CPU delivering IRQ0
228+ /// (timer), which guarantees a matching hardware-pushed frame already sits
229+ /// on the stack for the final `iretq` to consume.
230+ #[ unsafe( naked) ]
231+ unsafe extern "C" fn timer_entry ( ) {
232+ core:: arch:: naked_asm!(
233+ "push rax" ,
234+ "push rbx" ,
235+ "push rcx" ,
236+ "push rdx" ,
237+ "push rsi" ,
238+ "push rdi" ,
239+ "push rbp" ,
240+ "push r8" ,
241+ "push r9" ,
242+ "push r10" ,
243+ "push r11" ,
244+ "push r12" ,
245+ "push r13" ,
246+ "push r14" ,
247+ "push r15" ,
248+ "mov rdi, rsp" ,
249+ // See `scheduler::reschedule_entry`'s identical line for why this
250+ // is required, not optional: a hardware interrupt (this one) can
251+ // land with RSP at any alignment, unlike a real `call` site, and
252+ // `on_timer_tick` is an ordinary Rust function that can't safely
253+ // assume otherwise. Discarded, not restored, afterward — the very
254+ // next instruction unconditionally replaces RSP with whichever
255+ // frame `on_timer_tick` returns.
256+ "and rsp, -16" ,
257+ "call {on_timer_tick}" ,
258+ "mov rsp, rax" ,
259+ "pop r15" ,
260+ "pop r14" ,
261+ "pop r13" ,
262+ "pop r12" ,
263+ "pop r11" ,
264+ "pop r10" ,
265+ "pop r9" ,
266+ "pop r8" ,
267+ "pop rbp" ,
268+ "pop rdi" ,
269+ "pop rsi" ,
270+ "pop rdx" ,
271+ "pop rcx" ,
272+ "pop rbx" ,
273+ "pop rax" ,
274+ "iretq" ,
275+ on_timer_tick = sym on_timer_tick,
276+ ) ;
277+ }
278+
279+ /// The actual preemption tick: EOI, bump `TICKS`, the watchdog backstop
280+ /// check (see its own doc comment on why this is a backstop now, not the
281+ /// primary defense), then hand off to `scheduler::reschedule` — every
282+ /// timer tick forces a reschedule attempt, unconditionally, regardless of
283+ /// what's currently running. `frame` is a complete `TrapFrame` (hardware
284+ /// fields + `timer_entry`'s GPR pushes); the return value is what
285+ /// `timer_entry` resumes from — the same frame unchanged (nothing else was
286+ /// runnable) or a different thread's.
287+ extern "C" fn on_timer_tick ( frame : * mut crate :: scheduler:: TrapFrame ) -> * mut crate :: scheduler:: TrapFrame {
195288 let now = TICKS . fetch_add ( 1 , Ordering :: Relaxed ) + 1 ;
196289 unsafe {
197290 PICS . lock ( )
@@ -206,9 +299,25 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFr
206299 && now. saturating_sub ( LAST_YIELD_TICK . load ( Ordering :: Relaxed ) ) > WATCHDOG_THRESHOLD_TICKS
207300 {
208301 panic ! (
209- "scheduler watchdog: no thread called yield_now() for over {} ticks — \
210- something is stuck without cooperating ",
302+ "scheduler watchdog: no reschedule succeeded for over {} ticks — \
303+ the preemption mechanism itself is stuck, not just an uncooperative thread ",
211304 WATCHDOG_THRESHOLD_TICKS
212305 ) ;
213306 }
307+
308+ // `boot::init()` enables interrupts well before `main.rs` ever calls
309+ // `scheduler::init()` — the timer starts ticking immediately, with no
310+ // scheduler yet to reschedule against. Without this check, the very
311+ // first tick during that window would hit `reschedule`'s
312+ // `SCHEDULER.lock().expect(...)` and panic — confirmed for real the
313+ // first time this shipped (`scheduler::init() not called`, fired
314+ // during Phase 3, well before Phase 5 ever calls `scheduler::init()`).
315+ // Resuming the interrupted context unchanged is always correct here:
316+ // it's exactly what `reschedule` itself does whenever nothing else is
317+ // runnable, just decided one step earlier.
318+ if !crate :: scheduler:: is_initialized ( ) {
319+ return frame;
320+ }
321+
322+ crate :: scheduler:: reschedule ( frame)
214323}
0 commit comments