parlib: Add uthread_create()
[akaros.git] / user / parlib / uthread.c
index 89c9c54..70ef9b4 100644 (file)
@@ -9,6 +9,8 @@
 #include <parlib/uthread.h>
 #include <parlib/event.h>
 #include <stdlib.h>
+#include <parlib/assert.h>
+#include <parlib/arch/trap.h>
 
 /* SCPs have a default 2LS that only manages thread 0.  Any other 2LS, such as
  * pthreads, should override sched_ops in its init code. */
@@ -34,17 +36,8 @@ static void handle_vc_indir(struct event_msg *ev_msg, unsigned int ev_type,
                             void *data);
 static void __ros_uth_syscall_blockon(struct syscall *sysc);
 
-/* Helper, make the uthread code manage thread0.  This sets up uthread such
- * that the calling code and its TLS are tracked by the uthread struct, and
- * vcore0 thinks the uthread is running there.  Called only by lib_init (early
- * _S code) and 2ls_init (when initializing thread0 for use in a 2LS).
- *
- * Whether or not uthreads have TLS, thread0 has TLS, given to it by glibc.
- * This TLS will get set whenever we use thread0, regardless of whether or not
- * we use TLS for uthreads in general.  glibc cares about this TLS and will use
- * it at exit.  We can't simply use that TLS for VC0 either, since we don't know
- * where thread0 will be running when the program ends. */
-static void uthread_manage_thread0(struct uthread *uthread)
+/* Helper, initializes a fresh uthread to be thread0. */
+static void uthread_init_thread0(struct uthread *uthread)
 {
        assert(uthread);
        /* Save a pointer to thread0's tls region (the glibc one) into its tcb */
@@ -53,19 +46,33 @@ static void uthread_manage_thread0(struct uthread *uthread)
        current_uthread = uthread;
        /* Thread is currently running (it is 'us') */
        uthread->state = UT_RUNNING;
+       /* Thread is detached */
+       atomic_set(&uthread->join_ctl.state, UTH_JOIN_DETACHED);
+       /* Reset the signal state */
+       uthread->sigstate.mask = 0;
+       __sigemptyset(&uthread->sigstate.pending);
+       uthread->sigstate.data = NULL;
        /* utf/as doesn't represent the state of the uthread (we are running) */
        uthread->flags &= ~(UTHREAD_SAVED | UTHREAD_FPSAVED);
        /* need to track thread0 for TLS deallocation */
        uthread->flags |= UTHREAD_IS_THREAD0;
        uthread->notif_disabled_depth = 0;
-       /* Change temporarily to vcore0s tls region so we can save the newly created
-        * tcb into its current_uthread variable and then restore it.  One minor
-        * issue is that vcore0's transition-TLS isn't TLS_INITed yet.  Until it is
-        * (right before vcore_entry(), don't try and take the address of any of
-        * its TLS vars. */
+       /* setting the uthread's TLS var.  this is idempotent for SCPs (us) */
+       __vcoreid = 0;
+}
+
+/* Helper, makes VC ctx tracks uthread as its current_uthread in its TLS.
+ *
+ * Whether or not uthreads have TLS, thread0 has TLS, given to it by glibc.
+ * This TLS will get set whenever we use thread0, regardless of whether or not
+ * we use TLS for uthreads in general.  glibc cares about this TLS and will use
+ * it at exit.  We can't simply use that TLS for VC0 either, since we don't know
+ * where thread0 will be running when the program ends. */
+static void uthread_track_thread0(struct uthread *uthread)
+{
        set_tls_desc(get_vcpd_tls_desc(0));
        begin_safe_access_tls_vars();
-       /* We might have a basic uthread already installed (from lib_init), so
+       /* We might have a basic uthread already installed (from a prior call), so
         * free it before installing the new one. */
        if (current_uthread)
                free(current_uthread);
@@ -79,10 +86,6 @@ static void uthread_manage_thread0(struct uthread *uthread)
        __vcore_context = TRUE;
        end_safe_access_tls_vars();
        set_tls_desc(uthread->tls_desc);
-       begin_safe_access_tls_vars();
-       __vcoreid = 0;  /* setting the uthread's TLS var */
-       assert(!in_vcore_context());
-       end_safe_access_tls_vars();
 }
 
 /* The real 2LS calls this to transition us into mcp mode.  When it
@@ -90,7 +93,7 @@ static void uthread_manage_thread0(struct uthread *uthread)
 void uthread_mcp_init()
 {
        /* Prevent this from happening more than once. */
