test: on-hardware SMP self-test (revoke/kill must stop every hart) + make test-qemu
This commit is contained in:
@@ -49,11 +49,24 @@ fn virtio_id(region: CapSlot) -> u32 {
|
||||
id
|
||||
}
|
||||
|
||||
fn boot_info() -> &'static BootInfo {
|
||||
// SAFETY: boot maps the BootInfo page read-only at BOOT_INFO_VA before
|
||||
// init runs; readers check its magic.
|
||||
unsafe { &*(BOOT_INFO_VA as *const BootInfo) }
|
||||
}
|
||||
|
||||
/// The capability that halts the machine, if boot said where it is.
|
||||
pub fn system_control() -> Option<CapSlot> {
|
||||
let info = boot_info();
|
||||
if info.magic != types::bootinfo::MAGIC {
|
||||
return None;
|
||||
}
|
||||
slot(info.system_control_slot)
|
||||
}
|
||||
|
||||
/// Sorts the board's devices out.
|
||||
pub fn discover() -> Devices {
|
||||
// SAFETY: boot maps the BootInfo page read-only at BOOT_INFO_VA before
|
||||
// init runs; `devices()` checks its magic.
|
||||
let info = unsafe { &*(BOOT_INFO_VA as *const BootInfo) };
|
||||
let info = boot_info();
|
||||
let mut found = Devices { disk: Device::NONE, gpu: Device::NONE, input: Device::NONE };
|
||||
let mut uart = Device::NONE;
|
||||
for d in info.devices() {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
extern crate alloc;
|
||||
|
||||
mod devices;
|
||||
mod selftest;
|
||||
|
||||
use types::address::{EntryPoint, StackPointer, VirtualAddress};
|
||||
use types::bounded::{CapSlot, PageCount};
|
||||
@@ -12,7 +13,7 @@ use libkos::syscall;
|
||||
|
||||
const PAGE_SIZE: usize = 4096;
|
||||
const USER_CODE_BASE: usize = 0x10000;
|
||||
const USER_STACK_TOP: StackPointer = StackPointer::from_raw(USER_CODE_BASE);
|
||||
pub(crate) const USER_STACK_TOP: StackPointer = StackPointer::from_raw(USER_CODE_BASE);
|
||||
const USER_STACK_PAGES: usize = 4;
|
||||
/// Where a program file is read to before it is loaded (16 pages: the
|
||||
/// largest program init will load).
|
||||
@@ -36,12 +37,12 @@ fn read_file(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Opti
|
||||
read.filter(|r| Some(*r) == size)
|
||||
}
|
||||
|
||||
struct LoadedProcess {
|
||||
process_cap: CapSlot,
|
||||
entry_point: EntryPoint,
|
||||
pub(crate) struct LoadedProcess {
|
||||
pub(crate) process_cap: CapSlot,
|
||||
pub(crate) entry_point: EntryPoint,
|
||||
}
|
||||
|
||||
fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option<LoadedProcess> {
|
||||
pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option<LoadedProcess> {
|
||||
let file_size = read_file(storage, scratch, filename)?;
|
||||
let elf_data = &scratch[..file_size];
|
||||
let elf_info = match elf::parse(elf_data) {
|
||||
@@ -156,6 +157,13 @@ pub fn run() -> ! {
|
||||
libkos::println!("ok (entry={:#x})", loaded.entry_point.raw());
|
||||
}
|
||||
|
||||
// The test image carries the SMP self-test; it runs instead of the
|
||||
// desktop and halts the machine.
|
||||
if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) {
|
||||
libkos::println!("init: running the SMP self-test");
|
||||
selftest::run(&mut storage, scratch);
|
||||
}
|
||||
|
||||
libkos::print!("init: loading snake... ");
|
||||
if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") {
|
||||
// A client only calls: Execute, never Read, so it cannot pose as
|
||||
@@ -189,7 +197,7 @@ fn scratch() -> Option<&'static mut [u8]> {
|
||||
|
||||
/// Nothing to supervise yet: block on a notification nobody signals
|
||||
/// rather than spin a core on Yield.
|
||||
fn idle() -> ! {
|
||||
pub(crate) fn idle() -> ! {
|
||||
let idle = syscall::create_notification();
|
||||
loop {
|
||||
match idle {
|
||||
|
||||
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