test: on-hardware SMP self-test (revoke/kill must stop every hart) + make test-qemu

This commit is contained in:
2026-10-27 12:54:42 +01:00
parent 909b3f0503
commit b814bd9570
10 changed files with 262 additions and 12 deletions

119
crates/init/src/selftest.rs Normal file
View File

@@ -0,0 +1,119 @@
//! SMP self-test, run only when the disk carries `smptest.elf` (the test
//! image built by `make test-qemu`). It checks on real harts what host
//! tests cannot: that unmapping a page and killing a process take effect
//! on every other hart before the call returns.
//!
//! A spinner process runs one thread per hart, each incrementing its own
//! counter in a page init also maps. Init then cuts the spinner off
//! (revoking its copy of the page, or killing it) and requires that no
//! counter moves afterwards: a stale TLB entry or a thread left running
//! on another hart would keep counting.
use core::sync::atomic::{AtomicUsize, Ordering};
use libkos::fs::Storage;
use libkos::syscall;
use types::address::VirtualAddress;
use types::bounded::{CapSlot, PageCount};
use types::mapping::PagePerms;
use types::permission::{Permission, PermissionSet};
/// Where init sees the counter page; same layout as the spinner's.
const OBSERVE_VA: usize = 0x0140_0000;
const THREADS: usize = 4;
/// QEMU virt's timebase is 10 MHz.
const TICKS_PER_MS: u64 = 10_000;
fn word(i: usize) -> &'static AtomicUsize {
// SAFETY: OBSERVE_VA maps the counter page for the whole test.
unsafe { &*((OBSERVE_VA + i * 8) as *const AtomicUsize) }
}
fn counters() -> [usize; THREADS] {
core::array::from_fn(|i| word(1 + i).load(Ordering::SeqCst))
}
/// Spins (yielding) until `done` or `ms` milliseconds pass.
fn wait_until(ms: u64, mut done: impl FnMut() -> bool) -> bool {
let deadline = syscall::get_time().raw() + ms * TICKS_PER_MS;
while syscall::get_time().raw() < deadline {
if done() {
return true;
}
syscall::yield_now();
}
done()
}
/// Starts a spinner on `page`; its process capability.
fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot) -> Result<CapSlot, &'static str> {
for i in 0..=THREADS {
word(i).store(0, Ordering::SeqCst);
}
let spinner = crate::load_binary(storage, scratch, b"smptest.elf").ok_or("cannot load smptest.elf")?;
let rw = PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)
.with(Permission::Delegate);
if !syscall::delegate(page, spinner.process_cap, rw) {
return Err("cannot give the spinner its page");
}
syscall::spawn_thread_in(spinner.process_cap, spinner.entry_point, crate::USER_STACK_TOP);
if !wait_until(3000, || counters().iter().all(|c| *c > 0)) {
return Err("spinner threads did not all start");
}
Ok(spinner.process_cap)
}
/// After `stop`, no counter may move.
fn check_stopped(what: &'static str) -> Result<(), &'static str> {
let before = counters();
wait_until(200, || false);
if counters() != before {
return Err(what);
}
Ok(())
}
fn run_phases(storage: &mut Storage, scratch: &mut [u8]) -> Result<(), &'static str> {
let page = syscall::create_region(PageCount::new(1).ok_or("page count")?).map_err(|_| "no counter page")?;
syscall::map_region(page, VirtualAddress::from_raw(OBSERVE_VA), PagePerms::ReadWrite)
.map_err(|_| "cannot map the counter page")?;
// 1. Unmapping: revoking the spinner's copy of the page must unmap it
// on every hart before Revoke returns. The spinner then faults,
// which kills it from whichever hart faults first.
let spinner = start_spinner(storage, scratch, page)?;
if !syscall::revoke(page) {
return Err("revoke failed");
}
check_stopped("a counter moved after its page was revoked (stale TLB)")?;
if syscall::kill(spinner) {
return Err("the spinner survived losing its page");
}
libkos::println!("smptest: revoke stops every hart: ok");
// 2. Killing: a process whose threads are running on other harts must
// stop everywhere before Kill returns.
let spinner = start_spinner(storage, scratch, page)?;
if !syscall::kill(spinner) {
return Err("kill failed");
}
check_stopped("a counter moved after its process was killed")?;
libkos::println!("smptest: kill stops every hart: ok");
Ok(())
}
/// Runs the test, reports, and halts the machine.
pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! {
match run_phases(storage, scratch) {
Ok(()) => libkos::println!("SMPTEST PASS"),
Err(why) => libkos::println!("SMPTEST FAIL: {}", why),
}
match crate::devices::system_control() {
Some(control) => syscall::halt(control),
None => {
libkos::println!("smptest: no SystemControl capability to halt with");
crate::idle()
}
}
}