Skip to content

Commit 92e6342

Browse files
gburdnyh
authored andcommitted
libc: implement pthread_mutex_timedlock and pthread_mutex_clocklock
Both were stubs returning EINVAL, so code that uses a bounded mutex acquire (common in databases and glibc-based libraries) could not run. OSv's lockfree mutex has no native timed acquire, so implement them by polling try_lock() with a short (50 us) sleep between attempts until the absolute deadline, returning 0 on success or ETIMEDOUT when the deadline passes. The uncontended case takes the try_lock() fast path with no sleeping. pthread_mutex_clocklock supports CLOCK_REALTIME and CLOCK_MONOTONIC (converting the deadline against the right clock) and returns EINVAL for any other clock; a malformed timespec also returns EINVAL. This is a poll-and-backoff acquire rather than a native timed wait, which is fine for the rare contended timedlock; the lockfree mutex could grow a real timed wait later if it ever needs to be tight. Add tests/tst-pthread-timedlock.cc: uncontended success, contended timeout (ETIMEDOUT from another thread while the main thread holds the lock, no deadlock), success after unlock, CLOCK_MONOTONIC clocklock, and the EINVAL clock path. Passes on OSv under KVM; tst-pthread continues to pass. Fixes #834 Closes #1426
1 parent 3aba46c commit 92e6342

3 files changed

Lines changed: 120 additions & 4 deletions

File tree

libc/pthread.cc

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -452,19 +452,58 @@ int pthread_mutex_trylock(pthread_mutex_t *m)
452452
return 0;
453453
}
454454

455+
// Try to lock @m, spinning-with-backoff until the absolute deadline given by
456+
// @abs_timeout on clock @clock. OSv's lockfree mutex has no native timed lock,
457+
// so we poll try_lock() with a short sleep between attempts. Returns 0 on
458+
// success or ETIMEDOUT if the deadline passes first.
459+
//
460+
// ponytail: poll+backoff, not a native timed acquire. Fine for the rare
461+
// contended timedlock; if it ever needs to be tight, add a timed wait to the
462+
// lockfree mutex itself.
463+
static int mutex_timedlock_until(pthread_mutex_t *m, clockid_t clock,
464+
const struct timespec *abs_timeout)
465+
{
466+
if (from_libc(m)->try_lock()) {
467+
return 0; // uncontended fast path
468+
}
469+
if (!abs_timeout || abs_timeout->tv_nsec < 0 ||
470+
abs_timeout->tv_nsec >= 1000000000) {
471+
return EINVAL;
472+
}
473+
474+
auto deadline_ns = std::chrono::seconds(abs_timeout->tv_sec) +
475+
std::chrono::nanoseconds(abs_timeout->tv_nsec);
476+
auto now = [clock]() -> std::chrono::nanoseconds {
477+
if (clock == CLOCK_MONOTONIC) {
478+
return osv::clock::uptime::now().time_since_epoch();
479+
}
480+
return osv::clock::wall::now().time_since_epoch();
481+
};
482+
483+
while (now() < deadline_ns) {
484+
if (from_libc(m)->try_lock()) {
485+
return 0;
486+
}
487+
sched::thread::sleep(std::chrono::microseconds(50));
488+
}
489+
// One last attempt right at the deadline.
490+
return from_libc(m)->try_lock() ? 0 : ETIMEDOUT;
491+
}
492+
455493
int pthread_mutex_timedlock(pthread_mutex_t *m,
456494
const struct timespec *abs_timeout)
457495
{
458-
WARN_STUBBED();
459-
return EINVAL;
496+
return mutex_timedlock_until(m, CLOCK_REALTIME, abs_timeout);
460497
}
461498

462499
int pthread_mutex_clocklock(pthread_mutex_t *m,
463500
clockid_t clock,
464501
const struct timespec *abs_timeout)
465502
{
466-
WARN_STUBBED();
467-
return EINVAL;
503+
if (clock != CLOCK_REALTIME && clock != CLOCK_MONOTONIC) {
504+
return EINVAL;
505+
}
506+
return mutex_timedlock_until(m, clock, abs_timeout);
468507
}
469508