-       init_once_racy(return);
+       parlib_init_once_racy(return);
 
        /* Doing this after the init_once check, since we don't want to let the
         * process/2LS change their mind about being an MCP or not once they have
@@ -114,13 +117,13 @@ void uthread_mcp_init()
         * events should be spammed to vcores that are running, preferring whatever
         * the kernel thinks is appropriate.  And IPI them.
         *
-        * It is critical that these are either SPAM_PUB or INDIR|FALLBACK, so that
-        * yielding vcores do not miss the preemption messages. */
+        * It is critical that these are either SPAM_PUB or INDIR|SPAM_INDIR, so
+        * that yielding vcores do not miss the preemption messages. */
        register_ev_handler(EV_VCORE_PREEMPT, handle_vc_preempt, 0);
        register_ev_handler(EV_CHECK_MSGS, handle_vc_indir, 0);
-       preempt_ev_q = get_event_q();   /* small ev_q, mostly a vehicle for flags */
+       preempt_ev_q = get_eventq_slim();       /* small ev_q, mostly a vehicle for flags */
        preempt_ev_q->ev_flags = EVENT_IPI | EVENT_SPAM_PUBLIC | EVENT_VCORE_APPRO |
-                                                        EVENT_VCORE_MUST_RUN;
+                                                        EVENT_VCORE_MUST_RUN | EVENT_WAKEUP;
        /* Tell the kernel to use the ev_q (it's settings) for the two types.  Note
         * that we still have two separate handlers.  We just want the events
         * delivered in the same way.  If we ever want to have a big_event_q with
@@ -133,14 +136,56 @@ void uthread_mcp_init()
        vcore_change_to_m();
 }
 
-/* The real 2LS calls this, passing in a uthread representing thread0. */
-void uthread_2ls_init(struct uthread *uthread)
+/* The real 2LS calls this, passing in a uthread representing thread0, its 2LS
+ * ops, and its syscall handling routine.  (NULL is fine).
+ *
+ * When we're called, thread0 has it's handler installed.  We need to remove it
+ * and install our own (if they want one).  All of the actual changes  must be
+ * done atomically with respect to syscalls (i.e., no syscalls in between).  If
+ * the user did something like have an outstanding async syscall while we're in
+ * here, then they'll crash hard. */
+void uthread_2ls_init(struct uthread *uthread, struct schedule_ops *ops,
+                      void (*handle_sysc)(struct event_msg *, unsigned int,
+                                          void *),
+                      void *data)
 {
-       /* All we need to do is set up thread0 to run with our new 2LS specific
-        * uthread pointer. Under the hood, this function will free any previously
-        * allocated uthread structs representing thread0 (e.g. the one set up by
-        * uthread_lib_init() previously). */
-       uthread_manage_thread0(uthread);
+       struct ev_handler *old_h, *new_h = NULL;
+
+       if (handle_sysc) {
+               new_h = malloc(sizeof(struct ev_handler));
+               assert(new_h);
+               new_h->func = handle_sysc;
+               new_h->data = data;
+               new_h->next = NULL;
+       }
+       uthread_init_thread0(uthread);
+       /* We need to *atomically* change the current_uthread and the schedule_ops
+        * to the new 2LSs thread0 and ops, such that there is no moment when only
+        * one is changed and that we call a sched_ops.  There are sources of
+        * implicit calls to sched_ops.  Two big ones are sched_entry, called
+        * whenever we receive a notif (so we need to disable notifs), and
+        * syscall_blockon, called whenver we had a syscall that blocked (so we say
+        * tell the *uthread* that *it* is in vc ctx (TLS var).
+        *
+        * When disabling notifs, don't use a helper.  We're changing
+        * current_uthread under the hood, which messes with the helpers.  When
+        * setting __vcore_context, we're in thread0's TLS.  Even when we change
+        * current_uthread, we're still in the *same* TLS. */
+       __disable_notifs(0);
+       __vcore_context = TRUE;
+       cmb();
+       /* Under the hood, this function will free any previously allocated uthread
+        * structs representing thread0 (e.g. the one set up by uthread_lib_init()
+        * previously). */
+       uthread_track_thread0(uthread);
+       sched_ops = ops;
+       /* Racy, but we shouldn't be concurrent */
+       old_h = ev_handlers[EV_SYSCALL];
+       ev_handlers[EV_SYSCALL] = new_h;
+       cmb();
+       __vcore_context = FALSE;
+       enable_notifs(0);       /* will trigger a self_notif if we missed a notif */
+       free(old_h);
 }
 
 /* Helper: tells the kernel our SCP is capable of going into vcore context on
@@ -186,17 +231,37 @@ void __attribute__((constructor)) uthread_lib_init(void)
 {
        /* Use the thread0 sched's uth */
        extern struct uthread *thread0_uth;
