diff --git a/.validation/c3-e1/README.md b/.validation/c3-e1/README.md new file mode 100644 index 0000000..a1112b4 --- /dev/null +++ b/.validation/c3-e1/README.md @@ -0,0 +1,38 @@ +# C3 E1 validation + +Source: counters `e66288b8d641226267ee94d0d4e06648c68efb59` on `2e09191` (main after #99), plus `5ec5792`, which raises the hosted coverage pin and changes no kernel code. + +Detailed commands, environment identities and artifact hashes are archived in the private documentation repository: + +[Immutable receipt archive](https://github.com/Zero-kernel/Nilix-docs-private/tree/4428f515bf16b212d8d0a4d09cab07fbf8f78731/validation/c3-e1) · [Archive PR](https://github.com/Zero-kernel/Nilix-docs-private/pull/12) + +## Recorded results and limits + +- **Local.** + - fmt, clippy, lint and the build pass. + - The hosted sub-crate suites pass, 641 tests including the new `PT_LOCK` test. Removing the `contended` increment or the hold-time accumulation fails that test. + - All Python harness tests pass except one `iommu_device_probe_test` import error under plain unittest, which is unrelated; CI runs pytest. +- **Dev host, QEMU 8.2.** + - The runtime group is GATE-QUALIFIED, and runtime at 4 vCPUs is QUALIFIED (3) with passed=46. + - The strict group still FAILs on the remaining deferrals, with no undeclared scope. + - `schedstat_event_counters` reports `not_applicable scope=needs_cpus:2` at 1 vCPU and passes at 4 and 16. + - With the IPI handlers' counting removed, it fails (`handled 0->0`), as a negative control should. +- **ring3-mm.** + - It passes at 1, 4 and 8 vCPUs, including `syscall_test`'s 2 KiB `/proc/schedstat` read. + - At 16 vCPUs it hit its 900-second deadline while running beside a 16-vCPU stress guest. Run beside its parent `2e09191`, E1 completed in 820.6 s while the parent hit the deadline; at 8 vCPUs it took 210.7 s against 259.4 s. No slowdown from E1 is visible. + - Every source tried is marginal against 900 s on this host when a second 16-vCPU guest runs. +- **Format.** + - A 300-second `thread` run at 16 vCPUs is QUALIFIED (3) and shows the four new `/proc/schedstat` lines: 296 of the file's 876 bytes. + - That sample is the stress runner's round-1 copy. It is a format check, not a C3 measurement. + +None of these results is a strict pass. E2–E4 (#90) do the measurements. Ordinary builds and public CI do not require the private archive. + +The local-checks, gates and paired-run receipts have aggregate status 1, from the unrelated unittest import error, the remaining strict deferrals, and the 16-vCPU deadlines respectively. Individual statuses and log hashes are preserved. + +## Interpreting IPI samples + +`ipis_sent` and `ipis_handled` are independent cumulative event counts for each CPU, not a coherent snapshot. A target can handle an IPI before the sender records its ICR write, and events can occur between procfs reads. A sample with `sent=10` and `handled=11` is therefore valid. LAPIC coalescing also prevents the difference from measuring pending or dropped IPIs, even after send accounting catches up. + +E2–E4 analysis should compare each CPU's send and handler counts over an interval separately, using unsigned 64-bit wrapping deltas for samples from the same boot and resetting the baseline after a reboot. Neither a single sample's difference nor the difference of the two interval deltas is an exact backlog or loss metric. Readers must accept `handled > sent` without unsigned underflow, a fabricated backlog, or a delivery-failure verdict. + +Existing readers retain the raw counters and do not subtract them. This clarification changes only documentation and Rust doc comments; the recorded runtime results above remain bound to their original sources. diff --git a/kernel/arch/interrupts.rs b/kernel/arch/interrupts.rs index fcbb234..815906c 100644 --- a/kernel/arch/interrupts.rs +++ b/kernel/arch/interrupts.rs @@ -2446,7 +2446,10 @@ extern "x86-interrupt" fn reschedule_ipi_handler(_stack_frame: InterruptStackFra clac_if_smap(); // R69-3 FIX: Mark entering IRQ context - current_cpu().irq_enter(); + let this_cpu = current_cpu(); + this_cpu.irq_enter(); + // C3 E1 (#90): per-CPU handled count for /proc/schedstat. + kernel_core::process::note_ipi_handled(this_cpu.cpu_id.load(Ordering::Relaxed)); // R155-11 FIX: Save FPU state before any code that might use SSE. // Compiler auto-vectorization can emit SSE instructions in handler code, @@ -2494,7 +2497,10 @@ extern "x86-interrupt" fn tlb_shootdown_ipi_handler(_stack_frame: InterruptStack clac_if_smap(); // R69-3 FIX: Mark entering IRQ context - current_cpu().irq_enter(); + let this_cpu = current_cpu(); + this_cpu.irq_enter(); + // C3 E1 (#90): per-CPU handled count for /proc/schedstat. + kernel_core::process::note_ipi_handled(this_cpu.cpu_id.load(Ordering::Relaxed)); // R155-11 FIX: Save FPU state before any code that might use SSE. // Compiler auto-vectorization can emit SSE instructions in handler code, diff --git a/kernel/arch/ipi.rs b/kernel/arch/ipi.rs index 62dd16c..5d4ad7b 100644 --- a/kernel/arch/ipi.rs +++ b/kernel/arch/ipi.rs @@ -239,6 +239,10 @@ pub fn send_ipi(target_cpu: usize, ipi_type: IpiType) { apic::send_ipi_raw(dest_lapic, ipi_type.vector(), apic::icr_delivery::FIXED); } bump_stat(ipi_type, 1); + // C3 E1 (#90): per-target counts for /proc/schedstat. + if matches!(ipi_type, IpiType::Reschedule | IpiType::TlbShootdown) { + kernel_core::process::note_ipi_sent(target_cpu); + } } } diff --git a/kernel/kernel_core/process.rs b/kernel/kernel_core/process.rs index 6d21a10..0b430ec 100644 --- a/kernel/kernel_core/process.rs +++ b/kernel/kernel_core/process.rs @@ -6808,6 +6808,48 @@ pub fn note_task_migration(cpu: usize) { } } +/// C3 E1 (#90): reschedule and TLB-shootdown IPIs sent to each CPU, and the +/// handler runs on it. Counting by target needs no LAPIC read on the sending +/// side. These cumulative counts are sampled independently: handled may +/// temporarily exceed sent because send accounting follows the ICR write or +/// events occur between reads. The LAPIC can also coalesce sends, so their +/// difference is not a pending- or dropped-IPI count. +/// Statistics only: Relaxed, wrapping modulo 2^64. +static CPU_IPIS_SENT: [AtomicU64; cpu_local::max_cpus()] = + [const { AtomicU64::new(0) }; cpu_local::max_cpus()]; +static CPU_IPIS_HANDLED: [AtomicU64; cpu_local::max_cpus()] = + [const { AtomicU64::new(0) }; cpu_local::max_cpus()]; + +/// Record one reschedule or TLB-shootdown IPI sent to `cpu` (IRQ-safe, lock-free). +pub fn note_ipi_sent(cpu: usize) { + if let Some(counter) = CPU_IPIS_SENT.get(cpu) { + // lint-fetch-add: allow (statistics counter) + counter.fetch_add(1, Ordering::Relaxed); + } +} + +/// Record one reschedule or TLB-shootdown IPI handler run on `cpu`. +pub fn note_ipi_handled(cpu: usize) { + if let Some(counter) = CPU_IPIS_HANDLED.get(cpu) { + // lint-fetch-add: allow (statistics counter) + counter.fetch_add(1, Ordering::Relaxed); + } +} + +/// `(IPIs sent to, IPIs handled on)` `cpu` since boot. +/// +/// The two loads are not a coherent snapshot. For samples from the same boot, +/// compare each counter's wrapping interval delta separately; subtracting the +/// two fields does not measure outstanding or lost IPIs. +pub fn cpu_ipi_counters(cpu: usize) -> (u64, u64) { + let load = |counters: &[AtomicU64]| { + counters + .get(cpu) + .map_or(0, |counter| counter.load(Ordering::Relaxed)) + }; + (load(&CPU_IPIS_SENT), load(&CPU_IPIS_HANDLED)) +} + /// The CPUs currently online, as a bit mask (for `/proc/schedstat`). pub fn sched_online_cpu_mask() -> u64 { cpu_local::online_cpu_mask() diff --git a/kernel/mm/page_table.rs b/kernel/mm/page_table.rs index eacdd9a..dfd7510 100644 --- a/kernel/mm/page_table.rs +++ b/kernel/mm/page_table.rs @@ -51,28 +51,101 @@ static PT_LOCK: Mutex<()> = Mutex::new(()); const NO_PT_LOCK_OWNER: usize = usize::MAX; static PT_LOCK_OWNER_CPU: AtomicUsize = AtomicUsize::new(NO_PT_LOCK_OWNER); +/// C3 E1 (#90): `PT_LOCK` statistics for `/proc/schedstat`. Observability +/// only: Relaxed, and a wrap is harmless. Wait time is measured only for a +/// contended `lock_pt`; hold time runs from acquisition to guard drop. Both +/// are raw TSC deltas, since the guest tick clock lags host time under TCG. +static PT_LOCK_ACQUIRED: AtomicU64 = AtomicU64::new(0); +static PT_LOCK_CONTENDED: AtomicU64 = AtomicU64::new(0); +static PT_LOCK_BUSY: AtomicU64 = AtomicU64::new(0); +static PT_LOCK_WAIT_TSC: AtomicU64 = AtomicU64::new(0); +static PT_LOCK_WAIT_TSC_MAX: AtomicU64 = AtomicU64::new(0); +static PT_LOCK_HOLD_TSC: AtomicU64 = AtomicU64::new(0); +static PT_LOCK_HOLD_TSC_MAX: AtomicU64 = AtomicU64::new(0); +/// TSC at the current holder's acquisition, written and read only by it. +static PT_LOCK_SINCE_TSC: AtomicU64 = AtomicU64::new(0); + +/// Snapshot of the `PT_LOCK` statistics (see [`pt_lock_stats`]). +#[derive(Debug, Clone, Copy)] +pub struct PtLockStats { + /// Acquisitions by `lock_pt` and successful `try_lock_pt`. + pub acquired: u64, + /// `lock_pt` calls that found the lock held (counted before they spin). + pub contended: u64, + /// `try_lock_pt` calls that found the lock held (the #PF Busy path). + pub busy: u64, + /// Sum and maximum of contended `lock_pt` spin time, in TSC cycles. + pub wait_tsc: u64, + pub wait_tsc_max: u64, + /// Sum and maximum of hold time, in TSC cycles. + pub hold_tsc: u64, + pub hold_tsc_max: u64, +} + +/// Current `PT_LOCK` statistics; each field is read independently. +pub fn pt_lock_stats() -> PtLockStats { + PtLockStats { + acquired: PT_LOCK_ACQUIRED.load(Ordering::Relaxed), + contended: PT_LOCK_CONTENDED.load(Ordering::Relaxed), + busy: PT_LOCK_BUSY.load(Ordering::Relaxed), + wait_tsc: PT_LOCK_WAIT_TSC.load(Ordering::Relaxed), + wait_tsc_max: PT_LOCK_WAIT_TSC_MAX.load(Ordering::Relaxed), + hold_tsc: PT_LOCK_HOLD_TSC.load(Ordering::Relaxed), + hold_tsc_max: PT_LOCK_HOLD_TSC_MAX.load(Ordering::Relaxed), + } +} + +#[inline] +fn read_tsc() -> u64 { + // SAFETY: RDTSC only reads the time-stamp counter. + unsafe { core::arch::x86_64::_rdtsc() } +} + struct PtLockGuard { _guard: spin::MutexGuard<'static, ()>, } +impl PtLockGuard { + #[inline] + fn new(guard: spin::MutexGuard<'static, ()>) -> Self { + PT_LOCK_OWNER_CPU.store(cpu_local::current_cpu_id(), Ordering::Release); + PT_LOCK_ACQUIRED.fetch_add(1, Ordering::Relaxed); + PT_LOCK_SINCE_TSC.store(read_tsc(), Ordering::Relaxed); + Self { _guard: guard } + } +} + impl Drop for PtLockGuard { fn drop(&mut self) { + // Still the holder: `_guard` is released after this body. + let held = read_tsc().saturating_sub(PT_LOCK_SINCE_TSC.load(Ordering::Relaxed)); + PT_LOCK_HOLD_TSC.fetch_add(held, Ordering::Relaxed); + PT_LOCK_HOLD_TSC_MAX.fetch_max(held, Ordering::Relaxed); PT_LOCK_OWNER_CPU.store(NO_PT_LOCK_OWNER, Ordering::Release); } } #[inline] fn lock_pt() -> PtLockGuard { + if let Some(guard) = PT_LOCK.try_lock() { + return PtLockGuard::new(guard); + } + PT_LOCK_CONTENDED.fetch_add(1, Ordering::Relaxed); + let start = read_tsc(); let guard = PT_LOCK.lock(); - PT_LOCK_OWNER_CPU.store(cpu_local::current_cpu_id(), Ordering::Release); - PtLockGuard { _guard: guard } + let waited = read_tsc().saturating_sub(start); + PT_LOCK_WAIT_TSC.fetch_add(waited, Ordering::Relaxed); + PT_LOCK_WAIT_TSC_MAX.fetch_max(waited, Ordering::Relaxed); + PtLockGuard::new(guard) } #[inline] fn try_lock_pt() -> Option { - let guard = PT_LOCK.try_lock()?; - PT_LOCK_OWNER_CPU.store(cpu_local::current_cpu_id(), Ordering::Release); - Some(PtLockGuard { _guard: guard }) + let Some(guard) = PT_LOCK.try_lock() else { + PT_LOCK_BUSY.fetch_add(1, Ordering::Relaxed); + return None; + }; + Some(PtLockGuard::new(guard)) } /// CPU that currently owns the global page-table lock, if any. @@ -2122,6 +2195,45 @@ unsafe impl FrameAllocator for MmioPtRecorder<'_> { } } +/// Serializes the hosted tests that take `PT_LOCK`: the other asserts the lock +/// is free at its start and end. +#[cfg(test)] +static PT_LOCK_TEST_SERIAL: spin::Mutex<()> = spin::Mutex::new(()); + +#[cfg(test)] +mod pt_lock_stats_tests { + use super::*; + use std::{ + thread, + time::{Duration, Instant}, + }; + + /// C3 E1 (#90): a held `PT_LOCK` makes `try_lock_pt` busy and `lock_pt` + /// contended, and the wait and both holds are timed. + #[test] + fn busy_contention_wait_and_hold_are_counted() { + let _serial = PT_LOCK_TEST_SERIAL.lock(); + let before = pt_lock_stats(); + let holder = lock_pt(); + assert!(thread::spawn(|| try_lock_pt().is_none()).join().unwrap()); + let waiter = thread::spawn(|| drop(lock_pt())); + let deadline = Instant::now() + Duration::from_secs(5); + while pt_lock_stats().contended == before.contended && Instant::now() < deadline { + thread::yield_now(); + } + thread::sleep(Duration::from_millis(5)); + drop(holder); + waiter.join().unwrap(); + + let after = pt_lock_stats(); + assert!(after.acquired >= before.acquired + 2); + assert!(after.contended > before.contended); + assert!(after.busy > before.busy); + assert!(after.wait_tsc > before.wait_tsc && after.wait_tsc_max > 0); + assert!(after.hold_tsc > before.hold_tsc && after.hold_tsc_max > 0); + } +} + #[cfg(test)] mod cow_replace_tests { use super::*; @@ -2269,11 +2381,12 @@ mod kernel_stack_directory_tests { #[cfg(test)] mod pt_lock_tests { - use super::{pt_lock_owner_cpu, try_with_current_manager, with_pt_lock}; + use super::{pt_lock_owner_cpu, try_with_current_manager, with_pt_lock, PT_LOCK_TEST_SERIAL}; use x86_64::VirtAddr; #[test] fn try_manager_rejects_same_cpu_reentry() { + let _serial = PT_LOCK_TEST_SERIAL.lock(); assert_eq!(pt_lock_owner_cpu(), None); with_pt_lock(|| { let owner = pt_lock_owner_cpu(); diff --git a/kernel/mm/tlb_shootdown.rs b/kernel/mm/tlb_shootdown.rs index 1ee8376..1dac2d8 100644 --- a/kernel/mm/tlb_shootdown.rs +++ b/kernel/mm/tlb_shootdown.rs @@ -54,6 +54,13 @@ pub struct TlbShootdownStats { pub pages_flushed: u64, /// R106-5: Number of times mailbox saturation forced a full-flush fallback pub coalesced_fallbacks: u64, + /// C3 E1 (#90): target scans that found a CPU running the modified CR3 + pub targeted_shootdowns: u64, + /// C3 E1: target scans that fell back to every other online CPU (also + /// counted when there is none) + pub broadcast_fallbacks: u64, + /// C3 E1: shootdown IPIs sent, including ACK-timeout resends + pub shootdown_ipis: u64, } // Atomic statistics for SMP-safe updates @@ -99,6 +106,9 @@ static CPU_ACTIVE_CR3: [AtomicU64; cpu_local::max_cpus()] = /// Statistics for optimized TLB shootdowns static STATS_TARGETED_SHOOTDOWNS: AtomicU64 = AtomicU64::new(0); static STATS_BROADCAST_FALLBACK: AtomicU64 = AtomicU64::new(0); +/// C3 E1 (#90): shootdown IPIs sent; divided by the two counters above, the +/// mean fanout of a remote dispatch. +static STATS_SHOOTDOWN_IPIS: AtomicU64 = AtomicU64::new(0); /// Registered function used to send the TLB shootdown IPI /// Stored as usize to avoid function pointer in static @@ -723,6 +733,7 @@ fn post_requests(targets: CpuMask, cr3: u64, start: u64, len: u64, generation: u fn send_ipis(targets: CpuMask, sender: TlbIpiSender) { for cpu in targets.iter() { sender(cpu); + STATS_SHOOTDOWN_IPIS.fetch_add(1, Ordering::Relaxed); } } @@ -813,9 +824,7 @@ fn wait_for_acks_with_retry(targets: CpuMask, generation: u64, sender: TlbIpiSen if attempt < ACK_TIMEOUT_RETRIES { // Resend IPIs to unacked CPUs - for cpu in unacked.iter() { - sender(cpu); - } + send_ipis(unacked, sender); } else { // Final timeout - this is a critical error kprintln!( @@ -1258,6 +1267,9 @@ pub fn get_stats() -> TlbShootdownStats { range_flushes: STATS_RANGE_FLUSHES.load(Ordering::Relaxed), pages_flushed: STATS_PAGES_FLUSHED.load(Ordering::Relaxed), coalesced_fallbacks: STATS_COALESCED_FALLBACKS.load(Ordering::Relaxed), + targeted_shootdowns: STATS_TARGETED_SHOOTDOWNS.load(Ordering::Relaxed), + broadcast_fallbacks: STATS_BROADCAST_FALLBACK.load(Ordering::Relaxed), + shootdown_ipis: STATS_SHOOTDOWN_IPIS.load(Ordering::Relaxed), } } diff --git a/kernel/src/runtime_tests.rs b/kernel/src/runtime_tests.rs index d40e6d6..133bb44 100644 --- a/kernel/src/runtime_tests.rs +++ b/kernel/src/runtime_tests.rs @@ -1808,6 +1808,91 @@ impl RuntimeTest for IpiPingPongTest { } } +/// C3 E1 (#90): the `/proc/schedstat` IPI, shootdown and `PT_LOCK` counters +/// advance when their events happen. +struct SchedstatEventCountersTest; + +impl RuntimeTest for SchedstatEventCountersTest { + fn name(&self) -> &'static str { + "schedstat_event_counters" + } + + // An IPI check, not applicable below two online CPUs. + fn scope(&self) -> TestScope { + TestScope::NeedsCpus(2) + } + + fn description(&self) -> &'static str { + "IPI, TLB shootdown and PT_LOCK counters advance on their events" + } + + fn run(&self) -> TestResult { + use arch::ipi::{send_ipi, IpiType}; + use arch::{current_cpu_id, max_cpus, num_online_cpus}; + use kernel_core::process::cpu_ipi_counters; + use mm::page_table::{pt_lock_stats, with_pt_lock}; + use mm::tlb_shootdown::{flush_all_address_spaces, get_stats, is_cpu_online}; + + let online = num_online_cpus(); + let self_cpu = current_cpu_id(); + let Some(target) = (0..max_cpus()).find(|&id| id != self_cpu && is_cpu_online(id)) else { + return TestResult::Fail(String::from("No online AP found")); + }; + + let (sent_before, handled_before) = cpu_ipi_counters(target); + send_ipi(target, IpiType::Reschedule); + let mut handled = handled_before; + for _ in 0..5_000_000usize { + handled = cpu_ipi_counters(target).1; + if handled > handled_before { + break; + } + spin_loop(); + } + let sent = cpu_ipi_counters(target).0; + if sent <= sent_before || handled <= handled_before { + return TestResult::Fail(alloc::format!( + "CPU {} IPI counters did not advance: sent {}->{}, handled {}->{}", + target, + sent_before, + sent, + handled_before, + handled + )); + } + + // A CR3-0 request always falls back to every other online CPU and + // waits for each to acknowledge. + let tlb_before = get_stats(); + flush_all_address_spaces(); + let tlb = get_stats(); + let ipis = tlb.shootdown_ipis - tlb_before.shootdown_ipis; + if tlb.broadcast_fallbacks <= tlb_before.broadcast_fallbacks || ipis < (online - 1) as u64 { + return TestResult::Fail(alloc::format!( + "broadcast shootdown not counted: fallbacks {}->{}, {} IPIs for {} online CPUs", + tlb_before.broadcast_fallbacks, + tlb.broadcast_fallbacks, + ipis, + online + )); + } + + let pt_before = pt_lock_stats(); + with_pt_lock(spin_loop); + let pt = pt_lock_stats(); + if pt.acquired <= pt_before.acquired || pt.hold_tsc <= pt_before.hold_tsc { + return TestResult::Fail(alloc::format!( + "PT_LOCK use not counted: acquired {}->{}, hold_tsc {}->{}", + pt_before.acquired, + pt.acquired, + pt_before.hold_tsc, + pt.hold_tsc + )); + } + TestResult::Pass + } +} + /// Ensure TLB shootdown reaches remote CPUs and is acknowledged struct TlbShootdownCoherencyTest; @@ -2370,6 +2455,7 @@ fn runtime_test_registry() -> Vec<&'static dyn RuntimeTest> { &NetworkLoopbackTest, &SmpOnlineTest, &IpiPingPongTest, + &SchedstatEventCountersTest, &TlbShootdownCoherencyTest, &CpusetIsolationTest, &SchedulerAffinityTest, diff --git a/kernel/vfs/procfs.rs b/kernel/vfs/procfs.rs index 195f069..cc9ac4f 100644 --- a/kernel/vfs/procfs.rs +++ b/kernel/vfs/procfs.rs @@ -2081,6 +2081,21 @@ impl Inode for ProcSchedstatInode { /// One line per online CPU (`cpuN switches=S migrations_in=M`), then the RCU /// callback backlog, the W-3 wait over-wake count and the #92 idle self-arm /// count. Every value is read lock-free. +/// +/// C3 E1 (#90) appends four lines: +/// - `ipis_sent` and `ipis_handled`: reschedule and TLB-shootdown IPIs sent to +/// each online CPU and handled on it, one value per CPU in the order of the +/// `cpuN` lines (compact, so the 2 KiB `syscall_test` reader keeps room); +/// - `tlb_shootdowns targeted= broadcast= ipis=`: remote target scans by +/// outcome, and the shootdown IPIs sent, resends included; +/// - `pt_lock acquired= contended= busy= wait_tsc= wait_tsc_max= hold_tsc= +/// hold_tsc_max=`: global page-table lock use, in TSC cycles. +/// +/// These are independent cumulative samples, not a coherent snapshot. IPI +/// delivery can precede send accounting, and events can occur between reads, +/// so `ipis_handled` may exceed `ipis_sent`. LAPIC coalescing also means their +/// difference cannot measure pending or dropped requests. Compare same-boot +/// interval deltas for each CPU's counter separately, allowing for u64 wrap. fn generate_schedstat() -> Result { let mut content = AdmittedString::new(HeapClass::Procfs); let online = process::sched_online_cpu_mask(); @@ -2107,6 +2122,40 @@ fn generate_schedstat() -> Result { process::idle_rearm_count() )) .map_err(|_| FsError::NoSpace)?; + let ipi_lines: [(&str, fn((u64, u64)) -> u64); 2] = [ + ("ipis_sent", |(sent, _)| sent), + ("ipis_handled", |(_, handled)| handled), + ]; + for (label, pick) in ipi_lines { + content.try_push_str(label).map_err(|_| FsError::NoSpace)?; + let mut remaining = online; + while remaining != 0 { + let cpu = remaining.trailing_zeros() as usize; + remaining &= remaining - 1; + content + .write_fmt(format_args!(" {}", pick(process::cpu_ipi_counters(cpu)))) + .map_err(|_| FsError::NoSpace)?; + } + content.try_push_str("\n").map_err(|_| FsError::NoSpace)?; + } + let tlb = mm::tlb_shootdown::get_stats(); + let pt = mm::page_table::pt_lock_stats(); + content + .write_fmt(format_args!( + "tlb_shootdowns targeted={} broadcast={} ipis={}\n\ + pt_lock acquired={} contended={} busy={} wait_tsc={} wait_tsc_max={} hold_tsc={} hold_tsc_max={}\n", + tlb.targeted_shootdowns, + tlb.broadcast_fallbacks, + tlb.shootdown_ipis, + pt.acquired, + pt.contended, + pt.busy, + pt.wait_tsc, + pt.wait_tsc_max, + pt.hold_tsc, + pt.hold_tsc_max + )) + .map_err(|_| FsError::NoSpace)?; Ok(content) } diff --git a/scripts/ci/strict_scope.txt b/scripts/ci/strict_scope.txt index 1efddbc..c16f50b 100644 --- a/scripts/ci/strict_scope.txt +++ b/scripts/ci/strict_scope.txt @@ -20,6 +20,9 @@ RUNTIME r175_d0_cross_3_sched_atomic not_applicable needs_cpus:2 RUNTIME rf178_33_sched_smp_gate not_applicable needs_cpus:2 RUNTIME tlb_shootdown_pcid not_applicable needs_cpus:2 +# C3 E1 (#90): the IPI and shootdown counter check is an SMP check too. +RUNTIME schedstat_event_counters not_applicable needs_cpus:2 + # D3: Spectre hardware mitigations and retpoline are N6. SECURITY spectre_mitigation_status out_of_scope out_of_scope:N6 SECURITY spectre_v2_retpoline out_of_scope out_of_scope:N6 diff --git a/scripts/tools/hosted_subcrate_tests.sh b/scripts/tools/hosted_subcrate_tests.sh index 4da2d9f..0722ce9 100755 --- a/scripts/tools/hosted_subcrate_tests.sh +++ b/scripts/tools/hosted_subcrate_tests.sh @@ -111,7 +111,7 @@ run_standalone_rust_suite() { echo "=== hosted standalone Rust suite: ${name} (expected ${expected_passed} passed; default parallelism) ===" if ! ( cd "${repo_root}/${source_dir}" - NILIX_TEST_TOTAL=74 rustc +nightly-2025-12-08 \ + NILIX_TEST_TOTAL=75 rustc +nightly-2025-12-08 \ --edition=2021 "${rustc_flags[@]}" --test "${source_file}" -o "${test_binary}" "${test_binary}" ) 2>&1 | tee "${log_file}"; then @@ -172,7 +172,7 @@ run_suite livepatch 3 0 \ --manifest-path kernel/livepatch/Cargo.toml \ --target x86_64-unknown-linux-gnu --lib --locked -run_suite mm 54 0 \ +run_suite mm 55 0 \ --manifest-path kernel/mm/Cargo.toml \ --target x86_64-unknown-linux-gnu \ --features host_harness \