Skip to content

Commit 47a5259

Browse files
authored
Stopped the SMP Linux port losing a mutex wake-up to a suspend signal (#753)
The SMP Linux port suspends a thread with a signal whose handler calls sigsuspend and does not return until the thread is resumed. That signal can arrive while the thread is parked in pthread_mutex_lock on _tx_linux_mutex; the port knows it can, because _tx_linux_mutex_obtain sets tx_thread_linux_mutex_access around the lock call for exactly this case, and nothing anywhere reads that flag. glibc waits for a contended mutex in a loop that re-arms the futex wait after a signal, and this handler never returns to it. The next release hands its wake-up to that thread, which will not act on it, and any other thread parked on the mutex is never woken, leaving the mutex free with waiters on it. That deadlocks the process: the only thread that can resume the suspended one is the scheduler, and the scheduler takes this mutex on every pass. _tx_linux_mutex_obtain now waits with pthread_mutex_timedlock and retries, so the wait is re-armed every TX_LINUX_MUTEX_RETRY_NSEC and a lost wake-up costs one retry period instead of the process. The period is one millisecond, half the scheduler's own idle period. Nothing else changes. Four hung processes captured untraced, across three tests, show the same state: a thread in sigsuspend on top of pthread_mutex_lock, the scheduler blocked in pthread_mutex_lock, and the mutex reading free. Standalone, threadx_smp_random_resume_suspend_exclusion_test hung 3 times in 100 runs and 2 in 55 before the change and 0 in 400 after it. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
1 parent a9bd72d commit 47a5259

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

‎ports_smp/linux/gnu/inc/tx_port.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,16 @@ typedef struct
513513
} TX_LINUX_MUTEX;
514514

515515

516+
/* Define how long a thread waits on the Linux mutex before retrying. A thread
517+
parked on the mutex can be suspended by the port's signal handler and so never
518+
act on the wake-up the next release sends it, which leaves the wake-up lost
519+
and every other waiter parked on a mutex that is free. */
520+
521+
#ifndef TX_LINUX_MUTEX_RETRY_NSEC
522+
#define TX_LINUX_MUTEX_RETRY_NSEC 1000000
523+
#endif
524+
525+
516526
/* Define Linux-specific critical section APIs. */
517527

518528
void _tx_linux_mutex_obtain(TX_LINUX_MUTEX *mutex);

‎ports_smp/linux/gnu/src/tx_thread_schedule.c‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ void _tx_linux_mutex_obtain(TX_LINUX_MUTEX *mutex)
292292
TX_THREAD *thread_ptr;
293293
pthread_t current_thread_id;
294294
UINT i;
295+
INT linux_status;
296+
struct timespec ts;
295297

296298
/* Pickup the current thread ID. */
297299
current_thread_id = pthread_self();
@@ -343,8 +345,43 @@ UINT i;
343345
thread_ptr -> tx_thread_linux_mutex_access = TX_TRUE;
344346
}
345347

346-
/* Get the Linux mutex. */
347-
pthread_mutex_lock(&mutex -> tx_linux_mutex);
348+
/* Get the Linux mutex. The wait is timed and retried rather than left
349+
to pthread_mutex_lock, because a thread can be signalled into the
350+
port's suspend handler while it is parked on this mutex. That handler
351+
does not return until the thread is resumed, so the wake-up the next
352+
release sends is delivered to a thread that never retries and is lost.
353+
Any other thread parked on the mutex then waits on a mutex that is
354+
free, which on this port deadlocks the whole process: the thread that
355+
can resume the suspended one is the scheduler, and the scheduler takes
356+
this mutex on every pass. Retrying on a timeout costs nothing when
357+
the mutex is handed over normally, and turns that lost wake-up into a
358+
delay of at most the retry period. */
359+
do
360+
{
361+
362+
/* Set the deadline for this attempt. */
363+
clock_gettime(CLOCK_REALTIME, &ts);
364+
ts.tv_nsec = ts.tv_nsec + TX_LINUX_MUTEX_RETRY_NSEC;
365+
if (ts.tv_nsec >= 1000000000)
366+
{
367+
368+
ts.tv_nsec = ts.tv_nsec - 1000000000;
369+
ts.tv_sec++;
370+
}
371+
372+
linux_status = pthread_mutex_timedlock(&mutex -> tx_linux_mutex, &ts);
373+
374+
/* Anything but the deadline expiring is a real failure to obtain the
375+
mutex, so stop retrying and record it. */
376+
if ((linux_status != 0) && (linux_status != ETIMEDOUT))
377+
{
378+
379+
/* Increment the system error counter. */
380+
_tx_linux_system_error++;
381+
break;
382+
}
383+
384+
} while (linux_status != 0);
348385

349386
/* At this point we have the mutex. */
350387

0 commit comments

Comments
 (0)