]> git.proxmox.com Git - wasi-libc.git/commitdiff
threads: implement `pthread_create` (#325)
authorAndrew Brown <andrew.brown@intel.com>
Tue, 4 Oct 2022 14:21:18 +0000 (07:21 -0700)
committerGitHub <noreply@github.com>
Tue, 4 Oct 2022 14:21:18 +0000 (07:21 -0700)
* threads: implement `pthread_create`

As described in the [`wasi-threads`] proposal, this change implements
`pthread_create` using the new `wasi_thread_spawn(void *arg)` API. As
described there, `wasi-libc` exports the thread entry point with the
expected name, `wasi_thread_start`, and then unwraps the passed argument
`struct` to invoke the user function with the user argument `struct`.

[`wasi-threads`]: https://github.com/WebAssembly/wasi-threads/pull/5

Previously, the TID was only passed to the child thread entry point; the
parent thread was forced to wait until the child thread set the TID in
the pthread structure. With this change, the TID will be passed not only
to the child thread but also returned to the parent thread, so that
either side can make progress. The `i32.store` becomes an
`i32.atomic.store` to avoid concurrent writes.

Makefile
expected/wasm32-wasi/posix/defined-symbols.txt
expected/wasm32-wasi/posix/undefined-symbols.txt
libc-bottom-half/headers/public/wasi/api.h
libc-bottom-half/sources/__wasilibc_real.c
libc-top-half/musl/src/thread/pthread_create.c

index 6017cd5e471da6ca94461fc70f573a11bf7eb107..7f71a3e23d476d5db296988154feb06feabd8b8e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -203,6 +203,7 @@ LIBC_TOP_HALF_MUSL_SOURCES += \
         thread/pthread_condattr_init.c \
         thread/pthread_condattr_setclock.c \
         thread/pthread_condattr_setpshared.c \
+        thread/pthread_create.c \
         thread/pthread_mutex_consistent.c \
         thread/pthread_mutex_destroy.c \
         thread/pthread_mutex_init.c \
@@ -229,6 +230,7 @@ LIBC_TOP_HALF_MUSL_SOURCES += \
         thread/pthread_rwlockattr_destroy.c \
         thread/pthread_rwlockattr_init.c \
         thread/pthread_rwlockattr_setpshared.c \
+        thread/pthread_setcancelstate.c \
         thread/pthread_testcancel.c \
         thread/sem_destroy.c \
         thread/sem_getvalue.c \
@@ -237,7 +239,6 @@ LIBC_TOP_HALF_MUSL_SOURCES += \
         thread/sem_timedwait.c \
         thread/sem_trywait.c \
         thread/sem_wait.c \
-        thread/pthread_setcancelstate.c \
     )
 endif
 
index 30bf29ece0bb4fc44822441d517cc61b612e9deb..95f3c788dcccf5a93b7ec65e1f4b1a5f3a03cf32 100644 (file)
@@ -11,6 +11,7 @@ __EINVAL
 __ENOMEM
 __SIG_ERR
 __SIG_IGN
+__acquire_ptc
 __aio_close
 __asctime_r
 __assert_fail
@@ -41,6 +42,7 @@ __des_setkey
 __do_cleanup_pop
 __do_cleanup_push
 __do_des
+__do_orphaned_stdio_locks
 __duplocale
 __env_rm_add
 __exp2f_data
@@ -182,6 +184,8 @@ __private_cond_signal
 __progname
 __progname_full
 __pthread_cond_timedwait
+__pthread_create
+__pthread_exit
 __pthread_mutex_lock
 __pthread_mutex_timedlock
 __pthread_mutex_trylock
@@ -196,11 +200,15 @@ __pthread_rwlock_unlock
 __pthread_rwlock_wrlock
 __pthread_setcancelstate
 __pthread_testcancel
+__pthread_tsd_main
+__pthread_tsd_run_dtors
+__pthread_tsd_size
 __putenv
 __qsort_r
 __rand48_step
 __random_lockptr
 __reallocarray
+__release_ptc
 __rem_pio2
 __rem_pio2_large
 __rem_pio2f
@@ -257,6 +265,9 @@ __tanl
 __testcancel
 __timedwait
 __timedwait_cp
+__tl_lock
+__tl_sync
+__tl_unlock
 __tm_to_secs
 __tm_to_tzname
 __tolower_l
@@ -322,6 +333,7 @@ __wasi_sock_accept
 __wasi_sock_recv
 __wasi_sock_send
 __wasi_sock_shutdown
+__wasi_thread_spawn
 __wasilibc_access
 __wasilibc_cwd
 __wasilibc_cwd_lock
@@ -948,6 +960,8 @@ pthread_condattr_destroy
 pthread_condattr_init
 pthread_condattr_setclock
 pthread_condattr_setpshared
