mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
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.
119 lines
3.4 KiB
C
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;
|
|
}
|