Advanced Semaphore Patterns & Real-World Usage
Chapter 47 ยท The Linux Programming Interface ยท Part 6
๐ Producer-Consumer
๐ Reader-Writer
๐ Shared Memory Guard
Chapter 47 Series Navigation
Part 1: Introduction Part 2: semget() Part 3: semctl() Part 4: semop() Part 5: Limits โถ Part 6: Advanced
Reusable Binary Semaphore Library
Because System V semaphores require so much boilerplate, most projects wrap them in a small library. Here is a complete, reusable binary semaphore library that handles initialization, acquire, release, and cleanup:
binsem.h โ Header
/* binsem.h โ Reusable binary semaphore (mutex) library */
#ifndef BINSEM_H
#define BINSEM_H
#include <sys/sem.h>
/* Binary semaphore states */
#define BINSEM_LOCKED 0
#define BINSEM_UNLOCKED 1
/* Structure to hold semaphore info */
typedef struct {
int semid;
int semnum;
} BinSem;
/* Initialize semun union (required by semctl) */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
/* Function prototypes */
int binsem_create(BinSem *sem, key_t key, int semnum);
int binsem_open(BinSem *sem, key_t key, int semnum);
int binsem_lock(BinSem *sem);
int binsem_unlock(BinSem *sem);
int binsem_trylock(BinSem *sem);
int binsem_getval(BinSem *sem);
void binsem_delete(BinSem *sem);
#endif /* BINSEM_H */
binsem.c โ Implementation
/* binsem.c โ Binary semaphore library implementation */
#include "binsem.h"
#include <errno.h>
#include <stdio.h>
/*
* Create a new binary semaphore set with one semaphore.
* Returns 0 on success, -1 on error.
*/
int binsem_create(BinSem *sem, key_t key, int semnum) {
union semun arg;
int semid;
/* Create with IPC_EXCL to guarantee we are the initializer */
semid = semget(key, semnum + 1, IPC_CREAT | IPC_EXCL | 0660);
if (semid == -1) {
if (errno == EEXIST) {
/* Already created โ just open */
return binsem_open(sem, key, semnum);
}
perror("binsem_create: semget");
return -1;
}
/* Initialize the requested semaphore to 1 (unlocked) */
arg.val = BINSEM_UNLOCKED;
if (semctl(semid, semnum, SETVAL, arg) == -1) {
perror("binsem_create: SETVAL");
semctl(semid, 0, IPC_RMID); /* cleanup */
return -1;
}
/* Mark init complete via semop (stamps sem_otime) */
struct sembuf noop = {(unsigned short)semnum, 0, IPC_NOWAIT};
/* Wait-for-zero on unlocked sem: might not work โ use add+subtract */
struct sembuf stamp[2] = {
{(unsigned short)semnum, -1, 0}, /* lock */
{(unsigned short)semnum, +1, 0} /* immediately unlock */
};
semop(semid, stamp, 2);
sem->semid = semid;
sem->semnum = semnum;
return 0;
}
/* Open an existing binary semaphore set */
int binsem_open(BinSem *sem, key_t key, int semnum) {
union semun arg;
struct semid_ds ds;
int semid = semget(key, 0, 0660);
if (semid == -1) { perror("binsem_open: semget"); return -1; }
/* Wait until creator has finished initialization (sem_otime != 0) */
arg.buf = &ds;
int retries = 0;
while (retries++ < 500) {
if (semctl(semid, 0, IPC_STAT, arg) == -1) return -1;
if (ds.sem_otime != 0) break;
usleep(5000);
}
if (ds.sem_otime == 0) {
fprintf(stderr, "binsem_open: timed out waiting for init\n");
return -1;
}
sem->semid = semid;
sem->semnum = semnum;
return 0;
}
/* Acquire the semaphore (blocks until available) */
int binsem_lock(BinSem *sem) {
struct sembuf sop = {(unsigned short)sem->semnum, -1, SEM_UNDO};
int ret;
do {
ret = semop(sem->semid, &sop, 1);
} while (ret == -1 && errno == EINTR);
return ret;
}
/* Release the semaphore */
int binsem_unlock(BinSem *sem) {
struct sembuf sop = {(unsigned short)sem->semnum, +1, SEM_UNDO};
return semop(sem->semid, &sop, 1);
}
/* Try to acquire without blocking */
int binsem_trylock(BinSem *sem) {
struct sembuf sop = {(unsigned short)sem->semnum, -1, IPC_NOWAIT | SEM_UNDO};
if (semop(sem->semid, &sop, 1) == -1) {
if (errno == EAGAIN) return 1; /* locked */
perror("binsem_trylock"); return -1;
}
return 0; /* acquired */
}
/* Get current value */
int binsem_getval(BinSem *sem) {
return semctl(sem->semid, sem->semnum, GETVAL);
}
/* Delete the semaphore set */
void binsem_delete(BinSem *sem) {
semctl(sem->semid, 0, IPC_RMID);
}
Classic Producer-Consumer with 3 Semaphores
The producer-consumer pattern uses three semaphores: a mutex to protect the buffer, a counter for empty slots, and a counter for full slots.
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 5
#define NUM_ITEMS 10
/* Semaphore indices */
#define SEM_MUTEX 0
#define SEM_EMPTY 1
#define SEM_FULL 2
#define NUM_SEMS 3
union semun { int val; struct semid_ds *buf; unsigned short *array; };
/* Shared circular buffer in shared memory */
typedef struct {
int items[BUFFER_SIZE];
int head;
int tail;
int count;
} SharedBuffer;
/* sem_wait: subtract 1 from semaphore (blocks if 0) */
void sem_wait(int semid, int semnum) {
struct sembuf sop = {semnum, -1, SEM_UNDO};
int ret;
do { ret = semop(semid, &sop, 1); } while (ret == -1 && errno == EINTR);
if (ret == -1) { perror("sem_wait"); exit(1); }
}
/* sem_post: add 1 to semaphore */
void sem_post(int semid, int semnum) {
struct sembuf sop = {semnum, +1, SEM_UNDO};
if (semop(semid, &sop, 1) == -1) { perror("sem_post"); exit(1); }
}
/* Create and initialize semaphore set */
int init_semaphores(void) {
int semid;
union semun arg;
unsigned short init_vals[NUM_SEMS];
semid = semget(IPC_PRIVATE, NUM_SEMS, 0600);
if (semid == -1) { perror("semget"); exit(1); }
init_vals[SEM_MUTEX] = 1; /* mutex: unlocked */
init_vals[SEM_EMPTY] = BUFFER_SIZE; /* all slots free */
init_vals[SEM_FULL] = 0; /* no items yet */
arg.array = init_vals;
if (semctl(semid, 0, SETALL, arg) == -1) { perror("SETALL"); exit(1); }
return semid;
}
/* Producer: adds items to shared buffer */
void producer(int semid, SharedBuffer *buf) {
for (int i = 0; i < NUM_ITEMS; i++) {
int item = i * 10; /* produce item */
sem_wait(semid, SEM_EMPTY); /* wait for free slot */
sem_wait(semid, SEM_MUTEX); /* lock buffer */
/* Critical section: add item */
buf->items[buf->tail] = item;
buf->tail = (buf->tail + 1) % BUFFER_SIZE;
buf->count++;
printf("Producer: added item %d (buffer count=%d)\n", item, buf->count);
sem_post(semid, SEM_MUTEX); /* unlock */
sem_post(semid, SEM_FULL); /* signal consumer */
usleep(100000); /* 100ms between produces */
}
printf("Producer: done\n");
}
/* Consumer: takes items from shared buffer */
void consumer(int semid, SharedBuffer *buf) {
for (int i = 0; i < NUM_ITEMS; i++) {
sem_wait(semid, SEM_FULL); /* wait for available item */
sem_wait(semid, SEM_MUTEX); /* lock buffer */
/* Critical section: remove item */
int item = buf->items[buf->head];
buf->head = (buf->head + 1) % BUFFER_SIZE;
buf->count--;
printf("Consumer: got item %d (buffer count=%d)\n", item, buf->count);
sem_post(semid, SEM_MUTEX); /* unlock */
sem_post(semid, SEM_EMPTY); /* signal producer */
usleep(200000); /* 200ms between consumes (slower than producer) */
}
printf("Consumer: done\n");
}
int main(void) {
/* Create shared memory for the buffer */
int shmid = shmget(IPC_PRIVATE, sizeof(SharedBuffer), IPC_CREAT | 0600);
if (shmid == -1) { perror("shmget"); exit(1); }
SharedBuffer *buf = shmat(shmid, NULL, 0);
if (buf == (void*)-1) { perror("shmat"); exit(1); }
memset(buf, 0, sizeof(SharedBuffer));
/* Initialize semaphores */
int semid = init_semaphores();
pid_t pid = fork();
if (pid == 0) {
consumer(semid, buf);
exit(0);
} else {
producer(semid, buf);
wait(NULL);
}
/* Cleanup */
semctl(semid, 0, IPC_RMID);
shmdt(buf);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
Protecting Shared Memory with a Semaphore
The most common real-world use of System V semaphores is guarding a shared memory segment. Here is the full pattern:
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
union semun { int val; struct semid_ds *buf; unsigned short *array; };
#define SHM_KEY 0x1234
#define SEM_KEY 0x5678
#define SHM_SIZE 1024
typedef struct {
int version;
char data[256];
int data_len;
} SharedData;
/* Acquire mutex (subtract 1) */
void lock(int semid) {
struct sembuf sop = {0, -1, SEM_UNDO};
int ret;
do { ret = semop(semid, &sop, 1); } while (ret == -1 && errno == EINTR);
if (ret == -1) { perror("lock"); exit(1); }
}
/* Release mutex (add 1) */
void unlock(int semid) {
struct sembuf sop = {0, +1, SEM_UNDO};
if (semop(semid, &sop, 1) == -1) { perror("unlock"); exit(1); }
}
int main(void) {
int shmid, semid;
union semun arg;
SharedData *shared;
/* ---- Setup shared memory ---- */
shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0660);
if (shmid == -1) { perror("shmget"); exit(1); }
shared = shmat(shmid, NULL, 0);
if (shared == (void*)-1) { perror("shmat"); exit(1); }
/* ---- Setup semaphore (mutex initialized to 1) ---- */
semid = semget(SEM_KEY, 1, IPC_CREAT | 0660);
if (semid == -1) { perror("semget"); exit(1); }
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
/* ---- Process 1: Writer ---- */
pid_t pid = fork();
if (pid == 0) {
/* Writer: update shared data safely */
for (int i = 0; i < 5; i++) {
lock(semid);
/* Safe to modify shared memory */
shared->version++;
snprintf(shared->data, sizeof(shared->data),
"Update #%d from PID %d", i, getpid());
shared->data_len = strlen(shared->data);
printf("Writer: wrote version %d: '%s'\n",
shared->version, shared->data);
unlock(semid);
sleep(1);
}
shmdt(shared);
exit(0);
} else {
/* Process 2: Reader */
for (int i = 0; i < 5; i++) {
sleep(1); /* let writer go first */
lock(semid);
/* Safe to read shared memory */
printf("Reader: version %d: '%.*s'\n",
shared->version, shared->data_len, shared->data);
unlock(semid);
}
wait(NULL);
}
/* Cleanup */
shmdt(shared);
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
System V Semaphores vs POSIX Semaphores โ Choosing Wisely
| Feature | System V | POSIX (sem_open/sem_init) |
|---|---|---|
| API simplicity | Complex (3 syscalls, semun union, semaphore sets) | Simple (sem_init/sem_wait/sem_post) |
| Works across unrelated processes | Yes (via key) | Yes (named: sem_open with a name) |
| Works across parent-child | Yes (inherit semid) | Yes (unnamed in shared mem) |
| Atomic multi-semaphore ops | Yes (semop with multiple sops) | No |
| Auto-cleanup on process exit | Only with SEM_UNDO | Yes (named sems auto-unlink) |
| Kernel persistence | Persists until IPC_RMID or reboot | Persists until sem_unlink or reboot |
| Portable | POSIX (widely supported) | POSIX (cleaner standard) |
| Best for | Legacy systems; atomic multi-resource acquisition | New code; simple binary semaphores |
Recommendation: For new code, prefer POSIX semaphores. Use System V semaphores when: (a) working with legacy code that already uses System V IPC, or (b) you specifically need atomic operations across multiple semaphores in a single call.
Common Semaphore Bugs and How to Avoid Them
๐ Bug 1: Forgetting to delete the semaphore set
/* BAD: semaphore set leaks if process crashes */
int semid = semget(key, 1, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, arg);
/* ... program crashes here ... semaphore set stays in kernel forever */
/* GOOD: use SEM_UNDO and register a cleanup handler */
signal(SIGTERM, cleanup);
signal(SIGINT, cleanup);
/* In cleanup handler: semctl(semid, 0, IPC_RMID); */
๐ Bug 2: Not retrying on EINTR
/* BAD: semop may return prematurely due to signal */
if (semop(semid, &sop, 1) == -1) {
perror("semop"); /* this fires even on innocent SIGCHLD */
exit(1);
}
/* GOOD: retry loop */
int ret;
do {
ret = semop(semid, &sop, 1);
} while (ret == -1 && errno == EINTR);
if (ret == -1) { perror("semop"); exit(1); }
๐ Bug 3: Unbalanced lock/unlock (deadlock)
/* BAD: returning from critical section without unlocking */
void process_data(int semid) {
lock(semid);
if (error_condition) {
return; /* FORGOT TO UNLOCK! โ now deadlocked */
}
do_work();
unlock(semid);
}
/* GOOD: always unlock before returning */
void process_data(int semid) {
lock(semid);
if (error_condition) {
unlock(semid); /* always unlock first */
return;
}
do_work();
unlock(semid);
}
๐ Bug 4: Not checking EIDRM after semop() returns
/* BAD: assuming all semop errors are fatal */
if (semop(semid, &sop, 1) == -1) {
perror("semop");
exit(1); /* what if another process called IPC_RMID? */
}
/* GOOD: handle EIDRM gracefully */
if (semop(semid, &sop, 1) == -1) {
if (errno == EIDRM) {
fprintf(stderr, "Semaphore set was deleted โ shutting down\n");
/* graceful exit or reconnect */
} else if (errno != EINTR) {
perror("semop"); exit(1);
}
}
Mini Application: Multi-Process Counter with Semaphore Guard
/*
* counter_demo.c
* Demonstrates a shared counter protected by a semaphore.
* Run N worker processes, each increments the counter M times.
* Final value should be exactly N*M.
*/
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM_WORKERS 4
#define INCREMENTS 10000
union semun { int val; struct semid_ds *buf; unsigned short *array; };
/* Shared data */
typedef struct { long counter; } Shared;
void lock(int semid) { struct sembuf s={0,-1,SEM_UNDO}; semop(semid,&s,1); }
void unlock(int semid) { struct sembuf s={0,+1,SEM_UNDO}; semop(semid,&s,1); }
int main(void) {
/* Create shared memory */
int shmid = shmget(IPC_PRIVATE, sizeof(Shared), IPC_CREAT | 0600);
Shared *shared = shmat(shmid, NULL, 0);
shared->counter = 0;
/* Create mutex semaphore */
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
union semun arg = { .val = 1 };
semctl(semid, 0, SETVAL, arg);
/* Fork workers */
for (int i = 0; i < NUM_WORKERS; i++) {
if (fork() == 0) {
/* Each worker increments the counter INCREMENTS times */
for (int j = 0; j < INCREMENTS; j++) {
lock(semid);
shared->counter++; /* protected critical section */
unlock(semid);
}
shmdt(shared);
exit(0);
}
}
/* Wait for all workers */
for (int i = 0; i < NUM_WORKERS; i++) wait(NULL);
printf("Final counter = %ld\n", shared->counter);
printf("Expected = %d\n", NUM_WORKERS * INCREMENTS);
printf("Result = %s\n",
shared->counter == NUM_WORKERS * INCREMENTS ? "CORRECT โ" : "RACE CONDITION โ");
/* Cleanup */
shmdt(shared);
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
/*
* To demonstrate the race condition, comment out the lock/unlock calls.
* You will see a value much less than NUM_WORKERS*INCREMENTS.
*/
Final Interview Questions โ Advanced Patterns
Q1: How do you implement a counting semaphore (resource pool) using System V semaphores?
Initialize the semaphore to N (the number of available resources). Each process that wants a resource calls
semop with sem_op = -1; this blocks if all N resources are in use. When done, call semop with sem_op = +1 to return the resource. The semaphore value always equals the current number of free resources, and the kernel automatically manages the waiting queue.Q2: In a producer-consumer problem, why do you need THREE semaphores and not just one mutex?
You need: (1) a mutex to protect concurrent access to the buffer data structure, (2) an “empty slots” counter to block producers when the buffer is full, and (3) a “full slots” counter to block consumers when the buffer is empty. Using only a mutex would require the producer and consumer to busy-wait (spin) checking if the buffer is full/empty, which wastes CPU. The counting semaphores provide efficient blocking.
Q3: Can you use System V semaphores with shared memory? How?
Yes โ this is in fact the most common use case. Create a shared memory segment with
shmget() + shmat() and a semaphore set with semget(). Before any process reads or writes the shared memory, it acquires the semaphore (subtract 1); after finishing, it releases (add 1). This prevents two processes from modifying the shared data simultaneously. The semaphore and shared memory are typically created with the same or related keys so both can be found by any participating process.Q4: What is the semadj value and how does it relate to SEM_UNDO?
The semadj (semaphore adjustment) is a per-process, per-semaphore running total. Each time a process calls
semop() with SEM_UNDO, the kernel records the negation of sem_op in the process’s semadj for that semaphore. When the process exits, the kernel adds all non-zero semadj values back to their semaphores. For example, if a process acquired a semaphore (sem_op = -1) with SEM_UNDO and then exited, semadj = +1, so the kernel adds 1 to restore the semaphore, as if the process had called unlock before dying.Q5: Why is System V IPC considered “complex” and when would you still choose it over POSIX?
System V IPC requires multiple system calls (semget + semctl + semop), a manually-declared union (semun), a separate initialization step that risks race conditions, and explicit cleanup. POSIX semaphores have a much simpler API. However, System V semaphores are still chosen when: (1) the codebase already uses System V IPC (shmget/msgget) for consistency, (2) you need atomic operations across multiple semaphores in one call (impossible with POSIX), or (3) you need semaphore sets to group related semaphores under one identifier.
Chapter 47 Summary โ Key Takeaways
semget() โ create/open set
semctl() โ control operations
semop() โ perform operations
SEM_UNDO โ auto-cleanup
IPC_NOWAIT โ non-blocking
sem_otime โ init detection
semaphore sets โ atomic multi-ops
IPC_RMID โ delete set
SEMMNI/SEMMSL/SEMVMX limits