+pthread_create
+pthread_exit
 pthread_mutex_consistent
 pthread_mutex_destroy
 pthread_mutex_getprioceiling
@@ -1212,6 +1226,7 @@ vswprintf
 vswscanf
 vwprintf
 vwscanf
+wasi_thread_start
 wcpcpy
 wcpncpy
 wcrtomb
index f87e6e3dec8bb781b287a10c3974a41b70eb637f..2b0d2b3e6477fce8cb42dfff8863ec115f788100 100644 (file)
@@ -1,4 +1,7 @@
 __addtf3
+__copy_tls
+__default_guardsize
+__default_stacksize
 __divtf3
 __eqtf2
 __extenddftf2
@@ -56,6 +59,7 @@ __imported_wasi_snapshot_preview1_sock_accept
 __imported_wasi_snapshot_preview1_sock_recv
 __imported_wasi_snapshot_preview1_sock_send
 __imported_wasi_snapshot_preview1_sock_shutdown
+__imported_wasi_snapshot_preview2_thread_spawn
 __letf2
 __lock
 __lockfile
@@ -64,6 +68,7 @@ __main_argc_argv
 __netf2
 __stack_pointer
 __subtf3
+__thread_list_lock
 __tls_base
 __trunctfdf2
 __trunctfsf2
index 7bbe93508e8468070ad5ca58c62e0609f548212e..1ab76994ed177989ac7618d3c91178faac62d6a1 100644 (file)
@@ -2089,6 +2089,25 @@ __wasi_errno_t __wasi_sock_shutdown(
 ) __attribute__((__warn_unused_result__));
 /** @} */
 
+#ifdef _REENTRANT
+/**
+ * Request a new thread to be created by the host.
+ *
+ * The host will create a new instance of the current module sharing its
+ * memory, find an exported entry function--`wasi_thread_start`--, and call the
+ * entry function with `start_arg` in the new thread.
+ *
+ * @see https://github.com/WebAssembly/wasi-threads/#readme
+ */
+__wasi_errno_t __wasi_thread_spawn(
+    /**
+     * A pointer to an opaque struct to be passed to the module's entry
+     * function.
+     */
+    void *start_arg
+)  __attribute__((__warn_unused_result__));
+#endif
+
 #ifdef __cplusplus
 }
 #endif
index 370e09575f63fa985177607afc91c8ce6a6aaa4a..2648ac9faf51dfdb289435a6d78234f1347b007d 100644 (file)
@@ -659,3 +659,14 @@ __wasi_errno_t __wasi_sock_shutdown(
     return (uint16_t) ret;
 }
 
+#ifdef _REENTRANT
+int32_t __imported_wasi_snapshot_preview2_thread_spawn(int32_t arg0) __attribute__((
+    __import_module__("wasi_snapshot_preview2"),
+    __import_name__("thread_spawn")
+));
+
+__wasi_errno_t __wasi_thread_spawn(void* start_arg) {
+       int32_t ret = __imported_wasi_snapshot_preview2_thread_spawn((int32_t) start_arg);
+    return (uint16_t) ret;
+}
+#endif
index 6f187ee89d3dd4b9fced7e0060ceb5641601614f..d0168987f3e07f0719b02ac4923284ebee405806 100644 (file)
@@ -3,9 +3,14 @@
 #include "stdio_impl.h"
 #include "libc.h"
 #include "lock.h"
+#ifdef __wasilibc_unmodified_upstream
 #include <sys/mman.h>
+#endif
 #include <string.h>
 #include <stddef.h>
+#ifndef __wasilibc_unmodified_upstream
+#include <stdatomic.h>
+#endif
 
 static void dummy_0()
 {
@@ -14,8 +19,10 @@ weak_alias(dummy_0, __acquire_ptc);
 weak_alias(dummy_0, __release_ptc);
 weak_alias(dummy_0, __pthread_tsd_run_dtors);
 weak_alias(dummy_0, __do_orphaned_stdio_locks);
+#ifdef __wasilibc_unmodified_upstream
 weak_alias(dummy_0, __dl_thread_cleanup);
 weak_alias(dummy_0, __membarrier_init);
+#endif
 
 static int tl_lock_count;
 static int tl_lock_waiters;
@@ -69,7 +76,9 @@ _Noreturn void __pthread_exit(void *result)
 
        __pthread_tsd_run_dtors();
 
+#ifdef __wasilibc_unmodified_upstream
        __block_app_sigs(&set);
+#endif
 
        /* This atomic potentially competes with a concurrent pthread_detach
         * call; the loser is responsible for freeing thread resources. */
@@ -80,7 +89,9 @@ _Noreturn void __pthread_exit(void *result)
                 * explicitly wait for vmlock holders first. This must be
                 * done before any locks are taken, to avoid lock ordering
                 * issues that could lead to deadlock. */
+#ifdef __wasilibc_unmodified_upstream
                __vm_wait();
+#endif
        }
 
        /* Access to target the exiting thread with syscalls that use
@@ -101,16 +112,20 @@ _Noreturn void __pthread_exit(void *result)
                __tl_unlock();
                UNLOCK(self->killlock);
                self->detach_state = state;
+#ifdef __wasilibc_unmodified_upstream
                __restore_sigs(&set);
+#endif
                exit(0);
        }
 
        /* At this point we are committed to thread termination. */
 
