pthread_cond_wait() reacquires its mutex before honoring a cancellation, but mylock.c installed no cleanup handler to release it again -- a thread cancelled mid-wait terminated holding the lock forever. vtaskmgr's pthread_cancel()-based teardown could land there from three angles (dispatcher cancelled while polling the shared queue, or a vcpu racing back into it after its own shutdown signal), permanently deadlocking any other thread that touched the same queue and hanging vtaskmgr_fini()'s pthread_join() indefinitely. Reproduced live as xce_test hanging on Ctrl-D exit once a connection context was entered; gdb confirmed a vcpu thread blocked forever on a mutex owned by an already-exited thread ID. Fix: cbuf_put()/cbuf_get()/cbuf_timedget() now unlock via a pthread_cleanup_push/pop handler around the wait, so a cancelled waiter always releases the mutex. vcpu_idle()/vcpu_entry() also recognize the teardown poison pill and exit immediately instead of needlessly re-announcing into the already-dead shared queue. New test_vtaskmgr_teardown.c reproduces the race deterministically (self-pinned to one CPU, since true multi-core parallelism rarely hits the window) and bounds vtaskmgr_fini() with a timed join so a regression fails fast instead of hanging the suite. Logged as bug-1810. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
288 lines
6 KiB
C
288 lines
6 KiB
C
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <rtti.h>
|
|
|
|
#include "mylock.h"
|
|
|
|
int mylock_init(mylock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_mutex_init(l, NULL);
|
|
}
|
|
|
|
void mylock_fini(mylock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return;
|
|
}
|
|
|
|
pthread_mutex_destroy(l);
|
|
}
|
|
int mylock_try(mylock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_mutex_trylock(l);
|
|
}
|
|
|
|
int mylock_lock(mylock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_mutex_lock(l);
|
|
}
|
|
|
|
int mylock_unlock(mylock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_mutex_unlock(l);
|
|
}
|
|
|
|
int cbuf_init(cbuf_t* b, int cap, int isize)
|
|
{
|
|
if ((b == NULL) || (cap <= 0) || (isize <= 0)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
b->ncap = cap;
|
|
b->isize = isize;
|
|
b->data = (void*)calloc(cap, isize);
|
|
|
|
if (b->data == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
b->head = 0;
|
|
b->len = 0;
|
|
|
|
pthread_mutex_init(&(b->mutex), NULL);
|
|
pthread_cond_init(&(b->condget), NULL);
|
|
pthread_cond_init(&(b->condput), NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void cbuf_fini(cbuf_t* b)
|
|
{
|
|
if (b == NULL) {
|
|
return;
|
|
}
|
|
|
|
if (b->data != NULL) {
|
|
free(b->data);
|
|
b->data = NULL;
|
|
}
|
|
|
|
pthread_mutex_destroy(&(b->mutex));
|
|
pthread_cond_destroy(&(b->condput));
|
|
pthread_cond_destroy(&(b->condget));
|
|
}
|
|
|
|
int cbuf_put(cbuf_t* b, void* it)
|
|
{
|
|
int i;
|
|
|
|
if ((b == NULL) || (it == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
pthread_mutex_lock(&(b->mutex));
|
|
/* pthread_cond_wait() reacquires the mutex before honoring a pending
|
|
* cancellation (POSIX-mandated), so a caller cancelled while parked
|
|
* here would otherwise terminate holding b->mutex forever -- no other
|
|
* thread could ever lock it again. The cleanup handler releases it on
|
|
* that unwind; on the normal (non-cancelled) path pop(0) just removes
|
|
* the handler without running it, since the explicit unlock below
|
|
* still runs. */
|
|
pthread_cleanup_push((void (*)(void*))pthread_mutex_unlock, &(b->mutex));
|
|
while (b->len == b->ncap) {
|
|
pthread_cond_wait(&(b->condput), &(b->mutex));
|
|
}
|
|
pthread_cleanup_pop(0);
|
|
|
|
i = (b->head + b->len) % b->ncap;
|
|
|
|
memcpy((void*)((char*)b->data) + b->isize * i, it, b->isize);
|
|
|
|
b->len++;
|
|
|
|
pthread_cond_signal(&(b->condget));
|
|
|
|
pthread_mutex_unlock(&(b->mutex));
|
|
return 0;
|
|
}
|
|
|
|
int cbuf_tryput(cbuf_t* b, void* it)
|
|
{
|
|
int i;
|
|
int rc = 0;
|
|
|
|
if ((b == NULL) || (it == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
pthread_mutex_lock(&(b->mutex));
|
|
if (b->len < b->ncap) {
|
|
i = (b->head + b->len) % b->ncap;
|
|
|
|
memcpy((void*)((char*)b->data) + b->isize * i, it, b->isize);
|
|
|
|
b->len++;
|
|
|
|
pthread_cond_signal(&(b->condget));
|
|
} else {
|
|
rc = -EAGAIN;
|
|
}
|
|
|
|
pthread_mutex_unlock(&(b->mutex));
|
|
return rc;
|
|
}
|
|
|
|
int cbuf_get(cbuf_t* b, void* it)
|
|
{
|
|
if ((b == NULL) || (it == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
pthread_mutex_lock(&(b->mutex));
|
|
/* See cbuf_put()'s comment on the same pattern: without this cleanup
|
|
* handler, a caller cancelled while parked in pthread_cond_wait()
|
|
* would reacquire and then abandon b->mutex locked forever. */
|
|
pthread_cleanup_push((void (*)(void*))pthread_mutex_unlock, &(b->mutex));
|
|
while (b->len == 0) {
|
|
pthread_cond_wait(&(b->condget), &(b->mutex));
|
|
}
|
|
pthread_cleanup_pop(0);
|
|
|
|
memcpy(it, (void*)((char*)b->data) + b->isize * b->head, b->isize);
|
|
|
|
b->head = (b->head + 1) % b->ncap;
|
|
b->len--;
|
|
|
|
pthread_cond_signal(&(b->condput));
|
|
|
|
pthread_mutex_unlock(&(b->mutex));
|
|
return 0;
|
|
}
|
|
|
|
int cbuf_tryget(cbuf_t* b, void* it)
|
|
{
|
|
int rc = 0;
|
|
|
|
if ((b == NULL) || (it == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
pthread_mutex_lock(&(b->mutex));
|
|
if (b->len == 0) {
|
|
rc = -EAGAIN;
|
|
} else {
|
|
memcpy(it, (void*)((char*)b->data) + b->isize * b->head, b->isize);
|
|
|
|
b->head = (b->head + 1) % b->ncap;
|
|
b->len--;
|
|
|
|
pthread_cond_signal(&(b->condput));
|
|
}
|
|
pthread_mutex_unlock(&(b->mutex));
|
|
|
|
return rc;
|
|
}
|
|
|
|
int cbuf_timedget(cbuf_t* b, void* it, const struct timespec* deadline)
|
|
{
|
|
int timed_out = 0;
|
|
|
|
if ((b == NULL) || (it == NULL) || (deadline == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
pthread_mutex_lock(&(b->mutex));
|
|
/* See cbuf_put()'s comment on the same pattern: without this cleanup
|
|
* handler, a caller cancelled while parked in pthread_cond_timedwait()
|
|
* would reacquire and then abandon b->mutex locked forever.
|
|
* pthread_cleanup_push()/pop() must be a matched pair at the same
|
|
* lexical nesting level -- break out of the loop on timeout instead
|
|
* of returning from inside it, so pop() always runs right after. */
|
|
pthread_cleanup_push((void (*)(void*))pthread_mutex_unlock, &(b->mutex));
|
|
while (b->len == 0) {
|
|
int rc = pthread_cond_timedwait(&(b->condget), &(b->mutex), deadline);
|
|
if (rc == ETIMEDOUT) {
|
|
timed_out = 1;
|
|
break;
|
|
}
|
|
}
|
|
pthread_cleanup_pop(0);
|
|
|
|
if (timed_out) {
|
|
pthread_mutex_unlock(&(b->mutex));
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
memcpy(it, (void*)((char*)b->data) + b->isize * b->head, b->isize);
|
|
|
|
b->head = (b->head + 1) % b->ncap;
|
|
b->len--;
|
|
|
|
pthread_cond_signal(&(b->condput));
|
|
|
|
pthread_mutex_unlock(&(b->mutex));
|
|
return 0;
|
|
}
|
|
|
|
int my_rwlock_init(my_rwlock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_rwlock_init(l, NULL);
|
|
}
|
|
|
|
void my_rwlock_fini(my_rwlock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return;
|
|
}
|
|
|
|
pthread_rwlock_destroy(l);
|
|
}
|
|
|
|
int my_rwlock_unlock(my_rwlock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_rwlock_unlock(l);
|
|
}
|
|
|
|
int my_rwlock_rlock(my_rwlock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_rwlock_rdlock(l);
|
|
}
|
|
|
|
int my_rwlock_wlock(my_rwlock_t* l)
|
|
{
|
|
if (l == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pthread_rwlock_wrlock(l);
|
|
}
|