A semaphore is a kernel-maintained integer whose value is always greater than or equal to 0. It is used to synchronize the actions of multiple processes, not to transfer data between them.
Unlike pipes or message queues that move data from one process to another, semaphores are purely about coordination — they let processes agree on when it is safe to access a shared resource.
The most common use case is controlling access to a block of shared memory: before any process reads or writes the shared memory, it first checks (and adjusts) the semaphore to make sure no other process is currently using it.
The kernel lets you do four things with a semaphore value:
| Operation | Description | Blocks? |
|---|---|---|
| Set absolute value | Force the semaphore to a specific integer (e.g., set to 1) | No |
| Add a number | Increase the semaphore value (signal / release) | No |
| Subtract a number | Decrease the semaphore value (wait / acquire). Blocks if result would go below 0. | Yes |
| Wait for zero | Block until the semaphore value becomes 0 | Yes |
Key Rule: The kernel will never allow a semaphore to go below 0. If a subtraction would make it negative, the calling process is put to sleep until another process adds enough to make the subtraction possible.
The diagram below shows the classic producer-consumer synchronization using a semaphore initialized to 0:
value
Unlike POSIX semaphores where you create one semaphore at a time, System V semaphores are always created as a group called a semaphore set. Each set can contain one or more individual semaphores, all sharing the same semaphore ID.
This design is unusually complex compared to most OSes because:
- Creation (
semget) and initialization (semctl) are two separate steps - This separation can cause race conditions if two processes try to initialize at the same time
- The
semop()call can atomically operate on multiple semaphores in one set at once
Returns a semaphore ID (semid) for the set
Only ONE process should do this to avoid race conditions
Add, subtract, or wait-for-zero on semaphore values
Only ONE process should do this
| Feature | System V Semaphore | Mutex (pthread) |
|---|---|---|
| Scope | Between processes | Between threads in one process |
| Value range | 0 to SEMVMX (usually 32767) | Locked (0) or Unlocked (1) |
| Ownership | Any process can release | Only the locker can unlock |
| Persistence | Persists until explicitly deleted or reboot | Destroyed when process exits |
| Works in sets | Yes (semaphore sets) | No |
| Initialization | Separate step (race condition risk) | At creation time |
This program shows the basic usage of all three semaphore system calls. Run it in two modes:
- One argument: create a new semaphore set and initialize it to the given value
- Two arguments: perform an add/subtract operation on an existing semaphore
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
/* semun union - needed by semctl() */
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
};
/* Simple timestamp helper */
void print_time() {
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
char buf[20];
strftime(buf, sizeof(buf), "%H:%M:%S", tm_info);
printf("%s", buf);
}
int main(int argc, char *argv[])
{
int semid;
if (argc < 2 || argc > 3) {
fprintf(stderr, "Usage:\n");
fprintf(stderr, " %s <init-value> # create semaphore\n", argv[0]);
fprintf(stderr, " %s <semid> <operation> # operate on semaphore\n", argv[0]);
return 1;
}
if (argc == 2) {
/* ---- CREATE and INITIALIZE a new semaphore set ---- */
union semun arg;
/* semget: IPC_PRIVATE = unique key, 1 semaphore in set */
semid = semget(IPC_PRIVATE, 1, S_IRUSR | S_IWUSR);
if (semid == -1) {
perror("semget");
exit(1);
}
/* semctl SETVAL: set semaphore 0 to the given initial value */
arg.val = atoi(argv[1]);
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(1);
}
printf("Semaphore ID = %d\n", semid);
} else {
/* ---- OPERATE on an existing semaphore ---- */
struct sembuf sop;
semid = atoi(argv[1]); /* semaphore set ID */
sop.sem_num = 0; /* which semaphore in the set */
sop.sem_op = atoi(argv[2]); /* positive=add, negative=subtract, 0=wait-for-zero */
sop.sem_flg = 0; /* no special flags */
printf("%ld: about to semop at ", (long)getpid());
print_time();
printf("\n");
/* This call may block if sem_op would make value go below 0 */
if (semop(semid, &sop, 1) == -1) {
perror("semop");
exit(1);
}
printf("%ld: semop completed at ", (long)getpid());
print_time();
printf("\n");
}
return 0;
}
Shell session showing blocking behaviour:
# Step 1: Create semaphore initialized to 0
$ ./svsem_demo 0
Semaphore ID = 98307
# Step 2: Try to subtract 2 (will BLOCK because 0 - 2 would go below 0)
$ ./svsem_demo 98307 -2 &
23338: about to semop at 10:19:42
[1] 23338
# Step 3: Add 3 — this wakes up the background process
$ ./svsem_demo 98307 +3
23339: about to semop at 10:19:55
23339: semop completed at 10:19:55
23338: semop completed at 10:19:55 # background job unblocked!
[1]+ Done ./svsem_demo 98307 -2
After adding 3, the semaphore value = 3. The background subtract-2 can now proceed (3 – 2 = 1 ≥ 0), so the kernel wakes it up.
Initialize to 1. Process does subtract-1 before entering critical section, add-1 when done. Only one process can be inside at a time.
Initialize to N (number of resources). Each consumer subtracts 1. When all N are used, new requests block. Producers add 1 when returning resources.
Initialize to 0. Process B waits (subtract-1 blocks). Process A does its work, then adds 1, which wakes Process B. Replaces signals for parent-child sync after fork().
Pair with a shared memory segment. Semaphore protects the memory from simultaneous access by multiple processes.
#include <sys/types.h> /* Required for portability (older systems) */
#include <sys/ipc.h> /* Defines IPC_PRIVATE, IPC_CREAT, IPC_EXCL, ftok() */
#include <sys/sem.h> /* Declares semget(), semctl(), semop() */
/* Also defines: struct sembuf, struct semid_ds */
/* Also defines: SETVAL, GETVAL, IPC_RMID, etc. */
/* The semun union - must be declared by the programmer on Linux */
union semun {
int val; /* used with SETVAL */
struct semid_ds *buf; /* used with IPC_STAT / IPC_SET */
unsigned short *array; /* used with GETALL / SETALL */
struct seminfo *__buf; /* used with IPC_INFO (Linux-specific) */
};
semun union is not defined in any system header. You must declare it yourself in your code. POSIX specifies its members, but Linux requires you to write it.semop() system call can operate on several semaphores in a set in a single atomic step, which avoids deadlocks that would occur if the operations were done individually.semget) and initialization (semctl SETVAL/SETALL) are two separate steps. If two processes execute both steps concurrently, one might start using a semaphore that the other is still initializing. The solution is to designate exactly one process as the initializer (usually using IPC_CREAT | IPC_EXCL in semget so only one process creates it), and the initializer must complete semctl before other processes begin operations.semun union be declared by the programmer on Linux?semctl() is a union semun that the caller must supply. However, the POSIX standard does not mandate that this union be defined in the system headers — it leaves that to the implementation. Linux system headers intentionally omit it (per the standard), so the programmer must declare it manually in their source file before using semctl().