threads(M2): thread_spawn/thread_exit + runtime.Thread.spawn
A thread is a task sharing the caller's address space. New private syscalls thread_spawn(entry, stack_top, arg)=37 and thread_exit=38: thread_spawn goes through scheduler.spawnThread (retains the shared aspace), thread_exit ends the task like a process exit(0) (terminateCurrent -> releaseAspace, so the space survives while siblings hold it). The closure pointer reaches the new thread in rdi via a new jump_to_user_arg asm path and a per-task user_arg (0 for a normal process, whose _start ignores it) - so the runtime trampoline is a plain C-ABI Zig function, no naked asm. runtime.Thread (library/runtime/thread.zig) mirrors std.Thread.spawn: mmap a stack, heap-allocate the args closure, hand the kernel the trampoline + closure. addThreadedUserBinary opts a binary into single_threaded=false; thread-test is the first, and proves a worker runs in the shared address space via a shared global the main thread polls. Gate thread-spawn PASS; 16 guardrail cases green (incl. args/init/process on the new jump_to_user_arg path) + aspace-refcount; build + host tests clean.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
//! `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).
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
const std = @import("std");
|
||||
const sc = @import("system-call.zig");
|
||||
const system = @import("system.zig");
|
||||
const heap = @import("heap.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.
|
||||
tid: u32,
|
||||
|
||||
pub const Id = u32;
|
||||
|
||||
pub const SpawnConfig = struct {
|
||||
/// Bytes of stack, rounded up to whole pages by the kernel's mmap.
|
||||
stack_size: usize = default_stack_size,
|
||||
};
|
||||
|
||||
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).
|
||||
SystemResources,
|
||||
};
|
||||
|
||||
/// Start `function(args...)` on a new thread sharing this address space. Mirrors
|
||||
/// `std.Thread.spawn`. The thread's return value is discarded (as in `std.Thread`);
|
||||
/// return data through shared state.
|
||||
pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread {
|
||||
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.
|
||||
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);
|
||||
exitThread();
|
||||
}
|
||||
};
|
||||
|
||||
const closure = heap.allocator().create(Closure) catch return error.OutOfMemory;
|
||||
errdefer heap.allocator().destroy(closure);
|
||||
closure.* = .{ .args = args };
|
||||
|
||||
// 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) };
|
||||
}
|
||||
};
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// The kernel returns a real (small) task id on success and a wrapped `-1` on failure;
|
||||
/// no valid task id ever exceeds a u32.
|
||||
inline fn threadSpawnFailed(ret: usize) bool {
|
||||
return ret > std.math.maxInt(u32);
|
||||
}
|
||||
|
||||
/// End the calling thread. Never returns.
|
||||
fn exitThread() noreturn {
|
||||
_ = sc.systemCall0(.thread_exit);
|
||||
unreachable;
|
||||
}
|
||||
Reference in New Issue
Block a user