threaded, forked, rethreaded processes will deadlock
Brian Fundakowski Feldman
green at freebsd.org
Fri Jan 16 13:20:14 PST 2009
On Fri, Jan 09, 2009 at 09:39:08AM -0800, Julian Elischer wrote:
> Brian Fundakowski Feldman wrote:
>> On Thu, Jan 08, 2009 at 11:09:16PM -0800, Julian Elischer wrote:
>>> Brian Fundakowski Feldman wrote:
>>>> On Thu, Jan 08, 2009 at 10:44:20PM -0500, Daniel Eischen wrote:
>>>>> On Thu, 8 Jan 2009, Brian Fundakowski Feldman wrote:
>>>>>
>>>>>> It appears that the post-fork hooks for malloc(3) are somewhat broken such that
>>>>>> when a threaded program forks, and then its child attempts to go threaded, it
>>>>>> deadlocks because it already appears to have locks held. I am not familiar
>>>>>> enough with the current libthr/libc/rtld-elf interaction that I've been able
>>>>>> to fix it myself, unfortunately.
>>>>> There's really nothing to fix - according to POSIX you are only
>>>>> allowed to call async-signal-safe functions in the child forked
>>>>> from a threaded process. If you are trying to do anything other
>>>>> than that, it may or may not work on FreeBSD, but it is not
>>>>> guaranteed and is not portable.
>>>>>
>>>>> The rationale is that what is the point of forking and creating
>>>>> more threads, when you can just as easily create more threads in
>>>>> the parent without forking? The only reason to fork from a threaded
>>>>> process is to call one of the exec() functions.
>>>> Well, it worked until the last major set of changes to malloc. For me, the point
>>>> was that I was able to have transparent background worker threads in any program
>>>> regardless of its architecture, using the standard pthread fork hooks. Could you
>>>> point me to the POSIX section covering fork and threads? If it's really not
>>>> supposed to work then that's fine, but there's an awful lot of code there dedicated
>>>> to support going threaded again after a fork.
>>>>
>>> Practically, you can't go threaded again after a fork
>>> (by which I mean creating new threads or use things
>>> like mutexes etc.) in any posix system I know of.
>>>
>>> It would require that:
>>> The forking thread stop until:
>>> Every other thread has released every resource it owns
>>> and reports itself to be in a "safe quiescent state",
>>> or at least report every resource it owns, especially
>>> locks,
>>> and
>>> After the fork:
>>> The child, post fork, to take ownership of all
>>> of them, and free them.
>>>
>>> You might be able to do that in a simple
>>> threaded program, but consider then that the libraries may have
>>> threads running in them of which you are unaware, and that
>>> some of the resources may interract with resources owned by the
>>> forking thread.
>>>
>>> Add to this that there may be a signal thrown into this mix as well
>>>
>>> (signals are the bane of thread developement)
>>
>> Well, I wouldn't mind showing all of you what I can of what I had been doing
>> with the background threads -- it works pretty well modulo this particular
>> malloc lock bug. Due to it being inappropriate to share library resources
>> between a child and parent for an open socket connection, I always considered
>> the only "safe" behavior to be going single-threaded for the duration of the fork
>> processes in both the parent and child, and the pthread_atfork(3) hooks have been
>> sufficient to do so.
>
>
> ahhhh
> well going single threaded for the duration of the fork, changes
> everything!
Could you, and anyone else who would care to, check this out? It's a regression
fix but it also makes the code a little bit clearer. Thanks!
Index: lib/libc/stdlib/malloc.c
===================================================================
--- lib/libc/stdlib/malloc.c (revision 187160)
+++ lib/libc/stdlib/malloc.c (working copy)
@@ -415,6 +415,7 @@
/* Set to true once the allocator has been initialized. */
static bool malloc_initialized = false;
+static bool malloc_during_fork = false;
/* Used to avoid initialization races. */
static malloc_mutex_t init_lock = {_SPINLOCK_INITIALIZER};
@@ -1205,7 +1206,7 @@
malloc_mutex_lock(malloc_mutex_t *mutex)
{
- if (__isthreaded)
+ if (__isthreaded || malloc_during_fork)
_SPINLOCK(&mutex->lock);
}
@@ -1213,7 +1214,7 @@
malloc_mutex_unlock(malloc_mutex_t *mutex)
{
- if (__isthreaded)
+ if (__isthreaded || malloc_during_fork)
_SPINUNLOCK(&mutex->lock);
}
@@ -1260,7 +1261,7 @@
{
unsigned ret = 0;
- if (__isthreaded) {
+ if (__isthreaded || malloc_during_fork) {
if (_pthread_mutex_trylock(lock) != 0) {
/* Exponentially back off if there are multiple CPUs. */
if (ncpus > 1) {
@@ -1296,7 +1297,7 @@
malloc_spin_unlock(pthread_mutex_t *lock)
{
- if (__isthreaded)
+ if (__isthreaded || malloc_during_fork)
_pthread_mutex_unlock(lock);
}
@@ -5515,9 +5516,8 @@
void
_malloc_prefork(void)
{
+ arena_t *larenas[narenas];
bool again;
- unsigned i, j;
- arena_t *larenas[narenas], *tarenas[narenas];
/* Acquire all mutexes in a safe order. */
@@ -5530,19 +5530,23 @@
*/
memset(larenas, 0, sizeof(arena_t *) * narenas);
do {
+ unsigned int i;
+
again = false;
malloc_spin_lock(&arenas_lock);
for (i = 0; i < narenas; i++) {
if (arenas[i] != larenas[i]) {
+ arena_t *tarenas[narenas];
+ unsigned int j;
+
memcpy(tarenas, arenas, sizeof(arena_t *) *
narenas);
malloc_spin_unlock(&arenas_lock);
for (j = 0; j < narenas; j++) {
if (larenas[j] != tarenas[j]) {
larenas[j] = tarenas[j];
- malloc_spin_lock(
- &larenas[j]->lock);
+ malloc_spin_lock(&larenas[j]->lock);
}
}
again = true;
@@ -5558,6 +5562,7 @@
#ifdef MALLOC_DSS
malloc_mutex_lock(&dss_mtx);
#endif
+ malloc_during_fork = true;
}
void
@@ -5582,6 +5587,12 @@
if (larenas[i] != NULL)
malloc_spin_unlock(&larenas[i]->lock);
}
+ /*
+ * This ends the special post-__isthreaded exemption behavior for
+ * malloc stuff. We should really be single threaded right now
+ * in effect regardless of __isthreaded status.
+ */
+ malloc_during_fork = false;
}
/*
Index: lib/libthr/thread/thr_fork.c
===================================================================
--- lib/libthr/thread/thr_fork.c (revision 187160)
+++ lib/libthr/thread/thr_fork.c (working copy)
@@ -105,7 +105,7 @@
struct pthread_atfork *af;
pid_t ret;
int errsave;
- int unlock_malloc;
+ int was_threaded;
int rtld_locks[MAX_RTLD_LOCKS];
if (!_thr_is_inited())
@@ -122,16 +122,16 @@
}
/*
- * Try our best to protect memory from being corrupted in
- * child process because another thread in malloc code will
- * simply be kill by fork().
+ * All bets are off as to what should happen soon if the parent
+ * process was not so kindly as to set up pthread fork hooks to
+ * relinquish all running threads.
*/
if (_thr_isthreaded() != 0) {
- unlock_malloc = 1;
+ was_threaded = 1;
_malloc_prefork();
_rtld_atfork_pre(rtld_locks);
} else {
- unlock_malloc = 0;
+ was_threaded = 0;
}
/*
@@ -159,7 +159,7 @@
_thr_umutex_init(&curthread->lock);
_thr_umutex_init(&_thr_atfork_lock);
- if (unlock_malloc)
+ if (was_threaded)
_rtld_atfork_post(rtld_locks);
_thr_setthreaded(0);
@@ -173,7 +173,7 @@
/* Ready to continue, unblock signals. */
_thr_signal_unblock(curthread);
- if (unlock_malloc)
+ if (was_threaded)
_malloc_postfork();
/* Run down atfork child handlers. */
@@ -188,7 +188,7 @@
/* Ready to continue, unblock signals. */
_thr_signal_unblock(curthread);
- if (unlock_malloc) {
+ if (was_threaded) {
_rtld_atfork_post(rtld_locks);
_malloc_postfork();
}
Index: tools/regression/pthread/malloc_thread_fork_survival/no_deadlock.c
===================================================================
--- tools/regression/pthread/malloc_thread_fork_survival/no_deadlock.c (revision 0)
+++ tools/regression/pthread/malloc_thread_fork_survival/no_deadlock.c (revision 0)
@@ -0,0 +1,55 @@
+/*
+ * Public domain, originally by Brian Fundakowski Feldman <green at FreeBSD.org>.
+ * $FreeBSD$
+ */
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <pthread.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+void *
+noop(void *unused)
+{
+ return (NULL);
+}
+
+void *
+exit0(void *unused)
+{
+ const char ok[] = "ok\n";
+ write(1, ok, sizeof(ok));
+ exit(0);
+}
+
+void *
+exitN(int code)
+{
+ if (code == 0) {
+ exit0(NULL);
+ } else {
+ const char not_ok[] = "not ok\n";
+ write(1, not_ok, sizeof(not_ok));
+ exit(code);
+ }
+}
+
+int
+main()
+{
+ pthread_t thread;
+ int exited;
+
+ pthread_create(&thread, NULL, noop, NULL);
+ pthread_join(thread, NULL);
+ if (fork() == 0) {
+ alarm(1);
+ pthread_create(&thread, NULL, exit0, NULL);
+ pthread_join(thread, NULL);
+ /* UNREACHED */
+ }
+ wait(&exited);
+ exitN(WIFSIGNALED(exited) ? WTERMSIG(exited) : WEXITSTATUS(exited));
+}
Index: tools/regression/pthread/malloc_thread_fork_survival/Makefile
===================================================================
--- tools/regression/pthread/malloc_thread_fork_survival/Makefile (revision 0)
+++ tools/regression/pthread/malloc_thread_fork_survival/Makefile (working copy)
@@ -1,8 +1,8 @@
# $FreeBSD$
-PROG= cv_cancel1
+PROG= no_deadlock
NO_MAN=
-LDADD= -lpthread
+LDADD= -lthr
.include <bsd.prog.mk>
--
Brian Fundakowski Feldman \'[ FreeBSD ]''''''''''\
<> green at FreeBSD.org \ The Power to Serve! \
Opinions expressed are my own. \,,,,,,,,,,,,,,,,,,,,,,\
More information about the freebsd-hackers
mailing list