cosmopolitan/libc/thread/pthread_create.c
Justine Tunney f2af97711b
Make improvements
- Improve compatibility with Blink virtual machine
- Add non-POSIX APIs for joining threads and signal masks
- Never ever use anything except 32-bit integers for atomics
- Add some `#undef` statements to workaround `ctags` problems
2022-11-10 21:52:47 -08:00

332 lines
12 KiB
C

/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net 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/assert.h"
#include "libc/calls/blocksigs.internal.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/sigaltstack.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/asan.internal.h"
#include "libc/intrin/atomic.h"
#include "libc/intrin/bits.h"
#include "libc/log/internal.h"
#include "libc/macros.internal.h"
#include "libc/mem/mem.h"
#include "libc/runtime/clone.internal.h"
#include "libc/runtime/runtime.h"
#include "libc/runtime/stack.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/clone.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/ss.h"
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/spawn.h"
#include "libc/thread/thread.h"
#include "libc/thread/tls.h"
#include "libc/thread/wait0.internal.h"
#include "third_party/nsync/dll.h"
STATIC_YOINK("nsync_mu_lock");
STATIC_YOINK("nsync_mu_unlock");
STATIC_YOINK("_pthread_atfork");
#define MAP_ANON_OPENBSD 0x1000
#define MAP_STACK_OPENBSD 0x4000
void _pthread_free(struct PosixThread *pt) {
if (pt->flags & PT_STATIC) return;
free(pt->tls);
if ((pt->flags & PT_OWNSTACK) && //
pt->attr.__stackaddr && //
pt->attr.__stackaddr != MAP_FAILED) {
_npassert(!munmap(pt->attr.__stackaddr, pt->attr.__stacksize));
}
if (pt->altstack) {
free(pt->altstack);
}
free(pt);
}
static int PosixThread(void *arg, int tid) {
void *rc;
struct sigaltstack ss;
struct PosixThread *pt = arg;
enum PosixThreadStatus status;
_unassert(__get_tls()->tib_tid > 0);
if (pt->altstack) {
ss.ss_flags = 0;
ss.ss_size = SIGSTKSZ;
ss.ss_sp = pt->altstack;
if (sigaltstack(&ss, 0)) {
notpossible;
}
}
if (pt->attr.__inheritsched == PTHREAD_EXPLICIT_SCHED) {
_pthread_reschedule(pt);
}
// set long jump handler so pthread_exit can bring control back here
if (!setjmp(pt->exiter)) {
__get_tls()->tib_pthread = (pthread_t)pt;
_npassert(!sigprocmask(SIG_SETMASK, (sigset_t *)pt->attr.__sigmask, 0));
rc = pt->start(pt->arg);
// ensure pthread_cleanup_pop(), and pthread_exit() popped cleanup
_npassert(!pt->cleanup);
// calling pthread_exit() will either jump back here, or call exit
pthread_exit(rc);
}
// return to clone polyfill which clears tid, wakes futex, and exits
return 0;
}
static int FixupCustomStackOnOpenbsd(pthread_attr_t *attr) {
// OpenBSD: Only permits RSP to occupy memory that's been explicitly
// defined as stack memory. We need to squeeze the provided interval
// in order to successfully call mmap(), which will return EINVAL if
// these calculations should overflow.
size_t n;
int e, rc;
uintptr_t x, y;
n = attr->__stacksize;
x = (uintptr_t)attr->__stackaddr;
y = ROUNDUP(x, PAGESIZE);
n -= y - x;
n = ROUNDDOWN(n, PAGESIZE);
e = errno;
if (__sys_mmap((void *)y, n, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_FIXED | MAP_ANON_OPENBSD | MAP_STACK_OPENBSD,
-1, 0, 0) == (void *)y) {
attr->__stackaddr = (void *)y;
attr->__stacksize = n;
return 0;
} else {
rc = errno;
errno = e;
if (rc == EOVERFLOW) {
rc = EINVAL;
}
return rc;
}
}
static errno_t pthread_create_impl(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg,
sigset_t oldsigs) {
int rc, e = errno;
struct PosixThread *pt;
// create posix thread object
if (!(pt = calloc(1, sizeof(struct PosixThread)))) {
errno = e;
return EAGAIN;
}
pt->start = start_routine;
pt->arg = arg;
// create thread local storage memory
if (!(pt->tls = _mktls(&pt->tib))) {
free(pt);
errno = e;
return EAGAIN;
}
// setup attributes
if (attr) {
pt->attr = *attr;
attr = 0;
} else {
pthread_attr_init(&pt->attr);
}
// setup stack
if (pt->attr.__stackaddr) {
// caller supplied their own stack
// assume they know what they're doing as much as possible
if (IsOpenbsd()) {
if ((rc = FixupCustomStackOnOpenbsd(&pt->attr))) {
_pthread_free(pt);
return rc;
}
}
} else {
// cosmo is managing the stack
// 1. in mono repo optimize for tiniest stack possible
// 2. in public world optimize to *work* regardless of memory
pt->flags = PT_OWNSTACK;
pt->attr.__stacksize = MAX(pt->attr.__stacksize, GetStackSize());
pt->attr.__stacksize = _roundup2pow(pt->attr.__stacksize);
pt->attr.__guardsize = ROUNDUP(pt->attr.__guardsize, PAGESIZE);
if (pt->attr.__guardsize + PAGESIZE >= pt->attr.__stacksize) {
_pthread_free(pt);
return EINVAL;
}
if (pt->attr.__guardsize == PAGESIZE) {
// user is wisely using smaller stacks with default guard size
pt->attr.__stackaddr =
mmap(0, pt->attr.__stacksize, PROT_READ | PROT_WRITE,
MAP_STACK | MAP_ANONYMOUS, -1, 0);
} else {
// user is tuning things, performance may suffer
pt->attr.__stackaddr =
mmap(0, pt->attr.__stacksize, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (pt->attr.__stackaddr != MAP_FAILED) {
if (IsOpenbsd() &&
__sys_mmap(
pt->attr.__stackaddr, pt->attr.__stacksize,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_FIXED | MAP_ANON_OPENBSD | MAP_STACK_OPENBSD,
-1, 0, 0) != pt->attr.__stackaddr) {
notpossible;
}
if (pt->attr.__guardsize && !IsWindows() &&
mprotect(pt->attr.__stackaddr, pt->attr.__guardsize, PROT_NONE)) {
notpossible;
}
}
}
if (pt->attr.__stackaddr == MAP_FAILED) {
rc = errno;
_pthread_free(pt);
errno = e;
if (rc == EINVAL || rc == EOVERFLOW) {
return EINVAL;
} else {
return EAGAIN;
}
}
if (IsAsan() && pt->attr.__guardsize) {
__asan_poison(pt->attr.__stackaddr, pt->attr.__guardsize,
kAsanStackOverflow);
}
}
// setup signal handler stack
if (_wantcrashreports && !IsWindows()) {
pt->altstack = malloc(SIGSTKSZ);
}
// set initial status
if (!pt->attr.__havesigmask) {
pt->attr.__havesigmask = true;
memcpy(pt->attr.__sigmask, &oldsigs, sizeof(oldsigs));
}
switch (pt->attr.__detachstate) {
case PTHREAD_CREATE_JOINABLE:
atomic_store_explicit(&pt->status, kPosixThreadJoinable,
memory_order_relaxed);
break;
case PTHREAD_CREATE_DETACHED:
atomic_store_explicit(&pt->status, kPosixThreadDetached,
memory_order_relaxed);
break;
default:
_pthread_free(pt);
return EINVAL;
}
// add thread to global list
// we add it to the end since zombies go at the beginning
nsync_dll_init_(&pt->list, pt);
pthread_spin_lock(&_pthread_lock);
_pthread_list = nsync_dll_make_last_in_list_(_pthread_list, &pt->list);
pthread_spin_unlock(&_pthread_lock);
// launch PosixThread(pt) in new thread
if ((rc = clone(PosixThread, pt->attr.__stackaddr,
pt->attr.__stacksize - (IsOpenbsd() ? 16 : 0),
CLONE_VM | CLONE_THREAD | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_SETTLS | CLONE_PARENT_SETTID |
CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID,
pt, &pt->ptid, pt->tib, &pt->tib->tib_tid))) {
pthread_spin_lock(&_pthread_lock);
_pthread_list = nsync_dll_remove_(_pthread_list, &pt->list);
pthread_spin_unlock(&_pthread_lock);
_pthread_free(pt);
return rc;
}
*thread = (pthread_t)pt;
return 0;
}
/**
* Creates thread, e.g.
*
* void *worker(void *arg) {
* fputs(arg, stdout);
* return "there\n";
* }
*
* int main() {
* void *result;
* pthread_t id;
* pthread_create(&id, 0, worker, "hi ");
* pthread_join(id, &result);
* fputs(result, stdout);
* }
*
* Here's the OSI model of threads in Cosmopolitan:
*
* ┌──────────────────┐
* │ pthread_create() │ - Standard
* └─────────┬────────┘ Abstraction
* ┌─────────┴────────┐
* │ clone() │ - Polyfill
* └─────────┬────────┘
* ┌────────┬──┴┬─┬─┬─────────┐ - Kernel
* ┌─────┴─────┐ │ │ │┌┴──────┐ │ Interfaces
* │ sys_clone │ │ │ ││ tfork │ ┌┴─────────────┐
* └───────────┘ │ │ │└───────┘ │ CreateThread │
* ┌───────────────┴──┐│┌┴────────┐ └──────────────┘
* │ bsdthread_create │││ thr_new │
* └──────────────────┘│└─────────┘
* ┌───────┴──────┐
* │ _lwp_create │
* └──────────────┘
*
* @param thread if non-null is used to output the thread id
* upon successful completion
* @param attr points to launch configuration, or may be null
* to use sensible defaults; it must be initialized using
* pthread_attr_init()
* @param start_routine is your thread's callback function
* @param arg is an arbitrary value passed to `start_routine`
* @return 0 on success, or errno on error
* @raise EAGAIN if resources to create thread weren't available
* @raise EINVAL if `attr` was supplied and had unnaceptable data
* @raise EPERM if scheduling policy was requested and user account
* isn't authorized to use it
* @returnserrno
* @threadsafe
*/
errno_t pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg) {
errno_t rc;
__require_tls();
pthread_decimate_np();
BLOCK_SIGNALS;
rc = pthread_create_impl(thread, attr, start_routine, arg, _SigMask);
ALLOW_SIGNALS;
return rc;
}