mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
shim: a forked child claims its own Worker slot; tests/x86-programs/thread-churn
A forked child's first instance claims a shim stack before gex_adopt_vma_blob has run, so arena_take floored that claim at the child's memory size: its parent's, grown over every inherited mapping. A mem64 parent with live threads has their stacks above 4 GiB, which put the floor over the arena's 2 GiB ceiling; the claim failed, and the child ran with no thread-locals (now: threw "no shim stack slot"). adoptProcess hands the image's region ends to gex_note_inherited before the claim, which keeps those under the ceiling as the floor. And gex_worker_slots_reset clears the slot bits first, so a child starts with only its own thread's slot whatever its memory carried. thread-churn (page leg): 100 threads created and joined in sequence, then a fork with 40 threads alive whose child runs 30 at once. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
This commit is contained in:
1 parent
cfd821bd6a
commit
5d7a80b260
4 files changed
+126
-2
No files matched your search
@@ -840,6 +840,10 @@ export class Gex {
|
||||
#kernel = null;
|
||||
// This Worker's shim stack, claimed on the way in. See `#workerStack`.
|
||||
#stackTop = null;
|
||||
/** This instance is a forked child's first: its slot bits are its parent's. */
|
||||
#forkChild = false;
|
||||
/** A forked child's inherited regions' ends, for its first arena claim. */
|
||||
#inheritedEnds = [];
|
||||
|
||||
/**
|
||||
* One turn of the event loop, to deliver messages that arrived while this
|
||||
@@ -962,7 +966,16 @@ export class Gex {
|
||||
// which there is the top of the 2 GiB.
|
||||
if (this.#windowsSpace > 0) this.#shim.exports.gex_adopt_windows_space?.(this.#windowsSpace);
|
||||
else if (this.#windowsSpace < 0) this.#shim.exports.gex_split_windows_space?.();
|
||||
const at = this.#shim.exports.gex_worker_init?.();
|
||||
// A forked child's memory carries its parent's slot bits (`adoptProcess`).
|
||||
if (this.#forkChild) {
|
||||
this.#forkChild = false;
|
||||
this.#shim.exports.gex_worker_slots_reset?.();
|
||||
// Where its inherited mappings end, before the claim below picks its
|
||||
// arena floor (`gex_note_inherited`).
|
||||
for (const end of this.#inheritedEnds) this.#shim.exports.gex_note_inherited?.(BigInt(end));
|
||||
this.#inheritedEnds = [];
|
||||
}
|
||||
const at = this.#shim.exports.gex_worker_init?.();
|
||||
// No slot: every one of the process's 64 is held by a live instance.
|
||||
// Running on would put this instance's thread-locals at address zero
|
||||
// (`__tls_base` unset), over the guest's memory.
|
||||
@@ -2411,6 +2424,8 @@ export class Gex {
|
||||
};
|
||||
step(`image copied, ${live()} non-zero bytes in the context`);
|
||||
this.#windowsSpace = child.windowsSpace ?? 0;
|
||||
this.#forkChild = true;
|
||||
this.#inheritedEnds = child.image.map((r) => Math.max(r.end ?? 0, r.at + r.bytes.byteLength));
|
||||
await this.attach(shimWasm, guestWasm);
|
||||
step(`instantiated, ${live()} non-zero bytes in the context`);
|
||||
const e = this.#shim.exports;
|
||||
|
||||
@@ -3484,6 +3484,32 @@ pub extern "C" fn gex_worker_init() -> u32 {
|
||||
|
||||
#[cfg(feature = "shared-memory")]
|
||||
const STACK_GUARD: u32 = 0x5747_5d0d;
|
||||
/// A forked child's first instance, before it claims: the memory it was
|
||||
/// given is its parent's, and so are the slot bits -- held by the parent's
|
||||
/// Workers, none of which run here. The child starts with every slot free
|
||||
/// (their arena spans stay, for reuse); a parent with many live threads
|
||||
/// otherwise left its child too few to start its own.
|
||||
#[cfg(feature = "shared-memory")]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_worker_slots_reset() {
|
||||
SLOT_USED.store(0, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
/// A forked child's inherited region ending at `end`, told before its first
|
||||
/// arena claim (`gex_worker_init` takes a stack there, before
|
||||
/// `gex_adopt_vma_blob` has run). Only regions under the arena's ceiling
|
||||
/// matter: the arena never hands out above it. Without this the first claim
|
||||
/// floored at the child's memory size -- its parent's, grown over every
|
||||
/// inherited mapping -- and a mem64 parent's threads' stacks above 4 GiB put
|
||||
/// that over the ceiling: the child got no slot and ran with no
|
||||
/// thread-locals.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_note_inherited(end: u64) {
|
||||
if end <= nt::arena_ceiling() {
|
||||
ADOPTED_TOP.fetch_max(end, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Give this instance's slot back (`gex_worker_init`): the last call the
|
||||
/// Worker makes into this instance, once its run is over or it trapped. The
|
||||
/// next instance to claim the slot runs on the same stack, so nothing of
|
||||
|
||||
@@ -25,7 +25,9 @@ SEED64=web/public/gex/seed.x86.m64.shared.wasm
|
||||
# function JIT livelocks on its chain natively (gexc --fjit, 2026-10-12).
|
||||
# clock-monotonic: the page's CLOCK_MONOTONIC never goes back, within a thread
|
||||
# or between two Workers, and is finer than a millisecond.
|
||||
PAGE_PROGRAMS=${PAGE_PROGRAMS:-"tgkill-futex exec-tail mem64-io mem64-smc mem64-reserve retire-during-lift clock-monotonic"}
|
||||
# thread-churn: more threads over a process's life than the page has Worker
|
||||
# slots, and a fork child starting threads while its parent holds 40.
|
||||
PAGE_PROGRAMS=${PAGE_PROGRAMS:-"tgkill-futex exec-tail mem64-io mem64-smc mem64-reserve retire-during-lift clock-monotonic thread-churn"}
|
||||
for src in "$HERE"/*.c; do
|
||||
name=$(basename "$src" .c)
|
||||
# mem64-*: a 64-bit guest memory (docs/mem64.md), x86-64 only.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* More threads over a process's life than the page has Worker slots (64):
|
||||
* 100 threads created and joined one after another, each using a
|
||||
* thread-local; then, with 40 threads alive, a fork whose child runs 30
|
||||
* threads at once. On the page every guest thread is a shim instance with a
|
||||
* stack and thread-local slot; slots that were never given back left the
|
||||
* 65th instance with no thread-locals (__tls_base zero, over guest memory),
|
||||
* and a forked child that kept its parent's slot bits had too few left. */
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static __thread int mine;
|
||||
|
||||
static void *one(void *arg)
|
||||
{
|
||||
mine = (int)(long)arg;
|
||||
getpid();
|
||||
return (void *)(long)(mine * 2);
|
||||
}
|
||||
|
||||
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
|
||||
static int released;
|
||||
|
||||
static void *held(void *arg)
|
||||
{
|
||||
mine = (int)(long)arg;
|
||||
pthread_mutex_lock(&mu);
|
||||
while (!released) pthread_cond_wait(&cv, &mu);
|
||||
pthread_mutex_unlock(&mu);
|
||||
return (void *)(long)mine;
|
||||
}
|
||||
|
||||
static pthread_barrier_t bar;
|
||||
|
||||
static void *together(void *arg)
|
||||
{
|
||||
mine = (int)(long)arg;
|
||||
pthread_barrier_wait(&bar);
|
||||
return (void *)(long)(mine + 1);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
long sum = 0;
|
||||
for (long i = 0; i < 100; i++) {
|
||||
pthread_t t;
|
||||
void *r;
|
||||
if (pthread_create(&t, NULL, one, (void *)i)) { printf("create %ld failed\n", i); return 1; }
|
||||
pthread_join(t, &r);
|
||||
sum += (long)r;
|
||||
}
|
||||
printf("sequential: 100 threads, sum %ld\n", sum);
|
||||
|
||||
pthread_t h[40];
|
||||
for (long i = 0; i < 40; i++)
|
||||
if (pthread_create(&h[i], NULL, held, (void *)i)) { printf("held %ld failed\n", i); return 1; }
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
pthread_t t[30];
|
||||
long s = 0;
|
||||
pthread_barrier_init(&bar, NULL, 30);
|
||||
for (long i = 0; i < 30; i++)
|
||||
if (pthread_create(&t[i], NULL, together, (void *)i)) _exit(2);
|
||||
for (int i = 0; i < 30; i++) { void *r; pthread_join(t[i], &r); s += (long)r; }
|
||||
_exit(s == 465 ? 0 : 3);
|
||||
}
|
||||
int st = 0;
|
||||
waitpid(pid, &st, 0);
|
||||
printf("fork child with 30 threads: %s\n", WIFEXITED(st) && WEXITSTATUS(st) == 0 ? "ok" : "failed");
|
||||
pthread_mutex_lock(&mu);
|
||||
released = 1;
|
||||
pthread_cond_broadcast(&cv);
|
||||
pthread_mutex_unlock(&mu);
|
||||
long hs = 0;
|
||||
for (int i = 0; i < 40; i++) { void *r; pthread_join(h[i], &r); hs += (long)r; }
|
||||
printf("held: 40 threads, sum %ld\n", hs);
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user