threads(M3): join, detach, and cross-core parallelism
thread_spawn takes a 4th arg, an exit-endpoint handle: spawnThreadSupervised resolves and refcounts it under the spawn lock (like spawnProcessSupervised), so a thread's death posts a child-exit notification carrying its tid. runtime Thread.join blocks in replyWait on that (private) endpoint for its tid, then munmaps the stack; detach relinquishes the join (stack reclaimed at process exit, for now). New current_core=39 syscall + Thread.currentCore() lets a worker observe which core it ran on. The closure now lives at the top of the thread's own (private) stack instead of the heap, so spawn/join never touch the not-yet-thread-safe runtime heap. thread-test gains a join mode: 4 workers x 100k atomic increments, joined, with counter == N*K and >1 core stamped (real parallelism), plus a detached worker. Gate thread-join PASS (4x, non-flaky); 17 guardrail/M1/M2 cases green; build + host tests clean.
This commit is contained in:
+64
-29
@@ -1,18 +1,19 @@
|
||||
//! `runtime.Thread` — threads for danos, shaped like Zig's `std.Thread` but built on
|
||||
//! danos's private thread ABI (docs/threading.md). Several tasks share one address
|
||||
//! space; `spawn` starts one, the kernel delivers the closure pointer in the new
|
||||
//! thread's rdi, and a small Zig trampoline runs the user function and calls
|
||||
//! `thread_exit`. See docs/threading.md for why this mirrors `std.Thread`'s API rather
|
||||
//! than being the literal type (the private, renumberable syscall ABI).
|
||||
//! thread's rdi, a plain Zig trampoline runs the user function and calls `thread_exit`,
|
||||
//! and `join` blocks on the thread's exit notification. See docs/threading.md for why
|
||||
//! this mirrors `std.Thread`'s API rather than being the literal type.
|
||||
//!
|
||||
//! M2 surface: `spawn` + a `Thread` handle. `join`/`detach` and the `Mutex`/`Condition`
|
||||
//! family arrive in later milestones (docs/threading-plan.md). A binary must be built
|
||||
//! multi-threaded (`addThreadedUserBinary`) before it may spawn.
|
||||
//! The closure (the function's captured args) lives at the **top of the thread's own
|
||||
//! stack**, not the heap — each thread's stack is private, so there is no shared-heap
|
||||
//! concurrency in the spawn/join machinery (the runtime heap is not yet thread-safe).
|
||||
//! A binary must be built multi-threaded (`addThreadedUserBinary`) before it may spawn.
|
||||
|
||||
const std = @import("std");
|
||||
const sc = @import("system-call.zig");
|
||||
const system = @import("system.zig");
|
||||
const heap = @import("heap.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;
|
||||
@@ -20,6 +21,11 @@ pub const default_stack_size: usize = 64 * 1024;
|
||||
pub const Thread = struct {
|
||||
/// The kernel task id of the spawned thread.
|
||||
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,
|
||||
|
||||
pub const Id = u32;
|
||||
|
||||
@@ -29,9 +35,7 @@ pub const Thread = struct {
|
||||
};
|
||||
|
||||
pub const SpawnError = error{
|
||||
/// The closure could not be allocated on the heap.
|
||||
OutOfMemory,
|
||||
/// The kernel refused the thread (task table full, or the stack mmap failed).
|
||||
/// The kernel refused the thread, the stack mmap failed, or no endpoint was free.
|
||||
SystemResources,
|
||||
};
|
||||
|
||||
@@ -42,38 +46,69 @@ pub const Thread = struct {
|
||||
const Args = @TypeOf(args);
|
||||
const Closure = struct {
|
||||
args: Args,
|
||||
|
||||
/// Entered directly by the kernel with `self` in rdi (C ABI). Runs the
|
||||
/// user function, then ends the thread — never returns.
|
||||
/// Entered directly by the kernel with `self` in rdi (C ABI). Runs the user
|
||||
/// function, then ends the thread — never returns.
|
||||
fn entry(self_addr: usize) callconv(.c) noreturn {
|
||||
const self: *@This() = @ptrFromInt(self_addr);
|
||||
const call_args = self.args;
|
||||
heap.allocator().destroy(self); // args copied out; closure no longer needed
|
||||
@call(.auto, function, call_args);
|
||||
@call(.auto, function, self.args);
|
||||
exitThread();
|
||||
}
|
||||
};
|
||||
|
||||
const closure = heap.allocator().create(Closure) catch return error.OutOfMemory;
|
||||
errdefer heap.allocator().destroy(closure);
|
||||
closure.* = .{ .args = args };
|
||||
// The endpoint the kernel posts this thread's exit notification to.
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return error.SystemResources;
|
||||
|
||||
// Stack: mmap zeroed pages, then hand the kernel a 16-byte-aligned-minus-8 top so
|
||||
// the C-ABI trampoline sees the alignment a `call` would have left (rsp % 16 == 8).
|
||||
const base = system.mmap(config.stack_size, system.PROT_READ | system.PROT_WRITE);
|
||||
if (system.mmapFailed(base)) return error.SystemResources;
|
||||
errdefer _ = system.munmap(base, config.stack_size);
|
||||
const stack_top = (base + config.stack_size) - 8;
|
||||
|
||||
const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, @intFromPtr(closure));
|
||||
if (threadSpawnFailed(tid)) return error.SystemResources;
|
||||
return .{ .tid = @intCast(tid) };
|
||||
// Lay the closure at the very top of the thread's own stack, then start the
|
||||
// thread's rsp just below it (16-aligned minus 8, the alignment a `call` leaves
|
||||
// for a C-ABI entry) so the growing stack never overwrites the args.
|
||||
var closure_addr = (base + config.stack_size) - @sizeOf(Closure);
|
||||
closure_addr &= ~@as(usize, @alignOf(Closure) - 1); // align the closure down
|
||||
const closure: *Closure = @ptrFromInt(closure_addr);
|
||||
closure.* = .{ .args = args };
|
||||
|
||||
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);
|
||||
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 };
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Relinquish the right to join: never wait for or reclaim this thread. Its stack is
|
||||
/// reclaimed at process exit (docs/threading-plan.md M3 — kernel-reaper stack reclaim
|
||||
/// for detached threads is a later refinement). Mirrors `std.Thread.detach`.
|
||||
pub fn detach(self: Thread) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// The dense 0-based index of the core the calling thread is running on. A danos
|
||||
/// extension beyond `std.Thread`, used to observe genuine cross-core parallelism.
|
||||
pub fn currentCore() Id {
|
||||
return @intCast(sc.systemCall0(.current_core));
|
||||
}
|
||||
};
|
||||
|
||||
/// thread_spawn(entry, stack_top, arg) -> tid, or a wrapped error (see below).
|
||||
fn threadSpawn(entry: usize, stack_top: usize, arg: usize) usize {
|
||||
return sc.systemCall3(.thread_spawn, entry, stack_top, arg);
|
||||
/// 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 {
|
||||
return sc.systemCall4(.thread_spawn, entry, stack_top, arg, exit_endpoint);
|
||||
}
|
||||
|
||||
/// The kernel returns a real (small) task id on success and a wrapped `-1` on failure;
|
||||
|
||||
Reference in New Issue
Block a user