threads(M9): thread_join syscall — retire the per-thread endpoint

join no longer needs a per-thread IPC endpoint. New thread_join(tid) syscall
blocks the caller until the task with id tid exits; the exit paths call
wakeJoinersLocked. join only reclaims the joined thread's USER stack, which the
thread vacates the instant it enters the kernel to exit, so waking at exit time
(not reap time) is safe — no reaper/aspace juggling or user-memory write, and
equally std-shaped (like pthread_join). thread_spawn drops the exit-endpoint arg
(runtime passes no_cap). thread-test's join mode runs 40 spawn+join cycles that
would exhaust the 16-slot handle table under the old endpoint scheme.

Also harden the M8 reaper: its single per-core reap slot could be overwritten by
a second death on that core before draining (a fresh-task/SMP timing window), an
intermittent one-stack leak that made task-reap ~20% flaky. Replace it with a
per-core reap LIST plus a .reaping task state so a pending slot can't be reused
before its stack is freed. task-reap now 11/11 isolated.

Deferred: detached-thread user-stack reclaim (still at process exit, as in M3).

Gate thread-join PASS (3x); full guardrail 26/26; build + host tests clean.
This commit is contained in:
2026-07-20 23:42:00 +01:00
parent ed3b3f1c45
commit 6bc329456a
7 changed files with 153 additions and 56 deletions
+7 -16
View File
@@ -14,16 +14,13 @@ const std = @import("std");
const abi = @import("abi");
const sc = @import("system-call.zig");
const system = @import("system.zig");
const ipc = @import("ipc.zig");
/// A thread stack, if the caller does not override it. 64 KiB of mmap'd, zeroed pages.
pub const default_stack_size: usize = 64 * 1024;
pub const Thread = struct {
/// The kernel task id of the spawned thread.
/// The kernel task id of the spawned thread — what `join` waits on.
tid: u32,
/// The endpoint the kernel notifies when this thread ends — what `join` blocks on.
exit_endpoint: ipc.Handle,
/// The mmap'd stack, reclaimed by `join` (or at process exit after `detach`).
stack_base: usize,
stack_size: usize,
@@ -56,9 +53,6 @@ pub const Thread = struct {
}
};
// The endpoint the kernel posts this thread's exit notification to.
const endpoint = ipc.createIpcEndpoint() orelse return error.SystemResources;
const base = system.mmap(config.stack_size, system.PROT_READ | system.PROT_WRITE);
if (system.mmapFailed(base)) return error.SystemResources;
@@ -73,24 +67,20 @@ pub const Thread = struct {
var stack_top = closure_addr & ~@as(usize, 15); // 16-align below the closure
stack_top -= 8; // ...then rsp % 16 == 8 at the C entry
const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, closure_addr, endpoint);
const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, closure_addr);
if (threadSpawnFailed(tid)) {
_ = system.munmap(base, config.stack_size);
return error.SystemResources;
}
return .{ .tid = @intCast(tid), .exit_endpoint = endpoint, .stack_base = base, .stack_size = config.stack_size };
return .{ .tid = @intCast(tid), .stack_base = base, .stack_size = config.stack_size };
}
/// Block until this thread finishes, then reclaim its stack. Mirrors
/// `std.Thread.join`. The exit endpoint is private to this thread, so the first
/// child-exit notification on it is this thread's.
pub fn join(self: Thread) void {
var receive: [0]u8 = undefined;
while (true) {
const got = ipc.replyWait(self.exit_endpoint, &.{}, &receive, null);
if (got.isChildExit() and got.childProcessId() == self.tid) break;
}
_ = system.munmap(self.stack_base, self.stack_size);
_ = sc.systemCall1(.thread_join, self.tid); // block until the thread has exited
_ = system.munmap(self.stack_base, self.stack_size); // reclaim its (now-vacated) stack
}
/// Relinquish the right to join: never wait for or reclaim this thread. Its stack is
@@ -233,7 +223,8 @@ pub const Thread = struct {
};
/// thread_spawn(entry, stack_top, arg, exit_endpoint) -> tid, or a wrapped error.
fn threadSpawn(entry: usize, stack_top: usize, arg: usize, exit_endpoint: ipc.Handle) usize {
fn threadSpawn(entry: usize, stack_top: usize, arg: usize) usize {
const exit_endpoint: usize = @intCast(abi.no_cap); // join uses thread_join, not an endpoint
return sc.systemCall4(.thread_spawn, entry, stack_top, arg, exit_endpoint);
}