danos/library/runtime/thread.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;
}