Files
velzie 178ef6b5f8 Guest threads
clone with CLONE_THREAD now spawns a real host thread, which is what PLAN
section 8 asks for: a futex wait is a real wait, not a scheduled one.

Three pieces, on top of the shared memory landed last commit.

A shared kernel with an explicit per-thread seam. Kernel keeps the
process state -- descriptors, the VMA tree, mounts, signal dispositions --
behind one mutex. The three fields describing the *calling* thread
(ctx_addr, tid, clear_child_tid) moved into the host's per-thread state
and are installed under the same lock that guards the syscall, so what
the kernel holds always describes whoever owns it.

Blocking syscalls run unlocked. A futex wait holding the kernel lock can
never be woken: the waker needs that same lock to issue the wake. futex
and nanosleep are now recognised before dispatch and answered as a
BlockOp the host performs with nothing held, then completes under the
lock again. The first version deadlocked on exactly this and printed
nothing at all.

A registry of translated modules. A new thread's dispatch table starts
with only what the image module installed, so everything translated since
is replayed into it -- instantiate, not retranslate -- and the same
registry answers a dispatch miss for code another thread already
compiled. Append-only and ordered, so a store catches up from its own
watermark.

The child's context is built from the parent's, so it inherits whatever
the ABI does not speak to, and differs in what the architecture and the
flags require: x0 zero, sp and the thread pointer from the caller, the
exclusive monitor clear because a monitor is per-thread.

Real parallelism, on a 24-core VM: 180M iterations/s on one thread,
1140M on sixteen -- sixteen times the work for two and a half times the
wall clock. The falloff past eight is partly the machine and partly the
single kernel lock every syscall takes.

threads.c covers eight workers over a mutex, thread-local storage,
pthread_once, a condition variable with separate producer and consumer,
and detached threads nobody joins. Every number it prints is
order-independent, so the comparison is of the arithmetic rather than the
scheduler; it ran 20 times against QEMU without a difference. Suite is 41
comparisons, plus SQLite, Lua and 2000 fuzz cases unchanged.

Also fixes the flake path double-counting a pass.
2026-08-05 08:37:28 -04:00

119 lines
3.4 KiB
C

/* Guest threads: clone, TLS, futex-based joins and locking.
*
* Every result printed is order-independent, because the point is that the
* arithmetic is right, not that the scheduler is deterministic.
*/
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define N 8
#define ITERS 20000
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static long guarded;
static __thread int tls_value;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static int once_count;
static void once_fn(void) { once_count++; }
struct arg { int id; long local; int tls_seen; };
static void *worker(void *p) {
struct arg *a = p;
pthread_once(&once, once_fn);
/* Thread-local storage: each thread must see only its own. */
tls_value = a->id * 100;
for (int i = 0; i < ITERS; i++) {
a->local += i;
pthread_mutex_lock(&lock);
guarded++;
pthread_mutex_unlock(&lock);
}
a->tls_seen = tls_value;
return &a->local;
}
/* A condition variable, which is a different futex pattern from a mutex. */
static pthread_mutex_t cv_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static int ready, consumed;
static void *producer(void *unused) {
(void)unused;
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&cv_lock);
ready++;
pthread_cond_signal(&cv);
pthread_mutex_unlock(&cv_lock);
}
return NULL;
}
static void *consumer(void *unused) {
(void)unused;
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&cv_lock);
while (ready == 0) pthread_cond_wait(&cv, &cv_lock);
ready--;
consumed++;
pthread_mutex_unlock(&cv_lock);
}
return NULL;
}
int main(void) {
pthread_t t[N];
struct arg args[N];
for (int i = 0; i < N; i++) {
args[i] = (struct arg){.id = i + 1, .local = 0, .tls_seen = -1};
if (pthread_create(&t[i], NULL, worker, &args[i]) != 0) {
printf("pthread_create failed\n");
return 1;
}
}
long total = 0;
int tls_ok = 1;
for (int i = 0; i < N; i++) {
void *r;
pthread_join(t[i], &r);
if (r != &args[i].local) tls_ok = 0;
total += args[i].local;
if (args[i].tls_seen != (i + 1) * 100) tls_ok = 0;
}
/* Each worker sums 0..ITERS-1 and there are N of them. */
printf("sum %ld expected %ld\n", total, (long)N * ITERS * (ITERS - 1) / 2);
printf("guarded %ld expected %d\n", guarded, N * ITERS);
printf("tls %s\n", tls_ok ? "ok" : "WRONG");
printf("once %d\n", once_count);
/* The main thread's own TLS slot must be untouched by the workers. */
printf("main tls %d\n", tls_value);
pthread_t p, c;
pthread_create(&c, NULL, consumer, NULL);
pthread_create(&p, NULL, producer, NULL);
pthread_join(p, NULL);
pthread_join(c, NULL);
printf("condvar consumed %d\n", consumed);
/* Detached threads, which exit without anyone joining them. */
pthread_attr_t at;
pthread_attr_init(&at);
pthread_attr_setdetachstate(&at, PTHREAD_CREATE_DETACHED);
for (int i = 0; i < 4; i++) {
pthread_t d;
struct arg *da = calloc(1, sizeof *da);
da->id = 100 + i;
pthread_create(&d, &at, worker, da);
}
usleep(200000);
printf("detached ran, guarded now %s\n", guarded > (long)N * ITERS ? "grew" : "did not grow");
printf("done\n");
return 0;
}