Merge bootprocess branch overlay into 0.2.0

Restore all bootprocess branch files that were overwritten by later 0.2.0
commits. This overlay brings back the complete boot infrastructure:

- Configs: redbear-full, redbear-mini, redbear-device-services, driver .d files
- Kernel: IRQ affinity, x2APIC, C-states, NUMA (SLIT/SRAT), MCS locks, cpuidle
- Base patches: P0-P55 + new P6 (lived block_size=512) + P57 (fbbootlogd graceful init)
- Driver infra: driver-manager, udev-shim, thermald, cpufreqd, iommu, redox-driver-sys/core
- GPU: redox-drm with improved connector handling
- System: redbear-info, redbear-hwutils phase-timer-check
- Build system: fetch.rs improvements, build-iso.sh, run_full.sh
- Kernel source: new ACPI (SLIT, SRAT), cpuidle, cstate, MCS lock modules

83 files changed, +3966/-1248 lines
This commit is contained in:
2026-05-27 06:47:23 +03:00
parent af05babbb2
commit b9de373b31
83 changed files with 3969 additions and 1251 deletions
@@ -3,6 +3,8 @@ use core::{
sync::atomic::{AtomicU8, Ordering},
};
use x86::time::rdtsc;
use crate::{
arch::{
device::local_apic::the_local_apic,
@@ -18,10 +20,95 @@ use crate::{
use super::{Madt, MadtEntry};
use alloc::collections::BTreeSet;
use alloc::vec::Vec;
/// Maximum number of APIC→CPU mappings we track for NUMA topology.
const MAX_APIC_MAPPINGS: usize = 256;
struct ApicMapping {
apic_id: u32,
cpu_id: LogicalCpuId,
}
const UNINIT_MAPPING: ApicMapping = ApicMapping { apic_id: u32::MAX, cpu_id: LogicalCpuId::new(0) };
static mut APIC_MAPPINGS: [ApicMapping; MAX_APIC_MAPPINGS] = [UNINIT_MAPPING; MAX_APIC_MAPPINGS];
static mut APIC_MAPPING_COUNT: usize = 0;
unsafe fn record_apic_mapping(apic_id: u32, cpu_id: LogicalCpuId) {
let count = APIC_MAPPING_COUNT;
if count < MAX_APIC_MAPPINGS {
APIC_MAPPINGS[count] = ApicMapping { apic_id, cpu_id };
APIC_MAPPING_COUNT = count + 1;
}
}
const AP_SPIN_LIMIT: u32 = 1_000_000;
const TRAMPOLINE: usize = 0x8000;
static TRAMPOLINE_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/trampoline"));
/// Estimate TSC frequency in MHz from CPUID.
///
/// Tries CPUID leaf 0x16 (Processor Frequency Information) first,
/// then CPUID leaf 0x15 (TSC/Core Crystal Clock Ratio).
/// Returns None if frequency cannot be determined.
fn tsc_freq_mhz_cpuid() -> Option<u64> {
let max_leaf = unsafe { core::arch::x86_64::__cpuid(0).eax as u32 };
// CPUID leaf 0x16: EAX = Core Base Frequency in MHz (Intel)
if max_leaf >= 0x16 {
let mhz = unsafe { core::arch::x86_64::__cpuid(0x16) }.eax as u64;
if mhz > 0 {
return Some(mhz);
}
}
// CPUID leaf 0x15: EAX = denominator, EBX = numerator, ECX = crystal Hz
if max_leaf >= 0x15 {
let res = unsafe { core::arch::x86_64::__cpuid(0x15) };
let denom = res.eax as u64;
let numer = res.ebx as u64;
let crystal_hz = res.ecx as u64;
if denom > 0 && numer > 0 && crystal_hz > 0 {
// TSC freq = crystal_hz * numer / denom
let tsc_hz = crystal_hz * numer / denom;
return Some(tsc_hz / 1_000_000); // Hz → MHz
}
}
None
}
/// Early-boot microsecond delay using the Time Stamp Counter.
///
/// Uses CPUID-based TSC frequency estimation when available.
/// Falls back to a conservative spin loop calibrated for the
/// minimum expected CPU speed (1 GHz).
///
/// # Safety
/// Must only be called after the BSP TSC is running (always true
/// after CPU reset on x86).
fn early_udelay(us: u64) {
if let Some(mhz) = tsc_freq_mhz_cpuid() {
// TSC-based delay: precise on invariant TSC (all modern x86).
// MHz = cycles per µs.
let target = unsafe { rdtsc() } + us * mhz;
while unsafe { rdtsc() } < target {
hint::spin_loop();
}
} else {
// Fallback: conservative spin loop.
// spin_loop() (PAUSE) is ~40 cycles on modern Intel, ~1 on AMD.
// At 1 GHz minimum: 1000 cycles/µs ÷ 40 cycles/iter = 25 iters/µs.
// Use 50 iters/µs for safety margin on slower/variable CPUs.
let iters = us.saturating_mul(50);
for _ in 0..iters {
hint::spin_loop();
}
}
}
fn current_x2apic_processor_uid(madt: &Madt, apic_id: u32) -> Option<u32> {
madt.iter().find_map(|entry| match entry {
MadtEntry::LocalX2Apic(x2apic) if x2apic.x2apic_id == apic_id => Some(x2apic.processor_uid),
@@ -61,6 +148,10 @@ pub(super) fn init(madt: Madt) {
}
if cfg!(not(feature = "multi_core")) {
unsafe {
record_apic_mapping(me.get(), LogicalCpuId::new(0));
}
crate::numa::init_default();
return;
}
@@ -94,22 +185,225 @@ pub(super) fn init(madt: Madt) {
}
}
// Detect whether MADT contains any LocalX2Apic entries.
// Some firmware (notably QEMU and some older BIOS) provides only 8-bit
// LocalApic entries even when the CPU supports x2APIC. In that case we must
// fall back to processing LocalApic entries with zero-extended IDs.
let has_x2apic_entries = madt.iter().any(|e| matches!(e, MadtEntry::LocalX2Apic(_)));
let x2apic_fallback = local_apic.x2 && !has_x2apic_entries;
if x2apic_fallback {
warn!("MADT: x2APIC mode active but no LocalX2Apic entries found; falling back to LocalApic entries with zero-extended IDs");
}
unsafe {
let preliminary_cpu_count = madt
.iter()
.filter(|entry| match entry {
MadtEntry::LocalApic(local) => u32::from(local.id) == me.get() || local.flags & 1 == 1,
MadtEntry::LocalX2Apic(local) => local.x2apic_id == me.get() || local.flags & 1 == 1,
// When x2APIC is active, LocalApic entries use 8-bit IDs that don't
// match the BSP's 32-bit x2APIC ID. Use LocalX2Apic entries instead.
MadtEntry::LocalApic(local) if !local_apic.x2 => {
u32::from(local.id) == me.get() || local.flags & 1 == 1
}
MadtEntry::LocalApic(local) if local_apic.x2 && x2apic_fallback => {
u32::from(local.id) == me.get() || local.flags & 1 == 1
}
MadtEntry::LocalApic(_) => false,
// xAPIC mode: cannot use 32-bit x2APIC IDs via 8-bit ICR.
// Skip LocalX2Apic entries and use LocalApic exclusively.
MadtEntry::LocalX2Apic(local) if local_apic.x2 => {
local.x2apic_id == me.get() || local.flags & 1 == 1
}
MadtEntry::LocalX2Apic(_) => false,
_ => false,
})
.count();
crate::profiling::allocate(preliminary_cpu_count as u32);
}
// Firmware bug detection: check for duplicate APIC IDs in MADT.
// Some firmware (especially on early BIOS/UEFI) may list the same
// processor multiple times. Keep first occurrence, warn on duplicates.
let mut seen_apic_ids: BTreeSet<u32> = BTreeSet::new();
{
let _ = seen_apic_ids.insert(me.get()); // BSP
for entry in madt.iter() {
match entry {
MadtEntry::LocalApic(local) if local.flags & 1 == 1 && !local_apic.x2 => {
let id = u32::from(local.id);
if !seen_apic_ids.insert(id) {
warn!("MADT: duplicate APIC ID {} in LocalApic entry, firmware bug", id);
}
}
MadtEntry::LocalApic(local) if local.flags & 1 == 1 && local_apic.x2 => {
if x2apic_fallback {
let id = u32::from(local.id);
if !seen_apic_ids.insert(id) {
warn!("MADT: duplicate APIC ID {} in LocalApic entry (x2APIC fallback), firmware bug", id);
}
} else {
debug!("MADT: ignoring 8-bit LocalApic ID {} in x2APIC mode", local.id);
}
}
MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 && local_apic.x2 => {
let id = local.x2apic_id;
if !seen_apic_ids.insert(id) {
warn!("MADT: duplicate x2APIC ID {} in LocalX2Apic entry, firmware bug", id);
}
}
MadtEntry::LocalX2Apic(local) if local.flags & 1 == 1 && !local_apic.x2 => {
// xAPIC mode: skip 32-bit x2APIC IDs; dedup only among LocalApic entries.
let id = local.x2apic_id; // Copy from packed struct
debug!("MADT: ignoring 32-bit x2APIC ID {} in xAPIC mode", id);
}
_ => {}
}
}
}
for madt_entry in madt.iter() {
debug!(" {:x?}", madt_entry);
if let MadtEntry::LocalApic(ap_local_apic) = madt_entry {
if u32::from(ap_local_apic.id) == me.get() {
// x2APIC mode: LocalApic entries have 8-bit IDs that don't match
// the BSP's 32-bit x2APIC ID. All entries would be treated as APs,
// and SIPI would target the wrong processors. Skip them and rely
// on LocalX2Apic entries exclusively.
if local_apic.x2 && !x2apic_fallback {
debug!(
" Skipping 8-bit LocalApic id={} (x2APIC active, using LocalX2Apic entries)",
ap_local_apic.id
);
} else if local_apic.x2 && x2apic_fallback {
let apic_id = u32::from(ap_local_apic.id);
if apic_id == me.get() {
debug!(" This is my local APIC (x2APIC fallback, id={})", apic_id);
} else if ap_local_apic.flags & 1 == 1 {
let alloc = match allocate_p2frame(4) {
Some(frame) => frame,
None => {
println!("KERNEL AP: CPU {} no memory for stack, skipping", apic_id);
continue;
}
};
let stack_start = RmmA::phys_to_virt(alloc.base()).data();
let stack_end = stack_start + (PAGE_SIZE << 4);
let cpu_id = LogicalCpuId::new(crate::CPU_COUNT.fetch_add(1, Ordering::SeqCst));
if cpu_id.get() >= crate::cpu_set::MAX_CPU_COUNT {
println!(
"KERNEL AP: CPU {} exceeds logical CPU limit, skipping",
apic_id
);
continue;
}
let pcr_ptr = crate::arch::gdt::allocate_and_init_pcr(cpu_id, stack_end);
let idt_ptr = crate::arch::idt::allocate_and_init_idt(cpu_id);
let args = KernelArgsAp {
stack_end: stack_end as *mut u8,
cpu_id,
pcr_ptr,
idt_ptr,
};
let ap_ready = (TRAMPOLINE + 8) as *mut u64;
let ap_args_ptr = unsafe { ap_ready.add(1) };
let ap_page_table = unsafe { ap_ready.add(2) };
let ap_code = unsafe { ap_ready.add(3) };
unsafe {
ap_ready.write(0);
ap_args_ptr.write(&args as *const _ as u64);
ap_page_table.write(page_table_physaddr as u64);
#[expect(clippy::fn_to_numeric_cast)]
ap_code.write(kstart_ap as u64);
core::sync::atomic::fence(Ordering::SeqCst);
};
AP_READY.store(false, Ordering::SeqCst);
// Clear APIC Error Status Register before starting AP.
unsafe { local_apic.esr(); }
// Send INIT IPI (Assert) — x2APIC uses 64-bit ICR format.
{
let mut icr = 0x4500u64;
icr |= u64::from(apic_id) << 32;
local_apic.set_icr(icr);
}
// Intel SDM Vol 3A §8.4.4: wait 10ms after INIT deassert
early_udelay(10_000);
// Send START IPI #1
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x0600 | ap_segment as u64;
icr |= u64::from(apic_id) << 32;
local_apic.set_icr(icr);
}
early_udelay(200);
// Send START IPI #2 (recommended for compatibility)
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x0600 | ap_segment as u64;
icr |= u64::from(apic_id) << 32;
local_apic.set_icr(icr);
}
early_udelay(200);
// Check ESR for delivery errors after SIPI sequence.
let esr_val = unsafe { local_apic.esr() };
if esr_val != 0 {
println!(
"KERNEL AP: CPU {} SIPI delivery error (ESR={:#x}), continuing",
apic_id, esr_val
);
}
let mut trampoline_ready = false;
for _ in 0..AP_SPIN_LIMIT {
if unsafe { (*ap_ready.cast::<AtomicU8>()).load(Ordering::SeqCst) } != 0 {
trampoline_ready = true;
break;
}
hint::spin_loop();
}
if !trampoline_ready {
println!("KERNEL AP: CPU {} trampoline timeout, skipping", apic_id);
continue;
}
let mut kernel_ready = false;
for _ in 0..AP_SPIN_LIMIT {
if AP_READY.load(Ordering::SeqCst) {
kernel_ready = true;
break;
}
hint::spin_loop();
}
if !kernel_ready {
println!("KERNEL AP: CPU {} AP_READY timeout, skipping", apic_id);
continue;
}
// Record APIC→CPU mapping for NUMA topology.
unsafe {
record_apic_mapping(apic_id, cpu_id);
}
// Set NUMA node from SRAT data.
if let Some(percpu) = crate::percpu::get_for_cpu(cpu_id) {
if let Some(node) = crate::acpi::srat::numa_node_for_apic(apic_id) {
percpu.numa_node.set(node);
}
}
RmmA::invalidate_all();
}
} else if u32::from(ap_local_apic.id) == me.get() {
debug!(" This is my local APIC");
} else if ap_local_apic.flags & 1 == 1 {
// Allocate a stack
@@ -123,15 +417,16 @@ pub(super) fn init(madt: Madt) {
let stack_start = RmmA::phys_to_virt(alloc.base()).data();
let stack_end = stack_start + (PAGE_SIZE << 4);
let next_cpu = crate::CPU_COUNT.load(Ordering::Relaxed);
if next_cpu >= crate::cpu_set::MAX_CPU_COUNT {
// Atomically allocate a CPU ID — fetch_add is SeqCst so that
// all later stores (PercpuBlock, NUMA node) are ordered after.
let cpu_id = LogicalCpuId::new(crate::CPU_COUNT.fetch_add(1, Ordering::SeqCst));
if cpu_id.get() >= crate::cpu_set::MAX_CPU_COUNT {
println!(
"KERNEL AP: CPU {} exceeds logical CPU limit, skipping",
ap_local_apic.id
);
continue;
}
let cpu_id = LogicalCpuId::new(next_cpu);
let pcr_ptr = crate::arch::gdt::allocate_and_init_pcr(cpu_id, stack_end);
@@ -157,14 +452,21 @@ pub(super) fn init(madt: Madt) {
#[expect(clippy::fn_to_numeric_cast)]
ap_code.write(kstart_ap as u64);
// TODO: Is this necessary (this fence)?
core::arch::asm!("");
// Ensure all trampoline writes are visible to the AP before
// it starts executing. asm!("") is only a compiler barrier;
// fence(SeqCst) is a full hardware memory barrier.
core::sync::atomic::fence(Ordering::SeqCst);
};
AP_READY.store(false, Ordering::SeqCst);
// Send INIT IPI
// Clear APIC Error Status Register before starting AP.
// Intel SDM §8.4.4: ESR should be cleared before sending SIPI.
unsafe { local_apic.esr(); }
// Send INIT IPI (Assert)
{
let mut icr = 0x4500;
// ICR: Delivery Mode=INIT(101), Level=Assert, Trigger=Edge
let mut icr = 0x4500u64;
if local_apic.x2 {
icr |= u64::from(ap_local_apic.id) << 32;
} else {
@@ -173,20 +475,53 @@ pub(super) fn init(madt: Madt) {
local_apic.set_icr(icr);
}
// Send START IPI
// Intel SDM Vol 3A §8.4.4: wait 10ms after INIT deassert
// before sending first SIPI. Modern CPUs may need less,
// but 10ms is the safe specification-compliant value.
early_udelay(10_000);
// Send START IPI #1
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x4600 | ap_segment as u64;
// ICR: Delivery Mode=StartUp(110), Vector=ap_segment
// Note: bit 14 (Level) must be 0 for SIPI per Intel SDM.
let mut icr = 0x0600 | ap_segment as u64;
if local_apic.x2 {
icr |= u64::from(ap_local_apic.id) << 32;
} else {
icr |= u64::from(ap_local_apic.id) << 56;
}
local_apic.set_icr(icr);
}
// Intel SDM: wait 200µs between SIPIs
early_udelay(200);
// Send START IPI #2 (recommended for compatibility)
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x0600 | ap_segment as u64;
if local_apic.x2 {
icr |= u64::from(ap_local_apic.id) << 32;
} else {
icr |= u64::from(ap_local_apic.id) << 56;
}
local_apic.set_icr(icr);
}
// Wait briefly for SIPI to be accepted
early_udelay(200);
// Check ESR for delivery errors after SIPI sequence.
// Bit 5 = Send Accept Error, Bit 6 = Send Illegal Vector.
let esr_val = unsafe { local_apic.esr() };
if esr_val != 0 {
println!(
"KERNEL AP: CPU {} SIPI delivery error (ESR={:#x}), continuing",
ap_local_apic.id, esr_val
);
}
// Wait for trampoline ready with timeout
let mut trampoline_ready = false;
for _ in 0..AP_SPIN_LIMIT {
@@ -214,7 +549,16 @@ pub(super) fn init(madt: Madt) {
continue;
}
crate::CPU_COUNT.fetch_add(1, Ordering::Relaxed);
// Record APIC→CPU mapping for NUMA topology.
unsafe {
record_apic_mapping(u32::from(ap_local_apic.id), cpu_id);
}
// Set NUMA node from SRAT data.
if let Some(percpu) = crate::percpu::get_for_cpu(cpu_id) {
if let Some(node) = crate::acpi::srat::numa_node_for_apic(u32::from(ap_local_apic.id)) {
percpu.numa_node.set(node);
}
}
RmmA::invalidate_all();
}
@@ -222,7 +566,14 @@ pub(super) fn init(madt: Madt) {
let apic_id = ap_x2apic.x2apic_id;
let flags = ap_x2apic.flags;
if apic_id == me.get() {
// xAPIC mode: cannot target 32-bit x2APIC IDs via 8-bit ICR.
// Skip LocalX2Apic entries; use LocalApic entries exclusively.
if !local_apic.x2 {
debug!(
" Skipping 32-bit x2APIC id={} (xAPIC mode, using LocalApic entries)",
apic_id
);
} else if apic_id == me.get() {
debug!(" This is my local x2APIC");
} else if flags & 1 == 1 {
let alloc = match allocate_p2frame(4) {
@@ -235,15 +586,16 @@ pub(super) fn init(madt: Madt) {
let stack_start = RmmA::phys_to_virt(alloc.base()).data();
let stack_end = stack_start + (PAGE_SIZE << 4);
let next_cpu = crate::CPU_COUNT.load(Ordering::Relaxed);
if next_cpu >= crate::cpu_set::MAX_CPU_COUNT {
// Atomically allocate a CPU ID — fetch_add is SeqCst so that
// all later stores (PercpuBlock, NUMA node) are ordered after.
let cpu_id = LogicalCpuId::new(crate::CPU_COUNT.fetch_add(1, Ordering::SeqCst));
if cpu_id.get() >= crate::cpu_set::MAX_CPU_COUNT {
println!(
"KERNEL AP: CPU {} exceeds logical CPU limit, skipping",
apic_id
);
continue;
}
let cpu_id = LogicalCpuId::new(next_cpu);
let pcr_ptr = crate::arch::gdt::allocate_and_init_pcr(cpu_id, stack_end);
let idt_ptr = crate::arch::idt::allocate_and_init_idt(cpu_id);
@@ -266,38 +618,55 @@ pub(super) fn init(madt: Madt) {
ap_page_table.write(page_table_physaddr as u64);
#[expect(clippy::fn_to_numeric_cast)]
ap_code.write(kstart_ap as u64);
core::arch::asm!("");
// Ensure all trampoline writes are visible to the AP.
core::sync::atomic::fence(Ordering::SeqCst);
}
AP_READY.store(false, Ordering::SeqCst);
// Clear APIC Error Status Register before starting AP.
unsafe { local_apic.esr(); }
// Send INIT IPI (Assert)
{
let mut icr = 0x4500u64;
icr |= u64::from(apic_id) << 32;
local_apic.set_icr(icr);
}
for _ in 0..100_000 {
hint::spin_loop();
}
// Intel SDM Vol 3A §8.4.4: wait 10ms after INIT
early_udelay(10_000);
// Send START IPI #1
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x4600u64 | ap_segment as u64;
let mut icr = 0x0600u64 | ap_segment as u64;
icr |= u64::from(apic_id) << 32;
local_apic.set_icr(icr);
}
for _ in 0..2_000_000 {
hint::spin_loop();
}
// Intel SDM: wait 200µs between SIPIs
early_udelay(200);
// Send START IPI #2 (recommended for compatibility)
{
let ap_segment = (TRAMPOLINE >> 12) & 0xFF;
let mut icr = 0x4600u64 | ap_segment as u64;
let mut icr = 0x0600u64 | ap_segment as u64;
icr |= u64::from(apic_id) << 32;
local_apic.set_icr(icr);
}
// Wait briefly for SIPI acceptance
early_udelay(200);
// Check ESR for delivery errors.
let esr_val = unsafe { local_apic.esr() };
if esr_val != 0 {
println!(
"KERNEL AP: CPU {} SIPI delivery error (ESR={:#x}), continuing",
apic_id, esr_val
);
}
let mut trampoline_ready = false;
for _ in 0..AP_SPIN_LIMIT {
if unsafe { (*ap_ready.cast::<AtomicU8>()).load(Ordering::SeqCst) } != 0 {
@@ -324,7 +693,17 @@ pub(super) fn init(madt: Madt) {
continue;
}
crate::CPU_COUNT.fetch_add(1, Ordering::Relaxed);
// Record APIC→CPU mapping for NUMA topology.
unsafe {
record_apic_mapping(apic_id, cpu_id);
}
// Set NUMA node from SRAT data.
if let Some(percpu) = crate::percpu::get_for_cpu(cpu_id) {
if let Some(node) = crate::acpi::srat::numa_node_for_apic(apic_id) {
percpu.numa_node.set(node);
}
}
RmmA::invalidate_all();
}
} else if let MadtEntry::LocalApicNmi(nmi) = madt_entry {
@@ -342,6 +721,33 @@ pub(super) fn init(madt: Madt) {
}
}
// Initialize NUMA topology from APIC→CPU mappings and SRAT.
{
let mappings = unsafe { &APIC_MAPPINGS[..APIC_MAPPING_COUNT] };
let mappings_ref: Vec<(u32, LogicalCpuId)> = mappings
.iter()
.map(|m| (m.apic_id, m.cpu_id))
.collect();
crate::numa::init_from_srat(&mappings_ref);
}
// Set BSP's NUMA node from SRAT.
if let Some(node) = crate::acpi::srat::numa_node_for_apic(me.get()) {
crate::percpu::PercpuBlock::current().numa_node.set(node);
}
// Log final CPU count vs maximum
let cpu_count = crate::CPU_COUNT.load(Ordering::SeqCst);
info!(
"SMP: {} CPUs online (max {})",
cpu_count, crate::cpu_set::MAX_CPU_COUNT
);
if cpu_count > crate::cpu_set::MAX_CPU_COUNT * 80 / 100 {
warn!(
"SMP: CPU count approaching MAX_CPU_COUNT limit ({}/{})",
cpu_count, crate::cpu_set::MAX_CPU_COUNT
);
}
// Unmap trampoline
if let Some((_frame, _, flush)) = unsafe {
KernelMapper::lock_rw()
@@ -34,6 +34,12 @@ impl Madt {
let madt = Madt::new(find_one_sdt!("APIC"));
if let Some(madt) = madt {
// Validate MADT checksum per ACPI 6.5 §5.2.2
if !madt.sdt.validate_checksum() {
error!("MADT checksum validation failed, skipping APIC initialization");
return;
}
// safe because no APs have been started yet.
unsafe { MADT.get().write(Some(madt)) };
@@ -20,6 +20,8 @@ mod rxsdt;
pub mod sdt;
#[cfg(target_arch = "aarch64")]
mod spcr;
pub mod slit;
pub mod srat;
mod xsdt;
unsafe fn map_linearly(addr: PhysicalAddress, len: usize, mapper: &mut crate::memory::PageMapper) {
@@ -163,7 +165,14 @@ pub unsafe fn init(already_supplied_rsdp: Option<*const u8>) {
// TODO: Enumerate processors in userspace, and then provide an ACPI-independent interface
// to initialize enumerated processors to userspace?
// Parse SRAT BEFORE MADT so NUMA node mapping is available
// when APs are started and PercpuBlocks are created.
srat::init();
Madt::init();
// Parse SLIT after MADT for the NUMA distance matrix.
slit::init();
//TODO: support this on any arch
// SPCR must be initialized after MADT for interrupt controllers
#[cfg(target_arch = "aarch64")]
@@ -24,4 +24,20 @@ impl Sdt {
let header_size = size_of::<Sdt>();
total_size.saturating_sub(header_size)
}
/// Validate the SDT checksum.
///
/// Per ACPI 6.5 §5.2.2: the entire table (including the checksum field)
/// must sum to 0 when all bytes are added together as unsigned 8-bit values.
pub fn validate_checksum(&self) -> bool {
let ptr = self as *const _ as *const u8;
let len = self.length as usize;
if len < size_of::<Sdt>() {
return false;
}
let sum = unsafe { core::slice::from_raw_parts(ptr, len) }
.iter()
.fold(0u8, |acc, &b| acc.wrapping_add(b));
sum == 0
}
}
@@ -0,0 +1,45 @@
//! SLIT (System Locality Information Table) parser.
//!
//! Parses the NUMA distance matrix for scheduler NUMA-aware work stealing.
use super::sdt::Sdt;
use crate::acpi::find_sdt;
const MAX_NODES: usize = 8;
static mut SLIT_MATRIX: [[u8; MAX_NODES]; MAX_NODES] = [[10u8; MAX_NODES]; MAX_NODES];
static mut SLIT_NUM_NODES: usize = 0;
static mut SLIT_AVAILABLE: bool = false;
pub fn is_available() -> bool { unsafe { SLIT_AVAILABLE } }
pub fn num_nodes() -> usize { unsafe { SLIT_NUM_NODES } }
pub fn distance(from: u8, to: u8) -> u8 {
if !unsafe { SLIT_AVAILABLE } { return 10; }
let (from, to) = (from as usize, to as usize);
if from >= MAX_NODES || to >= MAX_NODES { return 10; }
unsafe { SLIT_MATRIX[from][to] }
}
pub fn same_socket(node1: u8, node2: u8) -> bool { distance(node1, node2) <= 20 }
pub fn init() {
let sdt = match find_sdt("SLIT").as_slice() {
[] => return,
[x] => *x,
xs => { println!("SLIT: {} tables found, expected 1", xs.len()); return; }
};
if &sdt.signature != b"SLIT" { return; }
let data_addr = sdt.data_address();
let data_len = sdt.data_len();
if data_len < 8 { return; }
let num_nodes = unsafe { *(data_addr as *const u64) } as usize;
if num_nodes == 0 || num_nodes > MAX_NODES { println!("SLIT: {num_nodes} nodes (max {MAX_NODES}), ignoring"); return; }
let matrix_start = 8;
let matrix_size = num_nodes * num_nodes;
if data_len < matrix_start + matrix_size { println!("SLIT: matrix truncated ({data_len} < {})", matrix_start + matrix_size); return; }
let matrix = unsafe { &mut SLIT_MATRIX };
for i in 0..num_nodes { for j in 0..num_nodes { matrix[i][j] = unsafe { *((data_addr + matrix_start + i * num_nodes + j) as *const u8) }; } }
unsafe { SLIT_NUM_NODES = num_nodes; SLIT_AVAILABLE = true; }
debug!("SLIT: {} nodes, distance matrix loaded", num_nodes);
}
+102
View File
@@ -0,0 +1,102 @@
//! SRAT (System Resource Affinity Table) parser.
//!
//! Parses CPU-to-NUMA-node and memory-to-NUMA-node affinity information.
//! Called before MADT init so that NUMA data is available during AP startup.
use super::sdt::Sdt;
use crate::acpi::find_sdt;
const MAX_CPU_ENTRIES: usize = 256;
const MAX_MEM_ENTRIES: usize = 64;
#[derive(Clone, Copy)]
struct SratCpuEntry { apic_id: u32, node: u8, enabled: bool }
#[derive(Clone, Copy)]
struct SratMemEntry { node: u8, base: u64, length: u64, enabled: bool }
const CPU_NONE: SratCpuEntry = SratCpuEntry { apic_id: u32::MAX, node: 0, enabled: false };
const MEM_NONE: SratMemEntry = SratMemEntry { node: 0, base: 0, length: 0, enabled: false };
static mut SRAT_CPU_ENTRIES: [SratCpuEntry; MAX_CPU_ENTRIES] = [CPU_NONE; MAX_CPU_ENTRIES];
static mut SRAT_MEM_ENTRIES: [SratMemEntry; MAX_MEM_ENTRIES] = [MEM_NONE; MAX_MEM_ENTRIES];
static mut SRAT_CPU_COUNT: usize = 0;
static mut SRAT_MEM_COUNT: usize = 0;
static mut SRAT_AVAILABLE: bool = false;
pub fn is_available() -> bool { unsafe { SRAT_AVAILABLE } }
pub fn numa_node_for_apic(apic_id: u32) -> Option<u8> {
if !unsafe { SRAT_AVAILABLE } { return None; }
let count = unsafe { SRAT_CPU_COUNT };
let entries = unsafe { &SRAT_CPU_ENTRIES };
for i in 0..count {
if entries[i].apic_id == apic_id && entries[i].enabled { return Some(entries[i].node); }
}
None
}
pub fn numa_node_count() -> usize {
if !unsafe { SRAT_AVAILABLE } { return 1; }
let mut max_node: u8 = 0;
let count = unsafe { SRAT_CPU_COUNT };
let entries = unsafe { &SRAT_CPU_ENTRIES };
for i in 0..count { if entries[i].enabled && entries[i].node > max_node { max_node = entries[i].node; } }
(max_node as usize) + 1
}
#[repr(C, packed)]
struct SratLocalApic { _proximity_lo: u8, apic_id: u8, flags: u32, _local_sapic_eid: u8, _proximity_hi: [u8; 3], _clock_domain: u32 }
#[repr(C, packed)]
struct SratMemoryAffinity { proximity_domain: u32, _reserved1: u16, base_address_lo: u32, base_address_hi: u32, length_lo: u32, length_hi: u32, _reserved2: u32, flags: u32, _reserved3: u64 }
#[repr(C, packed)]
struct SratLocalX2Apic { _reserved: u16, proximity_domain: u32, x2apic_id: u32, flags: u32, _clock_domain: u32, _reserved2: u32 }
pub fn init() {
let sdt = match find_sdt("SRAT").as_slice() {
[] => return,
[x] => *x,
xs => { println!("SRAT: {} tables found, expected 1", xs.len()); return; }
};
if &sdt.signature != b"SRAT" { return; }
let data_addr = sdt.data_address();
let data_len = sdt.data_len();
if data_len < 12 { println!("SRAT: table too short ({data_len} bytes)"); return; }
let mut offset: usize = 12;
let cpu_entries = unsafe { &mut SRAT_CPU_ENTRIES };
let mem_entries = unsafe { &mut SRAT_MEM_ENTRIES };
let mut cpu_count: usize = 0;
let mut mem_count: usize = 0;
while offset + 2 <= data_len {
let entry_type = unsafe { *((data_addr + offset) as *const u8) };
let entry_len = unsafe { *((data_addr + offset + 1) as *const u8) } as usize;
if entry_len < 2 || offset + entry_len > data_len { break; }
let entry_data = data_addr + offset + 2;
match entry_type {
0x0 if entry_len >= size_of::<SratLocalApic>() + 2 => {
let e = unsafe { &*(entry_data as *const SratLocalApic) };
let enabled = (e.flags & 1) == 1;
let node = (e._proximity_lo as u32) | ((e._proximity_hi[0] as u32) << 8) | ((e._proximity_hi[1] as u32) << 16) | ((e._proximity_hi[2] as u32) << 24);
if cpu_count < MAX_CPU_ENTRIES { cpu_entries[cpu_count] = SratCpuEntry { apic_id: e.apic_id as u32, node: node as u8, enabled }; cpu_count += 1; }
}
0x1 if entry_len >= size_of::<SratMemoryAffinity>() + 2 => {
let e = unsafe { &*(entry_data as *const SratMemoryAffinity) };
let enabled = (e.flags & 1) == 1;
let base = (e.base_address_hi as u64) << 32 | e.base_address_lo as u64;
let length = (e.length_hi as u64) << 32 | e.length_lo as u64;
if mem_count < MAX_MEM_ENTRIES { mem_entries[mem_count] = SratMemEntry { node: e.proximity_domain as u8, base, length, enabled }; mem_count += 1; }
}
0x2 if entry_len >= size_of::<SratLocalX2Apic>() + 2 => {
let e = unsafe { &*(entry_data as *const SratLocalX2Apic) };
let enabled = (e.flags & 1) == 1;
if cpu_count < MAX_CPU_ENTRIES { cpu_entries[cpu_count] = SratCpuEntry { apic_id: e.x2apic_id, node: e.proximity_domain as u8, enabled }; cpu_count += 1; }
}
_ => {}
}
offset += entry_len;
}
unsafe { SRAT_CPU_COUNT = cpu_count; SRAT_MEM_COUNT = mem_count; SRAT_AVAILABLE = true; }
debug!("SRAT: {} CPU entries, {} memory entries", cpu_count, mem_count);
}
@@ -0,0 +1,186 @@
use core::cell::SyncUnsafeCell;
use core::sync::atomic::{AtomicUsize, Ordering};
use crate::arch::cpuid::cpuid;
use crate::syscall::error::{Error, Result, EINVAL};
#[repr(align(64))]
struct MonitorTarget {
value: AtomicUsize,
}
static MONITOR_TARGET: MonitorTarget = MonitorTarget {
value: AtomicUsize::new(0),
};
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CStateFlags: u32 {
const NEEDS_MONITOR = 1;
const NEEDS_WBINVD = 2;
}
}
#[derive(Clone, Copy, Debug)]
pub struct CState {
pub name: &'static str,
pub typ: u32,
pub latency: u32,
pub power: u32,
pub mwait_hint: u32,
pub flags: CStateFlags,
}
const MAX_CSTATES: usize = 8;
static CPUIDLE_STATES: SyncUnsafeCell<[Option<CState>; MAX_CSTATES]> =
SyncUnsafeCell::new([None; MAX_CSTATES]);
static NUM_CPUIDLE_STATES: AtomicUsize = AtomicUsize::new(0);
static CSTATE_POLICY_MAX: AtomicUsize = AtomicUsize::new(0);
fn has_mwait() -> bool {
cpuid().get_feature_info().map_or(false, |info| info.has_monitor_mwait())
}
fn add_state(index: usize, state: CState) {
unsafe {
(*CPUIDLE_STATES.get())[index] = Some(state);
}
}
pub fn init() {
add_state(0, CState {
name: "C1",
typ: 1,
latency: 1,
power: 1000,
mwait_hint: 0x00,
flags: CStateFlags::empty(),
});
let mut count = 1;
if has_mwait() {
add_state(count, CState {
name: "C1E",
typ: 1,
latency: 2,
power: 800,
mwait_hint: 0x01,
flags: CStateFlags::NEEDS_MONITOR,
});
count += 1;
add_state(count, CState {
name: "C2",
typ: 2,
latency: 10,
power: 500,
mwait_hint: 0x10,
flags: CStateFlags::NEEDS_MONITOR,
});
count += 1;
add_state(count, CState {
name: "C3",
typ: 3,
latency: 50,
power: 100,
mwait_hint: 0x20,
flags: CStateFlags::NEEDS_MONITOR | CStateFlags::NEEDS_WBINVD,
});
count += 1;
add_state(count, CState {
name: "C6",
typ: 6,
latency: 100,
power: 50,
mwait_hint: 0x50,
flags: CStateFlags::NEEDS_MONITOR | CStateFlags::NEEDS_WBINVD,
});
count += 1;
add_state(count, CState {
name: "C7",
typ: 7,
latency: 200,
power: 30,
mwait_hint: 0x60,
flags: CStateFlags::NEEDS_MONITOR | CStateFlags::NEEDS_WBINVD,
});
count += 1;
}
NUM_CPUIDLE_STATES.store(count, Ordering::SeqCst);
log::info!("cpuidle: initialized {} states (mwait={})", count, has_mwait());
}
pub fn policy_read() -> usize {
CSTATE_POLICY_MAX.load(Ordering::Relaxed)
}
pub fn policy_write(buf: &[u8]) -> Result<usize> {
let s = core::str::from_utf8(buf).map_err(|_| Error::new(EINVAL))?;
let s = s.trim();
let val: usize = s.parse().map_err(|_| Error::new(EINVAL))?;
let num_states = NUM_CPUIDLE_STATES.load(Ordering::Relaxed);
if val >= num_states {
return Err(Error::new(EINVAL));
}
CSTATE_POLICY_MAX.store(val, Ordering::Relaxed);
log::info!("cpuidle: policy set to max state {}", val);
Ok(s.len())
}
pub fn resource() -> Result<alloc::vec::Vec<u8>> {
let mut output = alloc::string::String::new();
let num_states = NUM_CPUIDLE_STATES.load(Ordering::Relaxed);
let policy = CSTATE_POLICY_MAX.load(Ordering::Relaxed);
output.push_str(&format!("policy_max: {}\n", policy));
output.push_str(&format!("num_states: {}\n", num_states));
for i in 0..num_states {
if let Some(state) = unsafe { (*CPUIDLE_STATES.get())[i] } {
output.push_str(&format!(
"state{}: name={} type={} latency={}us power={} hint={:#x} flags={:?}\n",
i, state.name, state.typ, state.latency, state.power, state.mwait_hint, state.flags
));
}
}
Ok(output.into_bytes())
}
pub unsafe fn enter_idle() {
let policy_max = CSTATE_POLICY_MAX.load(Ordering::Relaxed);
let num_states = NUM_CPUIDLE_STATES.load(Ordering::Relaxed);
let target_index = if num_states == 0 {
0
} else {
core::cmp::min(policy_max, num_states - 1)
};
if target_index == 0 {
unsafe { crate::arch::interrupt::enable_and_halt(); }
return;
}
let state = match unsafe { (*CPUIDLE_STATES.get())[target_index] } {
Some(s) => s,
None => {
unsafe { crate::arch::interrupt::enable_and_halt(); }
return;
}
};
if state.flags.contains(CStateFlags::NEEDS_MONITOR) {
let addr = &MONITOR_TARGET.value as *const AtomicUsize as *const u8;
unsafe { crate::arch::interrupt::monitor(addr, 0, 0); }
}
if state.flags.contains(CStateFlags::NEEDS_WBINVD) {
unsafe { core::arch::asm!("wbinvd", options(nostack)); }
}
unsafe {
crate::arch::interrupt::enable_and_mwait(state.mwait_hint, 0);
}
}
@@ -120,6 +120,21 @@ impl IoApic {
reg |= u64::from(mask) << 16;
let _ = guard.write_ioredtbl(idx, reg);
}
/// Change the destination APIC for a GSI by reprogramming the redirection table entry.
/// Preserves all other fields (vector, polarity, trigger mode, delivery mode, mask).
/// Returns true if the entry was successfully updated.
pub fn set_irq_affinity(&self, gsi: u32, dest: ApicId) -> bool {
let idx = (gsi - self.gsi_start) as u8;
let mut guard = self.regs.lock();
let Some(mut entry) = guard.read_ioredtbl(idx) else {
return false;
};
// Clear destination field (bits 63:56 for xAPIC physical mode)
// and set new destination APIC ID
entry &= !(0xFF_u64 << 56);
entry |= u64::from(dest.get()) << 56;
guard.write_ioredtbl(idx, entry)
}
}
#[repr(u8)]
@@ -474,3 +489,14 @@ pub unsafe fn unmask(irq: u8) {
};
apic.set_mask(gsi, false);
}
/// Change the destination CPU for an IRQ by reprogramming the IOAPIC redirection entry.
/// Resolves the legacy IRQ to its GSI, finds the owning IOAPIC, and updates the destination
/// APIC ID in the redirection table while preserving all other fields.
pub unsafe fn set_affinity(irq: u8, dest: ApicId) -> bool {
let gsi = resolve(irq);
match find_ioapic(gsi) {
Some(apic) => apic.set_irq_affinity(gsi, dest),
None => false,
}
}
@@ -59,10 +59,10 @@ impl LocalApic {
.is_some_and(|feature_info| feature_info.has_x2apic());
if !self.x2 {
debug!("Detected xAPIC at {:#x}", physaddr.data());
info!("Detected xAPIC at {:#x}", physaddr.data());
self.address = map_device_memory(physaddr, 4096).data();
} else {
debug!("Detected x2APIC");
info!("Detected x2APIC");
}
self.init_ap();
@@ -110,6 +110,8 @@ pub fn set_reserved(cpu_id: LogicalCpuId, index: u8, reserved: bool) {
}
pub fn available_irqs_iter(cpu_id: LogicalCpuId) -> impl Iterator<Item = u8> + 'static {
let count = (32..=254).filter(|&index| !is_reserved(cpu_id, index)).count();
info!("available_irqs_iter: cpu_id={} count={}", cpu_id.get(), count);
(32..=254).filter(move |&index| !is_reserved(cpu_id, index))
}
@@ -4,16 +4,10 @@ use crate::{
percpu::PercpuBlock,
syscall::FloatRegisters,
};
use core::{mem::offset_of, ptr, sync::atomic::AtomicBool};
use core::{mem::offset_of, ptr};
use spin::Once;
use syscall::{EnvRegisters, Result};
/// This must be used by the kernel to ensure that context switches are done atomically
/// Compare and exchange this to true when beginning a context switch on any CPU
/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch
/// This must be done, as no locks can be held on the stack during switch
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
// 512 bytes for registers, extra bytes for fpcr and fpsr
pub const KFX_ALIGN: usize = 16;
@@ -2,13 +2,11 @@ use crate::{
arch::interrupt::InterruptStack, context::context::Kstack, memory::RmmA, percpu::PercpuBlock,
syscall::FloatRegisters,
};
use core::{mem::offset_of, sync::atomic::AtomicBool};
use core::mem::offset_of;
use rmm::{Arch, VirtualAddress};
use spin::Once;
use syscall::{error::*, EnvRegisters};
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
pub const KFX_ALIGN: usize = 16;
#[derive(Clone, Debug, Default)]
@@ -1,4 +1,4 @@
use core::{mem::offset_of, sync::atomic::AtomicBool};
use core::mem::offset_of;
use rmm::{Arch, VirtualAddress};
use spin::Once;
use syscall::{error::*, EnvRegisters};
@@ -14,12 +14,6 @@ use crate::{
syscall::FloatRegisters,
};
/// This must be used by the kernel to ensure that context switches are done atomically
/// Compare and exchange this to true when beginning a context switch on any CPU
/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch
/// This must be done, as no locks can be held on the stack during switch
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
const ST_RESERVED: u128 = 0xFFFF_FFFF_FFFF_0000_0000_0000_0000_0000;
pub const KFX_ALIGN: usize = 16;
@@ -1,6 +1,5 @@
use core::{
ptr::{addr_of, addr_of_mut},
sync::atomic::AtomicBool,
};
use crate::syscall::FloatRegisters;
@@ -12,12 +11,6 @@ use spin::Once;
use syscall::{error::*, EnvRegisters};
use x86::msr;
/// This must be used by the kernel to ensure that context switches are done atomically
/// Compare and exchange this to true when beginning a context switch on any CPU
/// The `Context::switch_to` function will set it back to false, allowing other CPU's to switch
/// This must be done, as no locks can be held on the stack during switch
pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false);
const ST_RESERVED: u128 = 0xFFFF_FFFF_FFFF_0000_0000_0000_0000_0000;
#[cfg(cpu_feature_never = "xsave")]
@@ -14,8 +14,8 @@ use crate::{
memory::{RmmA, RmmArch, TableKind},
percpu::PercpuBlock,
sync::{
ArcRwLockWriteGuard, CleanLockToken, LockToken, Mutex, MutexGuard, RwLock, RwLockReadGuard,
RwLockWriteGuard, L0, L1, L2, L4,
ArcRwLockWriteGuard, CleanLockToken, LockToken, McsMutex, McsMutexGuard, Mutex,
MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard, L0, L1, L2, L4,
},
syscall::error::Result,
};
@@ -74,10 +74,12 @@ pub use self::arch::empty_cr3;
// the context file descriptors.
static CONTEXTS: RwLock<L2, BTreeSet<ContextRef>> = RwLock::new(BTreeSet::new());
// Actual context store for the scheduler
static RUN_CONTEXTS: Mutex<L1, RunContextData> = Mutex::new(RunContextData::new());
// Actual context store for the scheduler — uses MCS fair spinlock to
// eliminate cache-line bouncing under multi-CPU contention.
static RUN_CONTEXTS: McsMutex<L1, RunContextData> = McsMutex::new(RunContextData::new());
// Context that has been pushed out from RUN_CONTEXTS after being idle
// Context that has been pushed out from RUN_CONTEXTS after being idle.
// Uses regular Mutex (lower contention; wakeup_contexts uses try_lock).
static IDLE_CONTEXTS: Mutex<L2, VecDeque<WeakContextRef>> = Mutex::new(VecDeque::new());
pub struct RunContextData {
@@ -113,7 +115,7 @@ pub fn idle_contexts_try(
IDLE_CONTEXTS.try_lock(token)
}
pub fn run_contexts(token: LockToken<'_, L0>) -> MutexGuard<'_, L1, RunContextData> {
pub fn run_contexts(token: LockToken<'_, L0>) -> McsMutexGuard<'_, L1, RunContextData> {
RUN_CONTEXTS.lock(token)
}
@@ -15,7 +15,7 @@ use crate::{
use alloc::{sync::Arc, vec::Vec};
use core::{
cell::{Cell, RefCell},
hint, mem,
mem,
sync::atomic::Ordering,
};
use syscall::PtraceFlags;
@@ -26,6 +26,11 @@ enum UpdateResult {
Blocked,
}
/// Default number of PIT ticks before triggering a context switch.
/// At ~2.25 ms per tick, 3 ticks ≈ 6.75 ms timeslice.
/// Configurable per-CPU via `ContextSwitchPercpu::preempt_interval`.
const DEFAULT_PREEMPT_INTERVAL: usize = 3;
// A simple geometric series where value[i] ~= value[i - 1] * 1.25
const SCHED_PRIO_TO_WEIGHT: [usize; 40] = [
88761, 71755, 56483, 46273, 36291, 29154, 23254, 18705, 14949, 11916, 9548, 7620, 6100, 4904,
@@ -90,13 +95,15 @@ struct SwitchResultInner {
///
/// The function also calls the signal handler after switching contexts.
pub fn tick(token: &mut CleanLockToken) {
let ticks_cell = &PercpuBlock::current().switch_internals.pit_ticks;
let percpu = PercpuBlock::current();
let ticks_cell = &percpu.switch_internals.pit_ticks;
let new_ticks = ticks_cell.get() + 1;
ticks_cell.set(new_ticks);
// Trigger a context switch after every 3 ticks (approx. 6.75 ms).
if new_ticks >= 3 {
// Trigger a context switch when the per-CPU preempt interval is reached.
let interval = percpu.switch_internals.preempt_interval.get();
if new_ticks >= interval {
switch(token);
crate::context::signal::signal_handler(token);
}
@@ -120,7 +127,10 @@ pub unsafe extern "C" fn switch_finish_hook() {
crate::arch::stop::emergency_reset();
}
}
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
PercpuBlock::current()
.switch_internals
.in_context_switch
.set(false);
crate::percpu::switch_arch_hook();
}
}
@@ -150,16 +160,15 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
//set PIT Interrupt counter to 0, giving each process same amount of PIT ticks
percpu.switch_internals.pit_ticks.set(0);
// Acquire the global lock to ensure exclusive access during context switch and avoid
// issues that would be caused by the unsafe operations below
// TODO: Better memory orderings?
while arch::CONTEXT_SWITCH_LOCK
.compare_exchange_weak(false, true, Ordering::SeqCst, Ordering::Relaxed)
.is_err()
{
hint::spin_loop();
percpu.maybe_handle_tlb_shootdown();
}
// Acquire the per-CPU context switch flag. Each CPU can only be in one context
// switch at a time. The per-context write locks provide cross-CPU safety; this
// flag catches re-entrant switches on the same CPU (a kernel bug).
debug_assert!(
!percpu.switch_internals.in_context_switch.get(),
"context switch re-entry on CPU {}",
percpu.cpu_id
);
percpu.switch_internals.in_context_switch.set(true);
// Lock the previous context.
let prev_context_lock = crate::context::current();
@@ -167,8 +176,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
let mut prev_context_guard = unsafe { prev_context_lock.write_arc() };
if !prev_context_guard.is_preemptable() {
// Unset global lock
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
// Unset per-CPU context switch flag
percpu.switch_internals.in_context_switch.set(false);
// Pretend to have finished switching, so CPU is not idled
return SwitchResult::Switched;
@@ -292,8 +301,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
SwitchResult::Switched
}
_ => {
// No target was found, unset global lock and return
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
// No target was found, unset per-CPU context switch flag and return
percpu.switch_internals.in_context_switch.set(false);
percpu.stats.set_state(cpu_stats::CpuState::Idle);
@@ -352,6 +361,7 @@ fn wakeup_contexts(token: &mut CleanLockToken, switch_time: u128) -> Vec<(usize,
}
/// This is the scheduler function which currently utilises Deficit Weighted Round Robin Scheduler
/// with NUMA-aware context selection preference.
fn select_next_context(
token: &mut CleanLockToken,
percpu: &PercpuBlock,
@@ -377,6 +387,10 @@ fn select_next_context(
let total_contexts: usize = contexts_list.iter().map(|q| q.len()).sum();
let mut skipped_contexts = 0;
// NUMA-aware selection: remember cross-node fallback candidate.
let my_numa_node = percpu.numa_node.get();
let mut cross_node_fallback: Option<(usize, ArcContextLockWriteGuard)> = None;
'priority: loop {
i = (i + 1) % 40;
total_iters += 1;
@@ -441,9 +455,44 @@ fn select_next_context(
// Is this context runnable on this CPU?
let sw = unsafe { update_runnable(&mut next_context_guard, cpu_id, switch_time) };
if let UpdateResult::CanSwitch = sw {
next_context_guard_opt = Some(next_context_guard);
balance[i] -= SCHED_PRIO_TO_WEIGHT[20];
break 'priority;
// NUMA-aware selection: check if this context's last CPU was on the same node.
let same_node = if my_numa_node != u8::MAX {
next_context_guard.cpu_id
.map(|cid| {
crate::percpu::get_for_cpu(cid)
.map(|p| p.numa_node.get() == my_numa_node)
.unwrap_or(false)
})
.unwrap_or(true) // New context (no last CPU) — treat as same node
} else {
true // No NUMA info — treat all as same node
};
if same_node {
// Cache-warm: select immediately
percpu.current_prio.set(next_context_guard.prio);
next_context_guard_opt = Some(next_context_guard);
balance[i] -= SCHED_PRIO_TO_WEIGHT[20];
break 'priority;
} else {
// Cross-node candidate: save as fallback, keep scanning for same-node
if cross_node_fallback.is_none() {
// Cache the priority and balance for later
cross_node_fallback =
Some((next_context_guard.prio, next_context_guard));
balance[i] -= SCHED_PRIO_TO_WEIGHT[20];
// Don't break — keep looking for a same-node context
continue;
} else {
// Already have a cross-node fallback; push this one back
contexts.push_back(next_context_ref);
skipped_contexts += 1;
if skipped_contexts >= total_contexts {
break 'priority;
}
continue;
}
}
} else {
if matches!(sw, UpdateResult::Blocked) {
idle_contexts(token.token()).push_back(next_context_ref);
@@ -458,6 +507,15 @@ fn select_next_context(
}
}
}
// If we found a cross-node fallback but no same-node context, use it
if next_context_guard_opt.is_none() {
if let Some((prio, guard)) = cross_node_fallback {
percpu.current_prio.set(prio);
next_context_guard_opt = Some(guard);
}
}
percpu.balance.set(balance);
percpu.last_queue.set(i);
@@ -465,7 +523,10 @@ fn select_next_context(
// Send the old process to the back of the line (if it is still runnable)
let prev_ctx = WeakContextRef(Arc::downgrade(&prev_context_lock));
if prev_context_guard.status.is_runnable() {
let prio = prev_context_guard.prio;
let raw_prio = prev_context_guard.prio;
let prio = percpu.effective_prio(raw_prio);
// Clear PI donation — previous context is being re-queued
percpu.pi_donated_prio.store(u32::MAX, Ordering::Relaxed);
contexts_list[prio].push_back(prev_ctx);
} else {
idle_contexts(token.token()).push_back(prev_ctx);
@@ -477,7 +538,8 @@ fn select_next_context(
return Ok(Some(next_context_guard));
} else {
if !was_idle && !Arc::ptr_eq(&prev_context_lock, &idle_context) {
// We switch into the idle context
// Switching to idle context — cache lowest priority
percpu.current_prio.set(39);
Ok(Some(unsafe { idle_context.write_arc() }))
} else {
// We found no other process to run.
@@ -494,6 +556,13 @@ pub struct ContextSwitchPercpu {
switch_result: Cell<Option<SwitchResultInner>>,
switch_time: Cell<u128>,
pit_ticks: Cell<usize>,
/// Per-CPU context switch flag. Set to true during a context switch on this CPU.
/// Replaced the global CONTEXT_SWITCH_LOCK to eliminate cross-CPU serialization.
in_context_switch: Cell<bool>,
/// Number of PIT ticks before triggering a context switch.
/// Default: 3 (≈6.75 ms). Lower values improve interactive responsiveness;
/// higher values improve throughput for batch/compute workloads.
preempt_interval: Cell<usize>,
current_ctxt: RefCell<Option<Arc<ContextLock>>>,
@@ -508,6 +577,8 @@ impl ContextSwitchPercpu {
switch_result: Cell::new(None),
switch_time: Cell::new(0),
pit_ticks: Cell::new(0),
in_context_switch: Cell::new(false),
preempt_interval: Cell::new(DEFAULT_PREEMPT_INTERVAL),
current_ctxt: RefCell::new(None),
idle_ctxt: RefCell::new(None),
being_sigkilled: Cell::new(false),
+4 -3
View File
@@ -42,17 +42,18 @@ impl core::fmt::Display for LogicalCpuId {
}
#[cfg(target_pointer_width = "64")]
pub const MAX_CPU_COUNT: u32 = 128;
pub const MAX_CPU_COUNT: u32 = 256;
#[cfg(target_pointer_width = "32")]
pub const MAX_CPU_COUNT: u32 = 32;
const SET_WORDS: usize = (MAX_CPU_COUNT / usize::BITS) as usize;
// TODO: Support more than 128 CPUs.
// TODO: Support more than 256 CPUs.
// The maximum number of CPUs on Linux is configurable, and the type for LogicalCpuSet and
// LogicalCpuId may be optimized accordingly. In that case, box the mask if it's larger than some
// base size (probably 256 bytes).
// base size (probably 256 bytes). AMD EPYC has 128C/256T, Threadripper PRO 96C/192T —
// 256 covers current hardware.
#[derive(Debug)]
pub struct LogicalCpuSet([AtomicUsize; SET_WORDS]);
+3
View File
@@ -70,6 +70,9 @@ mod log;
/// Memory management
mod memory;
/// NUMA topology
mod numa;
/// Panic
mod panic;
+41 -22
View File
@@ -1,13 +1,15 @@
/// NUMA topology hints for the kernel scheduler.
/// NUMA discovery (SRAT/SLIT parsing) is performed by a userspace daemon
/// (numad) via /scheme/acpi/, then pushed to the kernel via scheme:numa.
/// The kernel stores a lightweight copy for O(1) scheduling lookups.
///
/// NUMA discovery (SRAT/SLIT parsing) is performed during kernel ACPI init
/// (`acpi::init()`). The kernel stores a lightweight copy for O(1) scheduling
/// lookups. If no SRAT is found, `init_default()` creates a single-node topology.
use crate::acpi::srat;
use crate::cpu_set::{LogicalCpuId, LogicalCpuSet};
use core::sync::atomic::{AtomicBool, Ordering};
const MAX_NUMA_NODES: usize = 8;
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct NumaHint {
pub node_id: u8,
pub cpus: LogicalCpuSet,
@@ -21,17 +23,12 @@ pub struct NumaTopology {
impl NumaTopology {
pub const fn new() -> Self {
const NONE: Option<NumaHint> = None;
Self {
nodes: [NONE; MAX_NUMA_NODES],
initialized: AtomicBool::new(false),
}
Self { nodes: [NONE; MAX_NUMA_NODES], initialized: AtomicBool::new(false) }
}
pub fn node_for_cpu(&self, cpu: LogicalCpuId) -> Option<u8> {
for node in self.nodes.iter().flatten() {
if node.cpus.contains(cpu) {
return Some(node.node_id);
}
if node.cpus.contains(cpu) { return Some(node.node_id); }
}
None
}
@@ -43,20 +40,42 @@ impl NumaTopology {
static mut NUMA_TOPOLOGY: NumaTopology = NumaTopology::new();
pub fn topology() -> &'static NumaTopology {
unsafe { &NUMA_TOPOLOGY }
}
pub fn topology() -> &'static NumaTopology { unsafe { &NUMA_TOPOLOGY } }
pub fn init_default() {
/// Initialize NUMA topology from SRAT data parsed during ACPI init.
pub fn init_from_srat(apic_ids: &[(u32, LogicalCpuId)]) {
let topo = topology();
if topo.initialized.swap(true, Ordering::AcqRel) {
return;
}
if topo.initialized.swap(true, Ordering::AcqRel) { return; }
if !srat::is_available() { init_default_inner(); return; }
unsafe {
let topo_mut = &mut *core::ptr::addr_of_mut!(NUMA_TOPOLOGY);
topo_mut.nodes[0] = Some(NumaHint {
node_id: 0,
cpus: LogicalCpuSet::all(),
});
for &(apic_id, cpu_id) in apic_ids {
if let Some(node) = srat::numa_node_for_apic(apic_id) {
let idx = node as usize;
if idx < MAX_NUMA_NODES {
topo_mut.nodes[idx].get_or_insert_with(|| NumaHint { node_id: node, cpus: LogicalCpuSet::empty() }).cpus.atomic_set(cpu_id);
}
}
}
if topo_mut.nodes.iter().all(|n| n.is_none()) {
topo_mut.nodes[0] = Some(NumaHint { node_id: 0, cpus: LogicalCpuSet::all() });
}
}
let node_count = topology().nodes.iter().filter(|n| n.is_some()).count();
debug!("NUMA: {node_count} node(s) from SRAT");
}
/// Fallback: single-node topology.
pub fn init_default() {
let topo = topology();
if topo.initialized.swap(true, Ordering::AcqRel) { return; }
init_default_inner();
}
fn init_default_inner() {
unsafe {
let topo_mut = &mut *core::ptr::addr_of_mut!(NUMA_TOPOLOGY);
topo_mut.nodes[0] = Some(NumaHint { node_id: 0, cpus: LogicalCpuSet::all() });
}
debug!("NUMA: single-node topology (no SRAT)");
}
+184 -7
View File
@@ -4,9 +4,14 @@ use alloc::{
};
use core::{
cell::{Cell, RefCell},
sync::atomic::{AtomicBool, AtomicPtr, Ordering},
hint,
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering},
};
/// Maximum number of pages to flush individually using INVLPG before falling
/// back to a full TLB flush (CR3 reload).
const TLB_RANGE_THRESHOLD: u32 = 32;
use rmm::Arch;
use syscall::PtraceFlags;
@@ -16,7 +21,7 @@ use crate::{
cpu_set::{LogicalCpuId, MAX_CPU_COUNT},
cpu_stats::{CpuStats, CpuStatsData},
ptrace::Session,
sync::CleanLockToken,
sync::{mcs::McsNode, mcs::McsRawLock, CleanLockToken},
syscall::debug::SyscallDebugInfo,
};
@@ -34,6 +39,38 @@ pub struct PercpuBlock {
pub balance: Cell<[usize; 40]>,
pub last_queue: Cell<usize>,
/// Per-CPU MCS node for the scheduler run-queue lock (RUN_CONTEXTS).
pub mcs_sched_node: McsNode,
/// Counts how many times the scheduler MCS lock acquisition was contended.
pub mcs_contention_count: Cell<u64>,
/// TLB shootdown range: start virtual address (page-aligned).
/// Set to 0 for a full flush. Only valid when `wants_tlb_shootdown` is true.
pub tlb_flush_start: AtomicU64,
/// TLB shootdown range: number of pages to invalidate.
pub tlb_flush_count: AtomicU32,
/// Priority inheritance donation. When another CPU is blocked waiting on a
/// lock this CPU holds, the blocked CPU may donate its priority here.
/// `u32::MAX` means no donation; otherwise it's a priority level (0-39).
pub pi_donated_prio: AtomicU32,
/// Cached priority of the currently-running context on this CPU.
/// Set by the scheduler when selecting a new context. Read by the MCS
/// lock during priority donation — avoids acquiring the context RwLock
/// from the spin loop. Default 39 (lowest priority).
pub current_prio: Cell<usize>,
/// NUMA proximity domain for this CPU. Set during ACPI init from SRAT.
/// `u8::MAX` means unknown (no SRAT or APIC ID not listed).
pub numa_node: Cell<u8>,
/// Pointer to the MCS lock this CPU is currently spinning on (for transitive PI).
/// `null` when not waiting on any lock. Set in McsRawLock::acquire() before
/// entering the spin loop, cleared upon acquisition.
pub waiting_on_lock: AtomicPtr<McsRawLock>,
// TODO: Put mailbox queues here, e.g. for TLB shootdown? Just be sure to 128-byte align it
// first to avoid cache invalidation.
pub profiling: Option<&'static crate::profiling::RingBuffer>,
@@ -57,6 +94,15 @@ pub unsafe fn init_tlb_shootdown(id: LogicalCpuId, block: *mut PercpuBlock) {
ALL_PERCPU_BLOCKS[id.get() as usize].store(block, Ordering::Release)
}
/// Get a reference to another CPU's PercpuBlock by logical CPU ID.
pub fn get_for_cpu(id: LogicalCpuId) -> Option<&'static PercpuBlock> {
unsafe {
ALL_PERCPU_BLOCKS[id.get() as usize]
.load(Ordering::Acquire)
.as_ref()
}
}
pub fn get_all_stats() -> Vec<(LogicalCpuId, CpuStatsData)> {
let mut res = ALL_PERCPU_BLOCKS
.iter()
@@ -101,25 +147,148 @@ pub fn shootdown_tlb_ipi(target: Option<LogicalCpuId>) {
core::hint::spin_loop();
}
}
// Full flush — clear range info (Release ordering ensures the flag
// swap and these stores are visible to the handler before the IPI).
percpublock.tlb_flush_start.store(0, Ordering::Release);
percpublock.tlb_flush_count.store(0, Ordering::Release);
crate::ipi::ipi_single(crate::ipi::IpiKind::Tlb, percpublock);
} else {
// Broadcast TLB shootdown: set flag on all other CPUs, then send a single
// IPI with "all except self" destination shorthand instead of N individual IPIs.
let my_percpublock = PercpuBlock::current();
for id in 0..crate::cpu_count() {
// TODO: Optimize: use global counter and percpu ack counters, send IPI using
// destination shorthand "all CPUs".
shootdown_tlb_ipi(Some(LogicalCpuId::new(id)));
let target_id = LogicalCpuId::new(id);
if target_id == my_percpublock.cpu_id {
continue;
}
let Some(percpublock) = (unsafe {
ALL_PERCPU_BLOCKS[id as usize]
.load(Ordering::Acquire)
.as_ref()
}) else {
continue;
};
// Wait if this CPU still has a pending shootdown from a previous request
#[expect(clippy::bool_comparison)]
while percpublock
.wants_tlb_shootdown
.swap(true, Ordering::Release)
== true
{
while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true {
my_percpublock.maybe_handle_tlb_shootdown();
hint::spin_loop();
}
}
// Full flush — clear range info (Release ordering)
percpublock.tlb_flush_start.store(0, Ordering::Release);
percpublock.tlb_flush_count.store(0, Ordering::Release);
}
// Single broadcast IPI to all other CPUs using destination shorthand
crate::ipi::ipi(crate::ipi::IpiKind::Tlb, crate::ipi::IpiTarget::Other);
}
}
/// Range-based TLB shootdown IPI. Only invalidates the specified virtual address
/// range using INVLPG per page for ranges up to TLB_RANGE_THRESHOLD pages.
/// Falls back to full flush for larger ranges.
pub fn shootdown_tlb_ipi_range(target: Option<LogicalCpuId>, start: usize, count: usize) {
if cfg!(not(feature = "multi_core")) {
return;
}
let start_aligned = start as u64 & !0xFFF;
let count_u32 = count as u32;
let use_range = count_u32 > 0 && count_u32 <= TLB_RANGE_THRESHOLD;
let set_range = |percpublock: &PercpuBlock| {
if use_range {
percpublock.tlb_flush_start.store(start_aligned, Ordering::Release);
percpublock.tlb_flush_count.store(count_u32, Ordering::Release);
} else {
percpublock.tlb_flush_start.store(0, Ordering::Release);
percpublock.tlb_flush_count.store(0, Ordering::Release);
}
};
if let Some(target) = target {
let my_percpublock = PercpuBlock::current();
assert_ne!(target, my_percpublock.cpu_id);
let Some(percpublock) = (unsafe {
ALL_PERCPU_BLOCKS[target.get() as usize]
.load(Ordering::Acquire)
.as_ref()
}) else {
return;
};
#[expect(clippy::bool_comparison)]
while percpublock.wants_tlb_shootdown.swap(true, Ordering::Release) == true {
while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true {
my_percpublock.maybe_handle_tlb_shootdown();
hint::spin_loop();
}
}
set_range(percpublock);
crate::ipi::ipi_single(crate::ipi::IpiKind::Tlb, percpublock);
} else {
let my_percpublock = PercpuBlock::current();
for id in 0..crate::cpu_count() {
let target_id = LogicalCpuId::new(id);
if target_id == my_percpublock.cpu_id {
continue;
}
let Some(percpublock) = (unsafe {
ALL_PERCPU_BLOCKS[id as usize]
.load(Ordering::Acquire)
.as_ref()
}) else {
continue;
};
#[expect(clippy::bool_comparison)]
while percpublock.wants_tlb_shootdown.swap(true, Ordering::Release) == true {
while percpublock.wants_tlb_shootdown.load(Ordering::Relaxed) == true {
my_percpublock.maybe_handle_tlb_shootdown();
hint::spin_loop();
}
}
set_range(percpublock);
}
crate::ipi::ipi(crate::ipi::IpiKind::Tlb, crate::ipi::IpiTarget::Other);
}
}
impl PercpuBlock {
/// Return the effective scheduling priority, accounting for priority inheritance.
/// Lower number = higher priority (0-39 range).
pub fn effective_prio(&self, context_prio: usize) -> usize {
let donated = self.pi_donated_prio.load(Ordering::Relaxed);
if donated < context_prio as u32 {
donated as usize
} else {
context_prio
}
}
pub fn maybe_handle_tlb_shootdown(&self) {
#[expect(clippy::bool_comparison)]
if self.wants_tlb_shootdown.swap(false, Ordering::Relaxed) == false {
return;
}
// TODO: Finer-grained flush
crate::memory::RmmA::invalidate_all();
let start = self.tlb_flush_start.load(Ordering::Acquire);
let count = self.tlb_flush_count.load(Ordering::Acquire);
if start != 0 && count > 0 && count <= TLB_RANGE_THRESHOLD {
// Range-based flush using INVLPG per page — cheaper than full CR3 reload.
for i in 0..count {
let addr = start + (i as u64) * 4096;
crate::memory::RmmA::invalidate(rmm::VirtualAddress::new(addr as usize));
}
} else {
// Full TLB flush (CR3 reload) for large ranges or global shootdowns.
crate::memory::RmmA::invalidate_all();
}
if let Some(addrsp) = &*self.current_addrsp.borrow() {
addrsp.tlb_ack.fetch_add(1, Ordering::Release);
@@ -189,6 +358,14 @@ impl PercpuBlock {
wants_tlb_shootdown: AtomicBool::new(false),
balance: Cell::new([0; 40]),
last_queue: Cell::new(39),
mcs_sched_node: McsNode::new(),
mcs_contention_count: Cell::new(0),
tlb_flush_start: AtomicU64::new(0),
tlb_flush_count: AtomicU32::new(0),
pi_donated_prio: AtomicU32::new(u32::MAX),
current_prio: Cell::new(39),
numa_node: Cell::new(u8::MAX),
waiting_on_lock: AtomicPtr::new(core::ptr::null_mut()),
ptrace_flags: Cell::new(PtraceFlags::empty()),
ptrace_session: RefCell::new(None),
inside_syscall: Cell::new(false),
+27 -4
View File
@@ -18,6 +18,9 @@ use syscall::{
use crate::context::file::InternalFlags;
use super::{CallerCtx, HandleMap, OpenResult, SchemeExt, StrOrBytes};
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
use crate::arch::device::{ioapic, local_apic::ApicId};
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
use crate::arch::interrupt::{available_irqs_iter, irq::acknowledge, is_reserved, set_reserved};
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
@@ -80,7 +83,7 @@ pub fn irq_trigger(irq: u8, token: &mut CleanLockToken) {
#[allow(dead_code)]
enum Handle {
SchemeRoot,
Irq { ack: AtomicUsize, irq: u8 },
Irq { ack: AtomicUsize, irq: u8, cpu_id: LogicalCpuId },
Avail(LogicalCpuId),
TopLevel,
Phandle(u8, Vec<u8>),
@@ -90,7 +93,7 @@ enum Handle {
impl Handle {
fn as_irq_handle(&self) -> Option<(&AtomicUsize, u8)> {
match self {
&Self::Irq { ref ack, irq } => Some((ack, irq)),
&Self::Irq { ref ack, irq, cpu_id: _ } => Some((ack, irq)),
_ => None,
}
}
@@ -144,6 +147,7 @@ impl IrqScheme {
Handle::Irq {
ack: AtomicUsize::new(0),
irq: irq_number,
cpu_id: LogicalCpuId::BSP,
},
InternalFlags::empty(),
)
@@ -162,6 +166,7 @@ impl IrqScheme {
Handle::Irq {
ack: AtomicUsize::new(0),
irq: irq_number,
cpu_id,
},
InternalFlags::empty(),
)
@@ -203,6 +208,7 @@ impl IrqScheme {
Handle::Irq {
ack: AtomicUsize::new(0),
irq: irq_number as u8,
cpu_id: LogicalCpuId::new(0),
},
InternalFlags::empty(),
)
@@ -346,6 +352,7 @@ impl crate::scheme::KernelScheme for IrqScheme {
Handle::Irq {
ack: AtomicUsize::new(0),
irq: plain_irq_number,
cpu_id: LogicalCpuId::BSP,
},
InternalFlags::empty(),
)
@@ -401,6 +408,7 @@ impl crate::scheme::KernelScheme for IrqScheme {
}
}
Handle::Avail(cpu_id) => {
let mut listed = 0;
for vector in available_irqs_iter(cpu_id).skip(opaque) {
let irq = vector_to_irq(vector);
if cpu_id == LogicalCpuId::BSP && irq < BASE_IRQ_COUNT {
@@ -414,7 +422,9 @@ impl crate::scheme::KernelScheme for IrqScheme {
name: &intermediate,
next_opaque_id: u64::from(vector) + 1,
})?;
listed += 1;
}
info!("irq getdents Avail: cpu_id={} opaque={} listed={}", cpu_id.get(), opaque, listed);
}
_ => return Err(Error::new(ENOTDIR)),
}
@@ -449,11 +459,14 @@ impl crate::scheme::KernelScheme for IrqScheme {
let handle = handles_guard.get(id)?;
if let &Handle::Irq {
irq: handle_irq, ..
irq: handle_irq,
cpu_id: handle_cpu_id,
..
} = handle
&& handle_irq > BASE_IRQ_COUNT
{
set_reserved(LogicalCpuId::BSP, irq_to_vector(handle_irq), false);
info!("irq close: unreserving vector {} on cpu_id={}", irq_to_vector(handle_irq), handle_cpu_id.get());
set_reserved(handle_cpu_id, irq_to_vector(handle_irq), false);
}
Ok(())
}
@@ -480,12 +493,21 @@ impl crate::scheme::KernelScheme for IrqScheme {
if !cpus.contains(&(cpu_id as u8)) {
return Err(Error::new(EINVAL));
}
// Reprogram the IOAPIC redirection entry for x86 targets.
// Non-IOAPIC IRQs (e.g. MSI) will return false -> EIO.
#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
{
if !unsafe { ioapic::set_affinity(_handle_irq, ApicId::new(cpu_id)) } {
return Err(Error::new(EIO));
}
}
mask.store(cpu_id as usize, Ordering::Release);
Ok(size_of::<u32>())
}
&Handle::Irq {
irq: handle_irq,
ack: ref handle_ack,
cpu_id: _,
} => {
if buffer.len() < size_of::<usize>() {
return Err(Error::new(EINVAL));
@@ -600,6 +622,7 @@ impl crate::scheme::KernelScheme for IrqScheme {
Handle::Irq {
irq: handle_irq,
ack: ref handle_ack,
cpu_id: _,
} => {
if buffer.len() < size_of::<usize>() {
return Err(Error::new(EINVAL));
@@ -0,0 +1,15 @@
use alloc::vec::Vec;
use crate::{
arch::cpuidle,
sync::CleanLockToken,
syscall::error::{Error, Result, EINVAL},
};
pub fn resource(_token: &mut CleanLockToken) -> Result<Vec<u8>> {
cpuidle::resource()
}
pub fn policy_write(buf: &[u8], _token: &mut CleanLockToken) -> Result<usize> {
cpuidle::policy_write(buf)
}
@@ -45,6 +45,11 @@ enum Handle {
data: Arc<RwLock<L1, Option<Vec<u8>>>>,
},
SchemeRoot,
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Msr {
cpu: usize,
msr: u32,
},
}
#[derive(Clone, Copy)]
@@ -133,6 +138,28 @@ impl KernelScheme for SysScheme {
let id = HANDLES.write(token.token()).insert(Handle::TopLevel);
Ok(OpenResult::SchemeLocal(id, InternalFlags::POSITIONED))
} else if path.starts_with("msr/") {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
if ctx.uid != 0 {
return Err(Error::new(EPERM));
}
let rest = &path[4..];
let mut parts = rest.split('/');
let cpu_str = parts.next().ok_or(Error::new(EINVAL))?;
let msr_str = parts.next().ok_or(Error::new(EINVAL))?;
if parts.next().is_some() {
return Err(Error::new(EINVAL));
}
let cpu: usize = cpu_str.parse().map_err(|_| Error::new(EINVAL))?;
let msr: u32 = u32::from_str_radix(msr_str, 16).map_err(|_| Error::new(EINVAL))?;
let id = HANDLES.write(token.token()).insert(Handle::Msr { cpu, msr });
Ok(OpenResult::SchemeLocal(id, InternalFlags::POSITIONED))
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
{
Err(Error::new(ENOENT))
}
} else {
//Have to iterate to get the path without allocation
let entry = FILES
@@ -160,6 +187,8 @@ impl KernelScheme for SysScheme {
Handle::TopLevel => return Ok(0),
Handle::Resource { kind, data, .. } => (*kind, data.clone()),
Handle::SchemeRoot => return Err(Error::new(EBADF)),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Handle::Msr { .. } => return Ok(0),
}
};
if matches!(kind, Kind::Wr(_)) {
@@ -188,6 +217,16 @@ impl KernelScheme for SysScheme {
Handle::TopLevel => "",
Handle::Resource { path, .. } => path,
Handle::SchemeRoot => return Err(Error::new(EBADF)),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Handle::Msr { cpu, msr } => {
const FIRST: &[u8] = b"sys:msr/";
let mut bytes_read = buf.copy_common_bytes_from_slice(FIRST)?;
let suffix = format!("{}/{:x}", cpu, msr);
if let Some(remaining) = buf.advance(FIRST.len()) {
bytes_read += remaining.copy_common_bytes_from_slice(suffix.as_bytes())?;
}
return Ok(bytes_read);
}
};
const FIRST: &[u8] = b"sys:";
@@ -215,6 +254,15 @@ impl KernelScheme for SysScheme {
let (kind, data_lock) = {
match HANDLES.read(token.token()).get(id)? {
Handle::Resource { kind, data, .. } => (*kind, data.clone()),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Handle::Msr { cpu, msr } => {
if *cpu != crate::cpu_id().get() as usize {
return Err(Error::new(EINVAL));
}
let val = unsafe { x86::msr::rdmsr(*msr) };
let data = format!("{:016x}\n", val).into_bytes();
return buffer.copy_common_bytes_from_slice(&data[pos..]);
}
_ => return Err(Error::new(EBADF)),
}
};
@@ -253,6 +301,18 @@ impl KernelScheme for SysScheme {
let len = buffer.copy_common_bytes_to_slice(&mut intermediate)?;
(*handler, intermediate, len)
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Handle::Msr { cpu, msr } => {
if *cpu != crate::cpu_id().get() as usize {
return Err(Error::new(EINVAL));
}
let mut intermediate = [0_u8; 32];
let len = buffer.copy_common_bytes_to_slice(&mut intermediate)?;
let val_str = core::str::from_utf8(&intermediate[..len]).map_err(|_| Error::new(EINVAL))?;
let val = u64::from_str_radix(val_str.trim(), 16).map_err(|_| Error::new(EINVAL))?;
unsafe { x86::msr::wrmsr(*msr, val); }
return Ok(len);
}
Handle::SchemeRoot => return Err(Error::new(EBADF)),
};
handler(&intermediate[..len], token)
@@ -269,7 +329,8 @@ impl KernelScheme for SysScheme {
return Ok(0);
};
match HANDLES.read(token.token()).get(id)? {
Handle::Resource { .. } => Err(Error::new(ENOTDIR)),
Handle::Resource { .. }
| Handle::Msr { .. } => Err(Error::new(ENOTDIR)),
Handle::TopLevel => {
let mut buf = DirentBuf::new(buf, header_size).ok_or(Error::new(EIO))?;
for (this_idx, (name, _)) in FILES.iter().enumerate().skip(first_index) {
@@ -293,6 +354,18 @@ impl KernelScheme for SysScheme {
Handle::Resource { kind, data, .. } => Some((*kind, data.clone())),
Handle::TopLevel => None,
Handle::SchemeRoot => return Err(Error::new(EBADF)),
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
Handle::Msr { .. } => {
let stat = Stat {
st_mode: 0o600 | MODE_FILE,
st_uid: 0,
st_gid: 0,
st_size: 0,
..Default::default()
};
buf.copy_exactly(&stat)?;
return Ok(());
}
}
};
let stat = if let Some((kind, data_lock)) = stat_base {
+188
View File
@@ -0,0 +1,188 @@
//! MCS (Mellor-Crummey Scott) fair spinlock.
//!
//! Each waiter spins on its own local `locked` flag instead of a shared lock
//! word, eliminating cache-line bouncing under contention. FIFO ordering
//! guarantees fairness. O(1) cache-line transfers on unlock.
//!
//! Supports transitive priority inheritance: when CPU A waits on a lock held
//! by CPU B, and CPU B waits on a lock held by CPU C, A's priority is
//! propagated through the chain to C (up to MAX_PI_CHAIN_DEPTH hops).
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering};
use core::{hint, ptr};
use crate::percpu::PercpuBlock;
/// Maximum depth for transitive priority inheritance chain following.
/// Prevents infinite loops from theoretical lock cycles and bounds latency.
/// Linux uses 20; 8 is conservative for a microkernel with fewer nesting levels.
const MAX_PI_CHAIN_DEPTH: u32 = 8;
/// A node in the MCS lock queue.
pub struct McsNode {
pub next: AtomicPtr<McsNode>,
pub locked: AtomicBool,
}
impl McsNode {
pub const fn new() -> Self {
Self {
next: AtomicPtr::new(ptr::null_mut()),
locked: AtomicBool::new(false),
}
}
}
/// Raw MCS spinlock primitive.
pub struct McsRawLock {
tail: AtomicPtr<McsNode>,
/// CPU ID of the current lock holder (for priority inheritance).
/// `u32::MAX` means no holder.
holder_cpu: AtomicU32,
}
impl McsRawLock {
pub const fn new() -> Self {
Self {
tail: AtomicPtr::new(ptr::null_mut()),
holder_cpu: AtomicU32::new(u32::MAX),
}
}
#[inline]
pub fn acquire(&self, node: &McsNode) -> bool {
node.next.store(ptr::null_mut(), Ordering::Relaxed);
node.locked.store(true, Ordering::Relaxed);
let prev = self.tail.swap((node as *const McsNode).cast_mut(), Ordering::AcqRel);
if prev.is_null() {
// Uncontended — record ourselves as holder
let cpu_id = PercpuBlock::current().cpu_id.get();
self.holder_cpu.store(cpu_id, Ordering::Release);
return false;
}
unsafe {
(*prev).next.store((node as *const McsNode).cast_mut(), Ordering::Release);
}
let percpu = PercpuBlock::current();
// Record which lock we're spinning on (for transitive PI chain following)
percpu.waiting_on_lock.store(
(self as *const McsRawLock).cast_mut(),
Ordering::Release,
);
let mut donated = false;
while node.locked.load(Ordering::Acquire) {
percpu.maybe_handle_tlb_shootdown();
// Donate priority to the lock holder (transitively) once per acquisition
if !donated {
self.maybe_donate_priority(percpu);
donated = true;
}
hint::spin_loop();
}
// Clear waiting_on_lock before proceeding — we now hold the lock
percpu.waiting_on_lock.store(ptr::null_mut(), Ordering::Release);
self.holder_cpu.store(percpu.cpu_id.get(), Ordering::Release);
true
}
#[inline]
pub fn release(&self, node: &McsNode) {
// Clear priority inheritance donation — we no longer hold the lock
PercpuBlock::current().pi_donated_prio.store(u32::MAX, Ordering::Release);
// Clear holder CPU
self.holder_cpu.store(u32::MAX, Ordering::Release);
let next = node.next.load(Ordering::Acquire);
if next.is_null() {
if self
.tail
.compare_exchange(
(node as *const McsNode).cast_mut(),
ptr::null_mut(),
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
return;
}
while node.next.load(Ordering::Acquire).is_null() {
hint::spin_loop();
}
}
unsafe {
(*node.next.load(Ordering::Acquire)).locked.store(false, Ordering::Release);
}
}
#[inline]
pub fn try_acquire(&self, node: &McsNode) -> bool {
node.next.store(ptr::null_mut(), Ordering::Relaxed);
node.locked.store(true, Ordering::Relaxed);
let ok = self
.tail
.compare_exchange(
ptr::null_mut(),
(node as *const McsNode).cast_mut(),
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok();
if ok {
let cpu_id = PercpuBlock::current().cpu_id.get();
self.holder_cpu.store(cpu_id, Ordering::Release);
}
ok
}
/// Donate current CPU's context priority to the lock holder's CPU,
/// following the PI chain transitively (A→B→C).
///
/// Reads priority from PercpuBlock::current_prio (cached by the scheduler)
/// to avoid acquiring any lock in the MCS spin loop.
///
/// Chain following: if the holder is itself waiting on another lock,
/// we propagate our priority to that lock's holder too, up to
/// MAX_PI_CHAIN_DEPTH hops.
fn maybe_donate_priority(&self, my_percpu: &PercpuBlock) {
let my_prio = my_percpu.current_prio.get() as u32;
let mut current_holder_cpu = self.holder_cpu.load(Ordering::Relaxed);
for _ in 0..MAX_PI_CHAIN_DEPTH {
if current_holder_cpu == u32::MAX {
return;
}
let holder_percpu = crate::percpu::get_for_cpu(
crate::cpu_set::LogicalCpuId::new(current_holder_cpu),
);
let Some(holder) = holder_percpu else {
return;
};
// Donate if our priority is higher (lower number) than current donation
let current_donated = holder.pi_donated_prio.load(Ordering::Relaxed);
if my_prio < current_donated {
holder.pi_donated_prio.store(my_prio, Ordering::Release);
}
// Follow the chain: is this holder also waiting on another lock?
let next_lock_ptr = holder.waiting_on_lock.load(Ordering::Relaxed);
if next_lock_ptr.is_null() {
return;
}
// SAFETY: The pointed-to McsRawLock is a long-lived struct field
// (e.g., part of the run queue). The holder is currently spinning
// in acquire(), so the pointer is valid. We only read holder_cpu
// (an atomic u32) — no mutable access needed.
let next_holder_cpu =
unsafe { (*next_lock_ptr).holder_cpu.load(Ordering::Relaxed) };
// Cycle detection: if the next holder is the same CPU we just visited, stop
if next_holder_cpu == current_holder_cpu {
return;
}
current_holder_cpu = next_holder_cpu;
}
// Chain depth exhausted — stop to bound latency
}
}
@@ -1,5 +1,6 @@
pub use self::{ordered::*, wait_condition::WaitCondition, wait_queue::WaitQueue};
pub mod mcs;
pub mod ordered;
pub mod wait_condition;
pub mod wait_queue;
@@ -52,7 +52,9 @@
//! *g1 = 12;
//! ```
use alloc::sync::Arc;
use core::cell::UnsafeCell;
use core::marker::PhantomData;
use core::ptr;
use crate::percpu::PercpuBlock;
@@ -732,3 +734,143 @@ impl<L: Level, T> Drop for ArcRwLockWriteGuard<L, T> {
/// This function can only be called if no lock is held by the calling thread/task
#[inline]
pub fn check_no_locks(_: LockToken<'_, L0>) {}
// ---------------------------------------------------------------------------
// MCS-based fair mutex (McsMutex)
// ---------------------------------------------------------------------------
/// A mutual exclusion lock using the MCS fair spinlock algorithm.
///
/// Unlike `Mutex<L, T>` which uses a simple spinlock (no fairness under
/// contention), `McsMutex` uses Mellor-Crummey Scott queue-based spinning:
///
/// - Each waiter spins on its **own** local flag — no shared cache-line bouncing.
/// - FIFO ordering prevents starvation.
/// - O(1) cache-line transfers on unlock.
///
/// The MCS node is stored in [`crate::percpu::PercpuBlock::mcs_sched_node`], so
/// this type is suitable for scheduler-internal locks where the holder is always
/// the current CPU.
pub struct McsMutex<L: Level, T> {
raw: crate::sync::mcs::McsRawLock,
data: UnsafeCell<T>,
_phantom: PhantomData<L>,
}
unsafe impl<L: Level, T: Send> Sync for McsMutex<L, T> {}
unsafe impl<L: Level, T: Send> Send for McsMutex<L, T> {}
impl<L: Level, T> McsMutex<L, T> {
pub const fn new(val: T) -> Self {
Self {
raw: crate::sync::mcs::McsRawLock::new(),
data: UnsafeCell::new(val),
_phantom: PhantomData,
}
}
}
impl<L: Level, T> McsMutex<L, T> {
pub fn lock<'a, LP: Lower<L> + 'a>(
&'a self,
lock_token: LockToken<'a, LP>,
) -> McsMutexGuard<'a, L, T> {
let percpu = PercpuBlock::current();
let contended = self.raw.acquire(&percpu.mcs_sched_node);
if contended {
percpu
.mcs_contention_count
.set(percpu.mcs_contention_count.get() + 1);
}
McsMutexGuard {
lock: self,
lock_token: LockToken::downgraded(lock_token),
}
}
pub fn try_lock<'a, LP: Lower<L> + 'a>(
&'a self,
lock_token: LockToken<'a, LP>,
) -> Option<McsMutexGuard<'a, L, T>> {
let percpu = PercpuBlock::current();
if self.raw.try_acquire(&percpu.mcs_sched_node) {
Some(McsMutexGuard {
lock: self,
lock_token: LockToken::downgraded(lock_token),
})
} else {
None
}
}
}
pub struct McsMutexGuard<'a, L: Level, T: 'a> {
lock: &'a McsMutex<L, T>,
lock_token: LockToken<'a, L>,
}
impl<'a, L: Level, T: 'a> McsMutexGuard<'a, L, T> {
pub fn token_split(&mut self) -> (&mut T, LockToken<'_, L>) {
unsafe { (&mut *self.lock.data.get(), self.lock_token.token()) }
}
pub fn into_split(self) -> (McsRawGuard<'a, L, T>, LockToken<'a, L>) {
let lock_ref = self.lock;
let token = unsafe { core::ptr::read(&self.lock_token) };
core::mem::forget(self);
(McsRawGuard { lock: lock_ref }, token)
}
pub fn from_split(raw: McsRawGuard<'a, L, T>, token: LockToken<'a, L>) -> Self {
let lock_ref = raw.lock;
core::mem::forget(raw);
Self {
lock: lock_ref,
lock_token: token,
}
}
}
impl<L: Level, T> core::ops::Deref for McsMutexGuard<'_, L, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.lock.data.get() }
}
}
impl<L: Level, T> core::ops::DerefMut for McsMutexGuard<'_, L, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { &mut *self.lock.data.get() }
}
}
impl<L: Level, T> Drop for McsMutexGuard<'_, L, T> {
fn drop(&mut self) {
let percpu = PercpuBlock::current();
self.lock.raw.release(&percpu.mcs_sched_node);
}
}
pub struct McsRawGuard<'a, L: Level, T: 'a> {
lock: &'a McsMutex<L, T>,
}
impl<L: Level, T> core::ops::Deref for McsRawGuard<'_, L, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.lock.data.get() }
}
}
impl<L: Level, T> core::ops::DerefMut for McsRawGuard<'_, L, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { &mut *self.lock.data.get() }
}
}
impl<L: Level, T> Drop for McsRawGuard<'_, L, T> {
fn drop(&mut self) {
let percpu = PercpuBlock::current();
self.lock.raw.release(&percpu.mcs_sched_node);
}
}
@@ -28,6 +28,11 @@ use crate::{
sync::CleanLockToken,
};
/// Local syscall numbers not yet in the redox_syscall crate.
/// These are allocated from the 987+ range to avoid collisions with crate numbers.
pub const SYS_SCHED_SETAFFINITY: usize = 987;
pub const SYS_SCHED_GETAFFINITY: usize = 988;
/// Debug
pub mod debug;
@@ -220,6 +225,10 @@ pub fn syscall(
unlinkat(fd, UserSlice::ro(c, d)?, e, f as _, g as _, token).map(|()| 0)
}
SYS_YIELD => sched_yield(token).map(|()| 0),
// P17-3: CPU affinity syscalls. Numbers allocated locally (not yet in redox_syscall crate).
SYS_SCHED_SETAFFINITY => sched_setaffinity(b, UserSlice::ro(c, d)?, token),
SYS_SCHED_GETAFFINITY => sched_getaffinity(b, UserSlice::wo(c, d)?, token),
SYS_NANOSLEEP => nanosleep(
UserSlice::ro(b, size_of::<TimeSpec>())?,
UserSlice::wo(c, size_of::<TimeSpec>())?.none_if_null(),
@@ -11,6 +11,7 @@ use crate::{
memory::{AddrSpace, Grant, PageSpan},
ContextRef,
},
cpu_set::RawMask,
event,
sync::{CleanLockToken, RwLock},
syscall::flag::{EventFlags, O_CREAT, O_RDWR},
@@ -295,3 +296,71 @@ fn insert_fd(scheme: SchemeId, number: usize, cloexec: bool, token: &mut CleanLo
.expect("failed to insert fd to current context")
.get()
}
/// Set CPU affinity mask for a process.
///
/// # Arguments (syscall ABI)
/// - `pid`: Process ID (0 = current process; other PIDs not yet supported)
/// - `mask_ptr`: Pointer to a `RawMask` (32 bytes on 64-bit, 256-bit bitmap)
/// - `mask_len`: Length of mask in bytes (must equal `size_of::<RawMask>()`)
pub fn sched_setaffinity(
pid: usize,
mask_ptr: super::usercopy::UserSliceRo,
token: &mut CleanLockToken,
) -> Result<usize> {
// Validate mask size
if mask_ptr.len() != core::mem::size_of::<RawMask>() {
return Err(Error::new(super::error::EINVAL));
}
// pid == 0 means current process
let target = if pid == 0 {
context::current()
} else {
// TODO: Support PID-based lookup (requires context list iteration
// with lock token downgrades). For now, only pid=0 is supported.
return Err(Error::new(super::error::ESRCH));
};
// Read mask from userspace
let raw_mask: RawMask = unsafe { mask_ptr.read_exact() }?;
// Apply to context's affinity mask
let mut ctx = target.write(token.token());
ctx.sched_affinity.override_from(&raw_mask);
Ok(0)
}
/// Get CPU affinity mask for a process.
///
/// # Arguments (syscall ABI)
/// - `pid`: Process ID (0 = current process; other PIDs not yet supported)
/// - `mask_ptr`: Pointer to a `RawMask` buffer (32 bytes on 64-bit)
/// - `mask_len`: Length of buffer in bytes (must equal `size_of::<RawMask>()`)
///
/// # Returns
/// Number of bytes written to mask_ptr on success.
pub fn sched_getaffinity(
pid: usize,
mask_ptr: super::usercopy::UserSliceWo,
token: &mut CleanLockToken,
) -> Result<usize> {
// Validate mask size
if mask_ptr.len() != core::mem::size_of::<RawMask>() {
return Err(Error::new(super::error::EINVAL));
}
// pid == 0 means current process
let target = if pid == 0 {
context::current()
} else {
return Err(Error::new(super::error::ESRCH));
};
let ctx = target.read(token.token());
let raw_mask = ctx.sched_affinity.to_raw();
mask_ptr.copy_common_bytes_from_slice(crate::cpu_set::mask_as_bytes(&raw_mask))?;
Ok(core::mem::size_of::<RawMask>())
}