+#ifdef __wasilibc_unmodified_upstream
        /* Process robust list in userspace to handle non-pshared mutexes
         * and the detached thread case where the robust list head will
         * be invalid when the kernel would process it. */
        __vm_lock();
+#endif
        volatile void *volatile *rp;
        while ((rp=self->robust_list.head) && rp != &self->robust_list.head) {
                pthread_mutex_t *m = (void *)((char *)rp
@@ -124,10 +139,14 @@ _Noreturn void __pthread_exit(void *result)
                if (cont < 0 || waiters)
                        __wake(&m->_m_lock, 1, priv);
        }
+#ifdef __wasilibc_unmodified_upstream
        __vm_unlock();
+#endif
 
        __do_orphaned_stdio_locks();
+#ifdef __wasilibc_unmodified_upstream
        __dl_thread_cleanup();
+#endif
 
        /* Last, unlink thread from the list. This change will not be visible
         * until the lock is released, which only happens after SYS_exit
@@ -139,6 +158,7 @@ _Noreturn void __pthread_exit(void *result)
        self->prev->next = self->next;
        self->prev = self->next = self;
 
+#ifdef __wasilibc_unmodified_upstream
        if (state==DT_DETACHED && self->map_base) {
                /* Detached threads must block even implementation-internal
                 * signals, since they will not have a stack in their last
@@ -154,6 +174,7 @@ _Noreturn void __pthread_exit(void *result)
                 * and then exits without touching the stack. */
                __unmapself(self->map_base, self->map_size);
        }
+#endif
 
        /* Wake any joiner. */
        a_store(&self->detach_state, DT_EXITED);
@@ -165,7 +186,11 @@ _Noreturn void __pthread_exit(void *result)
        self->tid = 0;
        UNLOCK(self->killlock);
 
+#ifdef __wasilibc_unmodified_upstream
        for (;;) __syscall(SYS_exit, 0);
+#else
+       for (;;) exit(0);
+#endif
 }
 
 void __do_cleanup_push(struct __ptcb *cb)
@@ -181,12 +206,19 @@ void __do_cleanup_pop(struct __ptcb *cb)
 }
 
 struct start_args {
+#ifdef __wasilibc_unmodified_upstream
        void *(*start_func)(void *);
        void *start_arg;
        volatile int control;
        unsigned long sig_mask[_NSIG/8/sizeof(long)];
+#else
+       void *(*start_func)(void *);
+       void *start_arg;
+       struct pthread *thread;
+#endif
 };
 
+#ifdef __wasilibc_unmodified_upstream
 static int start(void *p)
 {
        struct start_args *args = p;
@@ -195,11 +227,15 @@ static int start(void *p)
                if (a_cas(&args->control, 1, 2)==1)
                        __wait(&args->control, 0, 2, 1);
                if (args->control) {
+#ifdef __wasilibc_unmodified_upstream
                        __syscall(SYS_set_tid_address, &args->control);
                        for (;;) __syscall(SYS_exit, 0);
+#endif
                }
        }
+#ifdef __wasilibc_unmodified_upstream
        __syscall(SYS_rt_sigprocmask, SIG_SETMASK, &args->sig_mask, 0, _NSIG/8);
+#endif
        __pthread_exit(args->start_func(args->start_arg));
        return 0;
 }
@@ -211,6 +247,26 @@ static int start_c11(void *p)
        __pthread_exit((void *)(uintptr_t)start(args->start_arg));
        return 0;
 }
+#else
+__attribute__((export_name("wasi_thread_start")))
+int wasi_thread_start(int tid, void *p)
+{
+       struct start_args *args = p;
+       // Set the thread ID (TID) on the pthread structure. The TID is stored
+       // atomically since it is also stored by the parent thread; this way,
+       // whichever thread (parent or child) reaches this point first can proceed
+       // without waiting.
+       atomic_store((atomic_int *) &(args->thread->tid), tid);
+       // Save the pointer to the pthread structure as the global `pthread_self`.
+       __asm__("local.set %0\n"
+                 "global.set __wasilibc_pthread_self\n"
+          : "=r"(args->thread));
+       // Execute the user's start function.
+       int (*start)(void*) = (int(*)(void*)) args->start_func;
+       __pthread_exit((void *)(uintptr_t)start(args->start_arg));
+       return 0;
+}
+#endif
 
 #define ROUND(x) (((x)+PAGE_SIZE-1)&-PAGE_SIZE)
 
