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:
+28
-13
@@ -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.
|
> translate/unmap in a not-currently-loaded aspace — real complexity for a bounded leak.
|
||||||
> A follow-up when a consumer needs it.
|
> 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
|
Give each thread its own thread pointer and private TLS storage — the foundation
|
||||||
([zig-self-hosting.md](zig-self-hosting.md)) will force:
|
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
|
- [x] **Kernel** stores `fs_base` on `Task` and restores it on every context switch
|
||||||
(linker symbols: copy `.tdata`, zero `.tbss`, variant-II TCB self-pointer) and hands
|
**only when it changes** (the same conditional-load discipline as CR3;
|
||||||
its thread pointer to `thread_spawn`; the main thread sets its own via a new
|
`architecture.setFsBase` → `wrmsr IA32_FS_BASE`). A `set_thread_pointer(addr)` = 44
|
||||||
`set_thread_pointer` syscall in `_start`. The block is aspace memory → reclaimed on
|
syscall sets the caller's `fs_base` and loads it now. The kernel never touches FS, so
|
||||||
teardown.
|
there is no swapgs complication.
|
||||||
- [ ] **Kernel** stores `fs_base` on `Task`, loads it at first entry and restores it on
|
- [x] **Runtime** lays a small per-thread TLS block at the top of each thread's stack
|
||||||
context switch only when it changes (the same conditional-load pattern as CR3).
|
(self-pointer at `%fs:0` + scratch slots) and the thread trampoline calls
|
||||||
`getCurrentId` can then read a TLS self-slot instead of a syscall.
|
`set_thread_pointer` before any user code — so every spawned thread has a private,
|
||||||
- [ ] `-Dtest-case=thread-tls`: two threads each write and read their own `threadlocal`
|
switch-stable thread pointer. Reclaimed with the stack.
|
||||||
slot with no cross-talk, and observe distinct `getCurrentId`.
|
- [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.
|
**Gate:** `thread-tls` passes; full `thread-*` suite + guardrail green.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
/// 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 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 {
|
pub const Thread = struct {
|
||||||
/// The kernel task id of the spawned thread — what `join` waits on.
|
/// The kernel task id of the spawned thread — what `join` waits on.
|
||||||
tid: u32,
|
tid: u32,
|
||||||
@@ -43,11 +47,13 @@ pub const Thread = struct {
|
|||||||
pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread {
|
pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread {
|
||||||
const Args = @TypeOf(args);
|
const Args = @TypeOf(args);
|
||||||
const Closure = struct {
|
const Closure = struct {
|
||||||
|
tls_base: usize,
|
||||||
args: Args,
|
args: Args,
|
||||||
/// Entered directly by the kernel with `self` in rdi (C ABI). Runs the user
|
/// Entered directly by the kernel with `self` in rdi (C ABI). Establishes this
|
||||||
/// function, then ends the thread — never returns.
|
/// thread's TLS pointer, runs the user function, then ends the thread.
|
||||||
fn entry(self_addr: usize) callconv(.c) noreturn {
|
fn entry(self_addr: usize) callconv(.c) noreturn {
|
||||||
const self: *@This() = @ptrFromInt(self_addr);
|
const self: *@This() = @ptrFromInt(self_addr);
|
||||||
|
setThreadPointer(self.tls_base); // per-thread FS base before any user code
|
||||||
@call(.auto, function, self.args);
|
@call(.auto, function, self.args);
|
||||||
exitThread();
|
exitThread();
|
||||||
}
|
}
|
||||||
@@ -56,15 +62,21 @@ pub const Thread = struct {
|
|||||||
const base = system.mmap(config.stack_size, system.PROT_READ | system.PROT_WRITE);
|
const base = system.mmap(config.stack_size, system.PROT_READ | system.PROT_WRITE);
|
||||||
if (system.mmapFailed(base)) return error.SystemResources;
|
if (system.mmapFailed(base)) return error.SystemResources;
|
||||||
|
|
||||||
// Lay the closure at the very top of the thread's own stack, then start the
|
// Top of the thread's own stack, downward: the closure, then a small per-thread TLS
|
||||||
// thread's rsp just below it (16-aligned minus 8, the alignment a `call` leaves
|
// block (fs.base points here; slot 0 is the variant-II self-pointer, the rest is
|
||||||
// for a C-ABI entry) so the growing stack never overwrites the args.
|
// 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);
|
var closure_addr = (base + config.stack_size) - @sizeOf(Closure);
|
||||||
closure_addr &= ~@as(usize, @alignOf(Closure) - 1); // align the closure down
|
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
|
stack_top -= 8; // ...then rsp % 16 == 8 at the C entry
|
||||||
|
|
||||||
const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, closure_addr);
|
const tid = threadSpawn(@intFromPtr(&Closure.entry), stack_top, closure_addr);
|
||||||
@@ -240,6 +252,11 @@ fn exitThread() noreturn {
|
|||||||
unreachable;
|
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_*).
|
/// futex_wait(addr, expect, timeout_ns) -> status (abi.futex_*).
|
||||||
fn futexWait(addr: usize, expect: u32, timeout_ns: u64) usize {
|
fn futexWait(addr: usize, expect: u32, timeout_ns: u64) usize {
|
||||||
return sc.systemCall3(.futex_wait, addr, expect, timeout_ns);
|
return sc.systemCall3(.futex_wait, addr, expect, timeout_ns);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ pub const SystemCall = enum(u64) {
|
|||||||
futex_wake = 41, // futex_wake(addr, count) -> woken: wake up to `count` tasks blocked in futex_wait on `addr` in this address space
|
futex_wake = 41, // futex_wake(addr, count) -> woken: wake up to `count` tasks blocked in futex_wait on `addr` in this address space
|
||||||
thread_self = 42, // thread_self() -> tid: the calling thread's kernel task id (runtime.Thread.getCurrentId)
|
thread_self = 42, // thread_self() -> tid: the calling thread's kernel task id (runtime.Thread.getCurrentId)
|
||||||
thread_join = 43, // thread_join(tid) -> 0: block until the thread with id `tid` has exited (runtime.Thread.join; no per-thread IPC endpoint) (docs/threading.md)
|
thread_join = 43, // thread_join(tid) -> 0: block until the thread with id `tid` has exited (runtime.Thread.join; no per-thread IPC endpoint) (docs/threading.md)
|
||||||
|
set_thread_pointer = 44, // set_thread_pointer(addr) -> 0: set the caller's FS base (x86_64 user TLS thread pointer); restored per task across context switches (docs/threading-plan.md M10)
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -304,6 +304,15 @@ pub fn cpuLocal() usize {
|
|||||||
return pcpu.scheduler();
|
return pcpu.scheduler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ia32_fs_base = 0xC000_0100;
|
||||||
|
|
||||||
|
/// Set the FS-segment base — the x86_64 thread pointer for user-space TLS. The kernel
|
||||||
|
/// never touches FS, so this only affects the user task that runs next; the scheduler
|
||||||
|
/// restores it per task across context switches (docs/threading-plan.md M10).
|
||||||
|
pub fn setFsBase(base: u64) void {
|
||||||
|
io.wrmsr(ia32_fs_base, base);
|
||||||
|
}
|
||||||
|
|
||||||
// --- SMP: application-processor bring-up ----------------------------------
|
// --- SMP: application-processor bring-up ----------------------------------
|
||||||
|
|
||||||
/// Record the low (<1 MiB) frame reserved for the AP trampoline. Run once at boot.
|
/// Record the low (<1 MiB) frame reserved for the AP trampoline. Run once at boot.
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.current_core => systemCurrentCore(state),
|
.current_core => systemCurrentCore(state),
|
||||||
.thread_self => systemThreadSelf(state),
|
.thread_self => systemThreadSelf(state),
|
||||||
.thread_join => systemThreadJoin(state),
|
.thread_join => systemThreadJoin(state),
|
||||||
|
.set_thread_pointer => systemSetThreadPointer(state),
|
||||||
.futex_wait => systemFutexWait(state),
|
.futex_wait => systemFutexWait(state),
|
||||||
.futex_wake => systemFutexWake(state),
|
.futex_wake => systemFutexWake(state),
|
||||||
.thread_exit => {
|
.thread_exit => {
|
||||||
@@ -707,6 +708,20 @@ fn systemThreadSelf(state: *architecture.CpuState) void {
|
|||||||
architecture.setSystemCallResult(state, scheduler.currentId());
|
architecture.setSystemCallResult(state, scheduler.currentId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// set_thread_pointer(addr) -> 0: set the caller's FS base (its user-space TLS thread
|
||||||
|
/// pointer). The kernel never uses FS; the scheduler restores this per task across context
|
||||||
|
/// switches (docs/threading-plan.md M10). `addr` must be a user-half address.
|
||||||
|
fn systemSetThreadPointer(state: *architecture.CpuState) void {
|
||||||
|
const addr = architecture.systemCallArg(state, 0);
|
||||||
|
const t = scheduler.current();
|
||||||
|
if (t.aspace == 0) return fail(state); // kernel tasks have no user TLS
|
||||||
|
if (addr >= user_half_end) return fail(state);
|
||||||
|
const flags = sync.enter();
|
||||||
|
scheduler.setThreadPointerLocked(addr);
|
||||||
|
sync.leave(flags);
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/// thread_join(tid) -> 0: block until the thread with id `tid` has exited (docs/threading-
|
/// thread_join(tid) -> 0: block until the thread with id `tid` has exited (docs/threading-
|
||||||
/// plan.md M9). Needs no per-thread IPC endpoint. The compare-and-block is one critical
|
/// plan.md M9). Needs no per-thread IPC endpoint. The compare-and-block is one critical
|
||||||
/// section, so an exit cannot slip between "is it alive?" and the block.
|
/// section, so an exit cannot slip between "is it alive?" and the block.
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ pub const Task = struct {
|
|||||||
// The task id this task is blocked in `thread_join` on (0 = not joining). Woken by
|
// The task id this task is blocked in `thread_join` on (0 = not joining). Woken by
|
||||||
// `wakeJoinersLocked` when that task exits (docs/threading-plan.md M9).
|
// `wakeJoinersLocked` when that task exits (docs/threading-plan.md M9).
|
||||||
join_target: u32 = 0,
|
join_target: u32 = 0,
|
||||||
|
// The FS-base (x86_64 thread pointer) for this task's user-space TLS — 0 until the
|
||||||
|
// task sets it via `set_thread_pointer`. Restored on every context switch to this task
|
||||||
|
// (docs/threading-plan.md M10).
|
||||||
|
fs_base: u64 = 0,
|
||||||
// The mmap / MMIO grant-arena cursors moved from Task to the per-address-space object
|
// The mmap / MMIO grant-arena cursors moved from Task to the per-address-space object
|
||||||
// (`AspaceRef`, below) so threads sharing one address space hand out disjoint grants
|
// (`AspaceRef`, below) so threads sharing one address space hand out disjoint grants
|
||||||
// — see aspaceMmapNextPtr / aspaceDeviceMapNextPtr (docs/threading-plan.md M7).
|
// — see aspaceMmapNextPtr / aspaceDeviceMapNextPtr (docs/threading-plan.md M7).
|
||||||
@@ -284,6 +288,7 @@ pub const PerCpu = struct {
|
|||||||
index: u32 = 0, // dense 0-based core index
|
index: u32 = 0, // dense 0-based core index
|
||||||
online: bool = false, // has this core finished bring-up?
|
online: bool = false, // has this core finished bring-up?
|
||||||
loaded_aspace: u64 = 0, // the address-space root currently loaded on this core
|
loaded_aspace: u64 = 0, // the address-space root currently loaded on this core
|
||||||
|
loaded_fs_base: u64 = 0, // the FS base currently loaded on this core (docs/threading-plan.md M10)
|
||||||
// Tasks pinned to this core (affinity == index), per priority level + bitmap.
|
// Tasks pinned to this core (affinity == index), per priority level + bitmap.
|
||||||
pinned_head: [number_priorities]?*Task = .{null} ** number_priorities,
|
pinned_head: [number_priorities]?*Task = .{null} ** number_priorities,
|
||||||
pinned_tail: [number_priorities]?*Task = .{null} ** number_priorities,
|
pinned_tail: [number_priorities]?*Task = .{null} ** number_priorities,
|
||||||
@@ -568,6 +573,12 @@ fn switchTo(pc: *PerCpu, save_sp: *usize, next: *Task) void {
|
|||||||
architecture.loadPageTable(want);
|
architecture.loadPageTable(want);
|
||||||
pc.loaded_aspace = want;
|
pc.loaded_aspace = want;
|
||||||
}
|
}
|
||||||
|
// Restore the next task's user TLS thread pointer (FS base) — only on change, the same
|
||||||
|
// conditional-load discipline as CR3 above (docs/threading-plan.md M10).
|
||||||
|
if (next.fs_base != pc.loaded_fs_base) {
|
||||||
|
architecture.setFsBase(next.fs_base);
|
||||||
|
pc.loaded_fs_base = next.fs_base;
|
||||||
|
}
|
||||||
architecture.switchContext(save_sp, next.sp);
|
architecture.switchContext(save_sp, next.sp);
|
||||||
// Resumed now (switchContext returned into our own switchTo frame). Re-fetch the core
|
// Resumed now (switchContext returned into our own switchTo frame). Re-fetch the core
|
||||||
// via thisCpu(): the `pc` parameter is from *our* earlier switchTo call, so it names
|
// via thisCpu(): the `pc` parameter is from *our* earlier switchTo call, so it names
|
||||||
@@ -667,6 +678,16 @@ pub fn joinThreadLocked(tid: u32) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the calling task's user TLS thread pointer (FS base) and load it now. Persisted on
|
||||||
|
/// the Task so context switches restore it (docs/threading-plan.md M10). Caller holds the
|
||||||
|
/// kernel lock.
|
||||||
|
pub fn setThreadPointerLocked(addr: u64) void {
|
||||||
|
const pc = thisCpu();
|
||||||
|
pc.current.fs_base = addr;
|
||||||
|
architecture.setFsBase(addr);
|
||||||
|
pc.loaded_fs_base = addr;
|
||||||
|
}
|
||||||
|
|
||||||
/// Wake every task blocked in `thread_join` on `tid` — called from the exit paths once the
|
/// Wake every task blocked in `thread_join` on `tid` — called from the exit paths once the
|
||||||
/// exiting task's state is `.free`. Caller holds the lock.
|
/// exiting task's state is `.free`. Caller holds the lock.
|
||||||
fn wakeJoinersLocked(tid: u32) void {
|
fn wakeJoinersLocked(tid: u32) void {
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
threadAllocTest(boot_information);
|
threadAllocTest(boot_information);
|
||||||
} else if (eql(case, "task-reap")) {
|
} else if (eql(case, "task-reap")) {
|
||||||
taskReapTest(boot_information);
|
taskReapTest(boot_information);
|
||||||
|
} else if (eql(case, "thread-tls")) {
|
||||||
|
threadTlsTest(boot_information);
|
||||||
} else if (eql(case, "args")) {
|
} else if (eql(case, "args")) {
|
||||||
argsTest(boot_information);
|
argsTest(boot_information);
|
||||||
} else if (eql(case, "init")) {
|
} else if (eql(case, "init")) {
|
||||||
@@ -1740,6 +1742,49 @@ fn threadAllocTest(boot_information: *const BootInformation) void {
|
|||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-thread TLS / fs.base (docs/threading-plan.md M10): `thread-test` in tls mode has two
|
||||||
|
/// threads each set their own FS base and write a unique marker to `%fs:8`, then — after
|
||||||
|
/// both have written — read it back. If fs.base were not per-thread and restored across
|
||||||
|
/// context switches, the second write would clobber the first and a thread would read the
|
||||||
|
/// wrong marker. The verdict marker means both read their own value (no cross-talk).
|
||||||
|
fn threadTlsTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: thread-tls\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
var started = false;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(item.name, "thread-test")) continue;
|
||||||
|
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "tls" })) true else |_| false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("thread-test (tls mode) spawned", started);
|
||||||
|
|
||||||
|
const ok_marker = "thread-tls: ok";
|
||||||
|
const fail_marker = "thread-tls: FAIL";
|
||||||
|
scheduler.setPriority(1);
|
||||||
|
const deadline = architecture.millis() + 12000;
|
||||||
|
while (architecture.millis() < deadline) {
|
||||||
|
if (bufferHas(ok_marker) or bufferHas(fail_marker)) break;
|
||||||
|
scheduler.yield();
|
||||||
|
}
|
||||||
|
scheduler.setPriority(4);
|
||||||
|
|
||||||
|
check("each thread has its own fs.base TLS slot (no cross-talk across switches)", bufferHas(ok_marker) and !bufferHas(fail_marker));
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
/// The task reaper (docs/threading-plan.md M8): a dead task's kernel stack used to be
|
/// The task reaper (docs/threading-plan.md M8): a dead task's kernel stack used to be
|
||||||
/// leaked ("no reaper yet"). Spawn and kill many ring-3 processes and confirm the total
|
/// leaked ("no reaper yet"). Spawn and kill many ring-3 processes and confirm the total
|
||||||
/// kernel-stack bytes return to baseline — every stack reclaimed, no leak. (Threads exit
|
/// kernel-stack bytes return to baseline — every stack reclaimed, no leak. (Threads exit
|
||||||
|
|||||||
@@ -357,6 +357,59 @@ fn runAllocMode() void {
|
|||||||
write("thread-alloc: ok\n"); // the M7 verdict marker
|
write("thread-alloc: ok\n"); // the M7 verdict marker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- M10: tls mode (per-thread fs.base storage) -----------------------------
|
||||||
|
|
||||||
|
fn writeTlsSlot(value: u64) void {
|
||||||
|
asm volatile ("movq %[v], %%fs:8"
|
||||||
|
:
|
||||||
|
: [v] "r" (value),
|
||||||
|
: .{ .memory = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readTlsSlot() u64 {
|
||||||
|
return asm volatile ("movq %%fs:8, %[out]"
|
||||||
|
: [out] "=r" (-> u64),
|
||||||
|
:
|
||||||
|
: .{ .memory = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
var tls_written = std.atomic.Value(u32).init(0);
|
||||||
|
var tls_ok = std.atomic.Value(u32).init(0);
|
||||||
|
|
||||||
|
fn tlsWorker(marker: u64) void {
|
||||||
|
writeTlsSlot(marker);
|
||||||
|
_ = tls_written.fetchAdd(1, .release);
|
||||||
|
// Wait until both threads have written their own slot. If fs.base were shared, the
|
||||||
|
// second write would clobber the first, and the read below would return the wrong
|
||||||
|
// marker — cross-talk. Per-thread fs.base keeps each thread's slot private.
|
||||||
|
var spins: usize = 0;
|
||||||
|
while (tls_written.load(.acquire) < 2 and spins < 50_000_000) : (spins += 1) {
|
||||||
|
runtime.system.yield();
|
||||||
|
}
|
||||||
|
if (readTlsSlot() == marker and runtime.Thread.getCurrentId() != 0) {
|
||||||
|
_ = tls_ok.fetchAdd(1, .monotonic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runTlsMode() void {
|
||||||
|
write("thread-tls: starting\n");
|
||||||
|
const t0 = runtime.Thread.spawn(.{}, tlsWorker, .{@as(u64, 0xAAAA_0000)}) catch {
|
||||||
|
write("thread-tls: FAIL spawn\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const t1 = runtime.Thread.spawn(.{}, tlsWorker, .{@as(u64, 0xBBBB_0000)}) catch {
|
||||||
|
write("thread-tls: FAIL spawn\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
t0.join();
|
||||||
|
t1.join();
|
||||||
|
if (tls_ok.load(.acquire) == 2) {
|
||||||
|
write("thread-tls: ok\n"); // the M10 verdict marker
|
||||||
|
} else {
|
||||||
|
write("thread-tls: FAIL cross-talk (fs.base not per-thread)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main(init: runtime.process.Init) void {
|
pub fn main(init: runtime.process.Init) void {
|
||||||
const mode = init.arguments.get(1) orelse "spawn";
|
const mode = init.arguments.get(1) orelse "spawn";
|
||||||
if (std.mem.eql(u8, mode, "join")) {
|
if (std.mem.eql(u8, mode, "join")) {
|
||||||
@@ -369,6 +422,8 @@ pub fn main(init: runtime.process.Init) void {
|
|||||||
runIdMode();
|
runIdMode();
|
||||||
} else if (std.mem.eql(u8, mode, "alloc")) {
|
} else if (std.mem.eql(u8, mode, "alloc")) {
|
||||||
runAllocMode();
|
runAllocMode();
|
||||||
|
} else if (std.mem.eql(u8, mode, "tls")) {
|
||||||
|
runTlsMode();
|
||||||
} else {
|
} else {
|
||||||
runSpawnMode();
|
runSpawnMode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,6 +355,14 @@ CASES = [
|
|||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
|
||||||
|
# docs/threading-plan.md M10: per-thread fs.base — two threads keep private %fs:8 TLS
|
||||||
|
# slots across context switches (no cross-talk).
|
||||||
|
{"name": "thread-tls",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 60,
|
||||||
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# Process arguments: argv arrives on the SysV entry stack (argv[0] = the spawned
|
# Process arguments: argv arrives on the SysV entry stack (argv[0] = the spawned
|
||||||
# name, argv[1..] = the system_spawn argument blob) and echoes back intact.
|
# name, argv[1..] = the system_spawn argument blob) and echoes back intact.
|
||||||
{"name": "args",
|
{"name": "args",
|
||||||
|
|||||||
Reference in New Issue
Block a user