47.11 & 47.12
Design Flaws
Intermediate
Understanding the weaknesses of System V semaphores is critical for interviews at embedded and systems companies. You will be asked: “Why would you prefer POSIX semaphores or mutexes over System V semaphores?” The answer requires knowing exactly where System V falls short.
The PDF excerpt from TLPI Section 47.11 lists these disadvantages explicitly. This section covers each one with explanation and code context, and compares System V with the alternatives.
Key Terms
Most UNIX I/O and IPC mechanisms use file descriptors — sockets, pipes, message queues (POSIX), shared memory (POSIX), timerfd, signalfd, eventfd. File descriptors can all be monitored simultaneously using select(), poll(), or epoll().
System V semaphores use an integer semid that is NOT a file descriptor. You cannot pass a semid to select/poll. This means you cannot simultaneously wait on a semaphore AND an incoming network connection, for example.
| Mechanism | Uses fd? | Works with poll/epoll? |
|---|---|---|
| Pipe / FIFO | ✔ yes | ✔ yes |
| Socket | ✔ yes | ✔ yes |
| POSIX message queue | ✔ yes (mqd_t) | ✔ yes (Linux) |
| eventfd / timerfd | ✔ yes | ✔ yes |
| System V Semaphore | ✘ no (semid) | ✘ no |
Workaround: create a dedicated child process or thread to operate on the semaphore and write results to a pipe, which the main process monitors with poll().
/*
* Workaround: thread-based semaphore wait + pipe notification.
* Thread blocks on semop(), then writes to pipe; main loop uses poll() on pipe fd.
*/
#include <pthread.h>
#include <unistd.h>
#include <sys/sem.h>
int sem_pipe[2]; /* sem_pipe[0]=read, sem_pipe[1]=write */
int semid;
void *sem_watcher(void *arg)
{
struct sembuf sop = { 0, -1, 0 }; /* wait on sem[0] */
while (1) {
if (semop(semid, &sop, 1) == 0) {
char msg = 'S';
write(sem_pipe[1], &msg, 1); /* notify main via pipe */
}
}
return NULL;
}
/* main() uses poll() on sem_pipe[0] and socket fds simultaneously */
File-based IPC (pipes, UNIX domain sockets, POSIX shared memory via shm_open) use pathnames in the filesystem as identifiers. These are human-readable, easy to debug, subject to normal filesystem permissions, and can be listed with ls.
System V uses integer keys generated by ftok(). These are not visible in the filesystem, not discoverable without ipcs, and require careful key collision management. The developer must manually distribute the key agreement (same file + same project ID).
/* File-based approach (POSIX): clean, visible, debuggable */
sem_t *psem = sem_open("/my_app_lock", O_CREAT, 0600, 1);
/* Visible as /dev/shm/sem.my_app_lock on Linux */
/* ls /dev/shm/ shows it */
/* System V approach: opaque integer key */
key_t key = ftok("/some/file", 42);
int semid = semget(key, 1, IPC_CREAT | 0600);
/* No filesystem entry; invisible unless you run: ipcs -s */
As covered in Part 3, semget() creates the semaphore set but does NOT initialize the values — they default to 0. Initialization requires a separate semctl(SETVAL) call. Between these two calls, other processes can observe uninitialized state.
POSIX semaphores solve this elegantly: sem_open() accepts an initial value in one atomic call. There is no window for a race.
| API | Create + Init Atomic? | Race Possible? |
|---|---|---|
| sem_open() (POSIX) | ✔ yes | ✘ no race |
| semget() + semctl() (SysV) | ✘ no | ✔ yes — requires sem_otime trick |
File descriptors and POSIX semaphores are reference-counted. When the last process closes an fd, the resource can be cleaned up automatically. System V semaphore sets have no reference count. The kernel has no idea how many processes are using a given semaphore set.
This creates two practical problems: (1) you don’t know when it is safe to delete the semaphore set, and (2) if no process ever deletes it, it persists until reboot and leaks kernel memory.
/* Check if any processes are still waiting BEFORE deleting */
int ncnt = semctl(semid, 0, GETNCNT);
int zcnt = semctl(semid, 0, GETZCNT);
if (ncnt == 0 && zcnt == 0) {
/* No processes blocked — safe to delete (probably) */
semctl(semid, 0, IPC_RMID);
} else {
printf("%d processes waiting — not deleting yet\n", ncnt + zcnt);
}
/*
* But this is still not atomic — between the check and the IPC_RMID,
* another process may start waiting. System V gives you no perfect answer.
*/
Common cleanup pattern: use a dedicated “last server process” that deletes the semaphore set on exit, possibly combined with an atexit() handler or signal handler.
Most use cases only need one semaphore. But System V forces you to deal with sets, separate creation and initialization, union semun, and multiple command codes for semctl(). Compare the API sizes:
| POSIX Semaphore | System V Semaphore |
|---|---|
| sem_open() — create/open | semget() + semctl(SETVAL) |
| sem_wait() — acquire | semop() with sem_op=-1 |
| sem_post() — release | semop() with sem_op=+1 |
| sem_close() — close | (no close; just delete or let it persist) |
| sem_unlink() — delete | semctl(IPC_RMID) |
/* POSIX named semaphore — clean and simple */
#include <semaphore.h>
#include <fcntl.h>
sem_t *sem = sem_open("/my_sem", O_CREAT | O_EXCL, 0600, 1);
sem_wait(sem); /* acquire */
/* ... critical section ... */
sem_post(sem); /* release */
sem_close(sem);
sem_unlink("/my_sem"); /* delete from filesystem */
System V semaphores are subject to hard kernel limits (SEMMSL, SEMMNI, SEMOPM, etc.). If your application needs more than the defaults, someone must adjust /proc/sys/kernel/sem or /etc/sysctl.conf before deployment. This is an installation-time burden that POSIX semaphores and mutexes do not impose in the same way.
/* Checking system limits before allocating large semaphore sets */
#include <sys/sem.h>
#include <stdio.h>
union semun { int val; struct semid_ds *buf;
unsigned short *array; struct seminfo *__buf; };
void check_limits_before_create(int needed_sems_per_set, int needed_sets)
{
union semun arg;
struct seminfo si;
arg.__buf = &si;
if (semctl(0, 0, IPC_INFO, arg) == -1) {
perror("IPC_INFO"); return;
}
if (needed_sems_per_set > si.semmsl) {
fprintf(stderr, "ERROR: need %d sems per set but SEMMSL=%d\n",
needed_sems_per_set, si.semmsl);
fprintf(stderr, "Increase: echo 'kernel.sem = %d ...' >> /etc/sysctl.conf\n",
needed_sems_per_set);
return;
}
if (needed_sets > si.semmni) {
fprintf(stderr, "ERROR: need %d sets but SEMMNI=%d\n",
needed_sets, si.semmni);
return;
}
printf("Limits OK: can create %d sets of %d semaphores each\n",
needed_sets, needed_sems_per_set);
}
System V semaphores allow processes to synchronize actions — essential when a process needs exclusive access to a shared resource such as a region of shared memory.
Semaphores live in sets. Each semaphore in a set is a non-negative integer. The semop() system call lets you add an integer, subtract an integer (potentially blocking), or wait for a value of zero. Blocking operations remain suspended until the condition can be satisfied.
The kernel does not initialize semaphore values — you must call semctl(SETVAL) or semctl(SETALL) after semget(). When multiple peer processes may create the same semaphore, extra care is needed to avoid the initialization race condition (use the sem_otime trick).
Where multiple processes try to decrement by the same amount, the order is indeterminate. Where processes try to decrement by different amounts, operations complete in the order they become possible — watch for starvation.
The SEM_UNDO flag allows automatic undo of operations when the process terminates, preventing indefinite blocking by other processes after a crash.
System V semaphores are allocated in sets and can be adjusted by arbitrary amounts — more than most applications need. Binary semaphores (0 or 1) can be built on top using a simple wrapper library.
| Feature | System V Semaphore | POSIX Semaphore | POSIX Mutex (pthread) |
|---|---|---|---|
| Identified by | Integer key / semid | Pathname or memory | Memory pointer |
| Scope | System-wide | System-wide (named) / process (unnamed) | Process (default) / cross-process (PTHREAD_PROCESS_SHARED) |
| File descriptor? | No | No (sem_t*) | No |
| Init race condition? | Yes — sem_otime trick needed | No — atomic in sem_open() | No |
| Auto-delete on exit? | No — persists until IPC_RMID or reboot | No (named) / Yes (unnamed) | Yes (in-memory) |
| Reference counting? | No | Yes (named) | Yes |
| Set-based operations? | Yes — multiple ops atomic | No — one at a time | No |
| SEM_UNDO / cleanup on crash | Yes — SEM_UNDO flag | No built-in | Yes — robust mutex (PTHREAD_MUTEX_ROBUST) |
| API complexity | High | Low | Low |
| Kernel limits | Many (SEMMNI, SEMMSL etc.) | Minimal | Minimal |
From the TLPI text (Section 47.11): unlike message queues, there are fewer alternatives to System V semaphores. Key options:
| Alternative | When to Use | Limitation |
|---|---|---|
| POSIX semaphores (sem_open/sem_wait) | Cleaner API, cross-process semaphores | No atomic multi-semaphore ops |
| Record locking (fcntl F_SETLK) | Synchronization tied to file regions | Slower; requires file I/O overhead |
| pthread_mutex (PROCESS_SHARED) | Threads or shared-memory processes | Must be in shared memory; ownership rules apply |
| Futex (fast user-space mutex) | Performance-critical applications | Very low-level; complex to use directly |
Q1. List the six main disadvantages of System V semaphores.
(1) Identified by semids, not file descriptors — cannot use with poll/select. (2) Keys instead of filenames — harder to discover and debug. (3) Create and initialize are separate calls — race condition possible. (4) No reference count — hard to know when to safely delete. (5) Overly complex API for the most common single-semaphore use case. (6) Subject to many kernel limits requiring system administrator configuration.
Q2. What is a semaphore set and why does System V use them?
A semaphore set is a group of one or more semaphores treated as a single kernel object with one ID, one set of permissions, and one lifecycle. The motivation is to allow atomic operations on multiple semaphores simultaneously via a single semop() call, enabling deadlock-free acquisition of multiple resources.
Q3. Explain the starvation scenario with System V semaphores.
If Process A is waiting to decrement a semaphore by 5 and Process B is waiting to decrement by 1, and the semaphore value fluctuates between 1 and 4, Process B repeatedly gets satisfied while Process A never gets a chance. The kernel satisfies operations in the order they become possible, not in arrival order, so Process A may wait indefinitely.
Q4. What happens when IPC_RMID is called while processes are blocked on the semaphore?
The semaphore set is immediately removed. All blocked semop() calls in other processes return -1 with errno = EIDRM. Programs must handle this error in their semop() loops.
Q5. How do System V semaphores differ from POSIX semaphores from a kernel perspective?
System V semaphores are heavyweight kernel objects with their own namespace (IPC keys), no file descriptor, and persist across all process exits until explicitly deleted. POSIX named semaphores appear as filesystem entries (typically under /dev/shm), are reference-counted, and support standard open/close/unlink semantics. POSIX unnamed semaphores live in user-mapped memory and are lightweight.
Q6. You are writing a server that manages a pool of 10 database connections using a System V semaphore initialized to 10. A client crashes while holding a connection. What happens, and how do you prevent the connection from being permanently lost?
Without SEM_UNDO, the semaphore value stays at 9 permanently — one connection is lost from the pool. The fix: each client uses semop() with SEM_UNDO when decrementing. On crash, the kernel restores the value by +1, returning the connection to the pool.
Q7. Can you use System V semaphores to synchronize threads within the same process?
Yes, but it is wasteful. System V semaphores involve system calls even for same-process synchronization. POSIX unnamed semaphores or pthread mutexes are much lighter and preferred for intra-process thread synchronization.
Q8. Write the sequence of calls to correctly create and initialize a binary semaphore, handling the race condition.
(1) Call semget(key, 1, IPC_CREAT | IPC_EXCL | 0600). If it succeeds, you created it — immediately call semctl(semid, 0, SETVAL, 1) then do a dummy semop() to set sem_otime. If errno = EEXIST, you are not the creator. (2) Open it: semget(key, 0, 0600). (3) Poll IPC_STAT until sem_otime != 0. (4) The set is now safely initialized for use.
Q9. What is the difference between GETNCNT and GETZCNT?
GETNCNT returns the number of processes currently blocked waiting to decrement the semaphore (waiting for semval >= their required decrement). GETZCNT returns the number of processes blocked waiting for the semaphore to reach exactly 0 (sem_op = 0 operations).
Q10. From kernel 2.6 onwards, what alternative does Linux provide for process synchronization?
POSIX semaphores (Chapter 53 of TLPI). Linux also supports record locking (fcntl, Chapter 55) as an alternative that ties synchronization to file regions.
You have covered semget(), semop(), semctl(), binary semaphores, SEM_UNDO, disadvantages, and all key interview topics for System V Semaphores.
