The Core Operations
Once a semaphore is open/initialized, you interact with it using just two primary operations: sem_post() to increment (release/signal) and sem_wait() to decrement (acquire/wait). There are two variants of sem_wait: sem_trywait() (non-blocking) and sem_timedwait() (timeout-based).
Function Signatures
#include <semaphore.h>
/* Increment (unlock/signal) - never blocks */
int sem_post(sem_t *sem);
/* Decrement (lock/wait) - blocks if value is 0 */
int sem_wait(sem_t *sem);
/* Decrement if value > 0, else return EAGAIN immediately */
int sem_trywait(sem_t *sem);
/* Decrement with a timeout - blocks until timeout or signal */
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/* All return 0 on success, -1 on error */
sem_post() — Increment the Semaphore
sem_post() atomically increments the semaphore value by 1. If any processes/threads are blocked in sem_wait() on this semaphore, one of them is woken up and allowed to proceed (it gets to decrement the semaphore).
sem_post() is async-signal-safe. This means it can safely be called from a signal handler, unlike most other synchronization primitives. This is a significant advantage over mutexes.| sem_post(sem) called | → | Atomically increment sem value by 1 |
→ | Any blocked sem_wait() waiters? Wake one up |
/* sem_post() example */
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
void release_resource(sem_t *sem)
{
/* Signal that a resource is available */
if (sem_post(sem) == -1) {
perror("sem_post");
exit(EXIT_FAILURE);
}
/* Never blocks - always succeeds (unless overflow) */
printf("Resource released\n");
}
sem_wait() — Decrement the Semaphore (Blocking)
sem_wait() decrements (locks) the semaphore. If the current value is already 0, the caller blocks until the value becomes greater than 0 (i.e., until another thread/process calls sem_post()).
| sem_wait(sem) | → | Check: value > 0? |
YES →
NO ↓
|
Decrement value Return 0 (success) |
| ↓ | ||||
| Block until sem_post() wakes this thread |
||||
sem_wait() can be interrupted by a signal handler, returning -1 with errno == EINTR. Always use a loop to handle this in production code./* Robust sem_wait with EINTR handling */
#include <semaphore.h>
#include <errno.h>
#include <stdio.h>
int safe_sem_wait(sem_t *sem)
{
int ret;
/* Retry if interrupted by a signal */
while ((ret = sem_wait(sem)) == -1 && errno == EINTR)
continue; /* loop: try again */
return ret; /* 0 on success, -1 on real error */
}
int main(void)
{
sem_t *sem = sem_open("/mysem", O_CREAT, 0660, 1);
if (sem == SEM_FAILED) { perror("sem_open"); return 1; }
if (safe_sem_wait(sem) == -1) {
perror("sem_wait");
return 1;
}
/* --- critical section --- */
printf("In critical section\n");
/* --- end critical section --- */
sem_post(sem);
sem_close(sem);
return 0;
}
sem_trywait() — Non-blocking Decrement
sem_trywait() is the non-blocking version. If the semaphore value is 0, instead of blocking it immediately returns -1 with errno = EAGAIN. Use this when you want to try acquiring a resource but not wait for it.
#include <semaphore.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
sem_t *sem;
sem = sem_open("/resource", O_CREAT, 0660, 1);
if (sem == SEM_FAILED) { perror("sem_open"); exit(1); }
/* Try to acquire without blocking */
if (sem_trywait(sem) == 0) {
/* Got it! Do the work */
printf("Resource acquired! Doing work...\n");
sleep(1);
sem_post(sem); /* release */
printf("Resource released\n");
} else {
if (errno == EAGAIN) {
/* Resource not available right now */
printf("Resource busy, doing something else...\n");
} else {
perror("sem_trywait");
}
}
sem_close(sem);
sem_unlink("/resource");
return 0;
}
sem_timedwait() — Wait with Timeout
sem_timedwait() blocks like sem_wait(), but returns with an error if the semaphore cannot be decremented before the specified absolute timeout. It uses absolute time (not relative duration), so you must compute the deadline from clock_gettime().
abs_timeout argument is an absolute wall-clock time (e.g., “wait until 14:00:05”), not a duration (e.g., “wait for 5 seconds”). Use clock_gettime(CLOCK_REALTIME) and add your desired timeout interval.#include <semaphore.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(void)
{
sem_t *sem;
struct timespec ts;
sem = sem_open("/timed_sem", O_CREAT, 0660, 0); /* value=0: starts locked */
if (sem == SEM_FAILED) { perror("sem_open"); exit(1); }
/* Compute absolute timeout: current time + 5 seconds */
if (clock_gettime(CLOCK_REALTIME, &ts) == -1) {
perror("clock_gettime");
exit(1);
}
ts.tv_sec += 5; /* wait up to 5 seconds */
printf("Waiting up to 5 seconds for semaphore...\n");
if (sem_timedwait(sem, &ts) == 0) {
/* Semaphore acquired within timeout */
printf("Acquired semaphore within timeout!\n");
sem_post(sem);
} else {
if (errno == ETIMEDOUT) {
printf("Timeout: semaphore not available after 5 seconds\n");
} else if (errno == EINTR) {
printf("Interrupted by signal\n");
} else {
perror("sem_timedwait");
}
}
sem_close(sem);
sem_unlink("/timed_sem");
return 0;
}
Comparison: sem_wait Variants
| Function | Behavior when value = 0 | Error when blocked/failed | Use Case |
|---|---|---|---|
sem_wait() |
Blocks indefinitely | EINTR (signal interrupt) | Must acquire, willing to wait forever |
sem_trywait() |
Returns immediately | EAGAIN (not available) | Polling, try-lock without waiting |
sem_timedwait() |
Blocks until deadline | ETIMEDOUT or EINTR | Wait with a deadline (avoid deadlock) |
Practical Pattern: Producer-Consumer with Semaphores
One of the most common uses of semaphores is coordinating a producer that generates data and a consumer that processes it. Two semaphores are used: one to track empty slots, one to track filled slots.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_SIZE 5
#define NUM_ITEMS 10
int buffer[BUFFER_SIZE];
int in_idx = 0, out_idx = 0;
sem_t *sem_empty; /* counts empty slots */
sem_t *sem_full; /* counts filled slots */
sem_t *sem_mutex; /* protects buffer access */
void *producer(void *arg)
{
for (int i = 0; i < NUM_ITEMS; i++) {
/* Wait for an empty slot */
sem_wait(sem_empty);
/* Acquire buffer lock */
sem_wait(sem_mutex);
buffer[in_idx] = i;
printf("Produced: %d at slot %d\n", i, in_idx);
in_idx = (in_idx + 1) % BUFFER_SIZE;
sem_post(sem_mutex);
/* Signal that a slot is now full */
sem_post(sem_full);
}
return NULL;
}
void *consumer(void *arg)
{
int item;
for (int i = 0; i < NUM_ITEMS; i++) {
/* Wait for a filled slot */
sem_wait(sem_full);
/* Acquire buffer lock */
sem_wait(sem_mutex);
item = buffer[out_idx];
printf("Consumed: %d from slot %d\n", item, out_idx);
out_idx = (out_idx + 1) % BUFFER_SIZE;
sem_post(sem_mutex);
/* Signal that a slot is now empty */
sem_post(sem_empty);
}
return NULL;
}
int main(void)
{
pthread_t prod_tid, cons_tid;
/* Initialize semaphores */
sem_empty = sem_open("/prod_empty", O_CREAT | O_EXCL, 0660, BUFFER_SIZE);
sem_full = sem_open("/prod_full", O_CREAT | O_EXCL, 0660, 0);
sem_mutex = sem_open("/prod_mutex", O_CREAT | O_EXCL, 0660, 1);
if (sem_empty == SEM_FAILED || sem_full == SEM_FAILED || sem_mutex == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}
pthread_create(&prod_tid, NULL, producer, NULL);
pthread_create(&cons_tid, NULL, consumer, NULL);
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);
/* Cleanup */
sem_close(sem_empty); sem_unlink("/prod_empty");
sem_close(sem_full); sem_unlink("/prod_full");
sem_close(sem_mutex); sem_unlink("/prod_mutex");
return 0;
}
/* Compile and run */
gcc producer_consumer.c -o pc -pthread
./pc
Using Semaphore as a Mutex (Binary Semaphore)
A semaphore initialized to 1 acts like a mutex: only one thread/process can hold it at a time. Unlike a real mutex, a semaphore can be posted (unlocked) by a different thread than the one that waited (locked) it. This makes semaphores more flexible for signaling patterns.
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SEM_NAME "/binary_mutex"
static int shared_counter = 0;
static sem_t *mutex_sem;
void *increment_thread(void *arg)
{
for (int i = 0; i < 100000; i++) {
sem_wait(mutex_sem); /* lock */
shared_counter++; /* critical section */
sem_post(mutex_sem); /* unlock */
}
return NULL;
}
int main(void)
{
pthread_t t1, t2;
/* Binary semaphore: initial value 1 = unlocked */
mutex_sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0660, 1);
if (mutex_sem == SEM_FAILED) { perror("sem_open"); exit(1); }
pthread_create(&t1, NULL, increment_thread, NULL);
pthread_create(&t2, NULL, increment_thread, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter (should be 200000): %d\n", shared_counter);
sem_close(mutex_sem);
sem_unlink(SEM_NAME);
return 0;
}
Using sem_post() in a Signal Handler
Because sem_post() is async-signal-safe, it can be called from a signal handler to notify a waiting thread or process. This is a clean way to wake up a blocked sem_wait() from a signal handler.
#include <semaphore.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
static sem_t *sig_sem;
/* Signal handler: safely posts to the semaphore */
static void sigint_handler(int sig)
{
sem_post(sig_sem); /* async-signal-safe */
}
int main(void)
{
struct sigaction sa;
sig_sem = sem_open("/signal_sem", O_CREAT | O_EXCL, 0660, 0);
if (sig_sem == SEM_FAILED) { perror("sem_open"); exit(1); }
/* Install SIGINT handler */
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
printf("Waiting... Press Ctrl+C to continue\n");
/* Block waiting for the signal */
sem_wait(sig_sem);
printf("\nSignal received! Continuing...\n");
sem_close(sig_sem);
sem_unlink("/signal_sem");
return 0;
}
🅾 Interview Questions & Answers
Q1. What does sem_post() do when no process is waiting?
It simply increments the semaphore value. The value is not capped — it can go above its initial value. For example, a semaphore initialized to 3 can be posted multiple times to reach 5, 6, etc. (though this may indicate a logic error in the application). No blocking occurs.
Q2. Why does sem_timedwait() use absolute time instead of relative time?
Absolute time avoids drift in retry loops. If you use relative time and the call is interrupted by EINTR (signal), restarting with the same relative timeout would give you extra time. With an absolute deadline, each retry still targets the same endpoint. You compute: deadline = now + 5s, and every retry uses that same deadline.
Q3. What is the difference between a semaphore and a mutex?
A mutex must be unlocked by the same thread that locked it (ownership). A semaphore has no ownership: any thread/process can call sem_post(), even if it didn’t call sem_wait(). Semaphores are also used for signaling (one thread signals another), while mutexes are used strictly for mutual exclusion. Counting semaphores can also track multiple resources, which a mutex cannot.
Q4. Is sem_post() safe to call from a signal handler?
Yes. sem_post() is specified as async-signal-safe by POSIX. This is a key advantage of semaphores over condition variables + mutexes, which are NOT async-signal-safe and cannot be used in signal handlers.
Q5. What error does sem_trywait() return when the semaphore is 0?
It returns -1 and sets errno to EAGAIN (try again). This is distinct from a real error. Always check specifically for EAGAIN to distinguish “semaphore was unavailable” from actual system errors like EINVAL or EINTR.
Q6. How would you implement a counting semaphore that limits concurrent access to 3 threads?
Initialize the semaphore with value 3: sem_open("/limit", O_CREAT, 0660, 3). Each thread calls sem_wait() before accessing the shared resource (decrement: 3→2→1→0). When the value hits 0, additional threads block. Each thread calls sem_post() when done (increment: allows next waiter in). This is a classic counting semaphore for resource pool management.
Q7. What happens if sem_wait() is interrupted by a signal?
It returns -1 with errno == EINTR. The semaphore was NOT decremented — the caller did not acquire it. In production code, always wrap sem_wait() in a loop: while (sem_wait(sem) == -1 && errno == EINTR) continue; to transparently restart after signal interruption.
