[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:
parent
fefdf74c07
commit
2926d18a06
5 changed files with 264 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue