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
+28 -13
View File
@@ -397,21 +397,36 @@ guardrail 26/26 (incl. `process-kill`, `supervision`, `fault-recovery`, `task-re
> translate/unmap in a not-currently-loaded aspace — real complexity for a bounded leak.
> A follow-up when a consumer needs it.
### M10 — Per-thread TLS (`threadlocal`)
### M10 — Per-thread TLS: the `fs.base` mechanism ✅
Give each thread its own `threadlocal` storage — the piece self-hosting Zig
([zig-self-hosting.md](zig-self-hosting.md)) will force:
Give each thread its own thread pointer and private TLS storage — the foundation
self-hosting Zig ([zig-self-hosting.md](zig-self-hosting.md)) will build `threadlocal` on.
- [ ] **Runtime** allocates a per-thread TLS block from the binary's `PT_TLS` template
(linker symbols: copy `.tdata`, zero `.tbss`, variant-II TCB self-pointer) and hands
its thread pointer to `thread_spawn`; the main thread sets its own via a new
`set_thread_pointer` syscall in `_start`. The block is aspace memory → reclaimed on
teardown.
- [ ] **Kernel** stores `fs_base` on `Task`, loads it at first entry and restores it on
context switch only when it changes (the same conditional-load pattern as CR3).
`getCurrentId` can then read a TLS self-slot instead of a syscall.
- [ ] `-Dtest-case=thread-tls`: two threads each write and read their own `threadlocal`
slot with no cross-talk, and observe distinct `getCurrentId`.
- [x] **Kernel** stores `fs_base` on `Task` and restores it on every context switch
**only when it changes** (the same conditional-load discipline as CR3;
`architecture.setFsBase` → `wrmsr IA32_FS_BASE`). A `set_thread_pointer(addr)` = 44
syscall sets the caller's `fs_base` and loads it now. The kernel never touches FS, so
there is no swapgs complication.
- [x] **Runtime** lays a small per-thread TLS block at the top of each thread's stack
(self-pointer at `%fs:0` + scratch slots) 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.
- [x] `-Dtest-case=thread-tls` (`smp: 4`): two threads each write a unique marker to their
own `%fs:8` slot and — after both have written — read it back; a shared (non-per-thread)
fs.base would clobber one and cause cross-talk. Both read their own marker → pass.
**Gate (met):** `thread-tls` passes (3×); full guardrail 25/25 (the switch-time `fs.base`
restore touches every context switch); `zig build`/`zig build test` clean.
> **Deferred: the Zig `threadlocal` *compiler* layer.** Real `threadlocal` variables need
> the ELF **variant-II TLS** surface — `.tdata`/`.tbss` sections + a `PT_TLS` program header
> in `user.ld`, a runtime that copies the template with exact negative-offset layout, and
> the `.large`-code-model TLS section names — a high-uncertainty lift for a feature with
> **no consumer today** (threading.md scopes it "only if a consumer needs it"). What lands
> here is the load-bearing piece — per-thread `fs.base`, context-switched — so adding the
> compiler layer later is purely runtime+linker work on top, no kernel change. `getCurrentId`
> stays the `thread_self` syscall (M6) rather than an fs self-slot (which would need the
> main thread's TLS set up in `_start` too).
**Gate:** `thread-tls` passes; full `thread-*` suite + guardrail green.