threads(M10): per-thread fs.base — the TLS thread-pointer mechanism

Each thread gets its own x86_64 thread pointer (FS base) for user-space TLS.
Task.fs_base is restored on every context switch only when it changes (same
conditional-load discipline as CR3; architecture.setFsBase -> wrmsr
IA32_FS_BASE). New set_thread_pointer=44 syscall sets the caller's fs_base and
loads it now. The kernel never touches FS, so no swapgs complication.

The runtime lays a small per-thread TLS block at the top of each thread's stack
(self-pointer at %fs:0 + scratch) and the thread trampoline calls
set_thread_pointer before any user code — so every spawned thread has a private,
switch-stable thread pointer, reclaimed with the stack.

thread-test tls mode: two threads write unique markers to their own %fs:8 and,
after both wrote, read back — a shared fs.base would clobber one (cross-talk).

Deferred: the Zig threadlocal *compiler* layer (ELF variant-II PT_TLS + linker
sections + template copy) — high-uncertainty, no consumer today; this lands the
load-bearing per-thread fs.base it builds on. See docs/threading-plan.md M10.

Gate thread-tls PASS (3x); full guardrail 25/25; build + host tests clean.
This commit is contained in:
2026-07-20 23:55:51 +01:00
parent 6bc329456a
commit c7e9b5a4f6
9 changed files with 207 additions and 21 deletions
+25 -8
View File
@@ -18,6 +18,10 @@ const system = @import("system.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;
/// Bytes reserved at the top of each thread's stack for its per-thread TLS block (the
/// self-pointer plus scratch slots reachable via `%fs`). docs/threading-plan.md M10.
const tls_block_size: usize = 64;
pub const Thread = struct {
/// The kernel task id of the spawned thread — what `join` waits on.
tid: u32,
@@ -43,11 +47,13 @@ pub const Thread = struct {
pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread {
const Args = @TypeOf(args);
const Closure = struct {
tls_base: usize,
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). Establishes this
/// thread's TLS pointer, runs the user function, then ends the thread.
fn entry(self_addr: usize) callconv(.c) noreturn {
const self: *@This() = @ptrFromInt(self_addr);
setThreadPointer(self.tls_base); // per-thread FS base before any user code
@call(.auto, function, self.args);
exitThread();
}
@@ -56,15 +62,21 @@ pub const Thread = struct {
const base = system.mmap(config.stack_size, system.PROT_READ | system.PROT_WRITE);
if (system.mmapFailed(base)) return error.SystemResources;
// 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.
// Top of the thread's own stack, downward: the closure, then a small per-thread TLS
// block (fs.base points here; slot 0 is the variant-II self-pointer, the rest is
// scratch for user TLS), then the stack proper (rsp starts below the TLS block, so
// the growing stack never overwrites either).
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
const tls_base = (closure_addr - tls_block_size) & ~@as(usize, 15);
const tls: [*]usize = @ptrFromInt(tls_base);
tls[0] = tls_base; // self-pointer (fs:0), as the x86_64 TLS ABI expects
const closure: *Closure = @ptrFromInt(closure_addr);
closure.* = .{ .tls_base = tls_base, .args = args };
var stack_top = tls_base & ~@as(usize, 15); // 16-align below the TLS block
stack_top -= 8; // ...then rsp % 16 == 8 at the C entry
const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, closure_addr);
@@ -240,6 +252,11 @@ fn exitThread() noreturn {
unreachable;
}
/// Set the calling thread's FS base (its user TLS thread pointer).
fn setThreadPointer(addr: usize) void {
_ = sc.systemCall1(.set_thread_pointer, addr);
}
/// futex_wait(addr, expect, timeout_ns) -> status (abi.futex_*).
fn futexWait(addr: usize, expect: u32, timeout_ns: u64) usize {
return sc.systemCall3(.futex_wait, addr, expect, timeout_ns);