threads(M7): thread-safe allocation (per-aspace mmap arena + locked heap)

Move the mmap/mmio grant-arena cursors off Task into the per-address-space object
(scheduler aspace_refs, exposed via aspaceMmapNextPtr/aspaceDeviceMapNextPtr), so
sibling threads in one address space hand out disjoint grants. systemMmap reserves
a range under a brief lock then maps per page under a short-held lock (not the
whole grant): the big lock runs with interrupts disabled, so pinning it across a
multi-MiB memset+map froze other cores. Guard the runtime heap's rawAlloc/rawFree
with a Thread.Mutex, gated on !single_threaded so ordinary binaries compile it out.

thread-test gains an alloc mode: 4 threads x 500 alloc/fill/verify/free cycles;
any overlap between concurrent allocations is caught by the pattern check.

Also fix the affinity guardrail: its 3-billion-iteration busy-loop had
codegen-dependent wall-time (adding a function to tests.zig swung it ~4s -> ~63s
and timed it out). Reworked to wait on the wall clock instead.

Gate thread-alloc PASS (3x); full guardrail 23/23 green; build + host tests clean.
This commit is contained in:
2026-07-20 22:57:11 +01:00
parent f4813c8e99
commit 8259678f0a
7 changed files with 228 additions and 47 deletions
+53 -4
View File
@@ -151,6 +151,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
threadMutexTest(boot_information);
} else if (eql(case, "thread-id")) {
threadIdTest(boot_information);
} else if (eql(case, "thread-alloc")) {
threadAllocTest(boot_information);
} else if (eql(case, "args")) {
argsTest(boot_information);
} else if (eql(case, "init")) {
@@ -779,11 +781,15 @@ fn affinityTest() void {
return;
}
var spins: u64 = 0;
while (spins < 3_000_000_000) spins +%= 1; // many time slices across the cores
// Let many time slices pass so the scheduler runs the pinned worker across ticks.
// Wait on the wall clock, not a raw iteration count: a fixed-count busy-loop's
// wall-time is a codegen lottery (the optimiser may elide or vectorise it), so an
// unrelated change elsewhere in this file could swing this test from ~4 s to ~50 s.
const run_until = architecture.millis() + 400;
while (architecture.millis() < run_until) {}
affinity_running = false;
var settle: u64 = 0;
while (settle < 200_000_000) settle +%= 1; // let the worker see the flag and exit
const settle_until = architecture.millis() + 50;
while (architecture.millis() < settle_until) {} // let the worker see the flag and exit
var others: u32 = 0;
for (affinity_cores, 0..) |seen, c| {
@@ -1689,6 +1695,49 @@ fn threadIdTest(boot_information: *const BootInformation) void {
result();
}
/// Thread-safe allocation (docs/threading-plan.md M7): `thread-test` in alloc mode runs N
/// threads that each do many `alloc`/fill/verify/`free` cycles of varied sizes on the
/// shared runtime heap. If the heap lock or the per-address-space mmap arena were unsafe,
/// two threads' blocks would overlap and a thread would read another's pattern; the
/// verdict marker is emitted only when every thread completes with every block intact.
fn threadAllocTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: thread-alloc\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", "alloc" })) true else |_| false;
break;
}
check("thread-test (alloc mode) spawned", started);
const ok_marker = "thread-alloc: ok";
const fail_marker = "thread-alloc: 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("concurrent heap allocation stayed corruption-free (shared heap + per-aspace arena)", 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