Handle threading after a fork() (XCC)
authorBarret Rhoden <brho@cs.berkeley.edu>
Thu, 7 Jun 2018 18:59:28 +0000 (14:59 -0400)
committerBarret Rhoden <brho@cs.berkeley.edu>
Thu, 7 Jun 2018 19:22:15 +0000 (15:22 -0400)
Threading and fork() do not go together well, especially user-level
threads.  The kernel won't let you fork() an MCP.  However, you can have a
threaded SCP - dropbear is one such example.  It uses the pthread 2LS, but
just happens to be on one core.

Dropbear uses threads internally in its implementation of tty.c.  There are
two threads: one for pushing inbound traffic and another for outbound
traffic between the network and the child's pipes.

Recall another pain in the ass with fork: duplicated state between the
child and parent.  We have a bunch of things implemented in userspace, such
as select() and an alarm service.  Those are FDs that get carried over to
the child.  We have the "fork callbacks" to clean that stuff up.

Now, what happens when you fork a threaded process?  Its forked thread
returns in the child, and the child also has all of the memory structures
for the other threads.  If you exec() immediately, that all gets blown
away.  However, if that thread happens to block on a syscall, then the 2LS
takes over and it can run those other threads.

In dropbear's case, it returns from the fork, it closes the tty threads'
FDs (in one of its functions, not in a fork-cb), does a few other things,
etc.  One of those things was chdir(), which happens to block.  The reason
for that is chdir() calls synchronize_rcu(), which can block until the GP
ktask runs, at least under the current settings.

Once the thread (thread0) blocks in chdir, the other threads run.  However,
their FDs were closed, so they'll return an error.  Depending on whether or
not you have the DB bug fix, it'll either segfault under some circumstances
or just error out with a data_flow error.

To fix this, we now require all 2LSs that want to support forking to have
explicit support.  The thread0 sched trivially supports this.  The pthread
2LS will allow you to fork only from thread0, and it will suspend threading
for the duration of the fork in both the parent and the child.  During that
time, only thread0 will run.  After the fork(), the parent will resume
threading.  In the child, we'll never thread.  Those other threads will sit
on the ready_queue forever and will never run.  In DB's case, we don't want
them to run, since their FDs are closed.

Also, note that this is only a problem for the child, not the parent.
Further, SYS_fork will never appear to block for the child.  The syscall is
marked 'finished' before the child is RUNNABLE_S.

In general, let's just try to stop using fork().

Rebuild glibc, your shells, and dropbear.

Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
tools/compilers/gcc-glibc/glibc-2.19-akaros/sysdeps/akaros/Versions
tools/compilers/gcc-glibc/glibc-2.19-akaros/sysdeps/akaros/fork.c
tools/compilers/gcc-glibc/glibc-2.19-akaros/sysdeps/akaros/fork_cb.c
tools/compilers/gcc-glibc/glibc-2.19-akaros/sysdeps/akaros/sys/fork_cb.h
user/parlib/thread0_sched.c
user/pthread/pthread.c

index 3a8d19f..8d000dd 100644 (file)
@@ -114,5 +114,9 @@ libc {
     dtls_key_create;
     set_dtls;
     get_dtls;
+
+    # Function pointers in fork_cb.c
+    pre_fork_2ls;
+    post_fork_2ls;
   }
 }
index 696c992..a4ece10 100644 (file)
@@ -20,6 +20,7 @@
 #include <unistd.h>
 #include <sys/types.h>
 #include <stdlib.h>
+#include <stdio.h>
 #include <ros/syscall.h>
 #include <sys/fork_cb.h>
 
@@ -31,7 +32,13 @@ pid_t __fork(void)
        pid_t ret;
        struct fork_cb *cb;
 
+       if (!pre_fork_2ls || !post_fork_2ls) {
+               fprintf(stderr, "[user] Tried to fork without 2LS support!\n");
+               assert(0);
+       }
+       pre_fork_2ls();
        ret = ros_syscall(SYS_fork, 0, 0, 0, 0, 0, 0);
