feat: SMP stage 1 (4 harts, per-hart guarded stacks, home-core affinity, IPIs, TLB shootdown, one temporary kernel lock), 4-core storm

This commit is contained in:
2026-10-27 00:56:15 +01:00
parent b87a62475f
commit 2e665fd66e
22 changed files with 995 additions and 230 deletions

119
crates/kernel/src/smp.rs Normal file
View File

@@ -0,0 +1,119 @@
//! Cores. Every thread has a home core, chosen when it is created, and
//! only ever runs there: it waits in that core's run queue and is never
//! migrated. Making a thread ready from another core sends that core an
//! IPI when it has to look at its queue again (it is idle, or the thread
//! outranks what it is running), and so does taking away the thread a
//! core is running.
use domain::{
scheduler::{Scheduler, SchedulerDecision},
thread::ThreadState,
};
use platform::Platform;
use types::{
bounded::Priority,
id::{CoreId, ThreadId},
};
use crate::Kernel;
impl<P: Platform> Kernel<P> {
/// The core `tid` runs on.
pub fn home(&self, tid: ThreadId) -> CoreId {
self.homes
.get(tid.raw() as usize)
.copied()
.unwrap_or(CoreId::from_raw(0))
}
/// Homes a new thread on `core`.
pub(crate) fn set_home(&mut self, tid: ThreadId, core: CoreId) {
if let Some(h) = self.homes.get_mut(tid.raw() as usize) {
*h = core;
}
}
/// Homes a new thread on the core with the fewest live threads.
pub(crate) fn place(&mut self, tid: ThreadId) {
let cores = self.scheduler.num_cores().max(1);
let load = |core: CoreId| {
self.threads
.iter()
.filter(|(t, _)| *t != tid && self.home(*t) == core)
.count()
};
let best = (0..cores)
.map(CoreId::from_raw)
.min_by_key(|c| load(*c))
.unwrap_or(CoreId::from_raw(0));
self.set_home(tid, best);
}
/// Queues a ready thread on its home core, and kicks that core if it
/// is another one that should look at its queue now.
pub(crate) fn make_ready(&mut self, tid: ThreadId, priority: Priority) {
let home = self.home(tid);
self.scheduler.add(tid, priority, home);
if home == self.platform.core_id() {
return;
}
let outranks = match self.scheduler.current(home) {
None => true,
Some(running) => self
.threads
.get(running)
.is_ok_and(|t| priority > t.priority()),
};
if outranks {
self.platform.send_ipi(home);
}
}
/// Before `tid` is destroyed: if another core is running it, that core
/// must stop and pick something else.
pub(crate) fn evict(&mut self, tid: ThreadId) {
if let Ok(ThreadState::Running(core)) = self.threads.get(tid).map(|t| *t.state())
&& core != self.platform.core_id()
{
self.platform.send_ipi(core);
}
}
/// What `core` should do after an IPI: keep running its thread, or
/// switch to a higher-priority one now waiting. When it has no thread
/// (it was idle, or its thread was destroyed) the caller picks the
/// next one itself.
pub fn reschedule(&mut self, core: CoreId) -> SchedulerDecision {
let Some(current) = self.scheduler.current(core) else {
return SchedulerDecision::Continue;
};
let Ok(thread) = self.threads.get(current) else {
return SchedulerDecision::Continue;
};
let priority = thread.priority();
if self
.scheduler
.highest_waiting(core)
.is_none_or(|p| p <= priority)
{
return SchedulerDecision::Continue;
}
let Some(next) = self.scheduler.next(core) else {
return SchedulerDecision::Continue;
};
if self
.threads
.get_mut(next)
.and_then(|t| t.set_running(core))
.is_err()
{
return SchedulerDecision::Continue;
}
if let Ok(t) = self.threads.get_mut(current)
&& t.preempt().is_ok()
{
self.scheduler.add(current, priority, core);
}
SchedulerDecision::Preempt(next)
}
}