What is a Semaphore?
A semaphore is a non-negative integer variable that is used to control access to a shared resource. The key idea is simple: before using a shared resource, a process/thread decrements (waits on) the semaphore. When done, it increments (posts to) the semaphore. If the semaphore value is zero, any process trying to decrement it will block until another process increments it.
The name “semaphore” comes from the railway signaling system where a flag (semaphore) controls whether a train can enter a section of track. Only one train is allowed in a section at a time โ just like only one process should access a critical section at a time.
๐ก Real-World Analogy: Parking Lot
Think of a parking lot with 3 spaces. The semaphore value = 3 (number of available spaces). When a car enters, the count goes down (sem_wait). When a car leaves, count goes up (sem_post). If count = 0 (lot full), the next car waits at the gate until someone leaves. A binary semaphore (value 0 or 1) works like a single toilet with a lock.
POSIX Semaphores vs System V Semaphores
| Aspect | POSIX Semaphores | System V Semaphores |
|---|---|---|
| API complexity | Simple, clean | Complex, many flags |
| Header | <semaphore.h> |
<sys/sem.h> |
| Identifier | Name string or memory address | Integer key (IPC key) |
| Create & Init | Atomic (one call) | Two separate steps (race condition possible) |
| Semaphore sets | One semaphore per object | Can group multiple semaphores |
| Thread-safe | Yes (designed for threads too) | Process-oriented |
| Linux implementation | futex(2) system call | semop(2) system call |
| Persistence | Named: kernel persistence; Unnamed: memory lifetime | Kernel persistence (survives reboot only when re-created) |
| Preferred for | New code, threads, simple IPC | Legacy systems, complex semaphore sets |
Two Types of POSIX Semaphores
- Has a filesystem name like
/mysem - Created/opened with
sem_open() - Unrelated processes can share it
- Lives in
/dev/shm/sem.mysem - Kernel persistence (survives process death)
- Must call
sem_unlink()to delete
- No name โ lives in a memory location
- Created with
sem_init() - Shared between threads or related processes
- For processes: must be in shared memory
- Lifetime = lifetime of the memory region
- Must call
sem_destroy()to clean up
The sem_t Data Type
sem_t is an opaque data type defined in <semaphore.h>. You should never directly access or copy its fields. Always use the API functions to manipulate it.
sem_t variable and operate on the copy. The result is undefined behavior. Always use the pointer returned by sem_open() or the address of the sem_t variable passed to sem_init()./* WRONG - do not do this */
sem_t *sp = sem_open("/mysem", O_CREAT, 0660, 1);
sem_t copy = *sp; /* WRONG: copying sem_t */
sem_wait(©); /* UNDEFINED BEHAVIOR */
/* CORRECT */
sem_t *sp = sem_open("/mysem", O_CREAT, 0660, 1);
sem_wait(sp); /* correct: use the pointer directly */
How POSIX Semaphores Work Internally on Linux
On Linux 2.6+ with NPTL (Native POSIX Thread Library), semaphore increment and decrement operations are implemented using the futex(2) system call (Fast Userspace muTEX).
| sem_wait() called | โ | Check semaphore value in userspace (atomic) | โ | Value > 0? Decrement & return immediately (no syscall) |
| Value = 0: call futex() to block in kernel | โ | Value = 0: cannot decrement | ||
The key insight: when there is no contention (semaphore value > 0), the entire sem_wait() operation happens in userspace without entering the kernel. This makes it very fast. The kernel (via futex) is only involved when blocking is actually needed.
Minimal POSIX Semaphore Program
Here is the most basic structure of a program using a POSIX named semaphore:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h> /* O_CREAT, O_EXCL */
#include <sys/stat.h> /* mode constants */
int main(void)
{
sem_t *sem;
/* Open (or create) a named semaphore with initial value 1 */
sem = sem_open("/my_semaphore", O_CREAT, 0660, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}
/* --- Decrement (lock): enter critical section --- */
if (sem_wait(sem) == -1) {
perror("sem_wait");
exit(EXIT_FAILURE);
}
/* --- Critical section: access shared resource here --- */
printf("Inside critical section\n");
/* --- Increment (unlock): leave critical section --- */
if (sem_post(sem) == -1) {
perror("sem_post");
exit(EXIT_FAILURE);
}
/* Close our reference to the semaphore */
if (sem_close(sem) == -1) {
perror("sem_close");
exit(EXIT_FAILURE);
}
/* Remove the semaphore from the system (only once, by one process) */
if (sem_unlink("/my_semaphore") == -1) {
perror("sem_unlink");
exit(EXIT_FAILURE);
}
return 0;
}
gcc program.c -o program -pthread
# or
gcc program.c -o program -lrt
Linux Support Notes
| Linux Version | Support Level |
|---|---|
| Linux 2.4 | Partial: only unnamed thread-shared semaphores |
| Linux 2.6 + NPTL glibc | โ Full POSIX semaphore implementation (named + unnamed) |
| Linux 2.6+ (named) | โ Named semaphores supported since kernel 2.6 |
๐ พ Interview Questions & Answers
Q1. What is the difference between a binary semaphore and a counting semaphore?
A binary semaphore can only take values 0 or 1, behaving like a mutex lock. A counting semaphore can take any non-negative integer value, representing the count of available resources. POSIX semaphores support both: initialize with value 1 for binary behavior, or N for counting N resources.
Q2. What are the two types of POSIX semaphores?
Named semaphores: identified by a string name (e.g., /mysem), can be shared between unrelated processes, created with sem_open(). Unnamed semaphores: live in a memory region, shared between threads or related processes, created with sem_init().
Q3. Can a POSIX semaphore value go below zero?
No. The semaphore value is never permitted to go below 0. If a process tries to decrement a semaphore whose value is already 0, it either blocks (sem_wait) or returns an error (sem_trywait with EAGAIN).
Q4. Why are POSIX semaphores preferred over System V semaphores for new code?
POSIX semaphores have a simpler API, atomic creation + initialization (no race during init), work naturally with threads, and use the faster futex mechanism on Linux. System V semaphores require multiple calls to create and initialize, have complex semaphore sets, and are more suited to legacy code.
Q5. What Linux kernel feature is used to implement POSIX semaphores?
futex(2) โ Fast Userspace muTEX. It allows semaphore operations to complete entirely in userspace when there is no contention (semaphore value > 0). Only when blocking is needed does it make a system call into the kernel.
Q6. What happens if you copy a sem_t variable and use the copy?
The behavior is undefined. sem_t is an opaque type whose internals may include pointers or kernel-side references. Copying it with = does a shallow copy that is meaningless. You must always use the original pointer (for named semaphores) or the original variable’s address (for unnamed semaphores).
Q7. What is SEM_FAILED and when is it returned?
SEM_FAILED is the error return value of sem_open(). It is typically defined as ((sem_t *) 0) on Linux. It is returned when sem_open() fails, for example due to invalid name format, insufficient permissions, or when O_EXCL is specified and the semaphore already exists.