@@ -236,9 +292,11 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
        size_t size, guard;
        struct pthread *self, *new;
        unsigned char *map = 0, *stack = 0, *tsd = 0, *stack_limit;
+#ifdef __wasilibc_unmodified_upstream
        unsigned flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND
                | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS
                | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_DETACHED;
+#endif
        pthread_attr_t attr = { 0 };
        sigset_t set;
 
@@ -251,9 +309,13 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
                init_file_lock(__stdin_used);
                init_file_lock(__stdout_used);
                init_file_lock(__stderr_used);
+#ifdef __wasilibc_unmodified_upstream
                __syscall(SYS_rt_sigprocmask, SIG_UNBLOCK, SIGPT_SET, 0, _NSIG/8);
+#endif
                self->tsd = (void **)__pthread_tsd_main;
+#ifdef __wasilibc_unmodified_upstream
                __membarrier_init();
+#endif
                libc.threaded = 1;
        }
        if (attrp && !c11) attr = *attrp;
@@ -287,6 +349,7 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
        }
 
        if (!tsd) {
+#ifdef __wasilibc_unmodified_upstream
                if (guard) {
                        map = __mmap(0, size, PROT_NONE, MAP_PRIVATE|MAP_ANON, -1, 0);
                        if (map == MAP_FAILED) goto fail;
@@ -299,6 +362,10 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
                        map = __mmap(0, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
                        if (map == MAP_FAILED) goto fail;
                }
+#else
+               map = malloc(size);
+               if (!map) goto fail;
+#endif
                tsd = map + size - __pthread_tsd_size;
                if (!stack) {
                        stack = tsd - libc.tls_size;
@@ -332,6 +399,7 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
        struct start_args *args = (void *)stack;
        args->start_func = entry;
        args->start_arg = arg;
+#ifdef __wasilibc_unmodified_upstream
        args->control = attr._a_sched ? 1 : 0;
 
        /* Application signals (but not the synccall signal) must be
@@ -345,11 +413,25 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
        memcpy(&args->sig_mask, &set, sizeof args->sig_mask);
        args->sig_mask[(SIGCANCEL-1)/8/sizeof(long)] &=
                ~(1UL<<((SIGCANCEL-1)%(8*sizeof(long))));
+#else
+       /* The new thread needs a pointer to the pthread struct so that it can set
+        * up its `wasilibc_pthread_self` global. */
+       args->thread = new;
+#endif
 
        __tl_lock();
        if (!libc.threads_minus_1++) libc.need_locks = 1;
+#ifdef __wasilibc_unmodified_upstream
        ret = __clone((c11 ? start_c11 : start), stack, flags, args, &new->tid, TP_ADJ(new), &__thread_list_lock);
-
+#else
+       /* Instead of `__clone`, WASI uses a host API to instantiate a new version
+        * of the current module and start executing the entry function. The
+        * wasi-threads specification requires the module to export a
+        * `wasi_thread_start` function, which is invoked with `args`. */
+       ret = __wasi_thread_spawn((void *) args);
+#endif
+
+#ifdef __wasilibc_unmodified_upstream
        /* All clone failures translate to EAGAIN. If explicit scheduling
         * was requested, attempt it before unlocking the thread list so
         * that the failed thread is never exposed and so that we can
@@ -364,6 +446,20 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
                if (ret)
                        __wait(&args->control, 0, 3, 0);
        }
+#else
+       /* `wasi_thread_spawn` will either return a host-provided thread ID (TID)
+        * (`>= 0`) or an error code (`< 0`). As in the unmodified version, all
+        * spawn failures translate to EAGAIN; unlike the modified version, there is
+        * no need to "start up" the child thread--the host does this. If the spawn
+        * did succeed, then we store the TID atomically, since this parent thread
+        * is racing with the child thread to set this field; this way, whichever
+        * thread reaches this point first can continue without waiting. */
+       if (ret < 0) {
+               ret = -EAGAIN;
+       } else {
+               atomic_store((atomic_int *) &(args->thread->tid), ret);
+       }
+#endif
 
        if (ret >= 0) {
                new->next = self->next;
@@ -374,11 +470,17 @@ int __pthread_create(pthread_t *restrict res, const pthread_attr_t *restrict att
                if (!--libc.threads_minus_1) libc.need_locks = 0;
        }
        __tl_unlock();
+#ifdef __wasilibc_unmodified_upstream
        __restore_sigs(&set);
+#endif
        __release_ptc();
 
        if (ret < 0) {
+#ifdef __wasilibc_unmodified_upstream
                if (map) __munmap(map, size);
+#else
+               free(map);
+#endif
                return -ret;
        }