[libc][semaphore] Add post and wait operations for internal semaphore (#198959)

This pr implements https://github.com/llvm/llvm-project/issues/190847,
adding the post and wait operations in posix semaphore for linux using
futex.

Particularly, 
`sem_post`:
https://pubs.opengroup.org/onlinepubs/9799919799/functions/sem_post.html
`sem_trywait`, `sem_wait`:
https://pubs.opengroup.org/onlinepubs/9799919799/functions/sem_wait.html
`sem_timedwait`, `sem_clockwait`:
https://pubs.opengroup.org/onlinepubs/9799919799/functions/sem_clockwait.html#

The implementation is assisted by Claude Code Opus 4.7, specifically for
the unit tests generations.
This commit is contained in:
Pengxiang Huang 2026-07-17 18:19:10 -04:00 committed by GitHub
parent fefdf74c07
commit 2926d18a06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 264 additions and 15 deletions

View file

@ -3,10 +3,17 @@ add_header_library(
HDRS
semaphore.h
DEPENDS
libc.hdr.errno_macros
libc.hdr.time_macros
libc.hdr.types.clockid_t
libc.hdr.types.mode_t
libc.hdr.types.struct_timespec
libc.src.__support.CPP.atomic
libc.src.__support.CPP.limits
libc.src.__support.error_or
libc.src.__support.libc_assert
libc.src.__support.threads.futex_utils
libc.src.__support.time.abs_timeout
)
add_object_library(
@ -20,7 +27,6 @@ add_object_library(
libc.hdr.errno_macros
libc.hdr.fcntl_macros
libc.src.__support.CPP.array
libc.src.__support.CPP.limits
libc.src.__support.CPP.new
libc.src.__support.CPP.string_view
libc.src.__support.ctype_utils

View file

@ -11,7 +11,6 @@
#include "hdr/errno_macros.h"
#include "hdr/fcntl_macros.h"
#include "src/__support/CPP/array.h"
#include "src/__support/CPP/limits.h"
#include "src/__support/CPP/new.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/close.h"
@ -34,10 +33,6 @@ namespace LIBC_NAMESPACE_DECL {
namespace {
// define SEM_VALUE_MAX as INT_MAX
constexpr unsigned int SEM_VALUE_MAX =
static_cast<unsigned int>(cpp::numeric_limits<int>::max());
// Named semaphores are backed by files in /dev/shm/.
// a prefix "sem." is added to avoid name collision.
constexpr cpp::string_view SEM_PREFIX = "/dev/shm/sem.";
@ -152,7 +147,10 @@ ErrorOr<Semaphore *> Semaphore::open(const char *name, int oflag, mode_t mode,
}
Semaphore *sem = sem_or.value();
new (sem) Semaphore(value);
// Named semaphores are backed by MAP_SHARED memory and used across
// processes, so they use shared futex addressing mode.
new (sem) Semaphore(value, /*is_shared=*/true);
// atomically publish the fully initialized semaphore.
auto link_or = linux_syscalls::link(tmp_or->data(), path_or->data());

View file

@ -9,30 +9,61 @@
#ifndef LLVM_LIBC_SRC_SEMAPHORE_LINUX_SEMAPHORE_H
#define LLVM_LIBC_SRC_SEMAPHORE_LINUX_SEMAPHORE_H
#include "hdr/errno_macros.h"
#include "hdr/time_macros.h"
#include "hdr/types/clockid_t.h"
#include "hdr/types/mode_t.h"
#include "hdr/types/struct_timespec.h"
#include "src/__support/CPP/atomic.h"
#include "src/__support/CPP/limits.h"
#include "src/__support/common.h"
#include "src/__support/error_or.h"
#include "src/__support/libc_assert.h"
#include "src/__support/threads/futex_utils.h"
#include "src/__support/time/abs_timeout.h"
namespace LIBC_NAMESPACE_DECL {
// Define SEM_VALUE_MAX as INT_MAX
constexpr unsigned int SEM_VALUE_MAX =
static_cast<unsigned int>(cpp::numeric_limits<int>::max());
class Semaphore {
Futex value;
unsigned int canary;
// Whether the semaphore is shared between processes.
bool is_shared;
// A private constant canary used to detect use of uninitialized or
// destroyed semaphores. Chose "SEM1" in ASCII (0x53='S', 0x45='E',
// 0x4D='M', 0x31='1').
static constexpr unsigned int SEM_CANARY = 0x53454D31U;
public:
// TODO:
// Add the posting and waiting operations: sem_post, sem_wait,
// sem_trywait, sem_timedwait, sem_clockwait.
// Helper function for timed blocking wait, wait for a required
// absolute timeout.
LIBC_INLINE int wait_until(internal::AbsTimeout timeout) {
while (trywait() != 0) {
auto wait_or = value.wait(/*expected=*/0, timeout, is_shared);
LIBC_INLINE constexpr Semaphore(unsigned int value)
: value(value), canary(SEM_CANARY) {}
// returned error is not a benign race EAGAIN
if (!wait_or.has_value() && wait_or.error() != EAGAIN) {
// ETIMEDOUT still needs a final trywait() in case a post() raced with
// the deadline
if (wait_or.error() == ETIMEDOUT && trywait() == 0)
return 0;
// Any other error (e.g. EINVAL/EFAULT)
return wait_or.error();
}
}
return 0;
}
public:
LIBC_INLINE constexpr Semaphore(unsigned int value, bool shared)
: value(value), canary(SEM_CANARY), is_shared(shared) {}
// Sanity check to detect use of uninitialized or destroyed semaphores.
LIBC_INLINE bool is_valid() const { return canary == SEM_CANARY; }
@ -54,6 +85,93 @@ public:
const_cast<Futex &>(value).load(cpp::MemoryOrder::RELAXED));
}
// Atomically increments the semaphore value and
// wakes one blocked waiter if any.
LIBC_INLINE int post() {
FutexWordType v = value.load(cpp::MemoryOrder::RELAXED);
do {
if (v >= SEM_VALUE_MAX)
return EOVERFLOW;
// RELEASE on success, since post should publish prior writes
// RELAXED on failure, since no synchronization event occurs
} while (!value.compare_exchange_weak(v, v + 1, cpp::MemoryOrder::RELEASE,
cpp::MemoryOrder::RELAXED));
// Wake one waiter if any,
// waiter selection is left to linux futex implementation.
value.notify_one(is_shared);
return 0;
}
// Attempts to decrement the semaphore value without blocking.
LIBC_INLINE int trywait() {
FutexWordType v = value.load(cpp::MemoryOrder::RELAXED);
while (v > 0) {
// ACQUIRE on success to synchronize with the matching post().
if (value.compare_exchange_weak(v, v - 1, cpp::MemoryOrder::ACQUIRE,
cpp::MemoryOrder::RELAXED))
return 0;
}
return EAGAIN;
}
// Blocking wait, decrements the value when positive, or blocks until a
// post() makes it positive again.
LIBC_INLINE int wait() {
while (trywait() != 0) {
// Blocks when value was zero.
// Futex wait re-checks the value atomically,
// so a racing post() before sleep is not lost.
// Spurious wakeups just send back to trywait().
auto wait_or =
value.wait(/*expected=*/0, /*timeout=*/cpp::nullopt, is_shared);
// A successful wake or EAGAIN both just send it back to trywait().
// Any other error is unexpected.
if (!wait_or.has_value() && wait_or.error() != EAGAIN)
return wait_or.error();
}
return 0;
}
// Blocking wait against an absolute CLOCK_REALTIME deadline.
LIBC_INLINE int timedwait(const timespec *abstime) {
return clockwait(CLOCK_REALTIME, abstime);
}
// Blocking wait against a deadline on the given clock:
// CLOCK_REALTIME or CLOCK_MONOTONIC
LIBC_INLINE int clockwait(clockid_t clock_id, const timespec *abstime) {
if (clock_id != CLOCK_MONOTONIC && clock_id != CLOCK_REALTIME)
return EINVAL;
// If the semaphore can be locked immediately, succeed without consulting
// the clock or dereferencing abstime.
if (trywait() == 0)
return 0;
bool is_realtime = (clock_id == CLOCK_REALTIME);
auto timeout =
internal::AbsTimeout::from_timespec(*abstime, /*realtime=*/is_realtime);
if (LIBC_LIKELY(timeout.has_value()))
return wait_until(timeout.value());
internal::AbsTimeout::Error err = timeout.error();
LIBC_ASSERT(err == internal::AbsTimeout::Error::Invalid ||
err == internal::AbsTimeout::Error::BeforeEpoch);
switch (err) {
case internal::AbsTimeout::Error::Invalid:
return EINVAL;
case internal::AbsTimeout::Error::BeforeEpoch:
return ETIMEDOUT;
}
__builtin_trap();
}
// Named semaphore operations.
// creates or opens a named semaphore backed by a file in /dev/shm/.
// When O_CREAT is specified in oflag, mode and value are used for

View file

@ -9,6 +9,9 @@ add_libc_unittest(
DEPENDS
libc.hdr.errno_macros
libc.hdr.fcntl_macros
libc.hdr.time_macros
libc.hdr.types.struct_timespec
libc.src.__support.time.clock_gettime
libc.src.semaphore.linux.semaphore
libc.src.semaphore.linux.named_semaphore
)

View file

@ -8,24 +8,148 @@
#include "hdr/errno_macros.h"
#include "hdr/fcntl_macros.h"
#include "hdr/time_macros.h"
#include "hdr/types/struct_timespec.h"
#include "src/__support/time/clock_gettime.h"
#include "src/semaphore/linux/semaphore.h"
#include "test/UnitTest/Test.h"
using LIBC_NAMESPACE::Semaphore;
TEST(LlvmLibcSemaphoreTest, InitAndGetValue) {
Semaphore sem(3);
Semaphore sem(3, /*is_shared=*/false);
ASSERT_TRUE(sem.is_valid());
ASSERT_EQ(sem.getvalue(), 3);
}
TEST(LlvmLibcSemaphoreTest, Destroy) {
Semaphore sem(5);
Semaphore sem(5, /*is_shared=*/false);
ASSERT_TRUE(sem.is_valid());
sem.destroy();
ASSERT_FALSE(sem.is_valid());
}
TEST(LlvmLibcSemaphoreTest, TryWait) {
Semaphore sem(2, /*is_shared=*/false);
// two successful non-blocking decrements.
ASSERT_EQ(sem.trywait(), 0);
ASSERT_EQ(sem.trywait(), 0);
ASSERT_EQ(sem.getvalue(), 0);
// value is now zero, trywait must fail with EAGAIN.
ASSERT_EQ(sem.trywait(), EAGAIN);
ASSERT_EQ(sem.getvalue(), 0);
}
TEST(LlvmLibcSemaphoreTest, Post) {
Semaphore sem(0, /*is_shared=*/false);
ASSERT_EQ(sem.getvalue(), 0);
ASSERT_EQ(sem.post(), 0);
ASSERT_EQ(sem.getvalue(), 1);
// the posted value can be consumed by trywait.
ASSERT_EQ(sem.trywait(), 0);
ASSERT_EQ(sem.getvalue(), 0);
}
TEST(LlvmLibcSemaphoreTest, WaitNonBlocking) {
Semaphore sem(2, /*is_shared=*/false);
// value is positive: wait() should decrement without blocking.
ASSERT_EQ(sem.wait(), 0);
ASSERT_EQ(sem.getvalue(), 1);
ASSERT_EQ(sem.wait(), 0);
ASSERT_EQ(sem.getvalue(), 0);
}
TEST(LlvmLibcSemaphoreTest, TimedWaitNonBlocking) {
Semaphore sem(2, /*is_shared=*/false);
timespec ts{};
LIBC_NAMESPACE::internal::clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 60;
// value is positive: timedwait should decrement without consulting the
// clock.
ASSERT_EQ(sem.timedwait(&ts), 0);
ASSERT_EQ(sem.getvalue(), 1);
}
TEST(LlvmLibcSemaphoreTest, TimedWaitNullTime) {
// When the semaphore can be locked immediately, abstime must not be
// dereferenced.
Semaphore sem(1, /*is_shared=*/false);
ASSERT_EQ(sem.timedwait(nullptr), 0);
ASSERT_EQ(sem.getvalue(), 0);
Semaphore sem2(1, /*is_shared=*/false);
ASSERT_EQ(sem2.clockwait(CLOCK_MONOTONIC, nullptr), 0);
ASSERT_EQ(sem2.getvalue(), 0);
}
TEST(LlvmLibcSemaphoreTest, PostOverflow) {
// The value exceeds maximum, post() must fail with EOVERFLOW
Semaphore sem(LIBC_NAMESPACE::SEM_VALUE_MAX, /*is_shared=*/false);
ASSERT_EQ(sem.post(), EOVERFLOW);
ASSERT_EQ(sem.getvalue(), static_cast<int>(LIBC_NAMESPACE::SEM_VALUE_MAX));
}
TEST(LlvmLibcSemaphoreTest, TimedWaitTimeout) {
Semaphore sem(0, /*is_shared=*/false);
timespec ts{};
LIBC_NAMESPACE::internal::clock_gettime(CLOCK_REALTIME, &ts);
// a few milliseconds in the future.
ts.tv_nsec += 10'000'000;
if (ts.tv_nsec >= 1'000'000'000) {
ts.tv_sec += 1;
ts.tv_nsec -= 1'000'000'000;
}
// value is zero and no post() arrives: must time out.
ASSERT_EQ(sem.timedwait(&ts), ETIMEDOUT);
}
TEST(LlvmLibcSemaphoreTest, TimedWaitBeforeEpoch) {
Semaphore sem(0, /*is_shared=*/false);
// tv_sec < 0 is treated as an already expired deadline.
timespec ts{};
ts.tv_sec = -1;
ts.tv_nsec = 0;
ASSERT_EQ(sem.timedwait(&ts), ETIMEDOUT);
}
TEST(LlvmLibcSemaphoreTest, TimedWaitInvalidTimespec) {
Semaphore sem(0, /*is_shared=*/false);
// tv_nsec out of [0, 1e9) is malformed.
timespec ts{};
ts.tv_sec = 1;
ts.tv_nsec = 1'000'000'001;
ASSERT_EQ(sem.timedwait(&ts), EINVAL);
}
TEST(LlvmLibcSemaphoreTest, ClockWaitMonotonicTimeout) {
Semaphore sem(0, /*is_shared=*/false);
timespec ts{};
LIBC_NAMESPACE::internal::clock_gettime(CLOCK_MONOTONIC, &ts);
ts.tv_nsec += 10'000'000;
if (ts.tv_nsec >= 1'000'000'000) {
ts.tv_sec += 1;
ts.tv_nsec -= 1'000'000'000;
}
ASSERT_EQ(sem.clockwait(CLOCK_MONOTONIC, &ts), ETIMEDOUT);
}
TEST(LlvmLibcSemaphoreTest, ClockWaitUnsupportedClock) {
Semaphore sem(0, /*is_shared=*/false);
timespec ts{};
ts.tv_sec = 1;
ts.tv_nsec = 0;
// any clock other than CLOCK_MONOTONIC / CLOCK_REALTIME is rejected.
ASSERT_EQ(sem.clockwait(static_cast<clockid_t>(99), &ts), EINVAL);
}
// Named semaphore tests.
TEST(LlvmLibcSemaphoreTest, NamedOpenCloseUnlink) {