System V semaphores — generic advisory locking for controlling access to any shared resource.
File locking works great for coordinating access to files. But what if you need to protect something that isn't a file? Maybe you want to limit the number of processes accessing a shared memory segment, or ensure only one process at a time runs a critical section of code.
A semaphore is a generalized counter that processes can increment or decrement atomically. The three fundamental operations:
Click P (wait) to decrement and V (signal) to increment. Watch how the counter controls access.
With System V, you don't create individual semaphores — you create sets of semaphores. Even if you only need one, you still create a set of size 1.
c #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
| Argument | Purpose |
|---|---|
key | Unique key from ftok() — same as message queues |
nsems | Number of semaphores in the set |
semflg | Permissions OR'd with IPC_CREAT |
c key_t key; int semid; key = ftok("/home/beej/somefile", 'E'); semid = semget(key, 10, 0666 | IPC_CREAT);
This creates a set of 10 semaphores with read/write permissions. You get back a semid (semaphore set ID) that you'll use in all subsequent operations.
To initialize, read, or destroy semaphores, you use semctl():
c int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
The optional fourth argument is a union semun that you must define yourself:
c union semun { int val; /* for SETVAL */ struct semid_ds *buf; /* for IPC_STAT, IPC_SET */ unsigned short *array; /* for GETALL, SETALL */ };
| Command | Effect |
|---|---|
SETVAL | Set the value of one semaphore to arg.val |
GETVAL | Return the current value of one semaphore |
SETALL | Set all semaphores in the set from arg.array |
GETALL | Get all values into arg.array |
IPC_RMID | Remove the entire semaphore set |
IPC_STAT | Get status info into arg.buf |
The most common use: initializing a semaphore to 1 (creating a mutex):
c union semun arg; arg.val = 1; /* initial value */ semctl(semid, 0, SETVAL, arg); /* set semaphore 0 to 1 */
All the actual locking and unlocking happens through semop(). You fill out a struct sembuf that describes the operation:
c struct sembuf { unsigned short sem_num; /* semaphore number in the set */ short sem_op; /* operation (-, +, or 0) */ short sem_flg; /* flags: IPC_NOWAIT, SEM_UNDO */ };
The sem_op field is where the magic happens:
| sem_op | Behavior |
|---|---|
| Negative (e.g. -1) | Allocate / Lock: Block until sem value ≥ |sem_op|, then subtract. This is the P (wait) operation. |
| Positive (e.g. +1) | Release / Unlock: Add to the sem value immediately. This is the V (signal) operation. May wake blocked processes. |
| Zero | Wait for zero: Block until the sem value reaches exactly 0. |
c struct sembuf sb; sb.sem_num = 0; /* operate on semaphore 0 */ sb.sem_flg = SEM_UNDO; /* Lock (P operation): decrement by 1 */ sb.sem_op = -1; semop(semid, &sb, 1); /* ... critical section ... */ /* Unlock (V operation): increment by 1 */ sb.sem_op = 1; semop(semid, &sb, 1);
Here's the big gotcha with System V semaphores: creation and initialization are two separate steps. This creates a race condition.
If Process A creates the semaphore but hasn't initialized it yet, and Process B comes along and tries to use it — B is operating on garbage data. This is what W. Richard Stevens calls the semaphore's "fatal flaw."
An alternative approach: have a single dedicated "init" process create and initialize all semaphores before any worker processes start. This is simpler in practice.
What happens if a process locks a semaphore (decrements it) and then crashes before unlocking? The semaphore stays locked forever. Every other process waiting on it is stuck.
The SEM_UNDO flag solves this. When you pass SEM_UNDO in the sem_flg field of your struct sembuf, the kernel tracks the operation. If the process exits (for any reason, including SIGKILL), the kernel automatically reverses the operation.
c struct sembuf sb; sb.sem_num = 0; sb.sem_op = -1; /* lock */ sb.sem_flg = SEM_UNDO; /* auto-undo on exit */ semop(semid, &sb, 1);
Here's Beej's semaphore demo with Stevens' race-condition fix built in. It uses a semaphore to simulate file locking — press Enter to acquire, press Enter again to release:
c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #define MAX_RETRIES 10 union semun { int val; struct semid_ds *buf; unsigned short *array; }; /* Stevens-style init: handles the race condition */ int initsem(key_t key, int nsems) { int i; union semun arg; struct semid_ds buf; struct sembuf sb; int semid; semid = semget(key, nsems, IPC_CREAT|IPC_EXCL|0666); if (semid >= 0) { /* We created it first — initialize */ sb.sem_op = 1; sb.sem_flg = 0; for(sb.sem_num = 0; sb.sem_num < nsems; sb.sem_num++) semop(semid, &sb, 1); } else if (errno == EEXIST) { /* Someone else created it — wait for init */ semid = semget(key, nsems, 0); arg.buf = &buf; for(i = 0; i < MAX_RETRIES; i++) { semctl(semid, nsems-1, IPC_STAT, arg); if (arg.buf->sem_otime != 0) break; sleep(1); } } return semid; } int main(void) { key_t key; int semid; struct sembuf sb; sb.sem_num = 0; sb.sem_op = -1; sb.sem_flg = SEM_UNDO; key = ftok("semdemo.c", 'J'); semid = initsem(key, 1); printf("Press return to lock: "); getchar(); printf("Trying to lock...\n"); semop(semid, &sb, 1); /* P: lock */ printf("Locked.\n"); printf("Press return to unlock: "); getchar(); sb.sem_op = 1; /* V: unlock */ semop(semid, &sb, 1); printf("Unlocked.\n"); return 0; }
semctl(semid, 0, IPC_RMID), or use the ipcrm command from the shell.Like message queues, semaphore sets persist in the kernel until explicitly destroyed. Two methods:
ipcs -s lists semaphore sets. ipcrm -s <semid> removes one.semctl(semid, 0, IPC_RMID) destroys the set.semrm.c #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int main(void) { key_t key = ftok("semdemo.c", 'J'); int semid = semget(key, 1, 0); semctl(semid, 0, IPC_RMID); return 0; }
ipcs regularly to check for leaked IPC objects.Watch processes compete for a semaphore-protected resource. Each process must decrement (P) the semaphore before entering the critical section, and increment (V) when done.
Click "Request" to have a process try to enter the critical section. The semaphore only allows one at a time.
Semaphores are the go-to synchronization primitive when file locks aren't enough. They're often faster than file locks and work on non-file resources like shared memory.
| Feature | File Locks | System V Semaphores | POSIX Semaphores |
|---|---|---|---|
| Resource type | Files only | Anything | Anything |
| Read/Write distinction | Yes | No (just counters) | No |
| Persistence | Released on close | Kernel-persistent | Named: persistent |
| Crash recovery | Auto-release | SEM_UNDO flag | Manual |
| API complexity | Low | High | Medium |
"Whenever you have multiple processes running through a critical section, you need semaphores." — Beej