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:
parent
43afe6bf2e
commit
dba3939a0f
|
|
@ -70,6 +70,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
|
||||||
ipcTest();
|
ipcTest();
|
||||||
} else if (eql(case, "smp")) {
|
} else if (eql(case, "smp")) {
|
||||||
smpTest();
|
smpTest();
|
||||||
|
} else if (eql(case, "smp-stress")) {
|
||||||
|
stressTest();
|
||||||
} else if (eql(case, "fault-ud")) {
|
} else if (eql(case, "fault-ud")) {
|
||||||
faultInvalidOpcode();
|
faultInvalidOpcode();
|
||||||
} else if (eql(case, "fault-pf")) {
|
} else if (eql(case, "fault-pf")) {
|
||||||
|
|
@ -482,6 +484,92 @@ fn smpTest() void {
|
||||||
result();
|
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 {
|
fn faultInvalidOpcode() void {
|
||||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||||
asm volatile ("ud2");
|
asm volatile ("ud2");
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,12 @@ CASES = [
|
||||||
"smp": 4,
|
"smp": 4,
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"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-ud", "expect": r"invalid opcode \(vector 6\)"},
|
||||||
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
|
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
|
||||||
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
|
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
|
||||||
|
|
@ -184,7 +190,8 @@ def run_case(arch, case):
|
||||||
cmd += ["-smp", str(case["smp"])]
|
cmd += ["-smp", str(case["smp"])]
|
||||||
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
try:
|
try:
|
||||||
deadline = time.monotonic() + TIMEOUT
|
timeout = case.get("timeout", TIMEOUT)
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
text = ""
|
text = ""
|
||||||
|
|
@ -199,7 +206,7 @@ def run_case(arch, case):
|
||||||
if expect.search(text):
|
if expect.search(text):
|
||||||
return True, "matched " + repr(case["expect"])
|
return True, "matched " + repr(case["expect"])
|
||||||
return False, "QEMU exited before matching (triple fault?)"
|
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:
|
finally:
|
||||||
if qemu.poll() is None:
|
if qemu.poll() is None:
|
||||||
qemu.terminate()
|
qemu.terminate()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue