mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
browser: a fork brings spinning threads to a safepoint, and says when it can't
A thread running guest code that makes no call stopped for a fork's copy only when its safepoint countdown ran out on its own. gex_fork_hold now runs each other thread's counter out, as a signal for it does, so its next back-edge stops it. A thread that still isn't out of guest code after two seconds (lost in a host call) is said in one line: that child's memory may be torn. tests/fork-snapshot runs three cases: one writer that never makes a syscall; four such writers; and four writers, all but one making calls, beside a thread that creates and joins threads all along, whose children each start a thread of their own. Each case prints its mean ms per fork. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
This commit is contained in:
1 parent
d10c694f1e
commit
7e8e3a202e
3 files changed
+107
-21
No files matched your search
@@ -9992,26 +9992,47 @@ thread_local! {
|
||||
/// from different moments: libc's lists of threads and stacks as no thread
|
||||
/// ever left them, which the child walks after fork. Each other thread is
|
||||
/// told to hold and is waited for until it is out of guest code (it parks
|
||||
/// at its return to it) or parked at the hold; one that never gets there is
|
||||
/// given up on after `EXEC_WAIT_MAX_NS`. [`gex_fork_taken`] lets them go.
|
||||
/// at its return to it) or parked at the hold. One running guest code is
|
||||
/// brought to its next back-edge's safepoint, as a signal for it is: its
|
||||
/// safepoint counter is run out. One that never gets there (lost in a host
|
||||
/// call) is given up on after `EXEC_WAIT_MAX_NS`, and said so: that child's
|
||||
/// memory may be torn. [`gex_fork_taken`] lets them go.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fork_hold() {
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
let me = cur();
|
||||
let cells: Vec<Arc<ThreadCell>> =
|
||||
with_exec_lock(|h| h.threads.iter().filter(|(i, _)| *i != me).map(|(_, t)| t.state_arc()).collect());
|
||||
for c in &cells {
|
||||
let cells: Vec<(Arc<ThreadCell>, u64)> = with_exec_lock(|h| {
|
||||
h.threads.iter().filter(|(i, _)| *i != me).map(|(_, t)| (t.state_arc(), t.ctx_addr())).collect()
|
||||
});
|
||||
let mem = guest_bytes();
|
||||
for (c, at) in &cells {
|
||||
c.hold.store(1, SeqCst);
|
||||
// Run the counter out. A store of the thread's own countdown racing
|
||||
// this one puts it back at most `SAFEPOINT_INTERVAL` back-edges away.
|
||||
let at = (*at as usize).wrapping_add(ctx::SAFEPOINT_COUNTER);
|
||||
if let Some(w) = mem.get_mut(at..at + 4) {
|
||||
// SAFETY: an aligned word of the guest's context, in linear memory.
|
||||
unsafe { (*(w.as_mut_ptr() as *const std::sync::atomic::AtomicI32)).store(0, SeqCst) };
|
||||
}
|
||||
}
|
||||
let stopped = |c: &ThreadCell| c.out.load(SeqCst) || c.held.load(SeqCst) || c.halted.load(SeqCst);
|
||||
let until = now_ns() + EXEC_WAIT_MAX_NS;
|
||||
let nap = std::sync::atomic::AtomicI32::new(0);
|
||||
while cells.iter().any(|c| !c.out.load(SeqCst) && !c.held.load(SeqCst) && !c.halted.load(SeqCst)) && now_ns() < until {
|
||||
while cells.iter().any(|(c, _)| !stopped(c)) && now_ns() < until {
|
||||
#[cfg(feature = "shared-memory")]
|
||||
unsafe {
|
||||
warch::memory_atomic_wait32(nap.as_ptr(), 0, 50_000)
|
||||
};
|
||||
}
|
||||
HOLDING.with(|h| h.borrow_mut().extend(cells));
|
||||
let running = cells.iter().filter(|(c, _)| !stopped(c)).count();
|
||||
if running != 0 {
|
||||
warn_line(&format!(
|
||||
"[gex] pid {}: fork copied memory with {running} thread(s) still running guest code after {} ms; the child's memory may be torn",
|
||||
MY_PID.load(std::sync::atomic::Ordering::Relaxed),
|
||||
EXEC_WAIT_MAX_NS / 1_000_000
|
||||
));
|
||||
}
|
||||
HOLDING.with(|h| h.borrow_mut().extend(cells.into_iter().map(|(c, _)| c)));
|
||||
}
|
||||
|
||||
/// The memory for the fork is copied: the threads [`gex_fork_hold`] held go
|
||||
|
||||
@@ -5,36 +5,76 @@
|
||||
* (A's page early, B's later) gives the child an A well behind its B. At any
|
||||
* one moment A is B or B - 1, and the child says which it saw.
|
||||
*
|
||||
* main FORKS THREADS: prints "forks N torn T" and exits 0 when T is 0. */
|
||||
* Writer 0 never makes a syscall: only its back-edges' safepoints can stop
|
||||
* it. With CALLS, the others make one every 64 writes. With CHURN, one more
|
||||
* thread creates and joins threads all along, so libc's list of threads
|
||||
* changes under the fork, and each child starts and joins one of its own.
|
||||
*
|
||||
* main FORKS THREADS [CALLS [CHURN]]: prints "forks N torn T", then
|
||||
* "fork ms M" (the mean from fork to the child's reaping), and exits 0
|
||||
* when T is 0. */
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MAXT 8
|
||||
static volatile uint64_t A[MAXT];
|
||||
static volatile uint64_t *B;
|
||||
static volatile int stop;
|
||||
static int calls;
|
||||
|
||||
static void *writer(void *arg)
|
||||
{
|
||||
int t = (int)(intptr_t)arg;
|
||||
int call = calls && t != 0;
|
||||
for (uint64_t k = 1; !stop; k++) {
|
||||
B[t * 512] = k;
|
||||
__atomic_thread_fence(__ATOMIC_RELEASE);
|
||||
A[t] = k;
|
||||
__atomic_thread_fence(__ATOMIC_RELEASE);
|
||||
if (call && (k & 63) == 0) syscall(SYS_sched_yield);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *nothing(void *arg)
|
||||
{
|
||||
return arg;
|
||||
}
|
||||
|
||||
static volatile uint64_t churned;
|
||||
|
||||
static void *churn(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!stop) {
|
||||
pthread_t t;
|
||||
if (pthread_create(&t, NULL, nothing, NULL) == 0) pthread_join(t, NULL);
|
||||
churned++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static double now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int forks = argc > 1 ? atoi(argv[1]) : 10;
|
||||
int threads = argc > 2 ? atoi(argv[2]) : 2;
|
||||
calls = argc > 3 && atoi(argv[3]);
|
||||
int churning = argc > 4 && atoi(argv[4]);
|
||||
if (threads < 1) threads = 1;
|
||||
if (threads > MAXT) threads = MAXT;
|
||||
/* Far above .bss: 64 MiB of mapping, B at its end. */
|
||||
size_t len = 64u << 20;
|
||||
@@ -42,13 +82,24 @@ int main(int argc, char **argv)
|
||||
if (m == MAP_FAILED) return 2;
|
||||
B = (volatile uint64_t *)(m + len - MAXT * 512 * 8);
|
||||
pthread_t th[MAXT];
|
||||
pthread_t ch;
|
||||
for (int t = 0; t < threads; t++) pthread_create(&th[t], NULL, writer, (void *)(intptr_t)t);
|
||||
while (A[threads - 1] < 1000) usleep(1000);
|
||||
if (churning) pthread_create(&ch, NULL, churn, NULL);
|
||||
while (A[threads - 1] < 1000 || (churning && churned < 10)) usleep(1000);
|
||||
int torn = 0;
|
||||
double ms = 0;
|
||||
for (int i = 0; i < forks; i++) {
|
||||
double t0 = now_ms();
|
||||
pid_t p = fork();
|
||||
if (p == 0) {
|
||||
int bad = 0;
|
||||
if (churning) {
|
||||
pthread_t t;
|
||||
if (pthread_create(&t, NULL, nothing, NULL) != 0 || pthread_join(t, NULL) != 0) {
|
||||
printf("child %d: no thread\n", i);
|
||||
bad = 1;
|
||||
}
|
||||
}
|
||||
for (int t = 0; t < threads; t++) {
|
||||
uint64_t a = A[t], b = B[t * 512];
|
||||
if (b - a > 1) {
|
||||
@@ -61,10 +112,13 @@ int main(int argc, char **argv)
|
||||
}
|
||||
int st = 0;
|
||||
if (p < 0 || waitpid(p, &st, 0) != p || !WIFEXITED(st)) return 3;
|
||||
ms += now_ms() - t0;
|
||||
torn += WEXITSTATUS(st) != 0;
|
||||
}
|
||||
stop = 1;
|
||||
for (int t = 0; t < threads; t++) pthread_join(th[t], NULL);
|
||||
if (churning) pthread_join(ch, NULL);
|
||||
printf("forks %d torn %d\n", forks, torn);
|
||||
printf("fork ms %.1f\n", ms / forks);
|
||||
return torn ? 1 : 0;
|
||||
}
|
||||
+23
-12
@@ -6,7 +6,7 @@
|
||||
# memory had A from one moment and B from a later one, every fork; glibc's
|
||||
# child walked its stack lists as no thread ever left them and crashed.
|
||||
#
|
||||
# tests/fork-snapshot/run.sh
|
||||
# tests/fork-snapshot/run.sh (FORKS, GEX_WEB_DIR: another build of the shim and seed)
|
||||
#
|
||||
# Needs ~/toolchains/aarch64-linux-musl-cross (GEX_TOOLCHAIN) and the shared
|
||||
# browser build in web/public/gex (shims/browser/build.sh).
|
||||
@@ -15,18 +15,29 @@ here=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$here/../.." && pwd)
|
||||
tc=${GEX_TOOLCHAIN:-$HOME/toolchains/aarch64-linux-musl-cross}
|
||||
cc=$tc/bin/aarch64-linux-musl-gcc
|
||||
seed=$root/web/public/gex/seed.shared.wasm
|
||||
seed=${GEX_WEB_DIR:-$root/web/public/gex}/seed.shared.wasm
|
||||
for t in "$cc" "$seed"; do [ -e "$t" ] || { echo "skip: $t is not there"; exit 0; }; done
|
||||
w=${WORK:-$root/target/fork-snapshot-test}
|
||||
rm -rf "$w"; mkdir -p "$w"
|
||||
"$cc" -O1 -o "$w/main" "$here/main.c" -lpthread || exit 1
|
||||
forks=${FORKS:-20} threads=${THREADS:-4}
|
||||
(cd "$w" && GEX_SHARED=1 timeout "${TMO:-300}" node "$root/shims/browser/js/run-node.mjs" \
|
||||
--file "/lib/ld-musl-aarch64.so.1=$tc/aarch64-linux-musl/lib/libc.so" --file "$w/main=$w/main" \
|
||||
"$seed" "$w/main" "$forks" "$threads" > "$w/run.out" 2> "$w/run.err" < /dev/null); rc=$?
|
||||
if [ $rc = 0 ] && grep -q "^forks $forks torn 0$" "$w/run.out"; then
|
||||
echo "ok fork-snapshot ($forks forks beside $threads writing threads, each child one moment's memory)"
|
||||
else
|
||||
echo "FAIL fork-snapshot (rc=$rc): $(tail -1 "$w/run.out")"; grep -a '^child' "$w/run.out" | head -3
|
||||
exit 1
|
||||
fi
|
||||
forks=${FORKS:-20}
|
||||
pub=${GEX_WEB_DIR:-$root/web/public/gex}
|
||||
# THREADS writers, CALLS (the writers but the first make syscalls), CHURN
|
||||
# (a thread making and joining threads); the first case's one writer never
|
||||
# makes a syscall.
|
||||
fail=0
|
||||
for c in "1 0 0" "4 0 0" "4 1 1"; do
|
||||
set -- $c
|
||||
(cd "$w" && GEX_SHARED=1 GEX_WEB_DIR=$pub timeout "${TMO:-300}" node "$root/shims/browser/js/run-node.mjs" \
|
||||
--file "/lib/ld-musl-aarch64.so.1=$tc/aarch64-linux-musl/lib/libc.so" --file "$w/main=$w/main" \
|
||||
"$pub/seed.shared.wasm" "$w/main" "$forks" "$@" > "$w/run.out" 2> "$w/run.err" < /dev/null); rc=$?
|
||||
what="$forks forks beside $1 writing thread(s)$([ "$2" = 1 ] && echo ", all but one making calls")$([ "$3" = 1 ] && echo ", threads made and joined")"
|
||||
if [ $rc = 0 ] && grep -q "^forks $forks torn 0$" "$w/run.out"; then
|
||||
echo "ok fork-snapshot ($what; $(grep '^fork ms' "$w/run.out"))"
|
||||
else
|
||||
echo "FAIL fork-snapshot ($what, rc=$rc): $(grep -a '^forks' "$w/run.out")"; grep -a '^child' "$w/run.out" | head -3
|
||||
grep -a 'torn\|panic' "$w/run.err" | head -3
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
Reference in new issue
Block a user