+       extern void thread0_lib_init(void);
+       extern void thread0_handle_syscall(struct event_msg *ev_msg,
+                                          unsigned int ev_type, void *data);
        int ret;
 
+       /* Surprise!  Parlib's ctors also run in shared objects.  We can't have
+        * multiple versions of parlib (with multiple data structures). */
+       if (__in_fake_parlib())
+               return;
        /* Only run once, but make sure that vcore_lib_init() has run already. */
-       init_once_racy(return);
+       parlib_init_once_racy(return);
        vcore_lib_init();
 
        ret = posix_memalign((void**)&thread0_uth, __alignof__(struct uthread),
                             sizeof(struct uthread));
        assert(!ret);
        memset(thread0_uth, 0, sizeof(struct uthread)); /* aggressively 0 for bugs*/
-       uthread_manage_thread0(thread0_uth);
+       /* Need to do thread0_lib_init() first, since it sets up evq's referred to
+        * in thread0_handle_syscall(). */
+       thread0_lib_init();
+       uthread_2ls_init(thread0_uth, &thread0_2ls_ops, thread0_handle_syscall,
+                        NULL);
+       /* Switch our errno/errstr functions to be uthread-aware.  See glibc's
+        * errno.c for more info. */
+       ros_errno_loc = __ros_errno_loc;
+       ros_errstr_loc = __ros_errstr_loc;
+       register_ev_handler(EV_EVENT, handle_ev_ev, 0);
+       /* Now that we're ready (I hope) to operate as an MCP, we tell the kernel.
+        * We must set vcctx and blockon atomically with respect to syscalls,
+        * meaning no syscalls in between. */
+       cmb();
        scp_vcctx_ready();
        /* Change our blockon from glibc's internal one to the regular one, which
         * uses vcore context and works for SCPs (with or without 2LS) and MCPs.
@@ -205,11 +270,10 @@ void __attribute__((constructor)) uthread_lib_init(void)
        ros_syscall_blockon = __ros_uth_syscall_blockon;
        cmb();
        init_posix_signals();
-       /* Switch our errno/errstr functions to be uthread-aware.  See glibc's
-        * errno.c for more info. */
-       ros_errno_loc = __ros_errno_loc;
-       ros_errstr_loc = __ros_errstr_loc;
-       register_ev_handler(EV_EVENT, handle_ev_ev, 0);
+       /* Accept diagnostic events.  Other parts of the program/libraries can
+        * register handlers to run.  You can kick these with "notify PID 9". */
+       enable_kevent(EV_FREE_APPLE_PIE, 0, EVENT_IPI | EVENT_WAKEUP |
+                                           EVENT_SPAM_PUBLIC);
 }
 
 /* 2LSs shouldn't call uthread_vcore_entry directly */
@@ -257,6 +321,10 @@ void uthread_init(struct uthread *new_thread, struct uth_thread_attr *attr)
        int ret;
        assert(new_thread);
        new_thread->state = UT_NOT_RUNNING;
+       /* Set the signal state. */
+       new_thread->sigstate.mask = current_uthread->sigstate.mask;
+       __sigemptyset(&new_thread->sigstate.pending);
+       new_thread->sigstate.data = NULL;
        /* They should have zero'd the uthread.  Let's check critical things: */
        assert(!new_thread->flags && !new_thread->sysc);
        /* the utf holds the GP context of the uthread (set by the 2LS earlier).
@@ -264,6 +332,8 @@ void uthread_init(struct uthread *new_thread, struct uth_thread_attr *attr)
         * were interrupted off a core. */
        new_thread->flags |= UTHREAD_SAVED;
        new_thread->notif_disabled_depth = 0;
+       /* TODO: on a reinit, if they changed whether or not they want TLS, we'll
+        * have issues (checking tls_desc, assert in allocate_tls, maybe more). */
        if (attr && attr->want_tls) {
                /* Get a TLS.  If we already have one, reallocate/refresh it */
                if (new_thread->tls_desc)
@@ -282,6 +352,10 @@ void uthread_init(struct uthread *new_thread, struct uth_thread_attr *attr)
        } else {
                new_thread->tls_desc = UTH_TLSDESC_NOTLS;
        }
