90 lines
3.8 KiB
Zig
90 lines
3.8 KiB
Zig
//! `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;
|
|
}
|