parlib: Have exactly one specific 2LS
authorBarret Rhoden <brho@cs.berkeley.edu>
Wed, 3 May 2017 14:08:45 +0000 (10:08 -0400)
committerBarret Rhoden <brho@cs.berkeley.edu>
Wed, 3 May 2017 16:13:02 +0000 (12:13 -0400)
Processes in Akaros are given control over their cores, but with this
control comes a bootstrapping problem.  For very early code, such as ld.so
and parts of glibc, the kernel takes care of a few things.  This time is
referred to as "early-SCP context."  Eventually, the process is capable of
handling itself, and it takes over.

Previously, we would make the transition from the early-SCP to the thread0
scheduler, and then if a "real" 2LS (e.g. pthreads or vmm) was linked in,
it's constructor would switch us from thread0 to the real 2LS.

There's a bunch of problems with this.
- What's to stop you from having multiple 2LSs linked in, and then only the
  'last ctor wins'?
- It's very tricky to transition from thread0 to another 2LS.  We've had a
  bunch of bugs and tricky code related to races, blocking calls, and other
things during the transition.  Going from early-SCP to a 2LS is fine.  But
once the process has taken control, switching out schedulers is very
tricky.
- If glibc (or any code, like a ctor) makes a 2LS op after thread0 is
  running, but before the next 2LS takes over, the object we're operating
on will get confused.  For instance, consider a sync object initialized for
thread0, but then locked while pthreads is the 2LS.  Pthreads would see a
sync object that it didn't initialize.

For all these reasons, and especially the latter, it's time to go back to
the older style of 2LS ops: there's a weak version of the sched ops, which
is thread0s.  That will be the default 2LS unless another one is used.  If
you link in another 2LS (e.g. pthreads or vmm), then that will take over.
This will also catch issues of linking in multiple 2LSs.

Now that we know the specific 2LS statically, uthread_lib_init() can call
the 2LS's initialization function.  Previously we were forcing the 2LS
ctors to make sure uthread code ran first, but there was actually nothing
making sure the 2LSs ctor ran first.  Imagine a ctor in your application
that uses the 2LS via the GCC/C++ threading.  ld or whatever can figure out
that it needs parlib and run parlib's ctors first.  But it won't know that
e.g. pthread's ctor needs to run.  Now, that's not a problem.  The
dependency is on GCC/C++ threads -> uthreads, and the implied dependency
from uthreads to the real 2LS is sorted out by calling the 2LS's
sched_op->sched_init().

Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
user/parlib/include/parlib/uthread.h
user/parlib/thread0_sched.c
user/parlib/uthread.c
user/pthread/pthread.c
user/utest/pvcalarm.c
user/vmm/sched.c