+       if (attr && attr->detached)
+               atomic_set(&new_thread->join_ctl.state, UTH_JOIN_DETACHED);
+       else
+               atomic_set(&new_thread->join_ctl.state, UTH_JOIN_JOINABLE);
 }
 
 /* This is a wrapper for the sched_ops thread_runnable, for use by functions
@@ -298,17 +372,33 @@ void uthread_runnable(struct uthread *uthread)
  * the 2LS.  This is for informational purposes, and some semantic meaning
  * should be passed by flags (from uthread.h's UTH_EXT_BLK_xxx options).
  * Eventually, whoever calls this will call uthread_runnable(), giving the
- * thread back to the 2LS.
+ * thread back to the 2LS.  If the 2LS provide sync ops, it will have a say in
+ * which thread wakes up at a given time.
  *
  * If code outside the 2LS has blocked a thread (via uthread_yield) and ran its
  * own callback/yield_func instead of some 2LS code, that callback needs to
  * call this.
  *
+ * If sync is set, then the 2LS must save the uthread in the sync object.  Using
+ * the default implementatin is fine.  If sync is NULL, then there's nothing the
+ * 2LS should do regarding sync; it'll be told when the thread is runnable.
+ *
  * AKA: obviously_a_uthread_has_blocked_in_lincoln_park() */
-void uthread_has_blocked(struct uthread *uthread, int flags)
+void uthread_has_blocked(struct uthread *uthread, uth_sync_t sync, int flags)
 {
-       if (sched_ops->thread_has_blocked)
-               sched_ops->thread_has_blocked(uthread, flags);
+       assert(sched_ops->thread_has_blocked);
+       sched_ops->thread_has_blocked(uthread, sync, flags);
+}
+
+/* Function indicating an external event has temporarily paused a uthread, but
+ * it is ok to resume it if possible. */
+void uthread_paused(struct uthread *uthread)
+{
+       /* Call out to the 2LS to let it know the uthread was paused for some
+        * reason, but it is ok to resume it now. */
+    assert(uthread->state == UT_NOT_RUNNING);
+    assert(sched_ops->thread_paused);
+    sched_ops->thread_paused(uthread);
 }
 
 /* Need to have this as a separate, non-inlined function since we clobber the
@@ -396,7 +486,7 @@ void uthread_yield(bool save_state, void (*yield_func)(struct uthread*, void*),
                set_tls_desc(get_vcpd_tls_desc(vcoreid));
                begin_safe_access_tls_vars();
                assert(current_uthread == uthread);
-               /* If this assert fails, see the note in uthread_manage_thread0 */
+               /* If this assert fails, see the note in uthread_track_thread0 */
                assert(in_vcore_context());
                end_safe_access_tls_vars();
        } else {
@@ -437,6 +527,17 @@ void uthread_usleep(unsigned int usecs)
        sys_block(usecs);       /* usec sleep */
 }
 
+static void __sleep_forever_cb(struct uthread *uth, void *arg)
+{
+       uthread_has_blocked(uth, NULL, UTH_EXT_BLK_JUSTICE);
+}
+
+void __attribute__((noreturn)) uthread_sleep_forever(void)
+{
+       uthread_yield(FALSE, __sleep_forever_cb, NULL);
+       assert(0);
+}
+
 /* Cleans up the uthread (the stuff we did in uthread_init()).  If you want to
  * destroy a currently running uthread, you'll want something like
  * pthread_exit(), which yields, and calls this from its sched_ops yield. */
@@ -495,6 +596,24 @@ static void __ros_uth_syscall_blockon(struct syscall *sysc)
        uthread_yield(TRUE, sched_ops->thread_blockon_sysc, sysc);
 }
 
