Two Semaphore APIs on Linux
Linux provides two entirely separate semaphore interfaces: the older System V semaphores (introduced in Unix System V in the 1980s) and the newer POSIX semaphores (standardized in POSIX.1b, 1993). Both are fully supported on modern Linux. Understanding the differences helps you choose the right tool and answer interview questions confidently.
API Side-by-Side Comparison
| Aspect | System V Semaphores | POSIX Semaphores |
|---|---|---|
| Header | <sys/sem.h> |
<semaphore.h> |
| Create | semget(key, nsems, flags) |
sem_open(name, ...) or sem_init() |
| Unit of allocation | Array of semaphores (a “set”) | Individual semaphore |
| Wait (decrement) | semop(semid, sops, nsops) |
sem_wait() |
| Post (increment) | semop(semid, sops, nsops) |
sem_post() |
| Operate by N | Yes — sem_op can be any integer | No — always ±1 only |
| Atomic multi-semaphore ops | Yes — semop on array | No — one at a time |
| Undo on process exit | Yes — SEM_UNDO flag | No built-in undo |
| Kernel limit | Yes — SEMMNI, SEMMSL, SEMMNS | Named: limited by /dev/shm space Unnamed: limited by memory |
| Inspect with shell | ipcs -s, ipcrm -s |
ls /dev/shm/, rm /dev/shm/sem.* |
| Portability | Widely available, POSIX XSI extension | POSIX.1b — slightly less portable (macOS has bugs with named sems) |
| API complexity | Complex — semget, semctl, semop with structs | Simple — small set of clean functions |
| Performance (uncontended) | Slower — always a syscall | Faster — can use futex, no syscall when uncontended |
Performance: Why POSIX Wins on Uncontended Cases
On Linux, POSIX semaphores (and pthreads mutexes) are implemented using futexes (fast userspace mutexes). A futex only makes a kernel system call when there is actual contention. When the semaphore is free (value > 0 for wait, or no waiters for post), the operation is handled entirely in userspace using atomic CPU instructions — no kernel involvement at all.
System V semaphores always go through a kernel system call (semop()), even when uncontended. This makes them significantly slower in tight loops.
Benchmark Example: Measuring the Difference
This program (inspired by TLPI Exercise 53-4) measures POSIX semaphore increment+decrement throughput. Run a similar program with semop() to compare.
#include <stdio.h>
#include <time.h>
#include <semaphore.h>
#define ITERATIONS 1000000
int main(void)
{
sem_t sem;
struct timespec start, end;
long elapsed_ns;
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);
elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000L
+ (end.tv_nsec - start.tv_nsec);
printf("POSIX semaphore: %d iterations in %ld ms\n",
ITERATIONS, elapsed_ns / 1000000);
printf("Average: %.1f ns per wait+post pair\n",
(double)elapsed_ns / ITERATIONS);
sem_destroy(&sem);
return 0;
}
/* Typical output on Linux:
POSIX semaphore: 1000000 iterations in 12 ms
Average: 12.3 ns per wait+post pair */
System V Only: SEM_UNDO — Automatic Rollback on Process Exit
One feature unique to System V semaphores is the SEM_UNDO flag for semop(). When used, the kernel records each operation and automatically undoes it when the process exits — even if the process crashes. This prevents semaphores from being left in a locked state.
POSIX semaphores have no equivalent. If a process dies holding a POSIX semaphore, the semaphore stays in its current state. Other processes waiting on it will block forever. This must be handled at the application level (e.g., using sem_timedwait with a timeout, or a watchdog process).
/* System V SEM_UNDO example */
#include <sys/sem.h>
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1; /* decrement (lock) */
op.sem_flg = SEM_UNDO; /* undo on exit: kernel restores value if we crash */
semop(semid, &op, 1);
/* ... critical section ... */
op.sem_op = 1; /* increment (unlock) */
op.sem_flg = SEM_UNDO;
semop(semid, &op, 1);
When to Use Which
- Synchronizing threads within one process
- You want a clean, simple API
- Performance matters (uncontended hot path)
- New code — greenfield development
- The semaphore is tied to a short-lived resource
- You need SEM_UNDO (crash-safe locking)
- You need atomic operations on multiple semaphores at once
- You need increment/decrement by values other than 1
- Maintaining legacy code that already uses SysV IPC
- Maximum portability to very old Unix systems
POSIX Semaphore vs POSIX Mutex (pthread_mutex)
For thread synchronization within a single process, pthread mutexes are generally preferred over semaphores. Here is why:
| Feature | pthread_mutex | POSIX Semaphore |
|---|---|---|
| Ownership | Yes — only locker can unlock | No — anyone can post |
| Error detection | EDEADLK, EPERM with error-checking type | None |
| Priority inheritance | Available (PTHREAD_PRIO_INHERIT) | Not available |
| Best use case | Protecting critical sections | Signaling between threads/processes, counting resources |
| Signal handler safe | No | sem_post() only |
| Cross-process use | Yes (PTHREAD_PROCESS_SHARED) | Yes (named or pshared unnamed) |
Complete Example: System V vs POSIX for the Same Problem
Both programs solve the same problem: a simple binary lock around a counter. Compare the API complexity.
System V version
#include <stdio.h>
#include <sys/sem.h>
#include <sys/ipc.h>
/* Required union for semctl */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main(void)
{
int semid;
struct sembuf lock_op = { 0, -1, 0 };
struct sembuf unlock_op = { 0, 1, 0 };
union semun arg;
/* Create a set of 1 semaphore */
semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); return 1; }
/* Set initial value to 1 */
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
/* Lock */
semop(semid, &lock_op, 1);
printf("Inside critical section.\n");
/* Unlock */
semop(semid, &unlock_op, 1);
/* Remove semaphore */
semctl(semid, 0, IPC_RMID);
return 0;
}
POSIX version — same logic, far simpler
#include <stdio.h>
#include <semaphore.h>
int main(void)
{
sem_t sem;
sem_init(&sem, 0, 1); /* initial value = 1 */
sem_wait(&sem); /* lock */
printf("Inside critical section.\n");
sem_post(&sem); /* unlock */
sem_destroy(&sem);
return 0;
}
Chapter 53 Summary
- POSIX semaphores come in two flavors: named (filesystem-based,
sem_open/close/unlink) and unnamed (memory-based,sem_init/destroy). - All operations (
sem_wait,sem_trywait,sem_timedwait,sem_post) work on both types via asem_t *. sem_timedwait()takes an absolute CLOCK_REALTIME timestamp — always compute it withclock_gettime()plus your offset.sem_post()is async-signal-safe;sem_wait()is not.sem_getvalue()is for diagnostics only — never use it to make synchronization decisions.SEM_VALUE_MAXis at least 32,767 (POSIX) andINT_MAXon Linux.- POSIX semaphores are simpler and faster than System V semaphores for most use cases. System V wins only when you need SEM_UNDO or atomic multi-semaphore operations.
- For thread synchronization within one process, prefer
pthread_mutex_tover semaphores — better error detection, ownership semantics, and priority inheritance support.
Interview Questions – POSIX vs System V & Chapter Summary
ipcs -s lists all semaphore sets; ipcrm -s semid removes one. POSIX named: ls /dev/shm/sem.* lists them; rm /dev/shm/sem.name or sem_unlink("/name") removes them. Unnamed semaphores have no independent existence outside their containing memory and cannot be listed.Chapter 53 Complete!
You have covered all POSIX semaphore concepts from Chapter 53 of TLPI.
