test: on-hardware SMP self-test (revoke/kill must stop every hart) + make test-qemu
This commit is contained in:
119
crates/init/src/selftest.rs
Normal file
119
crates/init/src/selftest.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user