+/* 2LS helper.  Run this from vcore context.  It will block a uthread on it's
+ * internal syscall struct, which should be an async call.  You'd use this in
+ * e.g. thread_refl_fault when the 2LS initiates a syscall on behalf of the
+ * uthread. */
+void __block_uthread_on_async_sysc(struct uthread *uth)
+{
+       assert(in_vcore_context());
+       uth->sysc = &uth->local_sysc;
+       /* If a DONT_MIGRATE issued a syscall that blocks, we gotta spin, same as
+        * with the usual blockon. */
+       if (uth->flags & UTHREAD_DONT_MIGRATE) {
+               __ros_vcore_ctx_syscall_blockon(uth->sysc);
+               uth->sysc = 0;
+               return;
+       }
+       sched_ops->thread_blockon_sysc(uth, uth->sysc);
+}
+
 /* Simply sets current uthread to be whatever the value of uthread is.  This
  * can be called from outside of sched_entry() to highjack the current context,
  * and make sure that the new uthread struct is used to store this context upon
@@ -538,16 +657,36 @@ static void set_uthread_tls(struct uthread *uthread, uint32_t vcoreid)
 /* Attempts to handle a fault for uth, etc */
 static void handle_refl_fault(struct uthread *uth, struct user_context *ctx)
 {
-       sched_ops->thread_refl_fault(uth, __arch_refl_get_nr(ctx),
-                                    __arch_refl_get_err(ctx),
-                                    __arch_refl_get_aux(ctx));
+       sched_ops->thread_refl_fault(uth, ctx);
+}
+
+/* 2LS helper: stops the current uthread, saves its state, and returns a pointer
+ * to it.  Unlike __uthread_pause, which is called by non-specific 2LS code,
+ * this function is called by a specific 2LS to stop it's current uthread. */
+struct uthread *stop_current_uthread(void)
+{
+       struct uthread *uth;
+       struct preempt_data *vcpd = vcpd_of(vcore_id());
+
+       uth = current_uthread;
+       current_uthread = 0;
+       if (!(uth->flags & UTHREAD_SAVED)) {
+               uth->u_ctx = vcpd->uthread_ctx;
+               uth->flags |= UTHREAD_SAVED;
+       }
+       if ((uth->u_ctx.type != ROS_SW_CTX) && !(uth->flags & UTHREAD_FPSAVED)) {
+               save_fp_state(&uth->as);
+               uth->flags |= UTHREAD_FPSAVED;
+       }
+       uth->state = UT_NOT_RUNNING;
+       return uth;
 }
 
 /* Run the thread that was current_uthread, from a previous run.  Should be
  * called only when the uthread already was running, and we were interrupted by
  * the kernel (event, etc).  Do not call this to run a fresh uthread, even if
  * you've set it to be current. */
