What is an Unnamed Semaphore?
An unnamed semaphore is simply a sem_t variable — it has no name and is not visible in the filesystem. It is initialized directly in memory using sem_init(). Because it lives in memory, sharing depends entirely on where that memory is located.
Two modes of sharing are possible. In thread-shared mode, the sem_t is a global or heap variable and all threads in the same process can access it. In process-shared mode, the sem_t is placed in shared memory (e.g., POSIX shared memory or a mmap’d file) so multiple processes can access it.
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
/* Returns 0 on success, -1 on error */
| Parameter | Meaning |
|---|---|
sem |
Pointer to the sem_t variable to initialize |
pshared |
0 = thread-shared (within same process). Non-zero = process-shared (must be in shared memory) |
value |
Initial value of the semaphore (1 for binary, N for counting semaphore) |
- Initializing an already-initialized unnamed semaphore causes undefined behavior. Only one process or thread must call sem_init().
- Operations must always be on the original sem_t variable. Never copy it and operate on the copy.
- There are no permission settings on unnamed semaphores (no mode argument). Access is controlled by the permissions on the shared memory region.
- The NPTL (Native POSIX Thread Library on Linux) implementation ignores pshared since no special action is needed. Still, always pass the correct value for portability.
- SUSv3 left the return value on success undefined; SUSv4 clarified it returns 0 on success.
#include <semaphore.h>
int sem_destroy(sem_t *sem);
/* Returns 0 on success, -1 on error */
Destroys the unnamed semaphore pointed to by sem. After destruction, the memory can be reused or the sem_t can be reinitialized with sem_init().
- Before freeing the memory containing the sem_t
- If on stack: before the function returns
- If in shared memory: after all processes stop using it, before shm_unlink()
- Do NOT destroy a semaphore while another thread/process is waiting on it
- Do NOT destroy and then use the semaphore without reinitializing
- Omitting sem_destroy() may cause resource leaks on some implementations
Two threads increment a global counter. An unnamed semaphore (initialized to 1) acts as a binary mutex protecting the critical section. This is the canonical example from TLPI.
/* thread_incr_psem.c
* Two threads race to increment a global variable.
* An unnamed semaphore (binary) protects the critical section.
*
* Compile: gcc -o thread_incr_psem thread_incr_psem.c -lpthread
* Usage: ./thread_incr_psem [num-loops]
* Default: 10000000 loops per thread
*/
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
static int glob = 0;
static sem_t sem; /* Unnamed, thread-shared semaphore */
static void *
threadFunc(void *arg)
{
int loops = *((int *) arg);
int loc, j;
for (j = 0; j < loops; j++) {
/* Acquire semaphore (decrement: 1 -> 0, blocks if already 0) */
if (sem_wait(&sem) == -1) {
perror("sem_wait");
return NULL;
}
/* --- Critical section --- */
loc = glob; /* Read global into local */
loc++; /* Increment local copy */
glob = loc; /* Write back */
/* --- End critical section --- */
/* Release semaphore (increment: 0 -> 1) */
if (sem_post(&sem) == -1) {
perror("sem_post");
return NULL;
}
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1) ? atoi(argv[1]) : 10000000;
/* Initialize unnamed semaphore: pshared=0 (thread-shared), value=1 */
if (sem_init(&sem, 0, 1) == -1) {
perror("sem_init");
exit(EXIT_FAILURE);
}
/* Create two competing threads */
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s != 0) { fprintf(stderr, "pthread_create t1 failed\n"); exit(1); }
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s != 0) { fprintf(stderr, "pthread_create t2 failed\n"); exit(1); }
/* Wait for both threads to finish */
pthread_join(t1, NULL);
pthread_join(t2, NULL);
/* Without the semaphore, glob would be less than loops*2 due to races */
printf("glob = %d (expected: %d)\n", glob, loops * 2);
sem_destroy(&sem);
exit(EXIT_SUCCESS);
}
The pattern loc = glob; loc++; glob = loc; (instead of just glob++) intentionally exposes the race condition for demonstration. Without the semaphore, thread A could read glob=5, get preempted, thread B reads glob=5 too, both write back 6 instead of 7. The semaphore prevents this interleaving.
For process-sharing, the sem_t must live in a shared memory region that both processes can map. Here is a self-contained example using a parent-child fork pattern.
/* process_shared_sem.c
* Parent and child share a semaphore placed in anonymous shared memory.
* The child waits for the parent to post before continuing.
*
* Compile: gcc -o proc_sem process_shared_sem.c -lpthread
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/wait.h>
int main(void)
{
sem_t *sem;
pid_t pid;
/*
* Create anonymous shared memory region using mmap.
* MAP_SHARED | MAP_ANONYMOUS: shared between parent and child,
* not backed by a file.
*/
sem = mmap(NULL, sizeof(sem_t),
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (sem == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
/*
* Initialize semaphore in shared memory.
* pshared = 1 means process-shared.
* Initial value = 0: child will block until parent posts.
*/
if (sem_init(sem, 1, 0) == -1) {
perror("sem_init");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* Child process: wait for parent to signal */
printf("Child (PID %d): waiting for parent...\n", getpid());
if (sem_wait(sem) == -1) {
perror("sem_wait");
exit(EXIT_FAILURE);
}
printf("Child (PID %d): got signal, proceeding!\n", getpid());
exit(EXIT_SUCCESS);
} else {
/* Parent process: do some work, then signal child */
printf("Parent (PID %d): doing work for 2 seconds...\n", getpid());
sleep(2);
printf("Parent (PID %d): posting semaphore\n", getpid());
if (sem_post(sem) == -1) {
perror("sem_post");
exit(EXIT_FAILURE);
}
wait(NULL); /* Wait for child to finish */
}
/* Cleanup: destroy before unmapping */
sem_destroy(sem);
munmap(sem, sizeof(sem_t));
printf("Parent: done.\n");
return 0;
}
Classic producer-consumer with a bounded buffer of size 1. Two semaphores signal “empty slot available” and “item available”.
/* prod_cons_sem.c
* Single producer, single consumer sharing a buffer of size 1.
* Two semaphores:
* empty: starts at 1 (buffer empty, producer can write)
* full: starts at 0 (no item yet, consumer must wait)
*
* Compile: gcc -o prod_cons prod_cons_sem.c -lpthread
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define NUM_ITEMS 5
static sem_t empty; /* 1 = buffer has space */
static sem_t full; /* 1 = buffer has item */
static int buffer; /* Shared buffer (size 1) */
static void *producer(void *arg) {
int i;
for (i = 1; i <= NUM_ITEMS; i++) {
sem_wait(&empty); /* Wait for empty slot */
buffer = i;
printf("Produced: %d\n", buffer);
sem_post(&full); /* Signal item is ready */
sleep(1);
}
return NULL;
}
static void *consumer(void *arg) {
int i, item;
for (i = 0; i < NUM_ITEMS; i++) {
sem_wait(&full); /* Wait for an item */
item = buffer;
printf("Consumed: %d\n", item);
sem_post(&empty); /* Signal buffer is free again */
}
return NULL;
}
int main(void) {
pthread_t prod_tid, cons_tid;
/* empty=1: producer can start immediately */
/* full=0: consumer must wait for first item */
sem_init(&empty, 0, 1);
sem_init(&full, 0, 0);
pthread_create(&prod_tid, NULL, producer, NULL);
pthread_create(&cons_tid, NULL, consumer, NULL);
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}
pshared = 0
Place sem_t anywhere: global variable, struct member, heap allocation
sem_init(&sem, 0, value);
pshared != 0
Place sem_t in shared memory: mmap(MAP_SHARED), POSIX shm, or System V shm
sem_init(shm_ptr, 1, value);
If pshared is 0, the semaphore is shared only among threads of the same process. If pshared is non-zero, the semaphore can be shared between processes, but it must be placed in a shared memory region (e.g., from mmap with MAP_SHARED, or a POSIX shared memory object).
This results in undefined behavior per SUSv3/SUSv4. Your application must be designed so that sem_init() is called exactly once per semaphore, typically at initialization time. To reinitialize, call sem_destroy() first, then sem_init() again.
Create a shared memory region using mmap() with MAP_SHARED | MAP_ANONYMOUS before calling fork(). Place the sem_t variable at the beginning of this region and initialize it with sem_init(…, 1, …). After fork(), both parent and child have the same mapping and share the same semaphore.
NPTL (Native POSIX Thread Library — the Linux thread implementation) ignores pshared because its sem_t implementation works the same way regardless. The semaphore is accessible by anyone who has the address of the sem_t. Portability still requires setting the correct value, since other platforms may enforce the distinction.
Before the memory holding the sem_t is released. For a local variable: before the function returns. For heap memory: before free(). For shared memory: after all processes have stopped using it and before shm_unlink(). While some implementations won’t crash if you skip it, others leak resources — so always call it for portable code.
No. Both SUSv3 and SUSv4 state that the result of using a copy of a sem_t is undefined. All operations must be performed on the original variable whose address was passed to sem_init(). This rule also applies to named semaphores — never copy the sem_t pointer returned by sem_open.
Next: Semaphore Comparisons
Learn how POSIX semaphores compare to System V semaphores and Pthreads mutexes.
