cosmopolitan/libc/thread/pthread_atfork.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

180 lines
7 KiB
C
Raw Normal View History

/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi
Copyright 2022 Justine Alexandra Roberts Tunney
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/errno.h"
#include "libc/intrin/strace.h"
#include "libc/mem/mem.h"
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/thread.h"
struct AtFork {
struct AtFork *p[2];
atfork_f f[3];
};
struct AtForks {
pthread_once_t once;
pthread_mutex_t lock;
struct AtFork *list;
};
static struct AtForks _atforks = {
.once = PTHREAD_ONCE_INIT,
.lock = PTHREAD_MUTEX_INITIALIZER,
};
static void pthread_atfork_clear(void) {
struct AtFork *a, *b;
for (a = _atforks.list; a; a = b) {
b = a->p[0];
free(a);
}
}
static void pthread_atfork_init(void) {
atexit(pthread_atfork_clear);
}
static void _pthread_onfork(int i, const char *op) {
struct AtFork *a;
for (a = _atforks.list; a; a = a->p[!i]) {
if (a->f[i]) {
STRACE("pthread_atfork(%s, %t)", op, a->f[i]);
a->f[i]();
}
_atforks.list = a;
}
}
void _pthread_onfork_prepare(void) {
_pthread_onfork(0, "prepare");
}
void _pthread_onfork_parent(void) {
_pthread_onfork(1, "parent");
}
void _pthread_onfork_child(void) {
pthread_mutex_wipe_np(&_atforks.lock);
_pthread_onfork(2, "child");
}
/**
* Registers fork callbacks.
*
* When fork happens, your prepare functions will be called in the
* reverse order they were registered. Then, in the parent and child
* processes, their callbacks will be called in the same order they were
* registered.
*
* One big caveat with fork() is that it hard kills all threads except
* the calling thread. So let's say one of those threads was printing to
* stdout while it was killed. In that case, the stdout lock will still
* be held when your child process comes alive, which means that the
* child will deadlock next time it tries to print.
*
* The solution for that is simple. Every lock in your process should be
* registered with this interface. However there's one highly important
* thing you need to know. Locks must follow a consistent hierarchy. So
* the order in which you register locks matters. If nested locks aren't
* acquired in the same order globally, then rarely occurring deadlocks
* will happen. So what we recommend is that you hunt down all the locks
* that exist in your app and its dependencies, and register them all at
* once from your main() function at startup. This ensures a clear order
* and if you aren't sure what that order should be, cosmo libc has got
* you covered. Simply link your program with the `cosmocc -mdbg` flag
* and cosmo will detect locking violations with your `pthread_mutex_t`
* objects and report them by printing the strongly connected component.
* This will include the demangled symbol name of each mutex, assuming
* the `pthread_mutex_t` objects are stored in static memory. cosmo.h
* also exposes a deadlock API that lets you incorporate your own lock
* object types into this error checking system, which we also use to
* verify the entire libc runtime itself. See libc/intrin/deadlock.c.
*
* Special care should be taken when using this interface in libraries.
* While it may seem tempting to use something like a `__constructor__`
* attribute to register your mutexes in a clean and abstracted way, it
* is only appropriate if your mutex is guarding pure memory operations
* and poses zero risk of nesting with locks outside your library. For
* example, calling open() or printf() while holding your lock will do
* just that, since the C runtime functions you may consider pure will
* actually use mutexes under the hood, which are also validated under
* `cosmocc -mdbg` builds. So if your locks can't be made unnestable
* pure memory operations, then you should consider revealing their
* existence to users of your library.
*
* Here's an example of how pthread_atfork() can be used:
*
* static struct {
* pthread_once_t once;
* pthread_mutex_t lock;
* // data structures...
* } g_lib;
*
* static void lib_lock(void) {
* pthread_mutex_lock(&g_lib.lock);
* }
*
* static void lib_unlock(void) {
* pthread_mutex_unlock(&g_lib.lock);
* }
*
* static void lib_wipe(void) {
* pthread_mutex_wipe_np(&g_lib.lock);
* }
*
* static void lib_setup(void) {
* pthread_mutex_init(&g_lib.lock, 0);
* pthread_atfork(lib_lock, lib_unlock, lib_wipe);
* }
*
* static void lib_init(void) {
* pthread_once(&g_lib.once, lib_setup);
* }
*
* void lib(void) {
* lib_init();
* lib_lock();
* // do stuff...
* lib_unlock();
* }
*
* @param prepare is run by fork() before forking happens
* @param parent is run by fork() after forking happens in parent process
* @param child is run by fork() after forking happens in childe process
* @return 0 on success, or errno on error
* @raise ENOMEM if we require more vespene gas
*/
int pthread_atfork(atfork_f prepare, atfork_f parent, atfork_f child) {
pthread_once(&_atforks.once, pthread_atfork_init);
struct AtFork *a;
if (!(a = calloc(1, sizeof(struct AtFork))))
return ENOMEM;
a->f[0] = prepare;
a->f[1] = parent;
a->f[2] = child;
pthread_mutex_lock(&_atforks.lock);
a->p[0] = 0;
a->p[1] = _atforks.list;
if (_atforks.list)
_atforks.list->p[0] = a;
_atforks.list = a;
pthread_mutex_unlock(&_atforks.lock);
return 0;
}