test: on-hardware SMP self-test (revoke/kill must stop every hart) + make test-qemu
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -294,6 +294,14 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smptest"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libkos",
|
||||||
|
"types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snake"
|
name = "snake"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ members = [
|
|||||||
"crates/adapter-riscv",
|
"crates/adapter-riscv",
|
||||||
"crates/apps/hello",
|
"crates/apps/hello",
|
||||||
"crates/apps/echo",
|
"crates/apps/echo",
|
||||||
|
"crates/apps/smptest",
|
||||||
"crates/apps/snake",
|
"crates/apps/snake",
|
||||||
"crates/domain",
|
"crates/domain",
|
||||||
"crates/elf",
|
"crates/elf",
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: check fmt clippy test kani build all image run ci kernel-fmt kernel-clippy kernel-test
|
.PHONY: check fmt clippy test kani build all image run ci kernel-fmt kernel-clippy kernel-test test-qemu
|
||||||
|
|
||||||
RISCV_TARGET = riscv64gc-unknown-none-elf
|
RISCV_TARGET = riscv64gc-unknown-none-elf
|
||||||
KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv
|
KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv
|
||||||
@@ -52,11 +52,17 @@ build:
|
|||||||
cargo build --target $(RISCV_TARGET) --release \
|
cargo build --target $(RISCV_TARGET) --release \
|
||||||
-p adapter-riscv -p init -p echo -p snake \
|
-p adapter-riscv -p init -p echo -p snake \
|
||||||
-p display-qemu -p input-qemu -p storage-qemu \
|
-p display-qemu -p input-qemu -p storage-qemu \
|
||||||
-p log-server -p permission-manager -p session-server
|
-p log-server -p permission-manager -p session-server -p smptest
|
||||||
|
|
||||||
image: build
|
image: build
|
||||||
scripts/make-disk-image.sh
|
scripts/make-disk-image.sh
|
||||||
|
|
||||||
|
# Boots a test image (normal image + smptest.elf) on $(SMP) harts; init runs
|
||||||
|
# the SMP self-test and halts. Passes only on "SMPTEST PASS".
|
||||||
|
test-qemu: build
|
||||||
|
IMG=test.img SMPTEST=1 scripts/make-disk-image.sh > /dev/null
|
||||||
|
SMP=$(SMP) scripts/test-qemu.sh test.img
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
|
qemu-system-riscv64 -machine virt -smp $(SMP) -nographic -bios default \
|
||||||
-kernel $(KERNEL_BIN)
|
-kernel $(KERNEL_BIN)
|
||||||
|
|||||||
13
crates/apps/smptest/Cargo.toml
Normal file
13
crates/apps/smptest/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "smptest"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "smptest"
|
||||||
|
test = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
types = { workspace = true }
|
||||||
|
libkos = { workspace = true }
|
||||||
51
crates/apps/smptest/src/main.rs
Normal file
51
crates/apps/smptest/src/main.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//! The spinner half of init's SMP self-test (see init's `selftest`):
|
||||||
|
//! maps the counter page it was given in slot 0 and runs one thread per
|
||||||
|
//! hart, each incrementing its own counter for ever. Init watches the
|
||||||
|
//! counters through its own mapping of the same page.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate libkos;
|
||||||
|
|
||||||
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use libkos::syscall;
|
||||||
|
use types::address::{EntryPoint, StackPointer, VirtualAddress};
|
||||||
|
use types::bounded::{CapSlot, PageCount};
|
||||||
|
use types::mapping::PagePerms;
|
||||||
|
|
||||||
|
/// Where the counter page is mapped. Layout (words): 0 the next free
|
||||||
|
/// counter index, 1..=THREADS the counters.
|
||||||
|
const COUNTERS_VA: usize = 0x0100_0000;
|
||||||
|
const THREADS: usize = 4;
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
extern "C" fn main() {
|
||||||
|
let page = CapSlot::const_new(0);
|
||||||
|
if syscall::map_region(page, VirtualAddress::from_raw(COUNTERS_VA), PagePerms::ReadWrite).is_err() {
|
||||||
|
libkos::println!("smptest: cannot map the counter page");
|
||||||
|
syscall::exit();
|
||||||
|
}
|
||||||
|
for _ in 1..THREADS {
|
||||||
|
let Some(stack) = PageCount::new(1).and_then(syscall::request_pages) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let top = StackPointer::from_raw(stack.raw() + 4096);
|
||||||
|
syscall::spawn_thread(EntryPoint::from_raw(spin as *const () as usize), top);
|
||||||
|
}
|
||||||
|
spin();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn spin() -> ! {
|
||||||
|
// SAFETY: COUNTERS_VA maps the counter page; every access is atomic.
|
||||||
|
let word = |i: usize| unsafe { &*((COUNTERS_VA + i * 8) as *const AtomicUsize) };
|
||||||
|
let mine = 1 + word(0).fetch_add(1, Ordering::Relaxed) % THREADS;
|
||||||
|
loop {
|
||||||
|
// Yield now and then: a spinner sharing init's hart would
|
||||||
|
// otherwise starve it until the next (1 s) timer tick.
|
||||||
|
for _ in 0..1024 {
|
||||||
|
word(mine).fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
syscall::yield_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,11 +49,24 @@ fn virtio_id(region: CapSlot) -> u32 {
|
|||||||
id
|
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.
|
/// Sorts the board's devices out.
|
||||||
pub fn discover() -> Devices {
|
pub fn discover() -> Devices {
|
||||||
// SAFETY: boot maps the BootInfo page read-only at BOOT_INFO_VA before
|
let info = boot_info();
|
||||||
// init runs; `devices()` checks its magic.
|
|
||||||
let info = unsafe { &*(BOOT_INFO_VA as *const BootInfo) };
|
|
||||||
let mut found = Devices { disk: Device::NONE, gpu: Device::NONE, input: Device::NONE };
|
let mut found = Devices { disk: Device::NONE, gpu: Device::NONE, input: Device::NONE };
|
||||||
let mut uart = Device::NONE;
|
let mut uart = Device::NONE;
|
||||||
for d in info.devices() {
|
for d in info.devices() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
mod devices;
|
mod devices;
|
||||||
|
mod selftest;
|
||||||
|
|
||||||
use types::address::{EntryPoint, StackPointer, VirtualAddress};
|
use types::address::{EntryPoint, StackPointer, VirtualAddress};
|
||||||
use types::bounded::{CapSlot, PageCount};
|
use types::bounded::{CapSlot, PageCount};
|
||||||
@@ -12,7 +13,7 @@ use libkos::syscall;
|
|||||||
|
|
||||||
const PAGE_SIZE: usize = 4096;
|
const PAGE_SIZE: usize = 4096;
|
||||||
const USER_CODE_BASE: usize = 0x10000;
|
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;
|
const USER_STACK_PAGES: usize = 4;
|
||||||
/// Where a program file is read to before it is loaded (16 pages: the
|
/// Where a program file is read to before it is loaded (16 pages: the
|
||||||
/// largest program init will load).
|
/// 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)
|
read.filter(|r| Some(*r) == size)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LoadedProcess {
|
pub(crate) struct LoadedProcess {
|
||||||
process_cap: CapSlot,
|
pub(crate) process_cap: CapSlot,
|
||||||
entry_point: EntryPoint,
|
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 file_size = read_file(storage, scratch, filename)?;
|
||||||
let elf_data = &scratch[..file_size];
|
let elf_data = &scratch[..file_size];
|
||||||
let elf_info = match elf::parse(elf_data) {
|
let elf_info = match elf::parse(elf_data) {
|
||||||
@@ -156,6 +157,13 @@ pub fn run() -> ! {
|
|||||||
libkos::println!("ok (entry={:#x})", loaded.entry_point.raw());
|
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... ");
|
libkos::print!("init: loading snake... ");
|
||||||
if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") {
|
if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") {
|
||||||
// A client only calls: Execute, never Read, so it cannot pose as
|
// 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
|
/// Nothing to supervise yet: block on a notification nobody signals
|
||||||
/// rather than spin a core on Yield.
|
/// rather than spin a core on Yield.
|
||||||
fn idle() -> ! {
|
pub(crate) fn idle() -> ! {
|
||||||
let idle = syscall::create_notification();
|
let idle = syscall::create_notification();
|
||||||
loop {
|
loop {
|
||||||
match idle {
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMG="boot.img"
|
IMG="${IMG:-boot.img}"
|
||||||
IMG_SIZE_KB=4096
|
IMG_SIZE_KB=4096
|
||||||
TARGET="riscv64gc-unknown-none-elf"
|
TARGET="riscv64gc-unknown-none-elf"
|
||||||
BUILD_DIR="target/${TARGET}/release"
|
BUILD_DIR="target/${TARGET}/release"
|
||||||
@@ -30,6 +30,10 @@ copy log-server "log.elf"
|
|||||||
copy permission-manager "permis.elf"
|
copy permission-manager "permis.elf"
|
||||||
copy storage-qemu "storage.elf"
|
copy storage-qemu "storage.elf"
|
||||||
copy session-server "session.elf"
|
copy session-server "session.elf"
|
||||||
|
# Only the test image: its presence makes init run the SMP self-test.
|
||||||
|
if [ -n "${SMPTEST:-}" ]; then
|
||||||
|
copy smptest "smptest.elf"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Disk contents:"
|
echo "Disk contents:"
|
||||||
|
|||||||
27
scripts/test-qemu.sh
Executable file
27
scripts/test-qemu.sh
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Boots IMAGE headless and waits for init's SMP self-test to report; the
|
||||||
|
# machine halts itself when done. Exit 0 only on "SMPTEST PASS".
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
IMAGE="${1:?usage: test-qemu.sh IMAGE}"
|
||||||
|
SMP="${SMP:-4}"
|
||||||
|
KERNEL="target/riscv64gc-unknown-none-elf/release/adapter-riscv"
|
||||||
|
LOG="$(mktemp -t kos-test-qemu.XXXXXX)"
|
||||||
|
|
||||||
|
timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default \
|
||||||
|
-kernel "${KERNEL}" \
|
||||||
|
-drive "file=${IMAGE},if=none,format=raw,id=hd0" -device virtio-blk-device,drive=hd0 \
|
||||||
|
-device virtio-gpu-device -device virtio-keyboard-device \
|
||||||
|
-display none -serial "file:${LOG}" -monitor none
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
grep -a "smptest:\|SMPTEST" "${LOG}"
|
||||||
|
if grep -aq "SMPTEST PASS" "${LOG}"; then
|
||||||
|
echo "test-qemu: pass (${SMP} harts)"
|
||||||
|
rm -f "${LOG}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
[ "${status}" -eq 124 ] && echo "test-qemu: timed out"
|
||||||
|
echo "test-qemu: FAIL, last serial output (${LOG}):"
|
||||||
|
tail -20 "${LOG}"
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user