threads(M2): thread_spawn/thread_exit + runtime.Thread.spawn
A thread is a task sharing the caller's address space. New private syscalls thread_spawn(entry, stack_top, arg)=37 and thread_exit=38: thread_spawn goes through scheduler.spawnThread (retains the shared aspace), thread_exit ends the task like a process exit(0) (terminateCurrent -> releaseAspace, so the space survives while siblings hold it). The closure pointer reaches the new thread in rdi via a new jump_to_user_arg asm path and a per-task user_arg (0 for a normal process, whose _start ignores it) - so the runtime trampoline is a plain C-ABI Zig function, no naked asm. runtime.Thread (library/runtime/thread.zig) mirrors std.Thread.spawn: mmap a stack, heap-allocate the args closure, hand the kernel the trampoline + closure. addThreadedUserBinary opts a binary into single_threaded=false; thread-test is the first, and proves a worker runs in the shared address space via a shared global the main thread polls. Gate thread-spawn PASS; 16 guardrail cases green (incl. args/init/process on the new jump_to_user_arg path) + aspace-refcount; build + host tests clean.
This commit is contained in:
+28
-18
@@ -120,29 +120,39 @@ full guardrail set passes unchanged — 13/13 (`smoke`, `sched`, `priority`, `sm
|
||||
`vfs-client-death`, `ipc`, `ipc-cap`, `display-service`); default `zig build` clean,
|
||||
`zig build test` green. The reframing is invisible until an aspace is actually shared.
|
||||
|
||||
## M2 — `thread_spawn` + `thread_exit`: a thread runs in the shared address space
|
||||
## M2 — `thread_spawn` + `thread_exit`: a thread runs in the shared address space ✅
|
||||
|
||||
Spawn only — no join yet. Prove a second task executes in the **caller's** address
|
||||
space and exits cleanly.
|
||||
|
||||
- [ ] [abi.zig](../system/abi.zig): `thread_spawn = 37`, `thread_exit = 38`. Handlers
|
||||
in process.zig; `thread_spawn` calls the `spawnUserLocked` path with the caller's
|
||||
aspace (`retainAspace`), `entry`, `user_sp`; `thread_exit` marks the task dead,
|
||||
hands back its user-stack range, and `releaseAspace`s.
|
||||
- [ ] `library/runtime/thread.zig` (barrel-exported as `runtime.Thread`): `spawn` maps
|
||||
a stack (`mmap`), heap-allocates the closure `{fn, args, done}`, writes the
|
||||
closure pointer to the stack top, calls `thread_spawn(&threadTrampoline, top,
|
||||
closure)`. `threadTrampoline` reads the closure, calls the function, calls
|
||||
`thread_exit`.
|
||||
- [ ] `addUserBinary` gains `threaded: bool = false` → `single_threaded = false`; a
|
||||
`thread-test` service (threaded) is the first opt-in binary.
|
||||
- [ ] `-Dtest-case=thread-spawn`: the parent spawns one thread that writes a sentinel
|
||||
to a **shared** global and sets `done`; the parent bounded-polls `done`, then
|
||||
asserts the shared global holds the sentinel (same address space) and frames
|
||||
return to baseline (thread exit released nothing extra — refcount held).
|
||||
- [x] [abi.zig](../system/abi.zig): `thread_spawn = 37`, `thread_exit = 38`. Handlers in
|
||||
process.zig; `thread_spawn` calls `scheduler.spawnThread` (shares the caller's
|
||||
aspace, `retainAspace`); `thread_exit` ends the task like a process `exit(0)`
|
||||
(`terminateCurrent` → `releaseAspace`). The closure pointer is delivered in the new
|
||||
thread's **rdi** via a new `jump_to_user_arg` asm path (`t.user_arg`, 0 for a
|
||||
process) — no naked runtime asm.
|
||||
- [x] `library/runtime/thread.zig` (barrel-exported as `runtime.Thread`): `spawn` maps a
|
||||
stack (`mmap`), heap-allocates the `{args}` closure, and calls
|
||||
`thread_spawn(&Closure.entry, stack_top, closure)`; `Closure.entry` (a plain C-ABI
|
||||
Zig fn, closure in rdi) runs the function and calls `thread_exit`. Stack top is
|
||||
16-aligned-minus-8 for the C entry.
|
||||
- [x] A `threaded` flag on the user-binary recipe (`addThreadedUserBinary` →
|
||||
`single_threaded = false`); `thread-test` is the first opt-in binary.
|
||||
- [x] `-Dtest-case=thread-spawn`: `thread-test` spawns a worker that writes a sentinel to
|
||||
a **shared** global and release-stores `done`; the main thread acquire-polls `done`
|
||||
and asserts the shared global holds the sentinel — proof the worker ran in the same
|
||||
address space.
|
||||
|
||||
**Gate:** `python3 test/qemu_test.py thread-spawn` logs `thread: child ran in shared
|
||||
aspace ok`; guardrail set green.
|
||||
**Gate (met):** `python3 test/qemu_test.py thread-spawn` passes
|
||||
(`thread-test: child ran in shared aspace ok` → `DANOS-TEST-RESULT: PASS`); guardrail set
|
||||
16/16 green (incl. `args`/`init`/`process`, which exercise the new `jump_to_user_arg`
|
||||
process path with arg 0) plus `aspace-refcount`; `zig build` clean, `zig build test`
|
||||
green.
|
||||
|
||||
> **Note (deferred to M3+):** the mmap arena is per-*task* (`heap_next`), so two threads
|
||||
> in one aspace that both `mmap` would collide. Fine for M2 (only the parent maps, for the
|
||||
> child's stack); make the arena per-aspace and the runtime heap thread-safe alongside the
|
||||
> `Mutex` work (M5).
|
||||
|
||||
## M3 — `join` + `detach` + real parallelism
|
||||
|
||||
|
||||
Reference in New Issue
Block a user