Skip to content

Commit 8e50433

Browse files
committed
feat(smp): re-add an anti-spurious-wakeup feature
This one has tons of comments to convince myself it works. I am relatively convinced, but concurrent processing is hard you know?
1 parent f4942c9 commit 8e50433

3 files changed

Lines changed: 135 additions & 1 deletion

File tree

src/arch/x86_64/kernel/apic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ pub fn ipi_tlb_flush() {
886886
#[allow(unused_variables)]
887887
pub fn wakeup_core(core_id_to_wakeup: CoreId) {
888888
#[cfg(all(feature = "smp", not(feature = "idle-poll")))]
889-
if core_id_to_wakeup != core_id() && !processor::supports_mwait() {
889+
if core_id_to_wakeup != core_id() && !processor::supports_mwait() && scheduler::core_wake_up(core_id_to_wakeup) {
890890
without_interrupts(|| {
891891
let apic_ids = CPU_LOCAL_APIC_IDS.lock();
892892
let local_apic_id = apic_ids[core_id_to_wakeup as usize];

src/arch/x86_64/kernel/interrupts.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ pub(crate) fn enable_and_wait() {
8282
);
8383
}
8484
} else {
85+
#[cfg(feature = "smp")]
86+
if !scheduler::core_sleep() {
87+
return;
88+
}
89+
8590
enable_and_hlt();
8691
}
8792
}

src/scheduler/mod.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use alloc::sync::Arc;
88
use alloc::vec::Vec;
99
use core::cell::RefCell;
1010
use core::ptr;
11+
#[cfg(all(target_arch = "x86_64", feature = "smp", not(feature = "idle-poll")))]
12+
use core::sync::atomic::AtomicU8;
1113
use core::sync::atomic::{AtomicI32, AtomicU32, Ordering};
1214

1315
use ahash::RandomState;
@@ -44,6 +46,129 @@ static WAITING_TASKS: InterruptTicketMutex<BTreeMap<TaskId, VecDeque<TaskHandle>
4446
/// Map between Task ID and TaskHandle
4547
static TASKS: InterruptTicketMutex<BTreeMap<TaskId, TaskHandle>> =
4648
InterruptTicketMutex::new(BTreeMap::new());
49+
#[cfg(all(target_arch = "x86_64", feature = "smp", not(feature = "idle-poll")))]
50+
static CORE_HLT_STATE: RwSpinLock<Vec<CoreState>> = RwSpinLock::new(Vec::new());
51+
52+
#[cfg(all(target_arch = "x86_64", feature = "smp", not(feature = "idle-poll")))]
53+
struct CoreState(AtomicU8);
54+
55+
/// # Race condition prevention
56+
///
57+
/// Two methods matter here: [arch::kernel::interrupts::enable_and_wait] and [arch::kernel::apic::wakeup_core].
58+
/// `enable_and_wait` checks the status, ensuring it is set to 0+setting it to 1, then sleeps.
59+
/// `wakeup_core` checks the status, ensuring it is set to 1+setting it to 0, then issues interrupt.
60+
///
61+
/// A race would happen if `go_to_sleep` returns true, and `wake_up` returns false.
62+
/// The result of these two methods must therefore be atomically determined in a single operation.
63+
/// No ordering should exist in which that condition can happen.
64+
/// Any other variant is okay:
65+
/// - if `go_to_sleep` is false and `wake_up` is true, we have a un-necessary core wakeup (which is okay)
66+
/// - if both are false, the core is not entering sleep and is therefore not woken up
67+
/// - if both are true, the core sleeps and is woken up
68+
#[cfg(all(target_arch = "x86_64", feature = "smp", not(feature = "idle-poll")))]
69+
impl CoreState {
70+
/// The core is currently busy and should not be interrupted for new tasks
71+
const STATUS_ACTIVE: u8 = 0;
72+
73+
/// The core is currently halted and can be interrupted for new tasks
74+
const STATUS_IDLE: u8 = 1;
75+
76+
/// Another core tried to wake up this core but it was already active.
77+
const STATUS_DONT_SLEEP: u8 = 2;
78+
79+
pub fn new() -> Self {
80+
Self(AtomicU8::new(CoreState::STATUS_ACTIVE))
81+
}
82+
83+
#[inline(always)]
84+
pub fn set_active(&self) {
85+
self.0.store(CoreState::STATUS_ACTIVE, Ordering::SeqCst);
86+
}
87+
88+
/// Indicates that this core will go to HLT.
89+
/// This must be called *before* entering a HLT loop, and *with interrupts disabled*.
90+
/// Returns a boolean indicating if the core should enter the HLT loop or not
91+
#[inline(always)]
92+
pub fn go_to_sleep(&self) -> bool {
93+
if self
94+
.0
95+
.compare_exchange(
96+
CoreState::STATUS_ACTIVE,
97+
CoreState::STATUS_IDLE,
98+
Ordering::SeqCst,
99+
Ordering::Relaxed,
100+
)
101+
.is_err()
102+
{
103+
// There is a possible race condition here, because the two atomic operations can be
104+
// interleaved, but this has no effect on the final result: we're not going to sleep
105+
// anyway.
106+
// If the state was already set to ACTIVE, this has no effect.
107+
// Normally, nothing can set the state to IDLE except this method.
108+
// So the race should have no effect.
109+
// IN ANY CASE: as per top-level comments, returning false is the safe default here.
110+
self.set_active();
111+
false
112+
} else {
113+
// We have correctly read status ACTIVE and set STATUS_IDLE. We go to sleep. This is
114+
// safe because:
115+
// - Another `wake_up` could be processing, either BEFORE or AFTER the atomic `swap`.
116+
// * BEFORE:
117+
// We have set the state to STATUS_IDLE, so `swap` will read that value and
118+
// wake_up will return true ==> SAFE.
119+
// * AFTER: **We cannot be here!** Indeed, `swap` has set the status to
120+
// `STATUS_DONT_SLEEP`, so `compare_exchange` is in the `Err` case.
121+
true
122+
}
123+
}
124+
125+
/// Request to wake up this core.
126+
/// Returns a boolean indicating if an interrupt should be sent, or if the core is already active
127+
#[inline(always)]
128+
pub fn wake_up(&self) -> bool {
129+
// Ask the core not to sleep.
130+
// This makes sure that if the two atomic operations become interleaved, the core will
131+
// not go to sleep with us assuming it was running.
132+
let previous_state = self.0.swap(CoreState::STATUS_DONT_SLEEP, Ordering::SeqCst);
133+
134+
// If the core was idle, we can actually wake it up
135+
if previous_state == CoreState::STATUS_IDLE {
136+
// Again, this operation is not necessarily ordered.
137+
// - Another `go_to_sleep` could be processing, either BEFORE or AFTER the atomic op.
138+
// * BEFORE:
139+
// We have set the state to STATUS_DONT_SLEEP, so the atomic op will read that value
140+
// and go_to_sleep will return false. We will send an interrupt which could have
141+
// been avoided, but that's okay.
142+
// * AFTER:
143+
// The value read at the atomic OP does not matter here, in any case we WILL send
144+
// the interrupt and wake the core up.
145+
// IN ANY CASE: as per top-level comments, returning true is the safe default here.
146+
self.set_active();
147+
true
148+
} else {
149+
// We don't wake up the core. This is safe, because:
150+
// - Another `go_to_sleep` could be processing, either BEFORE or AFTER the atomic op.
151+
// * BEFORE:
152+
// We have set the state to STATUS_DONT_SLEEP, so the atomic op will read that value
153+
// and go_to_sleep will return false ==> SAFE.
154+
// * AFTER: **We cannot be here!** Indeed, the atomic operation in `go_to_sleep`
155+
// also sets the state to STATUS_IDLE, so `previous_state` MUST BE STATUS_IDLE.
156+
false
157+
}
158+
}
159+
}
160+
161+
#[inline]
162+
#[cfg(all(target_arch = "x86_64", feature = "smp", not(feature = "idle-poll")))]
163+
pub fn core_sleep() -> bool {
164+
CORE_HLT_STATE.read()[usize::try_from(core_id()).unwrap()].go_to_sleep()
165+
}
166+
167+
#[inline]
168+
#[cfg(all(target_arch = "x86_64", feature = "smp", not(feature = "idle-poll")))]
169+
pub fn core_wake_up(core_id: CoreId) -> bool {
170+
CORE_HLT_STATE.read()[usize::try_from(core_id).unwrap()].wake_up()
171+
}
47172

48173
/// Unique identifier for a core.
49174
pub type CoreId = u32;
@@ -887,6 +1012,10 @@ pub(crate) fn add_current_core() {
8871012
core_id.try_into().unwrap(),
8881013
&CoreLocal::get().scheduler_input,
8891014
);
1015+
#[cfg(all(target_arch = "x86_64", not(feature = "idle-poll")))]
1016+
CORE_HLT_STATE
1017+
.write()
1018+
.insert(core_id.try_into().unwrap(), CoreState::new());
8901019
}
8911020
}
8921021

0 commit comments

Comments
 (0)