use core::sync::atomic::{AtomicUsize, Ordering};
use crate::{
ArchTrait, DCacheOp,
arch::Arch,
mem::{__kimage_va, cpu_area_phys_to_virt, dcache_range, virt_to_phys},
smp::{CpuBootPrepareError, CpuBootStatus, PerCpuMeta},
};
const NO_ACTIVE_SECONDARY_CPU: usize = usize::MAX;
static ACTIVE_SECONDARY_CPU: SecondaryCpuOwner = SecondaryCpuOwner::new();
pub fn shutdown() -> ! {
crate::arch::Arch::shutdown()
}
pub fn reset() -> ! {
crate::arch::Arch::reset()
}
pub fn start_secondary_cpu(cpu_idx: usize) -> Result<SecondaryCpuStartup, CpuOnError> {
if cpu_idx == NO_ACTIVE_SECONDARY_CPU {
return Err(CpuOnError::InvalidParameters);
}
ACTIVE_SECONDARY_CPU
.try_claim(cpu_idx)
.map_err(|active_logical_cpu| CpuOnError::StartupInProgress {
requested_logical_cpu: cpu_idx,
active_logical_cpu,
})?;
let entry = secondary_entry_addr();
debug!("Secondary entry address: {entry:#x}");
let Some(arg) = crate::smp::cpu_meta_addr(cpu_idx) else {
ACTIVE_SECONDARY_CPU
.release(cpu_idx)
.expect("the validating CPU must own the secondary startup");
return Err(CpuOnError::InvalidParameters);
};
debug!("Secondary entry argument (cpu meta address): {arg:#x}");
let meta = unsafe { &*(cpu_area_phys_to_virt(arg) as *const PerCpuMeta) };
debug!("Power on CPU {meta:#x?}");
let kimg = crate::mem::kimage_range();
let kimg_start = __kimage_va(kimg.start);
let size = kimg.end - kimg.start;
dcache_range(DCacheOp::Clean, kimg_start, size);
if let Err(error) = crate::smp::prepare_secondary_boot(cpu_idx) {
ACTIVE_SECONDARY_CPU
.release(cpu_idx)
.expect("the preparing CPU must own the secondary startup");
return Err(prepare_error(cpu_idx, meta.cpu_id, error));
}
Arch::kick_secondary_cpu(meta.cpu_id, entry, arg)?;
Ok(SecondaryCpuStartup {
logical_cpu: cpu_idx,
hardware_id: meta.cpu_id,
})
}
#[derive(Debug)]
#[must_use = "an in-flight secondary CPU must be observed and explicitly released"]
pub struct SecondaryCpuStartup {
logical_cpu: usize,
hardware_id: usize,
}
impl SecondaryCpuStartup {
pub const fn logical_cpu(&self) -> usize {
self.logical_cpu
}
pub const fn hardware_id(&self) -> usize {
self.hardware_id
}
pub fn status(&self) -> SecondaryCpuStartupStatus {
match crate::smp::secondary_boot_status(self.logical_cpu).unwrap_or_else(|error| {
panic!(
"cannot observe logical CPU {} (hardware ID {:#x}): {error}",
self.logical_cpu, self.hardware_id
)
}) {
CpuBootStatus::WaitingForAlive => SecondaryCpuStartupStatus::WaitingForAlive,
CpuBootStatus::Alive => SecondaryCpuStartupStatus::Alive,
}
}
pub fn release(self) -> Result<(), CpuOnError> {
ACTIVE_SECONDARY_CPU
.ensure_owned(self.logical_cpu)
.map_err(|active_logical_cpu| {
CpuOnError::Other(anyhow::anyhow!(
"cannot release logical CPU {} (hardware ID {:#x}) while startup owner is \
{:#x}",
self.logical_cpu,
self.hardware_id,
active_logical_cpu
))
})?;
crate::smp::release_secondary_boot(self.logical_cpu).map_err(|error| match error {
CpuBootPrepareError::UnexpectedState { .. } => CpuOnError::NotAlive {
logical_cpu: self.logical_cpu,
hardware_id: self.hardware_id,
},
CpuBootPrepareError::Missing { .. } => CpuOnError::InvalidParameters,
})?;
ACTIVE_SECONDARY_CPU
.release(self.logical_cpu)
.map_err(|active_logical_cpu| {
CpuOnError::Other(anyhow::anyhow!(
"released logical CPU {} (hardware ID {:#x}) while startup owner was {:#x}",
self.logical_cpu,
self.hardware_id,
active_logical_cpu
))
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecondaryCpuStartupStatus {
WaitingForAlive,
Alive,
}
fn prepare_error(logical_cpu: usize, hardware_id: usize, error: CpuBootPrepareError) -> CpuOnError {
CpuOnError::Other(anyhow::anyhow!(
"cannot kick logical CPU {logical_cpu} (hardware ID {hardware_id:#x}): {error}"
))
}
fn secondary_entry_addr() -> usize {
let ptr = crate::arch::Arch::secondary_entry_fn_address() as *const u8;
virt_to_phys(ptr)
}
struct SecondaryCpuOwner {
active_cpu: AtomicUsize,
}
impl SecondaryCpuOwner {
const fn new() -> Self {
Self {
active_cpu: AtomicUsize::new(NO_ACTIVE_SECONDARY_CPU),
}
}
fn try_claim(&self, logical_cpu: usize) -> Result<(), usize> {
self.active_cpu
.compare_exchange(
NO_ACTIVE_SECONDARY_CPU,
logical_cpu,
Ordering::AcqRel,
Ordering::Acquire,
)
.map(|_| ())
}
#[cfg(test)]
fn active_cpu(&self) -> Option<usize> {
match self.active_cpu.load(Ordering::Acquire) {
NO_ACTIVE_SECONDARY_CPU => None,
logical_cpu => Some(logical_cpu),
}
}
fn release(&self, logical_cpu: usize) -> Result<(), usize> {
self.active_cpu
.compare_exchange(
logical_cpu,
NO_ACTIVE_SECONDARY_CPU,
Ordering::AcqRel,
Ordering::Acquire,
)
.map(|_| ())
}
fn ensure_owned(&self, logical_cpu: usize) -> Result<(), usize> {
let active_logical_cpu = self.active_cpu.load(Ordering::Acquire);
if active_logical_cpu == logical_cpu {
Ok(())
} else {
Err(active_logical_cpu)
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum CpuOnError {
#[error("CPU on is not supported")]
NotSupported,
#[error("CPU is already on")]
AlreadyOn,
#[error("Invalid parameters")]
InvalidParameters,
#[error(
"cannot start logical CPU {requested_logical_cpu}: logical CPU {active_logical_cpu} \
already owns the secondary startup transport"
)]
StartupInProgress {
requested_logical_cpu: usize,
active_logical_cpu: usize,
},
#[error(
"logical CPU {logical_cpu} (hardware ID {hardware_id:#x}) has not reached the alive \
synchronization point"
)]
NotAlive {
logical_cpu: usize,
hardware_id: usize,
},
#[error(
"logical CPU {logical_cpu} (hardware ID {hardware_id:#x}) did not reach the alive \
synchronization point"
)]
AliveTimeout {
logical_cpu: usize,
hardware_id: usize,
},
#[error("Other error: {0}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn active_secondary_cpu_rejects_a_second_start_without_spinning() {
let owner = SecondaryCpuOwner::new();
assert_eq!(owner.try_claim(1), Ok(()));
assert_eq!(owner.try_claim(2), Err(1));
}
#[test]
fn active_secondary_cpu_is_released_only_explicitly() {
let owner = SecondaryCpuOwner::new();
owner.try_claim(1).unwrap();
assert_eq!(owner.active_cpu(), Some(1));
assert_eq!(owner.ensure_owned(2), Err(1));
assert_eq!(owner.release(2), Err(1));
assert_eq!(owner.active_cpu(), Some(1));
owner.release(1).unwrap();
assert_eq!(owner.active_cpu(), None);
assert_eq!(owner.try_claim(2), Ok(()));
}
#[test]
fn dropping_an_incomplete_handle_keeps_the_transport_claimed() {
const LOGICAL_CPU: usize = 17;
ACTIVE_SECONDARY_CPU.try_claim(LOGICAL_CPU).unwrap();
let startup = SecondaryCpuStartup {
logical_cpu: LOGICAL_CPU,
hardware_id: 0x11,
};
drop(startup);
assert_eq!(ACTIVE_SECONDARY_CPU.active_cpu(), Some(LOGICAL_CPU));
assert_eq!(ACTIVE_SECONDARY_CPU.try_claim(18), Err(LOGICAL_CPU));
ACTIVE_SECONDARY_CPU.release(LOGICAL_CPU).unwrap();
}
}