threads(M5): Mutex, Condition, and Semaphore over the futex

runtime.Thread.Mutex is the classic three-state futex mutex (unlocked/locked/
contended): the fast path is a single CAS and only a contended lock enters the
kernel. Condition is a futex sequence counter (wait/timedWait/signal/broadcast,
spurious wakeups allowed, use in a predicate loop); a signal racing the unlock
bumps the seq so it is never missed. Semaphore is permits guarded by
Mutex+Condition. All mirror std.Thread's shapes, ported onto runtime.Thread.Futex.

thread-test gains a mutex mode: 2 producers + 2 consumers move 2000 unique items
through an 8-slot ring (small enough that both sides block); the consumed
checksum and tally match exactly, proving the lock and condvars correct under
real cross-core contention.

Deferred with rationale (see docs/threading-plan.md): migrating join to a futex
completion word needs kernel clear-on-exit (else use-after-free munmapping a live
stack); host unit tests need a mockable Futex seam.

Gate thread-mutex PASS (3x); 17 guardrail/thread cases green; build + host tests
clean.
This commit is contained in:
2026-07-20 21:48:26 +01:00
parent b3a8147bd7
commit 1b33f48acd
5 changed files with 271 additions and 15 deletions
+46
View File
@@ -147,6 +147,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
threadJoinTest(boot_information);
} else if (eql(case, "thread-futex")) {
threadFutexTest(boot_information);
} else if (eql(case, "thread-mutex")) {
threadMutexTest(boot_information);
} else if (eql(case, "args")) {
argsTest(boot_information);
} else if (eql(case, "init")) {
@@ -1600,6 +1602,50 @@ fn threadFutexTest(boot_information: *const BootInformation) void {
result();
}
/// Mutex + Condition (docs/threading-plan.md M5): `thread-test` in mutex mode runs a
/// bounded producer/consumer — P producers and C consumers over one `Mutex` and two
/// `Condition`s move N unique items through a small ring. Every item is produced once;
/// if the lock and condition variables are correct under real cross-core contention,
/// the consumed checksum and tally match exactly (no lost or duplicated item, no
/// overrun). The verdict marker is emitted only when both match.
fn threadMutexTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: thread-mutex\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
var started = false;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "thread-test")) continue;
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "mutex" })) true else |_| false;
break;
}
check("thread-test (mutex mode) spawned", started);
const ok_marker = "thread-mutex: ok";
const fail_marker = "thread-mutex: FAIL";
scheduler.setPriority(1);
const deadline = architecture.millis() + 20000;
while (architecture.millis() < deadline) {
if (bufferHas(ok_marker) or bufferHas(fail_marker)) break;
scheduler.yield();
}
scheduler.setPriority(4);
check("producer/consumer over Mutex+Condition moved every item exactly once", bufferHas(ok_marker) and !bufferHas(fail_marker));
result();
}
/// The full PID-1 path: the bootloader read /system/services/init off the boot volume and
/// handed it over; load it as a user ELF and spawn it as a real ring-3 process
/// — the same call the normal boot path makes — then confirm it beats. init