+       post_fork_2ls(ret);
        if (ret == 0) {
                cb = fork_callbacks;
                while (cb) {
index 7a2ef56..f6fc033 100644 (file)
@@ -7,6 +7,9 @@
 #include <sys/fork_cb.h>
 #include <ros/common.h>
 
+void (*pre_fork_2ls)(void);
+void (*post_fork_2ls)(pid_t ret);
+
 struct fork_cb *fork_callbacks;
 
 void register_fork_cb(struct fork_cb *cb)
index fcb4db5..6735ce4 100644 (file)
 
 #pragma once
 
+#include <sys/types.h>
+
+/* 2LSs need to set these CBs if they want to be able to fork. */
+extern void (*pre_fork_2ls)(void);
+extern void (*post_fork_2ls)(pid_t ret);
+
 struct fork_cb {
        struct fork_cb                          *next;
        void (*func)(void);
index e43f770..ca1d0a0 100644 (file)
@@ -16,6 +16,7 @@
 #include <parlib/arch/trap.h>
 #include <parlib/ros_debug.h>
 #include <stdlib.h>
+#include <sys/fork_cb.h>
 
 static void thread0_sched_init(void);
 static void thread0_sched_entry(void);
@@ -74,6 +75,14 @@ void thread0_handle_syscall(struct event_msg *ev_msg,
        thread0_info.is_blocked = FALSE;
 }
 
+static void thread0_pre_fork(void)
+{
+}
+
+static void thread0_post_fork(pid_t ret)
+{
+}
+
 void thread0_sched_init(void)
 {
        int ret;
@@ -87,6 +96,8 @@ void thread0_sched_init(void)
        sysc_evq = get_eventq(EV_MBOX_BITMAP);
        sysc_evq->ev_flags = EVENT_INDIR | EVENT_WAKEUP;
        uthread_2ls_init(thread0_uth, thread0_handle_syscall, NULL);
+       pre_fork_2ls = thread0_pre_fork;
+       post_fork_2ls = thread0_post_fork;
 }
 
 /* Thread0 scheduler ops (for processes that haven't linked in a full 2LS) */
index 241e89c..a034c97 100644 (file)
@@ -19,6 +19,7 @@
 #include <parlib/arch/trap.h>
 #include <parlib/ros_debug.h>
 #include <parlib/stdio.h>
+#include <sys/fork_cb.h>
 
 /* TODO: eventually, we probably want to split this into the pthreads interface
  * and a default 2LS.  That way, apps can use the pthreads interface and use any
@@ -38,6 +39,7 @@ int threads_ready = 0;
 int threads_active = 0;
 atomic_t threads_total;
 bool need_tls = TRUE;
+static bool skip_non_thread0;
 
 /* Array of per-vcore structs to manage waiting on syscalls and handling
  * overflow.  Init'd in pth_init(). */
@@ -107,7 +109,12 @@ static void __attribute__((noreturn)) pth_sched_entry(void)
                handle_events(vcoreid);
                __check_preempt_pending(vcoreid);
                mcs_pdr_lock(&queue_lock);
-               new_thread = TAILQ_FIRST(&ready_queue);
+               TAILQ_FOREACH(new_thread, &ready_queue, tq_next) {
+                       if (skip_non_thread0 &&
+                           !uthread_is_thread0((struct uthread*)new_thread))
+                               continue;
+                       break;
+               }
                if (new_thread) {
                        TAILQ_REMOVE(&ready_queue, new_thread, tq_next);
                        assert(new_thread->state == PTH_RUNNABLE);
@@ -507,6 +514,24 @@ int pthread_getattr_np(pthread_t __th, pthread_attr_t *__attr)
        return 0;
 }
 
+/* All threading is suspended during a fork.  Parents will continue threading
+ * after the fork.  Children will never thread again.  If they fork, but don't
+ * exec, then their threading will be broken.  Oh well - stop using fork. */
+static void pth_pre_fork(void)
+{
+       if (!uthread_is_thread0(current_uthread))
+               panic("Tried to fork from a non-thread0 thread!");
+       if (in_multi_mode())
+               panic("Tried to fork from an MCP!");
+       skip_non_thread0 = true;
+}
+
+static void pth_post_fork(pid_t ret)
+{
+       if (ret)
+               skip_non_thread0 = false;
+}
+
 /* Do whatever init you want.  At some point call uthread_2ls_init() and pass it
  * a uthread representing thread0 (int main()) */
 void pth_sched_init(void)
@@ -587,6 +612,8 @@ void pth_sched_init(void)
 #endif
        uthread_2ls_init((struct uthread*)t, pth_handle_syscall, NULL);
        atomic_init(&threads_total, 1);                 /* one for thread0 */
+       pre_fork_2ls = pth_pre_fork;
+       post_fork_2ls = pth_post_fork;
 }
 
 /* Make sure our scheduler runs inside an MCP rather than an SCP. */