Make some fixups to POSIX threads

This commit is contained in:
Justine Tunney 2022-09-07 21:13:50 -07:00
parent de511bc71a
commit 6c323383e5
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
12 changed files with 168 additions and 69 deletions

View file

@ -593,7 +593,7 @@ typedef struct {
#define notpossible \ #define notpossible \
do { \ do { \
asm("hlt"); \ asm("ud2"); \
unreachable; \ unreachable; \
} while (0) } while (0)

View file

@ -18,38 +18,56 @@
*/ */
#include "libc/calls/strace.internal.h" #include "libc/calls/strace.internal.h"
#include "libc/dce.h" #include "libc/dce.h"
#include "libc/intrin/asmflag.h"
#include "libc/intrin/promises.internal.h" #include "libc/intrin/promises.internal.h"
#include "libc/nt/thread.h" #include "libc/nt/thread.h"
#include "libc/runtime/runtime.h" #include "libc/runtime/runtime.h"
#include "libc/sysv/consts/nr.h" #include "libc/sysv/consts/nr.h"
__msabi extern typeof(ExitThread) *const __imp_ExitThread;
/** /**
* Terminates thread with raw system call. * Terminates thread with raw system call.
* *
* If this is the main thread, or an orphaned child thread, then this
* function is equivalent to exiting the process; however, `rc` shall
* only be reported to the parent process on Linux, FreeBSD & Windows
* whereas on other platforms, it'll be silently coerced to zero.
*
* @param rc only works on Linux and Windows * @param rc only works on Linux and Windows
* @see cthread_exit() * @see cthread_exit()
* @threadsafe * @threadsafe
* @noreturn * @noreturn
*/ */
privileged wontreturn void _Exit1(int rc) { privileged wontreturn void _Exit1(int rc) {
struct WinThread *wt; char cf;
int ax, dx, di, si;
STRACE("_Exit1(%d)", rc); STRACE("_Exit1(%d)", rc);
if (!IsWindows() && !IsMetal()) { if (!IsWindows() && !IsMetal()) {
if (IsOpenbsd() && !PLEDGED(STDIO)) { // exit() on Linux
// thr_exit() on FreeBSD
// __threxit() on OpenBSD
// __lwp_exit() on NetBSD
// __bsdthread_terminate() on XNU
asm volatile(CFLAG_ASM("xor\t%%r10d,%%r10d\n\t"
"syscall")
: CFLAG_CONSTRAINT(cf), "=a"(ax), "=d"(dx), "=D"(di), "=S"(si)
: "1"(__NR_exit), "3"(IsLinux() ? rc : 0), "4"(0), "2"(0)
: "rcx", "r8", "r9", "r10", "r11", "memory");
if ((IsFreebsd() && !cf && !ax) || (SupportsFreebsd() && IsTiny())) {
// FreeBSD checks if this is either the main thread by itself, or
// the last running child thread in which case thr_exit() returns
// zero with an error. In that case we'll exit the whole process.
// FreeBSD thr_exit() can even clobber registers, like r8 and r9!
asm volatile("syscall" asm volatile("syscall"
: /* no outputs */ : /* no outputs */
: "a"(__NR_exit), "D"(rc) : "a"(__NR_exit_group), "D"(rc)
: "rcx", "r11", "memory"); : "rcx", "r11", "memory");
unreachable;
} }
asm volatile("xor\t%%r10d,%%r10d\n\t"
"syscall"
: /* no outputs */
: "a"(__NR_exit), "D"(IsLinux() ? rc : 0), "S"(0), "d"(0)
: "rcx", "r10", "r11", "memory");
} else if (IsWindows()) { } else if (IsWindows()) {
ExitThread(rc); __imp_ExitThread(rc);
} unreachable;
for (;;) {
asm("ud2");
} }
notpossible;
} }

View file

@ -100,7 +100,7 @@ static int pthread_mutex_lock_spin(pthread_mutex_t *mutex, int expect,
* PTHREAD_MUTEX_ERRORCHECK : 917c (296ns) * PTHREAD_MUTEX_ERRORCHECK : 917c (296ns)
* *
* @return 0 on success, or error number on failure * @return 0 on success, or error number on failure
* @see pthread_spin_lock * @see pthread_spin_lock()
*/ */
int pthread_mutex_lock(pthread_mutex_t *mutex) { int pthread_mutex_lock(pthread_mutex_t *mutex) {
int c, me, owner, tries; int c, me, owner, tries;

View file

@ -10,10 +10,46 @@ COSMOPOLITAN_C_START_
* @fileoverview Cosmopolitan POSIX Thread Internals * @fileoverview Cosmopolitan POSIX Thread Internals
*/ */
// LEGAL TRANSITIONS ┌──> TERMINATED
// pthread_create ─┬─> JOINABLE ─┴┬─> DETACHED ──> ZOMBIE
// └──────────────┘
enum PosixThreadStatus { enum PosixThreadStatus {
// this is a running thread that needs pthread_join()
//
// the following transitions are possible:
//
// - kPosixThreadJoinable -> kPosixThreadTerminated if start_routine()
// returns, or is longjmp'd out of by pthread_exit(), and the thread
// is waiting to be joined.
//
// - kPosixThreadJoinable -> kPosixThreadDetached if pthread_detach()
// is called on this thread.
kPosixThreadJoinable, kPosixThreadJoinable,
// this is a managed thread that'll be cleaned up by the library.
//
// the following transitions are possible:
//
// - kPosixThreadDetached -> kPosixThreadZombie if start_routine()
// returns, or is longjmp'd out of by pthread_exit(), and the thread
// is waiting to be joined.
kPosixThreadDetached, kPosixThreadDetached,
// this is a joinable thread that terminated.
//
// the following transitions are possible:
//
// - kPosixThreadTerminated -> _pthread_free() will happen when
// pthread_join() is called by the user.
kPosixThreadTerminated, kPosixThreadTerminated,
// this is a detached thread that terminated.
//
// the following transitions are possible:
//
// - kPosixThreadZombie -> _pthread_free() will happen whenever
// convenient, e.g. pthread_create() entry or atexit handler.
kPosixThreadZombie, kPosixThreadZombie,
}; };
@ -28,11 +64,11 @@ struct PosixThread {
pthread_attr_t attr; pthread_attr_t attr;
}; };
void pthread_free(struct PosixThread *) hidden; void _pthread_free(struct PosixThread *) hidden;
void pthread_wait(struct PosixThread *) hidden; void _pthread_wait(struct PosixThread *) hidden;
void pthread_zombies_add(struct PosixThread *) hidden; void _pthread_zombies_add(struct PosixThread *) hidden;
void pthread_zombies_decimate(void) hidden; void _pthread_zombies_decimate(void) hidden;
void pthread_zombies_harvest(void) hidden; void _pthread_zombies_harvest(void) hidden;
COSMOPOLITAN_C_END_ COSMOPOLITAN_C_END_
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */ #endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */

View file

@ -22,21 +22,23 @@
#include "libc/intrin/atomic.h" #include "libc/intrin/atomic.h"
#include "libc/intrin/pthread.h" #include "libc/intrin/pthread.h"
#include "libc/intrin/wait0.internal.h" #include "libc/intrin/wait0.internal.h"
#include "libc/intrin/weaken.h"
#include "libc/mem/mem.h" #include "libc/mem/mem.h"
#include "libc/nexgen32e/gettls.h" #include "libc/nexgen32e/gettls.h"
#include "libc/runtime/runtime.h" #include "libc/runtime/runtime.h"
#include "libc/sysv/consts/clone.h" #include "libc/sysv/consts/clone.h"
#include "libc/sysv/consts/map.h" #include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/prot.h" #include "libc/sysv/consts/prot.h"
#include "libc/thread/internal.h"
#include "libc/thread/posixthread.internal.h" #include "libc/thread/posixthread.internal.h"
#include "libc/thread/spawn.h" #include "libc/thread/spawn.h"
#include "libc/thread/thread.h" #include "libc/thread/thread.h"
void pthread_wait(struct PosixThread *pt) { void _pthread_wait(struct PosixThread *pt) {
_wait0(pt->spawn.ctid); _wait0(pt->spawn.ctid);
} }
void pthread_free(struct PosixThread *pt) { void _pthread_free(struct PosixThread *pt) {
free(pt->spawn.tls); free(pt->spawn.tls);
if (pt->stacksize) { if (pt->stacksize) {
munmap(&pt->spawn.stk, pt->stacksize); munmap(&pt->spawn.stk, pt->stacksize);
@ -51,6 +53,9 @@ static int PosixThread(void *arg, int tid) {
((cthread_t)__get_tls())->pthread = pt; ((cthread_t)__get_tls())->pthread = pt;
pt->rc = pt->start_routine(pt->arg); pt->rc = pt->start_routine(pt->arg);
} }
if (weaken(_pthread_key_destruct)) {
weaken(_pthread_key_destruct)(0);
}
if (atomic_load_explicit(&pt->status, memory_order_acquire) == if (atomic_load_explicit(&pt->status, memory_order_acquire) ==
kPosixThreadDetached) { kPosixThreadDetached) {
atomic_store_explicit(&pt->status, kPosixThreadZombie, atomic_store_explicit(&pt->status, kPosixThreadZombie,
@ -71,18 +76,18 @@ static int PosixThread(void *arg, int tid) {
* pthread_create() - Standard * pthread_create() - Standard
* Abstraction * Abstraction
* *
* _spawn() - Cosmopolitan
* Abstraction
*
* clone() - Polyfill * clone() - Polyfill
* *
* - Kernel * - Kernel
* Interfaces * Interfaces
* sys_clone tfork * sys_clone tfork
* * CreateThread
* CreateThread *
* bsdthread_create thr_new * bsdthread_create thr_new
* *
*
* _lwp_create
*
* *
* @param thread if non-null is used to output the thread id * @param thread if non-null is used to output the thread id
* upon successful completion * upon successful completion
@ -96,13 +101,14 @@ static int PosixThread(void *arg, int tid) {
* @raise EINVAL if `attr` was supplied and had unnaceptable data * @raise EINVAL if `attr` was supplied and had unnaceptable data
* @raise EPERM if scheduling policy was requested and user account * @raise EPERM if scheduling policy was requested and user account
* isn't authorized to use it * isn't authorized to use it
* @threadsafe
*/ */
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg) { void *(*start_routine)(void *), void *arg) {
int rc, e = errno; int rc, e = errno;
struct PosixThread *pt; struct PosixThread *pt;
pthread_attr_t default_attr; pthread_attr_t default_attr;
pthread_zombies_decimate(); _pthread_zombies_decimate();
// default attributes // default attributes
if (!attr) { if (!attr) {
@ -140,7 +146,7 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
pt->stacksize = attr->stacksize; pt->stacksize = attr->stacksize;
} else { } else {
rc = errno; rc = errno;
pthread_free(pt); _pthread_free(pt);
errno = e; errno = e;
if (rc == EINVAL || rc == EOVERFLOW) { if (rc == EINVAL || rc == EOVERFLOW) {
return EINVAL; return EINVAL;
@ -170,7 +176,7 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
} }
} }
// save the attributes for descriptive purposes // we only need to save this to support pthread_getattr_np()
pt->attr = *attr; pt->attr = *attr;
// set initial status // set initial status
@ -180,10 +186,10 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
break; break;
case PTHREAD_CREATE_DETACHED: case PTHREAD_CREATE_DETACHED:
pt->status = kPosixThreadDetached; pt->status = kPosixThreadDetached;
pthread_zombies_add(pt); _pthread_zombies_add(pt);
break; break;
default: default:
pthread_free(pt); _pthread_free(pt);
return EINVAL; return EINVAL;
} }
@ -195,7 +201,7 @@ int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
CLONE_CHILD_CLEARTID, CLONE_CHILD_CLEARTID,
pt, &pt->spawn.ptid, pt->spawn.tib, pt->spawn.ctid) == -1) { pt, &pt->spawn.ptid, pt->spawn.tib, pt->spawn.ctid) == -1) {
rc = errno; rc = errno;
pthread_free(pt); _pthread_free(pt);
errno = e; errno = e;
if (rc == EINVAL) { if (rc == EINVAL) {
return EINVAL; return EINVAL;

View file

@ -34,17 +34,24 @@ int pthread_detach(pthread_t thread) {
for (;;) { for (;;) {
status = atomic_load_explicit(&pt->status, memory_order_relaxed); status = atomic_load_explicit(&pt->status, memory_order_relaxed);
if (status == kPosixThreadDetached || status == kPosixThreadZombie) { if (status == kPosixThreadDetached || status == kPosixThreadZombie) {
// these two states indicate the thread was already detached, in
// which case it's already listed under _pthread_zombies.
break; break;
} else if (status == kPosixThreadTerminated) { } else if (status == kPosixThreadTerminated) {
pthread_wait(pt); // thread was joinable and finished running. since pthread_join
pthread_free(pt); // won't be called, it's safe to free the thread resources now.
break; _pthread_wait(pt);
} else if (status == kPosixThreadJoinable && _pthread_free(pt);
atomic_compare_exchange_weak_explicit(
&pt->status, &status, kPosixThreadDetached,
memory_order_acquire, memory_order_relaxed)) {
pthread_zombies_add(pt);
break; break;
} else if (status == kPosixThreadJoinable) {
if (atomic_compare_exchange_weak_explicit(
&pt->status, &status, kPosixThreadDetached, memory_order_acquire,
memory_order_relaxed)) {
_pthread_zombies_add(pt);
break;
}
} else {
notpossible;
} }
} }
return 0; return 0;

View file

@ -24,10 +24,22 @@
/** /**
* Terminates current POSIX thread. * Terminates current POSIX thread.
*
* If this function is called from the main thread, or a thread created
* with clone() or _spawn(), then this function is the same as _Exit1()
* in which case `rc` is coerced to a `uint8_t` exit status, which will
* only be reported to the parent process on Linux, FreeBSD and Windows
*
* @param rc is reported later to pthread_join()
* @threadsafe
* @noreturn
*/ */
void pthread_exit(void *rc) { wontreturn void pthread_exit(void *rc) {
struct PosixThread *pt; struct PosixThread *pt;
pt = ((cthread_t)__get_tls())->pthread; if ((pt = ((cthread_t)__get_tls())->pthread)) {
pt->rc = rc; pt->rc = rc;
longjmp(pt->exiter, 1); longjmp(pt->exiter, 1);
} else {
_Exit1((int)(intptr_t)rc);
}
} }

View file

@ -32,14 +32,15 @@
*/ */
int pthread_join(pthread_t thread, void **value_ptr) { int pthread_join(pthread_t thread, void **value_ptr) {
struct PosixThread *pt = thread; struct PosixThread *pt = thread;
if (pt->status == kPosixThreadDetached) { if (pt->status == kPosixThreadDetached || //
pt->status == kPosixThreadZombie) {
assert(!"badjoin"); assert(!"badjoin");
return EDEADLK; return EDEADLK;
} }
pthread_wait(pt); _pthread_wait(pt);
if (value_ptr) { if (value_ptr) {
*value_ptr = pt->rc; *value_ptr = pt->rc;
} }
pthread_free(pt); _pthread_free(pt);
return 0; return 0;
} }

View file

@ -41,7 +41,7 @@ int pthread_key_create(pthread_key_t *key, pthread_key_dtor dtor) {
} }
static textexit void _pthread_key_atexit(void) { static textexit void _pthread_key_atexit(void) {
_pthread_key_destruct(_pthread_keys); _pthread_key_destruct(0);
} }
__attribute__((__constructor__)) static textstartup void _pthread_key_init() { __attribute__((__constructor__)) static textstartup void _pthread_key_init() {

View file

@ -25,6 +25,7 @@ void _pthread_key_destruct(void *key[PTHREAD_KEYS_MAX]) {
uint64_t x; uint64_t x;
void *value; void *value;
pthread_key_dtor dtor; pthread_key_dtor dtor;
if (!key) key = _pthread_keys;
StartOver: StartOver:
for (i = 0; i < (PTHREAD_KEYS_MAX + 63) / 64; ++i) { for (i = 0; i < (PTHREAD_KEYS_MAX + 63) / 64; ++i) {
x = _pthread_key_usage[i]; x = _pthread_key_usage[i];

View file

@ -17,6 +17,7 @@
PERFORMANCE OF THIS SOFTWARE. PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "libc/intrin/atomic.h" #include "libc/intrin/atomic.h"
#include "libc/intrin/pthread.h"
#include "libc/mem/mem.h" #include "libc/mem/mem.h"
#include "libc/runtime/runtime.h" #include "libc/runtime/runtime.h"
#include "libc/thread/posixthread.internal.h" #include "libc/thread/posixthread.internal.h"
@ -29,46 +30,46 @@
static struct Zombie { static struct Zombie {
struct Zombie *next; struct Zombie *next;
struct PosixThread *pt; struct PosixThread *pt;
} * pthread_zombies; } * _pthread_zombies;
void pthread_zombies_add(struct PosixThread *pt) { void _pthread_zombies_add(struct PosixThread *pt) {
struct Zombie *z; struct Zombie *z;
if ((z = malloc(sizeof(struct Zombie)))) { if ((z = malloc(sizeof(struct Zombie)))) {
z->pt = pt; z->pt = pt;
z->next = atomic_load(&pthread_zombies); z->next = atomic_load(&_pthread_zombies);
for (;;) { for (;;) {
if (atomic_compare_exchange_weak(&pthread_zombies, &z->next, z)) { if (atomic_compare_exchange_weak(&_pthread_zombies, &z->next, z)) {
break; break;
} }
} }
} }
} }
static void pthread_zombies_collect(struct Zombie *z) { static void _pthread_zombies_collect(struct Zombie *z) {
pthread_wait(z->pt); _pthread_wait(z->pt);
pthread_free(z->pt); _pthread_free(z->pt);
free(z); free(z);
} }
void pthread_zombies_decimate(void) { void _pthread_zombies_decimate(void) {
struct Zombie *z; struct Zombie *z;
while ((z = atomic_load(&pthread_zombies)) && while ((z = atomic_load_explicit(&_pthread_zombies, memory_order_relaxed)) &&
atomic_load(&z->pt->status) == kPosixThreadZombie) { atomic_load(&z->pt->status) == kPosixThreadZombie) {
if (atomic_compare_exchange_strong(&pthread_zombies, &z, z->next)) { if (atomic_compare_exchange_weak(&_pthread_zombies, &z, z->next)) {
pthread_zombies_collect(z); _pthread_zombies_collect(z);
} }
} }
} }
void pthread_zombies_harvest(void) { void _pthread_zombies_harvest(void) {
struct Zombie *z; struct Zombie *z;
while ((z = atomic_load(&pthread_zombies))) { while ((z = atomic_load_explicit(&_pthread_zombies, memory_order_relaxed))) {
if (atomic_compare_exchange_weak(&pthread_zombies, &z, z->next)) { if (atomic_compare_exchange_weak(&_pthread_zombies, &z, z->next)) {
pthread_zombies_collect(z); _pthread_zombies_collect(z);
} }
} }
} }
__attribute__((__constructor__)) static void init(void) { __attribute__((__constructor__)) static void init(void) {
atexit(pthread_zombies_harvest); atexit(_pthread_zombies_harvest);
} }

View file

@ -17,12 +17,15 @@
PERFORMANCE OF THIS SOFTWARE. PERFORMANCE OF THIS SOFTWARE.
*/ */
#include "libc/calls/calls.h" #include "libc/calls/calls.h"
#include "libc/dce.h"
#include "libc/intrin/pthread.h" #include "libc/intrin/pthread.h"
#include "libc/mem/mem.h" #include "libc/mem/mem.h"
#include "libc/runtime/stack.h" #include "libc/runtime/stack.h"
#include "libc/testlib/subprocess.h"
#include "libc/testlib/testlib.h" #include "libc/testlib/testlib.h"
#include "libc/thread/thread.h" #include "libc/thread/thread.h"
#if 0
static void *Increment(void *arg) { static void *Increment(void *arg) {
ASSERT_EQ(gettid(), pthread_getthreadid_np()); ASSERT_EQ(gettid(), pthread_getthreadid_np());
return (void *)((uintptr_t)arg + 1); return (void *)((uintptr_t)arg + 1);
@ -98,3 +101,17 @@ TEST(pthread_detach, testCustomStack_withReallySmallSize) {
ASSERT_EQ(0, pthread_join(id, 0)); ASSERT_EQ(0, pthread_join(id, 0));
free(stk); free(stk);
} }
#endif
TEST(pthread_exit, mainThreadWorks) {
// _Exit1() can't set process exit code on XNU/NetBSD/OpenBSD.
if (IsLinux() || IsFreebsd() || IsWindows()) {
SPAWN(fork);
pthread_exit((void *)2);
EXITS(2);
} else {
SPAWN(fork);
pthread_exit((void *)0);
EXITS(0);
}
}