POSIX Semaphores (sem_open, sem_wait, sem_post, sem_init)
POSIX Semaphores provide counting synchronization primitives defined by IEEE Std 1003.1 to regulate access to shared resources across multiple threads or distinct processes.
A semaphore maintains an integer counter representing the number of available resource units:
sem_wait()(decrement / P operation): If the counter is greater than0, it decrements the counter and returns immediately. If the counter is0, the calling thread blocks until another process or thread increments it.sem_post()(increment / V operation): Increments the counter and unblocks one waiting thread.
Unlike mutexes, which have strict ownership semantics (only the thread that locked the mutex may unlock it), semaphores have no concept of ownership—any thread or process with access can call sem_post().
Named vs. Unnamed (Memory-Based) Semaphores
POSIX defines two distinct flavors of semaphores under <semaphore.h>:
| Feature | Named Semaphores (sem_open) |
Unnamed / Memory-Based (sem_init) |
|---|---|---|
| Identifier | String name in the filesystem (e.g., "/app_sem"). |
Variable of type sem_t allocated in memory. |
| Creation | sem_open(name, O_CREAT, mode, value) |
sem_init(&sem, pshared, value) |
| Sharing Scope | Any unrelated process knowing the name. | Threads of one process, or processes sharing memory via mmap(MAP_SHARED). |
| Destruction | sem_close(), sem_unlink() |
sem_destroy() |
| Filesystem Path | Linux stores them under /dev/shm/sem.<name>. |
Purely in memory; no filesystem entry. |
| macOS Support | Fully supported. | Deprecated / Unsupported (returns -1 with ENOSYS on macOS). |
1. Named Semaphores (Cross-Process IPC)
Named semaphores are ideal when unrelated processes need to synchronize:
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 1. Create or open the semaphore with an initial value of 1 (binary semaphore / mutex)
sem_t *sem = sem_open("/database_lock", O_CREAT, 0660, 1);
if (sem == SEM_FAILED) {
perror("sem_open failed");
exit(EXIT_FAILURE);
}
// 2. Acquire the lock (blocks if counter is 0)
if (sem_wait(sem) == -1) {
perror("sem_wait failed");
}
// --- Critical Section ---
printf("Process %d inside critical section\n", getpid());
sleep(2);
// 3. Release the lock (increments counter)
if (sem_post(sem) == -1) {
perror("sem_post failed");
}
// 4. Close the descriptor in this process
sem_close(sem);
// 5. In the cleanup process: unlink the name from the system
sem_unlink("/database_lock");
2. Unnamed Semaphores (Thread & Shared Memory)
Unnamed semaphores are allocated directly inside regular memory, heap, or shared memory:
#include <semaphore.h>
#include <pthread.h>
sem_t sem;
// pshared = 0: Shared only between threads of this process.
// pshared = 1: Shared between processes (must reside in mmap MAP_SHARED region).
// value = 5: Allows 5 concurrent workers.
sem_init(&sem, 0, 5);
// Acquire
sem_wait(&sem);
// Critical section...
// Release
sem_post(&sem);
// Clean up resources
sem_destroy(&sem);
Advanced Waiting Operations
POSIX provides non-blocking and timeout variations of sem_wait():
Non-Blocking: sem_trywait()
Returns immediately without blocking:
- If resource is available: decrements and returns
0. - If counter is
0: returns-1witherrno == EAGAIN.
Absolute Timeout: sem_timedwait()
Blocks only until a specific absolute wall-clock deadline (CLOCK_REALTIME):
#include <time.h>
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; // Wait at most 5 seconds
if (sem_timedwait(sem, &ts) == -1) {
if (errno == ETIMEDOUT) {
printf("Timed out waiting for semaphore\n");
}
}
Async-Signal Safety of sem_post()
A major engineering advantage of POSIX semaphores is that sem_post() is guaranteed by POSIX to be async-signal-safe.
In contrast, pthread_mutex_unlock() and pthread_cond_signal() are not async-signal-safe. If you attempt to unlock a mutex inside a signal handler, your application can deadlock if the signal interrupted the thread while holding the allocator or internal mutex lock.
Because sem_post() is signal-safe, the canonical Unix pattern for waking an event loop thread from a signal handler (e.g. SIGINT, SIGTERM, SIGHUP) is:
sem_t signal_sem;
void sig_handler(int signo) {
// Completely safe to call inside signal handler
sem_post(&signal_sem);
}
Frequently Asked Questions (FAQ)
Is sem_post() safe to call inside a signal handler?
Yes. sem_post() is explicitly defined as async-signal-safe by POSIX, making it one of the few safe synchronization primitives for notifying worker threads from signal handlers. Unlike pthread_mutex_unlock or pthread_cond_signal which can cause deadlocks if invoked while a thread holds internal runtime locks, sem_post() uses atomic instructions to increment the counter and wake waiters safely.
Why does sem_init() fail on macOS?
Apple macOS deprecated unnamed POSIX semaphores; calling sem_init() always returns -1 with errno == ENOSYS. On macOS, developers must either use named POSIX semaphores (sem_open), Grand Central Dispatch dispatch semaphores (dispatch_semaphore_create), or pthread_mutex_t and pthread_cond_t.
Can a POSIX semaphore be locked recursively by the same thread?
No. POSIX semaphores do not track thread ownership and will deadlock if a thread calls sem_wait() on a binary semaphore (value 0) it already acquired. Because semaphores are pure integer counters without identity checks, recursive locking requires a recursive mutex (PTHREAD_MUTEX_RECURSIVE) instead.
Related Topics
- POSIX IPC Overview: Comparing shared memory, semaphores, and message queues.
- POSIX Shared Memory (
shm_open,mmap): Using semaphores to synchronize shared memory blocks. - POSIX Threads (pthreads): Thread synchronization with mutexes and condition variables.
- POSIX Signals Architecture: Async-signal safety and signal handling best practices.
