What is a Binary Semaphore?
A binary semaphore is the simplest form of a semaphore. It can hold only two values: 1 (free / available) and 0 (reserved / in use). Think of it like a toilet key at a petrol station โ there is exactly one key. If it is hanging on the hook (value = 1), you can take it (reserve it). If someone else has it (value = 0), you must wait until they return it.
In Linux/UNIX programming, binary semaphores are used to protect shared resources so that only one process or thread can use the resource at a time. This is called mutual exclusion (mutex).
Every semaphore โ whether binary or counting โ supports these fundamental operations:
| Operation | Dijkstra Name | POSIX Name | What It Does |
|---|---|---|---|
| Reserve | P (Dutch: proberen) | wait / down | Decrement semaphore by 1. If already 0, block until it becomes 1. |
| Release | V (Dutch: verhogen) | post / up | Increment semaphore by 1. Wakes up any process waiting on it. |
| Conditional Reserve | โ | trywait | Non-blocking attempt. Returns error immediately if semaphore is 0. |
The P and V naming comes from the Dutch computer scientist Edsger Dijkstra who invented semaphores. “P” stands for proberen (to test/try) and “V” for verhogen (to increment).
In a binary semaphore implemented with System V semaphores:
The convention is: value 1 = free, value 0 = reserved. This is the opposite of what beginners might expect โ remember it this way: “1 is good to go, 0 means stop.”
The header file declares the API and exposes two global control variables:
- bsUseSemUndo โ if TRUE, adds SEM_UNDO flag so the kernel automatically undoes the semaphore operation if the process dies unexpectedly
- bsRetryOnEintr โ if TRUE, automatically retries a semop() call that was interrupted by a signal
/* binary_sems.h โ Header for binary semaphore implementation */
#ifndef BINARY_SEMS_H
#define BINARY_SEMS_H
#include "tlpi_hdr.h" /* Boolean type, error helpers */
/* Controls whether SEM_UNDO is used in semop() calls.
SEM_UNDO = kernel undoes semaphore on process exit.
Default: FALSE */
extern Boolean bsUseSemUndo;
/* Controls whether semop() is retried on EINTR (signal interrupt).
Default: TRUE */
extern Boolean bsRetryOnEintr;
/* Initialize semaphore to 1 โ "available" state */
int initSemAvailable(int semId, int semNum);
/* Initialize semaphore to 0 โ "in use" state */
int initSemInUse(int semId, int semNum);
/* Decrement (lock) the semaphore โ blocks if already 0 */
int reserveSem(int semId, int semNum);
/* Increment (unlock) the semaphore */
int releaseSem(int semId, int semNum);
#endif /* BINARY_SEMS_H */
Notice that the header does NOT include functions for creating or deleting semaphore sets. It assumes the semaphore set already exists โ creation/deletion is the caller’s responsibility.
Below is the complete implementation with detailed explanations for each function:
4.1 Global Variables and Includes
#include <sys/types.h>
#include <sys/sem.h>
#include "semun.h" /* Definition of union semun */
#include "binary_sems.h"
/* Default: do NOT use SEM_UNDO */
Boolean bsUseSemUndo = FALSE;
/* Default: DO retry if interrupted by a signal */
Boolean bsRetryOnEintr = TRUE;
4.2 initSemAvailable() โ Set to 1 (Free)
/* Initialize semaphore to 1 = "available" = can be reserved.
semId : ID of the semaphore set
semNum : Index of semaphore within the set (0-based)
Returns 0 on success, -1 on error */
int initSemAvailable(int semId, int semNum)
{
union semun arg;
arg.val = 1; /* Set value to 1 = free */
return semctl(semId, semNum, SETVAL, arg);
}
4.3 initSemInUse() โ Set to 0 (In Use)
/* Initialize semaphore to 0 = "in use" = must wait before using.
Use this when a resource starts in a locked/busy state. */
int initSemInUse(int semId, int semNum)
{
union semun arg;
arg.val = 0; /* Set value to 0 = in use */
return semctl(semId, semNum, SETVAL, arg);
}
4.4 reserveSem() โ Lock / P / wait (Decrement)
/* Reserve the semaphore โ decrements it by 1.
If the semaphore is 0, this call BLOCKS until another process
releases it (increments it back to 1).
Returns 0 on success, -1 with errno=EINTR if interrupted
and bsRetryOnEintr is FALSE. */
int reserveSem(int semId, int semNum)
{
struct sembuf sops;
sops.sem_num = semNum; /* Which semaphore in the set */
sops.sem_op = -1; /* Decrement by 1 (the P / wait operation) */
sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;
/*
* SEM_UNDO: If this process crashes or exits without calling
* releaseSem(), the kernel automatically undoes this operation.
* This prevents deadlocks caused by crashed processes.
*/
/* Keep retrying if interrupted by a signal (unless told not to) */
while (semop(semId, &sops, 1) == -1) {
if (errno != EINTR || !bsRetryOnEintr)
return -1;
/* errno == EINTR and bsRetryOnEintr is TRUE: loop and retry */
}
return 0; /* Semaphore successfully decremented (reserved) */
}
4.5 releaseSem() โ Unlock / V / post (Increment)
/* Release the semaphore โ increments it by 1.
This wakes up any process that is blocked in reserveSem().
Returns 0 on success, -1 on error. */
int releaseSem(int semId, int semNum)
{
struct sembuf sops;
sops.sem_num = semNum;
sops.sem_op = 1; /* Increment by 1 (the V / post operation) */
sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;
return semop(semId, &sops, 1);
/*
* No retry loop here โ release cannot block (incrementing
* a semaphore never blocks), so EINTR just means "return error".
*/
}
This example creates a semaphore set, forks a child process, and uses binary semaphores to protect a shared memory counter so both parent and child can update it safely without race conditions.
/* bsem_counter.c โ Protect a shared counter with a binary semaphore
Compile: gcc bsem_counter.c binary_sems.c -o bsem_counter
Run: ./bsem_counter
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include "binary_sems.h"
#define SEM_KEY 0xABCD1234
#define SHM_KEY 0xABCD5678
#define LOOPS 10000
int main(void)
{
int semId, shmId;
long *counter;
/* ---- Create semaphore set with 1 semaphore ---- */
semId = semget(SEM_KEY, 1, IPC_CREAT | IPC_EXCL | 0600);
if (semId == -1) { perror("semget"); exit(EXIT_FAILURE); }
/* Initialize the single semaphore to 1 (available) */
if (initSemAvailable(semId, 0) == -1) {
perror("initSemAvailable"); exit(EXIT_FAILURE);
}
/* ---- Create shared memory for the counter ---- */
shmId = shmget(SHM_KEY, sizeof(long), IPC_CREAT | IPC_EXCL | 0600);
if (shmId == -1) { perror("shmget"); exit(EXIT_FAILURE); }
counter = (long *) shmat(shmId, NULL, 0);
if (counter == (void *) -1) { perror("shmat"); exit(EXIT_FAILURE); }
*counter = 0;
/* ---- Fork child ---- */
switch (fork()) {
case -1:
perror("fork"); exit(EXIT_FAILURE);
case 0: /* Child process */
for (int i = 0; i < LOOPS; i++) {
reserveSem(semId, 0); /* Lock */
(*counter)++; /* Critical section */
releaseSem(semId, 0); /* Unlock */
}
exit(EXIT_SUCCESS);
default: /* Parent process */
for (int i = 0; i < LOOPS; i++) {
reserveSem(semId, 0); /* Lock */
(*counter)++; /* Critical section */
releaseSem(semId, 0); /* Unlock */
}
wait(NULL); /* Wait for child */
break;
}
printf("Final counter = %ld (expected %d)\n", *counter, 2 * LOOPS);
/* Without semaphore protection, this would be less than 20000
due to race conditions. With protection, it is always 20000. */
/* ---- Cleanup ---- */
semctl(semId, 0, IPC_RMID);
shmdt(counter);
shmctl(shmId, 0, IPC_RMID);
return 0;
}
Final counter = 20000 (expected 20000)
Without the semaphore, the counter would often be less than 20000 because both processes read-modify-write the shared memory simultaneously (race condition).
The SEM_UNDO flag is one of the most important safety features in System V semaphores. Here is what it does and when to use it:
| Scenario | Without SEM_UNDO | With SEM_UNDO |
|---|---|---|
| Process reserves semaphore then crashes | Semaphore stays at 0 forever โ deadlock! | Kernel automatically restores semaphore to 1 โ safe |
| Process reserves semaphore, finishes normally | Normal release by releaseSem() | Normal release by releaseSem() (undo adjustment cancels) |
/* Demonstrating SEM_UNDO behavior */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include "binary_sems.h"
int main(void)
{
int semId;
semId = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
if (semId == -1) { perror("semget"); exit(1); }
initSemAvailable(semId, 0);
/* Enable SEM_UNDO so kernel cleans up on exit */
bsUseSemUndo = TRUE;
/* Reserve the semaphore */
if (reserveSem(semId, 0) == -1) { perror("reserveSem"); exit(1); }
printf("Semaphore reserved (value=0). Exiting WITHOUT releasing...\n");
printf("If SEM_UNDO works, the semaphore will be restored to 1\n");
printf("by the kernel when this process exits.\n");
/* We exit WITHOUT calling releaseSem().
With SEM_UNDO, the kernel will increment the semaphore by 1
(reversing our -1 operation), restoring it to 1 (free). */
/* Other processes waiting on this semaphore will be woken up */
semctl(semId, 0, IPC_RMID); /* cleanup */
return 0;
}
Rule of thumb: Always set bsUseSemUndo = TRUE in production code unless you have a specific reason not to. It prevents semaphore leaks if processes crash.
When a process is blocked in semop() waiting for a semaphore, and a signal arrives, the kernel wakes up the process and semop() returns -1 with errno = EINTR. The implementation handles this with a retry loop:
/* The retry loop in reserveSem() explained: */
while (semop(semId, &sops, 1) == -1) {
if (errno != EINTR || !bsRetryOnEintr)
return -1; /* Real error OR caller wants no retry */
/*
* errno == EINTR means we were interrupted by a signal.
* bsRetryOnEintr == TRUE means "just try again".
* So we loop back and call semop() again.
*/
}
The table below clarifies the two-variable decision:
| errno | bsRetryOnEintr | Action |
|---|---|---|
| EINTR | TRUE | Retry semop() automatically |
| EINTR | FALSE | Return -1 (caller handles) |
| Other error (e.g. EINVAL) | Any | Return -1 immediately |
/* Example: Using bsRetryOnEintr = FALSE for signal-aware code */
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include "binary_sems.h"
volatile sig_atomic_t got_signal = 0;
void sig_handler(int sig) {
got_signal = 1;
}
int main(void)
{
int semId;
struct sigaction sa;
/* Set up SIGINT handler */
sa.sa_handler = sig_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* No SA_RESTART โ let semop() be interrupted */
sigaction(SIGINT, &sa, NULL);
semId = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
initSemInUse(semId, 0); /* Start locked so we will block */
/* Tell the library: return EINTR to us, don't retry */
bsRetryOnEintr = FALSE;
printf("Waiting for semaphore... Press Ctrl+C to interrupt\n");
if (reserveSem(semId, 0) == -1) {
if (errno == EINTR && got_signal) {
printf("Interrupted by signal โ handling gracefully\n");
} else {
perror("reserveSem");
}
}
semctl(semId, 0, IPC_RMID);
return 0;
}
Here is the standard pattern used everywhere in real embedded / Linux system code:
/*
* STANDARD BINARY SEMAPHORE USAGE PATTERN
*
* 1. Create and initialize semaphore (once, at startup)
* 2. Before critical section: reserveSem() [blocks if busy]
* 3. Do critical section work
* 4. After critical section: releaseSem() [wakes next waiter]
*/
/* ---- Startup (typically in server/init process) ---- */
int semId = semget(MY_KEY, 1, IPC_CREAT | 0600);
initSemAvailable(semId, 0); /* value = 1, ready to use */
bsUseSemUndo = TRUE; /* Safety: kernel cleans up on crash */
/* ---- In every process/thread that uses shared resource ---- */
/* Acquire the lock */
if (reserveSem(semId, 0) == -1) {
perror("reserveSem");
/* Handle error โ do NOT proceed to critical section */
exit(EXIT_FAILURE);
}
/* === CRITICAL SECTION START === */
/* Only one process is here at a time */
read_from_shared_memory();
write_to_shared_memory();
update_shared_file();
/* === CRITICAL SECTION END === */
/* Release the lock */
if (releaseSem(semId, 0) == -1) {
perror("releaseSem");
/* Log the error; resource may now be permanently locked */
}
releaseSem() on every error path inside the critical section. Always use a cleanup label or wrapper to guarantee the semaphore is always released:/* Safe pattern: always release even on error */
if (reserveSem(semId, 0) == -1) { perror("reserve"); return -1; }
int rc = do_critical_work(); /* may fail */
releaseSem(semId, 0); /* always runs, regardless of rc */
return rc;
A binary semaphore is a semaphore with only two states: 1 (free) and 0 (in use). A mutex (mutual exclusion lock) is conceptually similar but with a key difference: a mutex has ownership โ only the thread that locked it can unlock it. A binary semaphore has no ownership โ one process can reserve it and a different process can release it. System V binary semaphores are closer in nature to counting semaphores restricted to [0,1] values.
P (proberen = to test) decrements the semaphore โ equivalent to “wait” or “lock”. V (verhogen = to increment) increments the semaphore โ equivalent to “post” or “unlock”. They were invented by Dutch computer scientist Edsger Dijkstra, who also invented shortest-path algorithms (Dijkstra’s algorithm). In POSIX, these are called sem_wait() and sem_post().
SEM_UNDO is a flag for semop() that tells the kernel to record the operation in the process’s semaphore adjustment list. When the process exits (for any reason โ normal exit, crash, signal), the kernel reverses all pending semaphore adjustments. Without SEM_UNDO, if a process holds a semaphore and crashes, the semaphore stays locked forever โ causing a deadlock for all other processes waiting on it.
reserveSem() decrements (P operation) and can block if the semaphore is 0. While blocked, a signal can arrive, interrupting the semop() call with errno = EINTR. The while loop retries the call in that case. releaseSem() increments (V operation) and never blocks โ incrementing a semaphore always succeeds immediately, so there is nothing to retry.
Because the reserve operation subtracts 1 from the current value. If the semaphore starts at 1 (free), the first process to call reserveSem() subtracts 1, bringing it to 0. Any subsequent process trying to reserve will find 0 and block (because decrementing would go negative, which semop() prevents). When the first process calls releaseSem(), it adds 1 back to 0, making it 1 again and waking up any waiters. Starting at 0 would mean the semaphore is immediately locked with nobody holding it.
IPC_PRIVATE (value 0) creates a private semaphore set guaranteed to have a unique ID. It is used when the parent and child processes share the semaphore via fork() โ the ID is passed down through the process relationship. A user-defined key (like 0xABCD1234) allows unrelated processes to access the same semaphore by agreeing on the key value in advance (like a shared password).
/* Non-blocking reserveSem using IPC_NOWAIT */
int tryReserveSem(int semId, int semNum)
{
struct sembuf sops;
sops.sem_num = semNum;
sops.sem_op = -1;
sops.sem_flg = IPC_NOWAIT; /* Return immediately if semaphore is 0 */
if (semop(semId, &sops, 1) == -1) {
if (errno == EAGAIN) {
/* Semaphore is 0 โ currently reserved by someone else */
return -1;
}
perror("semop tryReserve");
return -1;
}
return 0; /* Successfully reserved */
}
The kernel guarantees atomicity of semop(). Only one of the two processes will successfully decrement the semaphore from 1 to 0. The other will find the semaphore already at 0 and will block until the first process calls releaseSem(). This is the fundamental guarantee that makes semaphores useful for mutual exclusion โ no race condition can occur in the decrement itself.
| Function | sem_op | Blocks? | Purpose |
|---|---|---|---|
initSemAvailable() |
SETVAL to 1 | No | Set semaphore to 1 (unlocked) |
initSemInUse() |
SETVAL to 0 | No | Set semaphore to 0 (locked) |
reserveSem() |
-1 | Yes (if value = 0) | Lock / acquire (P operation) |
releaseSem() |
+1 | Never | Unlock / release (V operation) |
