Semaphores in Linux: Advanced Synchronization Patterns and Real-World Applications
Semaphores are fundamental synchronization primitives that have stood the test of time since Dijkstra introduced them in 1965. In modern Linux systems, they remain essential for coordinating access to shared resources, implementing producer-consumer patterns, and solving complex synchronization problems that mutexes alone cannot handle elegantly.
Semaphores in Linux: Advanced Synchronization Patterns
Understanding Semaphores
At its core, a semaphore is an integer variable that can never go below zero, combined with two atomic operations:
- wait() (P operation): Decrement if positive, otherwise block
- post() (V operation): Increment and potentially wake a waiting thread
This simple abstraction enables powerful synchronization patterns that go beyond basic mutual exclusion.
POSIX vs System V Semaphores
Linux provides two semaphore implementations:
// POSIX Semaphores (recommended)
#include <semaphore.h>
// System V Semaphores (legacy, but still widely used)
#include <sys/sem.h>
// Key differences:
// - POSIX: Simpler API, better performance
// - System V: Persistent, more complex operations
// - POSIX: Thread and process support
// - System V: Process-only, but with arrays
POSIX Semaphore Fundamentals
Basic Usage Pattern
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// Global semaphore
sem_t resource_sem;
void* worker_thread(void* arg) {
int id = *(int*)arg;
printf("Thread %d: Waiting for resource\n", id);
// Acquire resource
if (sem_wait(&resource_sem) != 0) {
perror("sem_wait");
return NULL;
}
printf("Thread %d: Got resource, working...\n", id);
sleep(2); // Simulate work
printf("Thread %d: Releasing resource\n", id);
// Release resource
if (sem_post(&resource_sem) != 0) {
perror("sem_post");
return NULL;
}
return NULL;
}
int main() {
const int NUM_THREADS = 5;
const int NUM_RESOURCES = 2;
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
// Initialize semaphore with 2 resources
if (sem_init(&resource_sem, 0, NUM_RESOURCES) != 0) {
perror("sem_init");
exit(1);
}
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, worker_thread, &thread_ids[i]);
}
// Wait for all threads
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// Cleanup
sem_destroy(&resource_sem);
return 0;
}
Named Semaphores for IPC
#include <fcntl.h>
#include <sys/stat.h>
// Process 1: Create and initialize
void create_named_semaphore() {
sem_t *sem = sem_open("/myapp_resource",
O_CREAT | O_EXCL,
0644,
3); // Initial value: 3
if (sem == SEM_FAILED) {
perror("sem_open");
return;
}
printf("Named semaphore created\n");
// Use semaphore...
sem_wait(sem);
printf("Resource acquired\n");
sleep(5);
sem_post(sem);
sem_close(sem);
}
// Process 2: Open existing
void use_named_semaphore() {
sem_t *sem = sem_open("/myapp_resource", 0);
if (sem == SEM_FAILED) {
perror("sem_open");
return;
}
printf("Waiting for resource...\n");
sem_wait(sem);
printf("Got resource!\n");
// Use resource...
sleep(2);
sem_post(sem);
sem_close(sem);
}
// Cleanup (run once when done)
void cleanup_named_semaphore() {
sem_unlink("/myapp_resource");
}
Advanced Synchronization Patterns
Producer-Consumer with Bounded Buffer
#define BUFFER_SIZE 10
typedef struct {
int buffer[BUFFER_SIZE];
int in;
int out;
sem_t mutex; // Mutual exclusion
sem_t empty; // Count of empty slots
sem_t full; // Count of full slots
} bounded_buffer_t;
void bb_init(bounded_buffer_t *bb) {
bb->in = 0;
bb->out = 0;
sem_init(&bb->mutex, 0, 1);
sem_init(&bb->empty, 0, BUFFER_SIZE);
sem_init(&bb->full, 0, 0);
}
void bb_produce(bounded_buffer_t *bb, int item) {
sem_wait(&bb->empty); // Wait for empty slot
sem_wait(&bb->mutex); // Enter critical section
bb->buffer[bb->in] = item;
bb->in = (bb->in + 1) % BUFFER_SIZE;
printf("Produced: %d\n", item);
sem_post(&bb->mutex); // Exit critical section
sem_post(&bb->full); // Signal item available
}
int bb_consume(bounded_buffer_t *bb) {
sem_wait(&bb->full); // Wait for item
sem_wait(&bb->mutex); // Enter critical section
int item = bb->buffer[bb->out];
bb->out = (bb->out + 1) % BUFFER_SIZE;
printf("Consumed: %d\n", item);
sem_post(&bb->mutex); // Exit critical section
sem_post(&bb->empty); // Signal slot available
return item;
}
Readers-Writers Problem
typedef struct {
sem_t mutex; // Protects reader_count
sem_t write_lock; // Exclusive access for writers
int reader_count; // Number of active readers
} rw_lock_t;
void rw_init(rw_lock_t *rw) {
sem_init(&rw->mutex, 0, 1);
sem_init(&rw->write_lock, 0, 1);
rw->reader_count = 0;
}
void rw_read_lock(rw_lock_t *rw) {
sem_wait(&rw->mutex);
rw->reader_count++;
if (rw->reader_count == 1) {
// First reader locks out writers
sem_wait(&rw->write_lock);
}
sem_post(&rw->mutex);
}
void rw_read_unlock(rw_lock_t *rw) {
sem_wait(&rw->mutex);
rw->reader_count--;
if (rw->reader_count == 0) {
// Last reader allows writers
sem_post(&rw->write_lock);
}
sem_post(&rw->mutex);
}
void rw_write_lock(rw_lock_t *rw) {
sem_wait(&rw->write_lock);
}
void rw_write_unlock(rw_lock_t *rw) {
sem_post(&rw->write_lock);
}
// Fair readers-writers (prevents writer starvation)
typedef struct {
sem_t order_mutex; // Ensures fair ordering
sem_t read_mutex; // Protects readers
sem_t write_mutex; // Exclusive write access
int readers;
} fair_rw_lock_t;
void fair_rw_read_lock(fair_rw_lock_t *rw) {
sem_wait(&rw->order_mutex);
sem_wait(&rw->read_mutex);
if (rw->readers == 0) {
sem_wait(&rw->write_mutex);
}
rw->readers++;
sem_post(&rw->order_mutex);
sem_post(&rw->read_mutex);
}
Barrier Implementation
typedef struct {
sem_t mutex;
sem_t barrier;
int count;
int n_threads;
} barrier_t;
void barrier_init(barrier_t *b, int n_threads) {
sem_init(&b->mutex, 0, 1);
sem_init(&b->barrier, 0, 0);
b->count = 0;
b->n_threads = n_threads;
}
void barrier_wait(barrier_t *b) {
sem_wait(&b->mutex);
b->count++;
if (b->count == b->n_threads) {
// Last thread releases all
for (int i = 0; i < b->n_threads - 1; i++) {
sem_post(&b->barrier);
}
b->count = 0; // Reset for reuse
sem_post(&b->mutex);
} else {
// Not last, wait
sem_post(&b->mutex);
sem_wait(&b->barrier);
}
}
System V Semaphores
While older, System V semaphores offer unique features:
#include <sys/sem.h>
#include <sys/ipc.h>
// Union for semctl (some systems require this)
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void sysv_semaphore_example() {
key_t key = ftok("/tmp", 'S');
// Create semaphore set with 3 semaphores
int semid = semget(key, 3, IPC_CREAT | 0666);
if (semid < 0) {
perror("semget");
return;
}
// Initialize semaphores
union semun arg;
unsigned short values[3] = {1, 5, 0}; // Initial values
arg.array = values;
semctl(semid, 0, SETALL, arg);
// Atomic operations on multiple semaphores
struct sembuf ops[2];
// Wait on semaphore 0 and 1
ops[0].sem_num = 0;
ops[0].sem_op = -1; // Decrement
ops[0].sem_flg = 0;
ops[1].sem_num = 1;
ops[1].sem_op = -2; // Decrement by 2
ops[1].sem_flg = 0;
// Atomic operation on both
if (semop(semid, ops, 2) < 0) {
perror("semop");
return;
}
printf("Acquired resources\n");
// Release
ops[0].sem_op = 1;
ops[1].sem_op = 2;
semop(semid, ops, 2);
// Cleanup
semctl(semid, 0, IPC_RMID);
}
Performance Considerations
Semaphore vs Mutex Performance
#include <time.h>
void benchmark_synchronization() {
const int iterations = 1000000;
struct timespec start, end;
// Benchmark mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < iterations; i++) {
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
}
clock_gettime(CLOCK_MONOTONIC, &end);
double mutex_time = (end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9;
// Benchmark binary semaphore
sem_t sem;
sem_init(&sem, 0, 1);
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < iterations; i++) {
sem_wait(&sem);
sem_post(&sem);
}
clock_gettime(CLOCK_MONOTONIC, &end);
double sem_time = (end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9;
printf("Mutex: %.2f ns/op\n", (mutex_time / iterations) * 1e9);
printf("Semaphore: %.2f ns/op\n", (sem_time / iterations) * 1e9);
pthread_mutex_destroy(&mutex);
sem_destroy(&sem);
}
Futex-Based Implementation
Modern POSIX semaphores use futexes for efficiency:
// Understanding the underlying futex usage
#include <linux/futex.h>
#include <sys/syscall.h>
typedef struct {
int value;
int waiters;
} my_semaphore_t;
void my_sem_wait(my_semaphore_t *sem) {
while (1) {
int val = __atomic_load_n(&sem->value, __ATOMIC_ACQUIRE);
if (val > 0) {
// Try to decrement
if (__atomic_compare_exchange_n(&sem->value, &val, val - 1,
0, __ATOMIC_ACQUIRE,
__ATOMIC_RELAXED)) {
return; // Success
}
} else {
// Need to wait
__atomic_fetch_add(&sem->waiters, 1, __ATOMIC_ACQUIRE);
// Check again before sleeping
val = __atomic_load_n(&sem->value, __ATOMIC_ACQUIRE);
if (val > 0) {
__atomic_fetch_sub(&sem->waiters, 1, __ATOMIC_ACQUIRE);
continue;
}
// Sleep on futex
syscall(SYS_futex, &sem->value, FUTEX_WAIT,
val, NULL, NULL, 0);
__atomic_fetch_sub(&sem->waiters, 1, __ATOMIC_ACQUIRE);
}
}
}
void my_sem_post(my_semaphore_t *sem) {
__atomic_fetch_add(&sem->value, 1, __ATOMIC_RELEASE);
// Wake one waiter if any
if (__atomic_load_n(&sem->waiters, __ATOMIC_ACQUIRE) > 0) {
syscall(SYS_futex, &sem->value, FUTEX_WAKE, 1, NULL, NULL, 0);
}
}
Real-World Applications
Connection Pool Implementation
typedef struct {
void **connections;
int max_connections;
int current;
sem_t available;
pthread_mutex_t mutex;
} connection_pool_t;
connection_pool_t* pool_create(int max_conn) {
connection_pool_t *pool = malloc(sizeof(connection_pool_t));
pool->connections = calloc(max_conn, sizeof(void*));
pool->max_connections = max_conn;
pool->current = 0;
sem_init(&pool->available, 0, 0);
pthread_mutex_init(&pool->mutex, NULL);
// Pre-create connections
for (int i = 0; i < max_conn; i++) {
pool->connections[i] = create_connection();
sem_post(&pool->available);
pool->current++;
}
return pool;
}
void* pool_acquire(connection_pool_t *pool) {
sem_wait(&pool->available);
pthread_mutex_lock(&pool->mutex);
void *conn = NULL;
for (int i = 0; i < pool->max_connections; i++) {
if (pool->connections[i] != NULL) {
conn = pool->connections[i];
pool->connections[i] = NULL;
break;
}
}
pthread_mutex_unlock(&pool->mutex);
return conn;
}
void pool_release(connection_pool_t *pool, void *conn) {
pthread_mutex_lock(&pool->mutex);
for (int i = 0; i < pool->max_connections; i++) {
if (pool->connections[i] == NULL) {
pool->connections[i] = conn;
break;
}
}
pthread_mutex_unlock(&pool->mutex);
sem_post(&pool->available);
}
Rate Limiter
typedef struct {
sem_t tokens;
pthread_t refill_thread;
int rate; // Tokens per second
int burst; // Maximum burst size
int running;
} rate_limiter_t;
void* refill_tokens(void *arg) {
rate_limiter_t *rl = (rate_limiter_t*)arg;
struct timespec interval = {
.tv_sec = 0,
.tv_nsec = 1000000000 / rl->rate // Nanoseconds between tokens
};
while (rl->running) {
int current;
sem_getvalue(&rl->tokens, ¤t);
if (current < rl->burst) {
sem_post(&rl->tokens);
}
nanosleep(&interval, NULL);
}
return NULL;
}
rate_limiter_t* rate_limiter_create(int rate, int burst) {
rate_limiter_t *rl = malloc(sizeof(rate_limiter_t));
sem_init(&rl->tokens, 0, burst); // Start with full burst
rl->rate = rate;
rl->burst = burst;
rl->running = 1;
pthread_create(&rl->refill_thread, NULL, refill_tokens, rl);
return rl;
}
int rate_limiter_try_acquire(rate_limiter_t *rl) {
return sem_trywait(&rl->tokens) == 0;
}
void rate_limiter_acquire(rate_limiter_t *rl) {
sem_wait(&rl->tokens);
}
Debugging Semaphore Issues
Common Pitfalls and Solutions
// Debugging helper
void debug_semaphore(sem_t *sem, const char *name) {
int value;
sem_getvalue(sem, &value);
printf("[DEBUG] Semaphore %s value: %d\n", name, value);
}
// Timeout-based operations to prevent deadlocks
int sem_wait_timeout(sem_t *sem, int timeout_sec) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_sec;
int result = sem_timedwait(sem, &ts);
if (result == -1 && errno == ETIMEDOUT) {
printf("Semaphore wait timed out after %d seconds\n",
timeout_sec);
return -1;
}
return result;
}
// Deadlock detection helper
typedef struct {
sem_t *sems[10];
int count;
pthread_t owner;
} sem_ownership_t;
__thread sem_ownership_t thread_sems = {0};
void track_sem_wait(sem_t *sem) {
// Add to thread's owned semaphores
thread_sems.sems[thread_sems.count++] = sem;
thread_sems.owner = pthread_self();
// Log for analysis
printf("Thread %lu waiting on semaphore %p\n",
pthread_self(), sem);
}
System-Wide Semaphore Monitoring
# List System V semaphores
ipcs -s
# Show detailed semaphore info
ipcs -s -i <semid>
# Monitor POSIX named semaphores
ls -la /dev/shm/sem.*
# Trace semaphore operations
strace -e semop,semget,semctl,sem_wait,sem_post ./program
Best Practices
- Initialize Properly: Always check return values from sem_init()
- Match wait/post: Ensure every wait has a corresponding post
- Avoid Deadlocks: Acquire multiple semaphores in consistent order
- Handle Interrupts: Check for EINTR in signal environments
- Clean Up: Destroy semaphores when done to avoid resource leaks
- Use Timeouts: Prefer sem_timedwait() in production code
- Document Intent: Clearly document what each semaphore protects
Conclusion
Semaphores remain a powerful tool in the Linux synchronization toolkit. While mutexes handle simple mutual exclusion elegantly, semaphores excel at resource counting, signaling between threads, and implementing complex synchronization patterns. Understanding when and how to use semaphores effectively is crucial for building robust concurrent applications.
From bounded buffers to rate limiters, from reader-writer locks to connection pools, semaphores provide the foundation for many real-world synchronization solutions. By mastering both POSIX and System V semaphores, along with their performance characteristics and debugging techniques, you’ll be well-equipped to tackle even the most challenging concurrent programming problems in Linux.