index 343ef92..34aa6e7 100644 (file)
@@ -100,7 +100,7 @@ void __uth_default_sync_enqueue(struct uthread *uth, uth_sync_t *sync);
 /* 2L-Scheduler operations.  Examples in pthread.c. */
 struct schedule_ops {
        /**** These functions must be defined ****/
-       /* Functions supporting thread ops */
+       void (*sched_init)(void);
        void (*sched_entry)(void);
        void (*thread_runnable)(struct uthread *);
        void (*thread_paused)(struct uthread *);
@@ -118,13 +118,11 @@ struct schedule_ops {
 };
 extern struct schedule_ops *sched_ops;
 
-/* Low-level _S code calls this for basic uthreading without a 2LS */
-void uthread_lib_init(void);
 /* Call this from your 2LS init routines.  Pass it a uthread representing
  * thread0, your 2LS ops, and your syscall handler + data.
  *
  * When it returns, you're in _M mode (thread0 on vcore0) */
-void uthread_2ls_init(struct uthread *uthread, struct schedule_ops *ops,
+void uthread_2ls_init(struct uthread *uthread,
                       void (*handle_sysc)(struct event_msg *, unsigned int,
                                           void *),
                       void *data);
index 203a189..27b8208 100644 (file)
@@ -17,6 +17,7 @@
 #include <parlib/ros_debug.h>
 #include <stdlib.h>
 
+static void thread0_sched_init(void);
 static void thread0_sched_entry(void);
 static void thread0_thread_blockon_sysc(struct uthread *uthread, void *sysc);
 static void thread0_thread_refl_fault(struct uthread *uth,
@@ -33,6 +34,7 @@ static bool thread0_sync_get_uth(uth_sync_t *s, struct uthread *uth);
 
 /* externed into uthread.c */
 struct schedule_ops thread0_2ls_ops = {
+       .sched_init = thread0_sched_init,
        .sched_entry = thread0_sched_entry,
        .thread_blockon_sysc = thread0_thread_blockon_sysc,
        .thread_refl_fault = thread0_thread_refl_fault,
@@ -47,6 +49,8 @@ struct schedule_ops thread0_2ls_ops = {
        .sync_get_uth = thread0_sync_get_uth,
 };
 
+struct schedule_ops *sched_ops __attribute__((weak)) = &thread0_2ls_ops;
+
 /* externed into uthread.c */
 struct uthread *thread0_uth;
 
@@ -65,12 +69,19 @@ void thread0_handle_syscall(struct event_msg *ev_msg,
        thread0_info.is_blocked = FALSE;
 }
 
-void thread0_lib_init(void)
+void thread0_sched_init(void)
 {
+       int ret;
+
+       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*/
        memset(&thread0_info, 0, sizeof(thread0_info));
        /* we don't care about the message, so don't bother with a UCQ */
        sysc_evq = get_eventq(EV_MBOX_BITMAP);
        sysc_evq->ev_flags = EVENT_INDIR | EVENT_WAKEUP;
+       uthread_2ls_init(thread0_uth, thread0_handle_syscall, NULL);
 }
 
 /* Thread0 scheduler ops (for processes that haven't linked in a full 2LS) */
index a1836f0..0bc800b 100644 (file)
 #include <parlib/arch/trap.h>
 #include <parlib/ros_debug.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. */
-extern struct schedule_ops thread0_2ls_ops;
-struct schedule_ops *sched_ops = &thread0_2ls_ops;
-
 __thread struct uthread *current_uthread = 0;
 /* ev_q for all preempt messages (handled here to keep 2LSs from worrying
  * extensively about the details.  Will call out when necessary. */
@@ -74,17 +69,7 @@ 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 a prior call), so
-        * free it before installing the new one. */
-       if (current_uthread)
-               free(current_uthread);
        current_uthread = uthread;
-       /* We may not be an MCP at this point (and thus not really working with
-        * vcores), but there is still the notion of something vcore_context-like
-        * even when running as an SCP (i.e. its more of a scheduler_context than a
-        * vcore_context).  Threfore we need to set __vcore_context to TRUE here to
-        * represent this (otherwise we will hit some asserts of not being in
-        * vcore_context when running in scheduler_context for the SCP. */
        __vcore_context = TRUE;
        end_safe_access_tls_vars();
        set_tls_desc(uthread->tls_desc);
@@ -138,58 +123,6 @@ void uthread_mcp_init()
        vcore_change_to_m();
 }
 
-/* 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)
-{
-       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
  * vcore 0.  Pairs with k/s/process.c scp_is_vcctx_ready(). */
 static void scp_vcctx_ready(void)
@@ -226,49 +159,93 @@ static char *__ros_errstr_loc(void)
                return current_uthread->err_str;
 }
 
-/* Sets up basic uthreading for when we are in _S mode and before we set up the
- * 2LS.  Some apps may not have a 2LS and thus never do the full
- * vcore/2LS/uthread init. */
-void __attribute__((constructor)) uthread_lib_init(void)
+static 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. */
-       parlib_init_once_racy(return);
+       /* Need to make sure the vcore_lib_init() ctor runs first */
        vcore_lib_init();
+       /* Instead of relying on ctors for the specific 2LS, we make sure they are
+        * called next.  They will call uthread_2ls_init().
+        *
+        * The potential issue here is that C++ ctors might make use of the GCC/C++
+        * threading callbacks, which require the full 2LS.  There's no linkage
+        * dependency  between C++ and the specific 2LS, so there's no way to be
+        * sure the 2LS actually turned on before we started calling into it.
+        *
+        * Hopefully, the uthread ctor was called in time, since the GCC threading
+        * functions link against parlib.  Note that, unlike parlib-compat.c, there
+        * are no stub functions available to GCC that could get called by
+        * accident and prevent the linkage. */
+       sched_ops->sched_init();
+}
+
+/* The 2LS calls this, passing in a uthread representing thread0 and its
+ * syscall handling routine.  (NULL is fine).  The 2LS sched_ops is known
+ * statically (via symbol overrides).
+ *
+ * This is where parlib (and whatever 2LS is linked in) takes over control of
+ * scheduling, including handling notifications, having sched_entry() called,
+ * blocking syscalls, and handling syscall completion events.  Before this
+ * call, these things are handled by slim functions in glibc (e.g. early
+ * function pointers for ros_blockon) and by the kernel.  The kerne's role was
+ * to treat the process specially until we call scp_vcctx_ready(): things like
+ * no __notify, no sched_entry, etc.
+ *
+ * We need to be careful to not start using the 2LS before it is fully ready.
+ * For instance, once we change ros_blockon, we could have a blocking syscall
+ * (e.g. for something glibc does) and the rest of the 2LS code expects things
+ * to be in place.
+ *
+ * In older versions of this code, we would hop from the thread0 sched to the
+ * real 2LSs sched, which meant we had to be very careful.  But now that we
+ * only do this once, we can do all the prep work and then take over from
+ * glibc's early SCP setup.  Specifically, notifs are disabled (due to the
+ * early SCP ctx) and syscalls won't use the __ros_uth_syscall_blockon, so we
+ * shouldn't get a syscall event.
+ *
+ * Still, if you have things like an outstanding async syscall, then you'll
+ * have issues.  Most likely it would complete and you'd never hear about it.
+ *
+ * Note that some 2LS ops can be called even before we've initialized the 2LS!
+ * Some ops, like the sync_obj ops, are called when initializing an uncontested
+ * mutex, which could be called from glibc (e.g. malloc).  Hopefully that's
+ * fine - we'll see!  I imagine a contested mutex would be a disaster (during
+ * the unblock), which shouldn't happen as we are single threaded. */
+void uthread_2ls_init(struct uthread *uthread,
+                      void (*handle_sysc)(struct event_msg *, unsigned int,
+                                          void *),
+                      void *data)
+{
+       struct ev_handler *new_h = NULL;
 
-       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*/
-       /* 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);
+       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;
+               assert(!ev_handlers[EV_SYSCALL]);
+               ev_handlers[EV_SYSCALL] = new_h;
+       }
+       uthread_init_thread0(uthread);
+       uthread_track_thread0(uthread);
        /* 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();
+       /* Now that we're ready (I hope) to operate as a full process, we tell the
+        * kernel.  We must set vcctx and blockon atomically with respect to
+        * syscalls, meaning no syscalls in between. */
        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.
-        * Once we tell the kernel we are ready to utilize vcore context, we need
-        * our blocking syscalls to utilize it as well. */
+        * Now that we told the kernel we are ready to utilize vcore context, we
+        * need our blocking syscalls to utilize it as well. */
        ros_syscall_blockon = __ros_uth_syscall_blockon;
        cmb();
        init_posix_signals();
@@ -1213,6 +1190,8 @@ bool uth_2ls_is_multithreaded(void)
 {
        /* thread 0 is single threaded.  For the foreseeable future, every other 2LS
         * will be multithreaded. */
+       extern struct schedule_ops thread0_2ls_ops;
+
        return sched_ops != &thread0_2ls_ops;
 }
 
index ceafca2..1fa14fd 100644 (file)
@@ -38,6 +38,7 @@ static inline void spin_to_sleep(unsigned int spins, unsigned int *spun);
 static inline void pthread_exit_no_cleanup(void *ret);
 
 /* Pthread 2LS operations */
+static void pth_sched_init(void);
 static void pth_sched_entry(void);
 static void pth_thread_runnable(struct uthread *uthread);
 static void pth_thread_paused(struct uthread *uthread);
@@ -54,6 +55,7 @@ static void pth_handle_syscall(struct event_msg *ev_msg, unsigned int ev_type,
                                void *data);
 
 struct schedule_ops pthread_sched_ops = {
+       .sched_init = pth_sched_init,
        .sched_entry = pth_sched_entry,
        .thread_runnable = pth_thread_runnable,
        .thread_paused = pth_thread_paused,
@@ -64,6 +66,8 @@ struct schedule_ops pthread_sched_ops = {
        .thread_create = pth_thread_create,
 };
 
+struct schedule_ops *sched_ops = &pthread_sched_ops;
+
 /* Static helpers */
 static void __pthread_free_stack(struct pthread_tcb *pt);
 static int __pthread_allocate_stack(struct pthread_tcb *pt);
@@ -480,16 +484,12 @@ int pthread_getattr_np(pthread_t __th, pthread_attr_t *__attr)
 
 /* Do whatever init you want.  At some point call uthread_2ls_init() and pass it
  * a uthread representing thread0 (int main()) */
-void __attribute__((constructor)) pthread_lib_init(void)
+void pth_sched_init(void)
 {
        uintptr_t mmap_block;
        struct pthread_tcb *t;
        int ret;
 
-       /* Only run once, but make sure that uthread_lib_init() has run already. */
-       parlib_init_once_racy(return);
-       uthread_lib_init();
-
        mcs_pdr_init(&queue_lock);
        /* Create a pthread_tcb for the main thread */
        ret = posix_memalign((void**)&t, __alignof__(struct pthread_tcb),
@@ -560,9 +560,7 @@ void __attribute__((constructor)) pthread_lib_init(void)
                sysc_mgmt[i].ev_q->ev_mbox = sysc_mbox;
        }
 #endif
-       /* Sched ops is set by 2ls_init */
-       uthread_2ls_init((struct uthread*)t, &pthread_sched_ops, pth_handle_syscall,
-                        NULL);
+       uthread_2ls_init((struct uthread*)t, pth_handle_syscall, NULL);
        atomic_init(&threads_total, 1);                 /* one for thread0 */
 }
 
index bc61048..7160a88 100644 (file)
@@ -66,7 +66,6 @@ bool test_sigperf(void)
                __sync_fetch_and_add(__count, 1);
        }
 
-       pthread_lib_init();
        parlib_never_yield = FALSE;
        parlib_never_vc_request = FALSE;
 
index 744cb39..33765c4 100644 (file)
@@ -33,6 +33,7 @@ static atomic_t nr_unblk_guests;
 /* Global evq for all syscalls.  Could make this per vcore or whatever. */
 static struct event_queue *sysc_evq;
 
+static void vmm_sched_init(void);
 static void vmm_sched_entry(void);
 static void vmm_thread_runnable(struct uthread *uth);
 static void vmm_thread_paused(struct uthread *uth);
@@ -45,6 +46,7 @@ static void vmm_thread_exited(struct uthread *uth);
 static struct uthread *vmm_thread_create(void *(*func)(void *), void *arg);
 
 struct schedule_ops vmm_sched_ops = {
+       .sched_init = vmm_sched_init,
        .sched_entry = vmm_sched_entry,
        .thread_runnable = vmm_thread_runnable,
        .thread_paused = vmm_thread_paused,
@@ -55,6 +57,8 @@ struct schedule_ops vmm_sched_ops = {
        .thread_create = vmm_thread_create,
 };
 
+struct schedule_ops *sched_ops = &vmm_sched_ops;
+
 /* Helpers */
 static void vmm_handle_syscall(struct event_msg *ev_msg, unsigned int ev_type,
                                void *data);
@@ -110,13 +114,10 @@ static struct event_queue *setup_sysc_evq(int vcoreid)
        return evq;
 }
 
-static void __attribute__((constructor)) vmm_lib_init(void)
+static void vmm_sched_init(void)
 {
        struct task_thread *thread0;
 
-       parlib_init_once_racy(return);
-       uthread_lib_init();
-
        /* Note that thread0 doesn't belong to a VM.  We can set this during
         * vmm_init() if we need to. */
        thread0 = (struct task_thread*)alloc_vmm_thread(0, VMM_THREAD_TASK);
@@ -126,8 +127,7 @@ static void __attribute__((constructor)) vmm_lib_init(void)
        thread0->stacktop = (void*)USTACKTOP;
        /* for lack of a better vcore, might as well send to 0 */
        sysc_evq = setup_sysc_evq(0);
-       uthread_2ls_init((struct uthread*)thread0, &vmm_sched_ops,
-                     vmm_handle_syscall, NULL);
+       uthread_2ls_init((struct uthread*)thread0, vmm_handle_syscall, NULL);
 }
 
 /* The scheduling policy is encapsulated in the next few functions (from here