-void run_current_uthread(void)
+void __attribute__((noreturn)) run_current_uthread(void)
 {
        struct uthread *uth;
        uint32_t vcoreid = vcore_id();
@@ -564,12 +703,7 @@ void run_current_uthread(void)
                /* we preemptively copy out and make non-running, so that there is a
                 * consistent state for the handler.  it can then block the uth or
                 * whatever. */
-               uth = current_uthread;
-               current_uthread = 0;
-               uth->u_ctx = vcpd->uthread_ctx;
-               save_fp_state(&uth->as);
-               uth->state = UT_NOT_RUNNING;
-               uth->flags |= UTHREAD_SAVED | UTHREAD_FPSAVED;
+               uth = stop_current_uthread();
                handle_refl_fault(uth, &vcpd->uthread_ctx);
                /* we abort no matter what.  up to the 2LS to reschedule the thread */
                set_stack_pointer((void*)vcpd->vcore_stack);
@@ -596,19 +730,24 @@ void run_current_uthread(void)
  * necessary (we only must do it once for an entire time in VC ctx, and in
  * loops), and might have been optimizing a rare event at a cost in both
  * instructions and complexity. */
-void run_uthread(struct uthread *uthread)
+void __attribute__((noreturn)) run_uthread(struct uthread *uthread)
 {
        uint32_t vcoreid = vcore_id();
        struct preempt_data *vcpd = vcpd_of(vcoreid);
        assert(!current_uthread);
        assert(uthread->state == UT_NOT_RUNNING);
        assert(uthread->flags & UTHREAD_SAVED);
-       /* For HW CTX, FPSAVED must match UTH SAVE (and both be on here).  For SW,
-        * FP should never be saved. */
-       if (uthread->u_ctx.type == ROS_HW_CTX)
+       /* For HW/VM CTX, FPSAVED must match UTH SAVE (and both be on here).  For
+        * SW, FP should never be saved. */
+       switch (uthread->u_ctx.type) {
+       case ROS_HW_CTX:
+       case ROS_VM_CTX:
                assert(uthread->flags & UTHREAD_FPSAVED);
-       else
+               break;
+       case ROS_SW_CTX:
                assert(!(uthread->flags & UTHREAD_FPSAVED));
+               break;
+       }
        if (has_refl_fault(&uthread->u_ctx)) {
                clear_refl_fault(&uthread->u_ctx);
                handle_refl_fault(uthread, &uthread->u_ctx);
@@ -675,9 +814,12 @@ static void copyout_uthread(struct preempt_data *vcpd, struct uthread *uthread,
 {
        assert(uthread);
        if (uthread->flags & UTHREAD_SAVED) {
-               /* I don't know of scenarios where HW ctxs FP state differs from GP */
-               if (uthread->u_ctx.type == ROS_HW_CTX)
+               /* I don't know of scenarios where HW/VM ctxs FP state differs from GP*/
+               switch (uthread->u_ctx.type) {
+               case ROS_HW_CTX:
+               case ROS_VM_CTX:
                        assert(uthread->flags & UTHREAD_FPSAVED);
+               }
                assert(vcore_local);
                return;
        }
@@ -765,11 +907,12 @@ bool __check_preempt_pending(uint32_t vcoreid)
 void uth_disable_notifs(void)
 {
        if (!in_vcore_context()) {
-               assert(current_uthread);
-               if (current_uthread->notif_disabled_depth++)
-                       goto out;
-               current_uthread->flags |= UTHREAD_DONT_MIGRATE;
-               cmb();  /* don't issue the flag write before the vcore_id() read */
+               if (current_uthread) {
+                       if (current_uthread->notif_disabled_depth++)
+                               goto out;
+                       current_uthread->flags |= UTHREAD_DONT_MIGRATE;
+                       cmb();  /* don't issue the flag write before the vcore_id() read */
+               }
                disable_notifs(vcore_id());
        }
 out:
@@ -780,11 +923,12 @@ out:
 void uth_enable_notifs(void)
 {
        if (!in_vcore_context()) {
-               assert(current_uthread);
-               if (--current_uthread->notif_disabled_depth)
-                       return;
-               current_uthread->flags &= ~UTHREAD_DONT_MIGRATE;
-               cmb();  /* don't enable before ~DONT_MIGRATE */
+               if (current_uthread) {
+                       if (--current_uthread->notif_disabled_depth)
+                               return;
+                       current_uthread->flags &= ~UTHREAD_DONT_MIGRATE;
+                       cmb();  /* don't enable before ~DONT_MIGRATE */
+               }
                enable_notifs(vcore_id());
        }
 }
@@ -827,8 +971,8 @@ static void handle_indirs(uint32_t rem_vcoreid)
        struct preempt_data *rem_vcpd = vcpd_of(rem_vcoreid);
        /* Turn off their message reception if they are still preempted.  If they
         * are no longer preempted, we do nothing - they will handle their own
-        * messages.  Turning on CAN_RCV will route this vcore's messages to
-        * fallback vcores (if applicable). */
+        * messages.  Turning off CAN_RCV will route this vcore's messages to
+        * fallback vcores (if those messages were 'spammed'). */
        do {
                old_flags = atomic_read(&rem_vcpd->flags);
                while (old_flags & VC_K_LOCK)
@@ -1030,62 +1174,6 @@ static void handle_vc_indir(struct event_msg *ev_msg, unsigned int ev_type,
        handle_indirs(rem_vcoreid);
 }
 
-/* Attempts to register ev_q with sysc, so long as sysc is not done/progress.
- * Returns true if it succeeded, and false otherwise.  False means that the
- * syscall is done, and does not need an event set (and should be handled
- * accordingly).
- * 
- * A copy of this is in glibc/sysdeps/ros/syscall.c.  Keep them in sync. */
-bool register_evq(struct syscall *sysc, struct event_queue *ev_q)
-{
-       int old_flags;
-       sysc->ev_q = ev_q;
-       wrmb(); /* don't let that write pass any future reads (flags) */
-       /* Try and set the SC_UEVENT flag (so the kernel knows to look at ev_q) */
-       do {
-               /* no cmb() needed, the atomic_read will reread flags */
-               old_flags = atomic_read(&sysc->flags);
-               /* Spin if the kernel is mucking with syscall flags */
-               while (old_flags & SC_K_LOCK)
-                       old_flags = atomic_read(&sysc->flags);
-               /* If the kernel finishes while we are trying to sign up for an event,
-                * we need to bail out */
-               if (old_flags & (SC_DONE | SC_PROGRESS)) {
-                       sysc->ev_q = 0;         /* not necessary, but might help with bugs */
-                       return FALSE;
-               }
-       } while (!atomic_cas(&sysc->flags, old_flags, old_flags | SC_UEVENT));
-       return TRUE;
-}
-
-/* De-registers a syscall, so that the kernel will not send an event when it is
- * done.  The call could already be SC_DONE, or could even finish while we try
- * to unset SC_UEVENT.
- *
- * There is a chance the kernel sent an event if you didn't do this in time, but
- * once this returns, the kernel won't send a message.
- *
- * If the kernel is trying to send a message right now, this will spin (on
- * SC_K_LOCK).  We need to make sure we deregistered, and that if a message
- * is coming, that it already was sent (and possibly overflowed), before
- * returning. */
-void deregister_evq(struct syscall *sysc)
-{
-       int old_flags;
-       sysc->ev_q = 0;
-       wrmb(); /* don't let that write pass any future reads (flags) */
-       /* Try and unset the SC_UEVENT flag */
-       do {
-               /* no cmb() needed, the atomic_read will reread flags */
-               old_flags = atomic_read(&sysc->flags);
-               /* Spin if the kernel is mucking with syscall flags */
-               while (old_flags & SC_K_LOCK)
-                       old_flags = atomic_read(&sysc->flags);
-               /* Note we don't care if the SC_DONE flag is getting set.  We just need
-                * to avoid clobbering flags */
-       } while (!atomic_cas(&sysc->flags, old_flags, old_flags & ~SC_UEVENT));
-}
-
 static inline bool __uthread_has_tls(struct uthread *uthread)
 {
        return uthread->tls_desc != UTH_TLSDESC_NOTLS;
@@ -1118,3 +1206,176 @@ static void __uthread_free_tls(struct uthread *uthread)
        free_tls(uthread->tls_desc);
        uthread->tls_desc = NULL;
 }
+
+bool uth_2ls_is_multithreaded(void)
+{
+       /* thread 0 is single threaded.  For the foreseeable future, every other 2LS
+        * will be multithreaded. */
+       return sched_ops != &thread0_2ls_ops;
+}
+
+struct uthread *uthread_create(void *(*func)(void *), void *arg)
+{
+       return sched_ops->thread_create(func, arg);
+}
+
+/* Who does the thread_exited callback (2LS-specific cleanup)?  It depends.  If
+ * the thread exits first, then join/detach does it.  o/w, the exit path does.
+ *
+ * What are the valid state changes?
+ *
+ *             JOINABLE   -> DETACHED (only by detach())
+ *             JOINABLE   -> HAS_JOINER (only by join())
+ *             JOINABLE   -> EXITED (only by uth_2ls_thread_exit())
+ *
+ * That's it.  The initial state is either JOINABLE or DETACHED. */
+void uthread_detach(struct uthread *uth)
+{
+       struct uth_join_ctl *jc = &uth->join_ctl;
+       long old_state;
+
+       do {
+               old_state = atomic_read(&jc->state);
+               switch (old_state) {
+               case UTH_JOIN_EXITED:
+                       sched_ops->thread_exited(uth);
+                       return;
+               case UTH_JOIN_DETACHED:
+                       panic("Uth %p has already been detached!", uth);
+               case UTH_JOIN_HAS_JOINER:
+                       panic("Uth %p has a pending joiner, can't detach!", uth);
+               };
+               assert(old_state == UTH_JOIN_JOINABLE);
+       } while (!atomic_cas(&jc->state, old_state, UTH_JOIN_DETACHED));
+}
+
+/* Helper.  We have a joiner.  So we'll write the retval to the final location
+ * (the one passed to join() and decref to wake the joiner.  This may seem a
+ * little odd for a normal join, but it works identically a parallel join - and
+ * there's only one wakeup (hence the kref). */
+static void uth_post_and_kick_joiner(struct uthread *uth, void *retval)
+{
+       struct uth_join_ctl *jc = &uth->join_ctl;
+
+       if (jc->retval_loc)
+               *jc->retval_loc = retval;
+       /* Note the JC has a pointer to the kicker.  There's one kicker for the
+        * joiner, but there could be many joinees. */
+       kref_put(&jc->kicker->kref);
+}
+
+/* Callback after the exiting uthread has yielded and is in vcore context.  Note
+ * that the thread_exited callback can be called concurrently (e.g., a racing
+ * call to detach()), so it's important to not be in the uthread's context. */
+static void __uth_2ls_thread_exit_cb(struct uthread *uth, void *retval)
+{
+       struct uth_join_ctl *jc = &uth->join_ctl;
+       long old_state;
+
+       do {
+               old_state = atomic_read(&jc->state);
+               switch (old_state) {
+               case UTH_JOIN_DETACHED:
+                       sched_ops->thread_exited(uth);
+                       return;
+               case UTH_JOIN_HAS_JOINER:
+                       uth_post_and_kick_joiner(uth, retval);
+                       sched_ops->thread_exited(uth);
+                       return;
+               case UTH_JOIN_JOINABLE:
+                       /* This write is harmless and idempotent; we can lose the race and
+                        * still be safe.  Assuming we don't, the joiner will look here for
+                        * the retval.  It's temporary storage since we don't know the final
+                        * retval location (since join hasn't happened yet). */
+                       jc->retval = retval;
+                       break;
+               };
+               assert(old_state == UTH_JOIN_JOINABLE);
+       } while (!atomic_cas(&jc->state, old_state, UTH_JOIN_EXITED));
+       /* We were joinable, now we have exited.  A detacher or joiner will trigger
+        * thread_exited. */
+}
+
+/* 2LSs call this when their threads are exiting.  The 2LS will regain control
+ * of the thread in sched_ops->thread_exited.  This will be after the
+ * join/detach/exit has completed, and might be in vcore context. */
+void __attribute__((noreturn)) uth_2ls_thread_exit(void *retval)
+{
+       uthread_yield(FALSE, __uth_2ls_thread_exit_cb, retval);
+       assert(0);
+}
+
+/* Helper: Attaches the caller (specifically the jk) to the target uthread.
+ * When the thread has been joined (either due to the UTH_EXITED case or due to
+ * __uth_2ls_thread_exit_cb), the join kicker will be decreffed. */
+static void join_one(struct uthread *uth, struct uth_join_kicker *jk,
+                     void **retval_loc)
+{
+       struct uth_join_ctl *jc = &uth->join_ctl;
+       long old_state;
+
+       /* We can safely write to the join_ctl, even if we don't end up setting
+        * HAS_JOINER.  There's only supposed to be one joiner, and if not, we'll
+        * catch the bad state. */
+       jc->retval_loc = retval_loc;
+       jc->kicker = jk;
+       do {
+               old_state = atomic_read(&jc->state);
+               switch (old_state) {
+               case UTH_JOIN_EXITED:
+                       if (retval_loc)
+                               *retval_loc = jc->retval;
+                       sched_ops->thread_exited(uth);
+                       kref_put(&jk->kref);
+                       return;
+               case UTH_JOIN_DETACHED:
+                       panic("Uth %p has been detached, can't join!", uth);
+               case UTH_JOIN_HAS_JOINER:
+                       panic("Uth %p has another pending joiner!", uth);
+               };
+               assert(old_state == UTH_JOIN_JOINABLE);
+       } while (!atomic_cas(&jc->state, old_state, UTH_JOIN_HAS_JOINER));
+}
+
+/* Bottom half of the join, in vcore context */
+static void __uth_join_cb(struct uthread *uth, void *arg)
+{
+       struct uth_join_kicker *jk = (struct uth_join_kicker*)arg;
+
+       uthread_has_blocked(uth, NULL, UTH_EXT_BLK_MUTEX);
+       /* After this, and after all threads join, we could be woken up. */
+       kref_put(&jk->kref);
+}
+
+static void kicker_release(struct kref *k)
+{
+       struct uth_join_kicker *jk = container_of(k, struct uth_join_kicker, kref);
+
+       uthread_runnable(jk->joiner);
+}
+
+void uthread_join_arr(struct uth_join_request reqs[], size_t nr_req)
+{
+       struct uth_join_kicker jk[1];
+
+       jk->joiner = current_uthread;
+       /* One ref for each target, another for *us*, which we drop in the yield
+        * callback.  As as soon as it is fully decreffed, our thread will be
+        * restarted.  We must block before that (in the yield callback). */
+       kref_init(&jk->kref, kicker_release, nr_req + 1);
+       for (int i = 0; i < nr_req; i++)
+               join_one(reqs[i].uth, jk, reqs[i].retval_loc);
+       uthread_yield(TRUE, __uth_join_cb, jk);
+}
+
+/* Unlike POSIX, we don't bother with returning error codes.  Anything that can
+ * go wrong is so horrendous that you should crash (the specs say the behavior
+ * is undefined). */
+void uthread_join(struct uthread *uth, void **retval_loc)
+{
+       struct uth_join_request req[1];
+
+       req->uth = uth;
+       req->retval_loc = retval_loc;
+       uthread_join_arr(req, 1);
+}