cosmopolitan/libc/thread/pthread_atfork.c
2024-12-23 21:57:52 -08:00

182 lines
7.1 KiB
C

/*-*- 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/atomic.h"
#include "libc/cosmo.h"
#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 {
atomic_uint once;
pthread_mutex_t lock;
struct AtFork *list;
};
static struct AtForks _atforks = {
.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_mutex_lock(&_atforks.lock);
_pthread_onfork(0, "prepare");
}
void _pthread_onfork_parent(void) {
_pthread_onfork(1, "parent");
_pthread_mutex_unlock(&_atforks.lock);
}
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) {
cosmo_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;
}