add an SMP lock-stress test case

Four pairs push 400k sequenced messages through small channels; checks FIFO order and cross-core execution. Verified to fail with the lock disabled.
This commit is contained in:
Daniel Samson 2026-07-08 12:45:02 +01:00
parent 43afe6bf2e
commit dba3939a0f
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
2 changed files with 97 additions and 2 deletions

View File

@ -70,6 +70,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
ipcTest();
} else if (eql(case, "smp")) {
smpTest();
} else if (eql(case, "smp-stress")) {
stressTest();
} else if (eql(case, "fault-ud")) {
faultInvalidOpcode();
} else if (eql(case, "fault-pf")) {
@ -482,6 +484,92 @@ fn smpTest() void {
result();
}
// --- SMP stress: hammer the big kernel lock across cores ------------------
const stress_pairs = 4; // producer/consumer pairs (8 tasks; fits the 16-task pool)
const stress_msgs = 100_000; // messages per pair
const stress_cap = 4; // small channel -> constant block/wake, more lock churn
var stress_chan = [_]ipc.Channel(u64, stress_cap){.{}} ** stress_pairs;
var stress_recv = [_]u64{0} ** stress_pairs; // messages received per pair
var stress_order_ok = [_]bool{true} ** stress_pairs; // FIFO order held per pair
var stress_cores = [_]bool{false} ** 8; // cores that ran a consumer
var stress_prod_claim: usize = 0;
var stress_cons_claim: usize = 0;
fn stressProducer() void {
// Claim a unique pair index (atomic: producers start on different cores).
const idx = @atomicRmw(usize, &stress_prod_claim, .Add, 1, .monotonic);
var v: u64 = 1;
while (v <= stress_msgs) : (v += 1) stress_chan[idx].send(v);
sched.exit();
}
fn stressConsumer() void {
const idx = @atomicRmw(usize, &stress_cons_claim, .Add, 1, .monotonic);
var expected: u64 = 1;
while (expected <= stress_msgs) : (expected += 1) {
const got = stress_chan[idx].recv();
if (got != expected) stress_order_ok[idx] = false; // lost/reordered => lock broke
const c = sched.currentCpuIndex();
if (c < stress_cores.len) stress_cores[c] = true;
stress_recv[idx] = expected;
}
sched.exit();
}
/// Stress the big kernel lock under sustained cross-core contention. Each pair drives
/// `stress_msgs` sequenced messages through a 4-slot channel every send and recv
/// takes the lock, and the small buffer forces constant block/wake (so the scheduler
/// churns too). A single-producer/single-consumer channel must deliver in strict FIFO
/// order; if the lock let two cores into a critical section at once, the ring buffer
/// corrupts and the consumer sees a wrong or out-of-order value (or the run hangs /
/// faults). Passing means ~320k lock acquisitions across the cores stayed consistent.
fn stressTest() void {
log("DANOS-TEST-BEGIN: smp-stress\n", .{});
stress_chan = [_]ipc.Channel(u64, stress_cap){.{}} ** stress_pairs;
stress_recv = [_]u64{0} ** stress_pairs;
stress_order_ok = [_]bool{true} ** stress_pairs;
stress_cores = [_]bool{false} ** 8;
stress_prod_claim = 0;
stress_cons_claim = 0;
var i: usize = 0;
while (i < stress_pairs) : (i += 1) sched.spawn(stressConsumer, 4);
i = 0;
while (i < stress_pairs) : (i += 1) sched.spawn(stressProducer, 4);
// Drop below the workers so they get the cores; wake periodically to check for
// completion. A broken lock instead hangs here (harness timeout) or faults.
sched.setPriority(1);
var spins: u64 = 0;
while (spins < 40_000_000_000) : (spins += 1) {
var done = true;
for (stress_recv) |n| {
if (n < stress_msgs) done = false;
}
if (done) break;
}
sched.setPriority(4);
var total: u64 = 0;
for (stress_recv) |n| total += n;
var order_ok = true;
for (stress_order_ok) |ok| {
if (!ok) order_ok = false;
}
var cores: u32 = 0;
for (stress_cores) |s| {
if (s) cores += 1;
}
log("DANOS-STRESS: {d}/{d} pairs complete on {d} cores\n", .{ total, @as(u64, stress_pairs) * stress_msgs, cores });
check("every message delivered", total == @as(u64, stress_pairs) * stress_msgs);
check("strict FIFO order held (no lock corruption)", order_ok);
check("contention was genuinely cross-core", cores >= 2);
result();
}
fn faultInvalidOpcode() void {
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
asm volatile ("ud2");

View File

@ -113,6 +113,12 @@ CASES = [
"smp": 4,
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Stress the big kernel lock across cores; heavier, so a longer timeout.
{"name": "smp-stress",
"smp": 4,
"timeout": 90,
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
{"name": "fault-ud", "expect": r"invalid opcode \(vector 6\)"},
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
@ -184,7 +190,8 @@ def run_case(arch, case):
cmd += ["-smp", str(case["smp"])]
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
deadline = time.monotonic() + TIMEOUT
timeout = case.get("timeout", TIMEOUT)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
time.sleep(0.2)
text = ""
@ -199,7 +206,7 @@ def run_case(arch, case):
if expect.search(text):
return True, "matched " + repr(case["expect"])
return False, "QEMU exited before matching (triple fault?)"
return False, f"timed out after {TIMEOUT}s without matching {case['expect']!r}"
return False, f"timed out after {timeout}s without matching {case['expect']!r}"
finally:
if qemu.poll() is None:
qemu.terminate()