Skip to content

Commit 0e490a9

Browse files
committed
changed the old name to the new one
1 parent af789e9 commit 0e490a9

5 files changed

Lines changed: 636 additions & 259 deletions

File tree

kernel/src/interrupts.rs

Lines changed: 148 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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`.
3333
static TICKS: AtomicU64 = AtomicU64::new(0);
3434

3535
pub 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.
5958
static 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.
6771
static 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.
7478
const 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.
8084
pub 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-
9393
lazy_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
}

kernel/src/main.rs

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,13 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
131131
runix_kernel::interrupts::ticks()
132132
);
133133

134-
// Cooperative round-robin scheduling: spawn three threads that each do
135-
// a bit of real work (proving their own saved context resumes exactly
136-
// where it left off — not just that switch_to() returns *somewhere*
137-
// without faulting), then let this thread (the boot context, folded
138-
// into the same run queue as a placeholder) drive several rounds of
139-
// yielding so their output actually interleaves instead of running to
140-
// completion back-to-back.
134+
// Round-robin scheduling: spawn three threads that each do a bit of
135+
// real work (proving their own saved context resumes exactly where it
136+
// left off — not just that a reschedule lands *somewhere* without
137+
// faulting), then let this thread (the boot context, folded into the
138+
// same run queue as a placeholder) drive several rounds of yielding so
139+
// their output actually interleaves instead of running to completion
140+
// back-to-back.
141141
runix_kernel::scheduler::init();
142142
runix_kernel::scheduler::spawn(thread_a);
143143
runix_kernel::scheduler::spawn(thread_b);
@@ -279,10 +279,11 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
279279
}
280280

281281
// Ring 3: map a user-accessible stack, grant ring 3 access to the one
282-
// code page `user_hello` lives on, then `iretq` into it. There's no
283-
// return path — this really is the last thing the boot thread does;
284-
// everything from here on runs at CPL 3, bouncing back into the
285-
// kernel only through the syscall gate.
282+
// code page `user_hello` lives on, then spawn it as a real scheduler
283+
// thread (its own dedicated kernel-entry stack, so the timer can safely
284+
// preempt it mid-spin — see `spawn_ring3_shared`'s doc comment for why
285+
// a raw, un-scheduled `enter_usermode` from the boot thread is no
286+
// longer safe now that preemption is real, not just cooperative).
286287
let user_stack_top = runix_kernel::memory::with_mapper_and_frame_allocator(
287288
runix_kernel::userspace::map_user_stack,
288289
)
@@ -291,17 +292,33 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
291292
runix_kernel::memory::with_mapper_and_frame_allocator(|mapper, _frame_allocator| unsafe {
292293
runix_kernel::userspace::allow_user_access(mapper, user_entry);
293294
});
294-
// The scheduler watchdog (see `interrupts.rs`) expects every thread it
295-
// knows about to keep calling `yield_now()` — true here up to this
296-
// point, but ring 3 code doesn't cooperate with the scheduler at all
297-
// yet (see `userspace.rs`'s note on why `SYS_YIELD` is meaningless for
298-
// `user_hello`). Left armed, the watchdog would eventually panic on
299-
// `user_hello`'s intentional forever-spin, mistaking deliberate
300-
// behavior for a real hang.
301-
runix_kernel::interrupts::disarm_watchdog();
295+
#[allow(static_mut_refs)]
296+
unsafe {
297+
USER_HELLO_ENTRY = user_entry.as_u64();
298+
USER_HELLO_STACK_TOP = user_stack_top.as_u64();
299+
}
302300
serial_println!("Runix kernel: entering ring 3 (Phase 7: user-space transition)");
301+
runix_kernel::scheduler::spawn_ring3_shared(user_hello_trampoline);
302+
303+
// The boot thread's own work is done — everything left to prove (ring 3
304+
// running, real preemption not corrupting anything) happens on other
305+
// threads now. It has no dedicated stack region of its own for
306+
// `exit_current_thread` to reclaim (see that function's doc comment),
307+
// so it joins the same forever-yield pattern `thread_a`/`b`/`c` use
308+
// rather than actually exiting.
309+
loop {
310+
runix_kernel::scheduler::yield_now();
311+
}
312+
}
313+
314+
static mut USER_HELLO_ENTRY: u64 = 0;
315+
static mut USER_HELLO_STACK_TOP: u64 = 0;
316+
317+
extern "C" fn user_hello_trampoline() -> ! {
318+
#[allow(static_mut_refs)]
319+
let (entry, stack_top) = unsafe { (USER_HELLO_ENTRY, USER_HELLO_STACK_TOP) };
303320
unsafe {
304-
runix_kernel::userspace::enter_usermode(user_entry, user_stack_top);
321+
runix_kernel::userspace::enter_usermode(VirtAddr::new(entry), VirtAddr::new(stack_top));
305322
}
306323
}
307324

kernel/src/memory.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,29 @@ pub fn install(mapper: OffsetPageTable<'static>, frame_allocator: BootInfoFrameA
173173

174174
/// Runs `f` with mutable access to the installed mapper and frame
175175
/// allocator. Panics if [`install`] hasn't run yet.
176+
///
177+
/// Runs with interrupts disabled for the same reason [`crate::scheduler`]'s
178+
/// `SCHEDULER` lock does (see its call sites) — with real preemption, the
179+
/// timer can land while this lock is held by whatever thread it interrupts
180+
/// (`scheduler::Thread::new`'s stack mapping, `userspace::map_user_stack`,
181+
/// heap init, ...), and `scheduler::reap_zombies` (called from inside
182+
/// `scheduler::reschedule`, itself already running with interrupts off)
183+
/// takes this same lock to unmap a zombie thread's stack. Without disabling
184+
/// interrupts around every *other* acquisition, that timer tick would try
185+
/// to lock a `spin::Mutex` this same CPU already holds, from a context that
186+
/// can never be preempted away from it — a permanent deadlock, not a stall.
187+
/// `kernel/tests/thread_reclaim.rs` (20,000 spawn/exit cycles, virtually
188+
/// guaranteed to eventually straddle a timer tick) hit exactly this before
189+
/// this fix, hanging with zero progress rather than just running slowly.
176190
pub fn with_mapper_and_frame_allocator<R>(
177191
f: impl FnOnce(&mut OffsetPageTable<'static>, &mut BootInfoFrameAllocator) -> R,
178192
) -> R {
179-
let mut guard = MAPPER_AND_FRAME_ALLOCATOR.lock();
180-
let (mapper, frame_allocator) = guard.as_mut().expect("memory::install() not called yet");
181-
f(mapper, frame_allocator)
193+
x86_64::instructions::interrupts::without_interrupts(|| {
194+
let mut guard = MAPPER_AND_FRAME_ALLOCATOR.lock();
195+
let (mapper, frame_allocator) =
196+
guard.as_mut().expect("memory::install() not called yet");
197+
f(mapper, frame_allocator)
198+
})
182199
}
183200

184201
/// Finds the physical frame backing `addr` in the kernel's own table — for

0 commit comments

Comments
 (0)