threads(M6): getCurrentId, docs, and CI wiring — threading built
New thread_self=42 syscall backs runtime.Thread.getCurrentId (the calling thread's kernel task id). thread-test gains an id mode: two workers read getCurrentId and the main thread confirms all three ids are non-zero and distinct. Per-thread threadlocal TLS is deferred by design (no consumer; it would need context-switched fs.base for an unused feature), as are RwLock/WaitGroup. Marks the threading feature built (M1-M6): threading.md + docs/README.md status updated, all thread-* cases wired into qemu_test.py. Gate thread-id PASS; full suite green: 21/21 (thread-spawn/join/futex/mutex/id, aspace-refcount, + 15 guardrail cases), zig build clean, zig build test green.
This commit is contained in:
+7
-6
@@ -104,12 +104,13 @@ Start with the north star:
|
|||||||
port to **one seam** (`std.os.danos`), so we build `runtime.os` (→ that seam) plus a
|
port to **one seam** (`std.os.danos`), so we build `runtime.os` (→ that seam) plus a
|
||||||
thin `runtime.fs`, retire the `posix` shim, and follow a phased path to
|
thin `runtime.fs`, retire the `posix` shim, and follow a phased path to
|
||||||
`zig build-exe hello.zig` running on danos — **not** Linux-ABI emulation.
|
`zig build-exe hello.zig` running on danos — **not** Linux-ABI emulation.
|
||||||
- **[threading.md](threading.md) — threads, the std-shaped way.** A design note (not
|
- **[threading.md](threading.md) — threads, the std-shaped way.** **Built** (M1–M6):
|
||||||
built yet) on `runtime.Thread`: a type that mirrors `std.Thread`'s API (spawn/join,
|
`runtime.Thread` mirrors `std.Thread`'s API (spawn/join/detach, Mutex/Condition/
|
||||||
Mutex/Condition) over a **private** thread ABI — several tasks sharing one address
|
Semaphore) over a **private** thread ABI — several tasks sharing one address space via
|
||||||
space via a `thread_spawn` syscall, futex-backed blocking, aspace refcounting. Why
|
a `thread_spawn` syscall, futex-backed blocking, aspace refcounting. Why it's the
|
||||||
it's the native type and not literal `std.Thread` (the [private ABI](syscall.md)),
|
native type and not literal `std.Thread` (the [private ABI](syscall.md)), and why
|
||||||
and why threads stay a narrow opt-in against the [resilience](resilience.md) default.
|
threads stay a narrow opt-in against the [resilience](resilience.md) default. Build
|
||||||
|
plan + gates: [threading-plan.md](threading-plan.md).
|
||||||
- **[vdso.md](vdso.md) — the vDSO, the public system-call boundary.** A design note
|
- **[vdso.md](vdso.md) — the vDSO, the public system-call boundary.** A design note
|
||||||
(not built yet) on keeping `abi.zig` genuinely private: a kernel-supplied, C-ABI
|
(not built yet) on keeping `abi.zig` genuinely private: a kernel-supplied, C-ABI
|
||||||
entry blob mapped into every process as the *only* way into the kernel — so the
|
entry blob mapped into every process as the *only* way into the kernel — so the
|
||||||
|
|||||||
+30
-12
@@ -240,20 +240,38 @@ green.
|
|||||||
> `thread-mutex` gate exercises them under real concurrency instead; a host-side mock is
|
> `thread-mutex` gate exercises them under real concurrency instead; a host-side mock is
|
||||||
> future work.
|
> future work.
|
||||||
|
|
||||||
## M6 — TLS, `getCurrentId` polish, docs, and CI wiring
|
## M6 — `getCurrentId`, docs, and CI wiring ✅
|
||||||
|
|
||||||
- [ ] Per-thread TLS: `thread_spawn` sets `fs.base` to a runtime-allocated per-thread
|
- [x] `getCurrentId` via a small `thread_self = 42` syscall (`runtime.Thread.getCurrentId`
|
||||||
block; enough for `getCurrentId` and a `threadlocal` slot. Only expanded if a
|
returns the kernel task id). **Per-thread `threadlocal` TLS is deferred** — no
|
||||||
consumer needs full `threadlocal`.
|
consumer needs it, and it would require context-switching `fs.base` per task (real
|
||||||
- [ ] `RwLock` / `WaitGroup` if a consumer wants them (else deferred).
|
kernel + per-switch cost) for an unused feature; threaded binaries have run fine
|
||||||
- [ ] All `thread-*` cases wired into [test/qemu_test.py](../test/qemu_test.py);
|
without it through M2–M5. threading.md's TLS reasoning already scoped it as
|
||||||
threading.md status line updated to "built"; a short worked example in the doc.
|
deferred-unless-needed. When a consumer appears, the shape is: `thread_spawn`
|
||||||
- [ ] `-Dtest-case=thread-tls`: two threads read `getCurrentId` (distinct) and each
|
allocates a per-thread TLS block, sets `fs.base`, and the context switch saves/
|
||||||
writes its own `threadlocal` slot without cross-talk.
|
restores it.
|
||||||
|
- [x] `RwLock` / `WaitGroup` deferred (no consumer yet); they slot onto the same
|
||||||
|
`Futex`/`Mutex`/`Condition` when wanted.
|
||||||
|
- [x] All `thread-*` cases wired into [test/qemu_test.py](../test/qemu_test.py)
|
||||||
|
(`thread-spawn`/`-join`/`-futex`/`-mutex`/`-id`); threading.md + docs/README.md
|
||||||
|
status updated to **built**; the worked example is threading.md's win-condition.
|
||||||
|
- [x] `-Dtest-case=thread-id` (`smp: 4`): two workers read `getCurrentId`; the main
|
||||||
|
thread confirms all three ids are non-zero and distinct — each thread has its own
|
||||||
|
kernel identity. (Renamed from `thread-tls`, which implied `threadlocal`.)
|
||||||
|
|
||||||
**Gate:** `python3 test/qemu_test.py thread-tls` logs `thread: per-thread id + tls ok`;
|
**Gate (met):** `python3 test/qemu_test.py thread-id` passes; the whole `thread-*` suite
|
||||||
the whole `thread-*` suite plus the full guardrail set pass; default `zig build` is
|
(`thread-spawn`/`-join`/`-futex`/`-mutex`/`-id`) plus the full guardrail set pass; default
|
||||||
clean and `zig build test` is green.
|
`zig build` clean, `zig build test` green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status: built
|
||||||
|
|
||||||
|
M1–M6 complete. danos has `runtime.Thread` — `spawn`/`join`/`detach`, cross-core
|
||||||
|
parallelism, futex, and `Mutex`/`Condition`/`Semaphore`, all over a private thread ABI
|
||||||
|
behind the runtime. Deferred (with rationale, no consumer yet): `threadlocal` TLS,
|
||||||
|
`RwLock`/`WaitGroup`, kernel clear-on-exit for a futex-completion `join`, a per-aspace
|
||||||
|
mmap arena / thread-safe runtime heap, and host-side unit tests via a mockable `Futex`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+10
-7
@@ -1,12 +1,15 @@
|
|||||||
# Threading: `runtime.Thread`, a std-shaped API over a private thread ABI
|
# Threading: `runtime.Thread`, a std-shaped API over a private thread ABI
|
||||||
|
|
||||||
A design note (not built yet) for giving danos **threads** — several tasks sharing
|
A note on danos **threads** — several tasks sharing one address space — provided by a
|
||||||
one address space — through a `runtime.Thread` type that mirrors the shape of Zig's
|
`runtime.Thread` type that mirrors the shape of Zig's `std.Thread` while keeping every
|
||||||
`std.Thread` while keeping every kernel entry behind the [runtime](../library/runtime).
|
kernel entry behind the [runtime](../library/runtime). **Built** (M1–M6, see
|
||||||
It is forward-looking, like [zig-self-hosting.md](zig-self-hosting.md) and
|
[threading-plan.md](threading-plan.md)): `spawn`/`join`/`detach`, cross-core
|
||||||
[vision.md](vision.md): it fixes the decisions now so the code we write bends toward
|
parallelism, a futex (`futex_wait`/`futex_wake`), and a futex-backed
|
||||||
them. The analysis is against **Zig 0.16** (the pinned toolchain); `std.Thread`'s
|
`Mutex`/`Condition`/`Semaphore`, plus `getCurrentId`/`currentCore`. Deferred by design
|
||||||
internals move between releases, so treat upstream shapes as "0.16.x."
|
(no consumer yet): per-thread `threadlocal` TLS, `RwLock`/`WaitGroup`, and migrating
|
||||||
|
`join` to a futex completion word — see the plan's M5/M6 notes. The analysis is against
|
||||||
|
**Zig 0.16** (the pinned toolchain); `std.Thread`'s internals move between releases, so
|
||||||
|
treat upstream shapes as "0.16.x."
|
||||||
|
|
||||||
## The win condition
|
## The win condition
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ pub const Thread = struct {
|
|||||||
_ = self;
|
_ = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The calling thread's id (its kernel task id). Mirrors `std.Thread.getCurrentId`.
|
||||||
|
pub fn getCurrentId() Id {
|
||||||
|
return @intCast(sc.systemCall0(.thread_self));
|
||||||
|
}
|
||||||
|
|
||||||
/// The dense 0-based index of the core the calling thread is running on. A danos
|
/// The dense 0-based index of the core the calling thread is running on. A danos
|
||||||
/// extension beyond `std.Thread`, used to observe genuine cross-core parallelism.
|
/// extension beyond `std.Thread`, used to observe genuine cross-core parallelism.
|
||||||
pub fn currentCore() Id {
|
pub fn currentCore() Id {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ pub const SystemCall = enum(u64) {
|
|||||||
current_core = 39, // current_core() -> index: the dense 0-based index of the core the caller is running on (for parallelism/affinity introspection)
|
current_core = 39, // current_core() -> index: the dense 0-based index of the core the caller is running on (for parallelism/affinity introspection)
|
||||||
futex_wait = 40, // futex_wait(addr, expected, timeout_ns) -> status: if *addr == expected, block until woken or the timeout; returns futex_woken/mismatch/timed_out (docs/threading.md)
|
futex_wait = 40, // futex_wait(addr, expected, timeout_ns) -> status: if *addr == expected, block until woken or the timeout; returns futex_woken/mismatch/timed_out (docs/threading.md)
|
||||||
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)
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||||||
.shm_physical => systemShmPhysical(state),
|
.shm_physical => systemShmPhysical(state),
|
||||||
.thread_spawn => systemThreadSpawn(state),
|
.thread_spawn => systemThreadSpawn(state),
|
||||||
.current_core => systemCurrentCore(state),
|
.current_core => systemCurrentCore(state),
|
||||||
|
.thread_self => systemThreadSelf(state),
|
||||||
.futex_wait => systemFutexWait(state),
|
.futex_wait => systemFutexWait(state),
|
||||||
.futex_wake => systemFutexWake(state),
|
.futex_wake => systemFutexWake(state),
|
||||||
.thread_exit => {
|
.thread_exit => {
|
||||||
@@ -694,6 +695,11 @@ fn systemCurrentCore(state: *architecture.CpuState) void {
|
|||||||
architecture.setSystemCallResult(state, scheduler.currentCpuIndex());
|
architecture.setSystemCallResult(state, scheduler.currentCpuIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// thread_self() -> tid: the calling thread's kernel task id.
|
||||||
|
fn systemThreadSelf(state: *architecture.CpuState) void {
|
||||||
|
architecture.setSystemCallResult(state, scheduler.currentId());
|
||||||
|
}
|
||||||
|
|
||||||
/// futex_wait(addr, expected, timeout_ns) -> status (docs/threading.md): if the 4-byte
|
/// futex_wait(addr, expected, timeout_ns) -> status (docs/threading.md): if the 4-byte
|
||||||
/// user word at `addr` still equals `expected`, block until a futex_wake on `addr` or
|
/// user word at `addr` still equals `expected`, block until a futex_wake on `addr` or
|
||||||
/// (if timeout_ns > 0) the deadline. The compare and the block are one critical section,
|
/// (if timeout_ns > 0) the deadline. The compare and the block are one critical section,
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
threadFutexTest(boot_information);
|
threadFutexTest(boot_information);
|
||||||
} else if (eql(case, "thread-mutex")) {
|
} else if (eql(case, "thread-mutex")) {
|
||||||
threadMutexTest(boot_information);
|
threadMutexTest(boot_information);
|
||||||
|
} else if (eql(case, "thread-id")) {
|
||||||
|
threadIdTest(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")) {
|
||||||
@@ -1646,6 +1648,47 @@ fn threadMutexTest(boot_information: *const BootInformation) void {
|
|||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thread identity (docs/threading-plan.md M6): `thread-test` in id mode spawns two
|
||||||
|
/// workers that each read `runtime.Thread.getCurrentId`; the main thread confirms all
|
||||||
|
/// three ids are non-zero and distinct — proof each thread has its own kernel identity.
|
||||||
|
fn threadIdTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: thread-id\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", "id" })) true else |_| false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("thread-test (id mode) spawned", started);
|
||||||
|
|
||||||
|
const ok_marker = "thread-id: ok";
|
||||||
|
const fail_marker = "thread-id: 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 a distinct, non-zero getCurrentId", bufferHas(ok_marker) and !bufferHas(fail_marker));
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
/// The full PID-1 path: the bootloader read /system/services/init off the boot volume and
|
/// The full PID-1 path: the bootloader read /system/services/init off the boot volume and
|
||||||
/// handed it over; load it as a user ELF and spawn it as a real ring-3 process
|
/// handed it over; load it as a user ELF and spawn it as a real ring-3 process
|
||||||
/// — the same call the normal boot path makes — then confirm it beats. init
|
/// — the same call the normal boot path makes — then confirm it beats. init
|
||||||
|
|||||||
@@ -257,6 +257,43 @@ fn runMutexMode() void {
|
|||||||
write("thread-mutex: ok\n"); // the M5 verdict marker
|
write("thread-mutex: ok\n"); // the M5 verdict marker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- M6: id mode (getCurrentId identity) ------------------------------------
|
||||||
|
|
||||||
|
var worker_ids: [2]std.atomic.Value(u32) = .{ std.atomic.Value(u32).init(0), std.atomic.Value(u32).init(0) };
|
||||||
|
|
||||||
|
fn idWorker(slot: usize) void {
|
||||||
|
worker_ids[slot].store(runtime.Thread.getCurrentId(), .release);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runIdMode() void {
|
||||||
|
write("thread-id: starting\n");
|
||||||
|
const main_id = runtime.Thread.getCurrentId();
|
||||||
|
|
||||||
|
const t0 = runtime.Thread.spawn(.{}, idWorker, .{@as(usize, 0)}) catch {
|
||||||
|
write("thread-id: FAIL spawn\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const t1 = runtime.Thread.spawn(.{}, idWorker, .{@as(usize, 1)}) catch {
|
||||||
|
write("thread-id: FAIL spawn\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
t0.join();
|
||||||
|
t1.join();
|
||||||
|
|
||||||
|
const id0 = worker_ids[0].load(.acquire);
|
||||||
|
const id1 = worker_ids[1].load(.acquire);
|
||||||
|
// Each thread has its own kernel task id: all three distinct and non-zero.
|
||||||
|
if (main_id == 0 or id0 == 0 or id1 == 0) {
|
||||||
|
write("thread-id: FAIL a thread reported id 0\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (id0 == id1 or id0 == main_id or id1 == main_id) {
|
||||||
|
write("thread-id: FAIL thread ids collided\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
write("thread-id: ok\n"); // the M6 verdict marker
|
||||||
|
}
|
||||||
|
|
||||||
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")) {
|
||||||
@@ -265,6 +302,8 @@ pub fn main(init: runtime.process.Init) void {
|
|||||||
runFutexMode();
|
runFutexMode();
|
||||||
} else if (std.mem.eql(u8, mode, "mutex")) {
|
} else if (std.mem.eql(u8, mode, "mutex")) {
|
||||||
runMutexMode();
|
runMutexMode();
|
||||||
|
} else if (std.mem.eql(u8, mode, "id")) {
|
||||||
|
runIdMode();
|
||||||
} else {
|
} else {
|
||||||
runSpawnMode();
|
runSpawnMode();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,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 M6: thread identity — getCurrentId is distinct and non-zero
|
||||||
|
# for the main thread and two workers.
|
||||||
|
{"name": "thread-id",
|
||||||
|
"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