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:
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@ mod log;
|
||||
/// Memory management
|
||||
mod memory;
|
||||
|
||||
/// NUMA topology
|
||||
mod numa;
|
||||
|
||||
/// Panic
|
||||
mod panic;
|
||||
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user