470509
int pthread_mutex_unlock(pthread_mutex_t *m)

modules/tests/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \
120120
tst-fpu.so tst-preempt.so tst-tracepoint.so tst-hub.so \
121121
misc-console.so misc-leak.so misc-readbench.so misc-mmap-anon-perf.so \
122122
tst-mmap-file.so misc-mmap-big-file.so tst-mmap.so tst-huge.so \
123+
tst-pthread-timedlock.so \
123124
tst-prctl.so \
124125
tst-mmap-file-cow.so \
125126
tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \

tests/tst-pthread-timedlock.cc

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (C) 2026 Waldemar Kozaczuk
3+
*
4+
* This work is open source software, licensed under the terms of the
5+
* BSD license as described in the LICENSE file in the top-level directory.
6+
*/
7+
8+
// Exercises pthread_mutex_timedlock / pthread_mutex_clocklock.
9+
// Built and run on the OSv test image.
10+
11+
#include <pthread.h>
12+
#include <time.h>
13+
#include <errno.h>
14+
15+
#include <cassert>
16+
#include <iostream>
17+
18+
static struct timespec deadline_in(clockid_t clk, long ms)
19+
{
20+
struct timespec ts;
21+
clock_gettime(clk, &ts);
22+
ts.tv_nsec += (ms % 1000) * 1000000L;
23+
ts.tv_sec += ms / 1000 + ts.tv_nsec / 1000000000L;
24+
ts.tv_nsec %= 1000000000L;
25+
return ts;
26+
}
27+
28+
struct tl_arg { pthread_mutex_t *m; int result; };
29+
30+
static void *hold_attempt(void *p)
31+
{
32+
auto *a = static_cast<tl_arg *>(p);
33+
struct timespec ts;
34+
clock_gettime(CLOCK_REALTIME, &ts);
35+
ts.tv_nsec += 200000000L; // 200 ms
36+
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
37+
a->result = pthread_mutex_timedlock(a->m, &ts);
38+
return nullptr;
39+
}
40+
41+
int main()
42+
{
43+
std::cerr << "Running pthread timedlock tests\n";
44+
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
45+
46+
// Uncontended: timedlock succeeds immediately.
47+
struct timespec t = deadline_in(CLOCK_REALTIME, 1000);
48+
assert(pthread_mutex_timedlock(&m, &t) == 0);
49+
50+
// Held by us: a second timedlock (on another thread) must time out rather
51+
// than deadlock.
52+
tl_arg a { &m, -999 };
53+
pthread_t th;
54+
pthread_create(&th, nullptr, hold_attempt, &a);
55+
pthread_join(th, nullptr);
56+
assert(a.result == ETIMEDOUT); // could not acquire; we still hold it
57+
58+
pthread_mutex_unlock(&m);
59+
60+
// After unlock, timedlock succeeds again.
61+
t = deadline_in(CLOCK_REALTIME, 1000);
62+
assert(pthread_mutex_timedlock(&m, &t) == 0);
63+
pthread_mutex_unlock(&m);
64+
65+
// clocklock with CLOCK_MONOTONIC: uncontended success.
66+
t = deadline_in(CLOCK_MONOTONIC, 1000);
67+
assert(pthread_mutex_clocklock(&m, CLOCK_MONOTONIC, &t) == 0);
68+
pthread_mutex_unlock(&m);
69+
70+
// clocklock with an unsupported clock -> EINVAL.
71+
t = deadline_in(CLOCK_MONOTONIC, 1000);
72+
assert(pthread_mutex_clocklock(&m, 99, &t) == EINVAL);
73+
74+
std::cerr << "pthread timedlock tests PASSED\n";
75+
return 0;
76+
}

0 commit comments

Comments
 (0)