Beej's Guide to Unix IPC — Chapter 8

Semaphores

System V semaphores — generic advisory locking for controlling access to any shared resource.

Prerequisites: File Locking (Ch. 6) + ftok() keys (Ch. 7). That's it.
10
Chapters
3+
Simulations
0
Assumed Knowledge

Chapter 0: Why Semaphores?

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:

Set
Initialize the semaphore to a value (e.g. 1 for a mutex)
Wait (P / decrement)
Decrement the value. If it would go below 0, block until it can succeed.
Signal (V / increment)
Increment the value. This may wake up a blocked process.
The mental model: Imagine a parking garage with a counter sign showing "3 spots available." Each car entering decrements the counter (wait/P). Each car leaving increments it (signal/V). When the counter reaches 0, incoming cars must wait. A semaphore with an initial value of 1 is a mutex — only one "car" at a time.
Semaphore Counter

Click P (wait) to decrement and V (signal) to increment. Watch how the counter controls access.

Semaphore: 1
What happens when a process tries to decrement (P) a semaphore that is already at 0?

Chapter 1: semget() — Grabbing Semaphores

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);
ArgumentPurpose
keyUnique key from ftok() — same as message queues
nsemsNumber of semaphores in the set
semflgPermissions 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.

Warning: When you first create semaphores, they are uninitialized. They're not automatically set to 0 or 1. You must initialize them yourself with semctl() or semop(). This is the root of the "fatal flaw" race condition we'll discuss later.
Are System V semaphores created individually or in sets?

Chapter 2: semctl() — Controlling Semaphores

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 */
};
CommandEffect
SETVALSet the value of one semaphore to arg.val
GETVALReturn the current value of one semaphore
SETALLSet all semaphores in the set from arg.array
GETALLGet all values into arg.array
IPC_RMIDRemove the entire semaphore set
IPC_STATGet 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 */
How do you initialize a semaphore's value using semctl()?

Chapter 3: semop() — Atomic Power

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_opBehavior
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.
ZeroWait 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);
Key insight: The semop() call is atomic. This means the check-and-modify happens as a single indivisible step. No race condition between checking the value and changing it — the kernel guarantees it. You can even batch multiple operations on different semaphores in one atomic call by passing an array of struct sembufs.
What does a sem_op of -1 do?

Chapter 4: The Fatal Flaw — Race Condition

Here's the big gotcha with System V semaphores: creation and initialization are two separate steps. This creates a race condition.

Step 1
semget(key, 1, IPC_CREAT) — creates the semaphore (uninitialized)
↓ another process might see it here, before initialization!
Step 2
semctl(semid, 0, SETVAL, 1) — initializes to 1

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."

Stevens' solution:
  1. Create with IPC_CREAT | IPC_EXCL. If you got it first, initialize it.
  2. If errno == EEXIST, another process created it first. Connect to it (without IPC_CREAT).
  3. Poll semctl() with IPC_STAT until sem_otime is nonzero (meaning the creator has done a semop() to initialize it).

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 is the "fatal flaw" of System V semaphores?

Chapter 5: SEM_UNDO — Safety Net

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);
Key insight: With SEM_UNDO, if the process decremented the semaphore by 1 and then crashes, the kernel automatically increments it by 1. The resource becomes available again. Always use SEM_UNDO unless you have a specific reason not to.
Note: Your program should still explicitly release semaphores when it's done. SEM_UNDO is a safety net for abnormal termination, not a substitute for proper cleanup.
What does SEM_UNDO do when a process exits?

Chapter 6: semdemo.c — The Full Program

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;
}
To destroy: Use a separate program (semrm.c) that calls semctl(semid, 0, IPC_RMID), or use the ipcrm command from the shell.
In semdemo.c, how does initsem() handle the race condition?

Chapter 7: Destroying Semaphores

Like message queues, semaphore sets persist in the kernel until explicitly destroyed. Two methods:

Command line: ipcs -s lists semaphore sets. ipcrm -s <semid> removes one.
Programmatically: 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;
}
Best practice: Always pair your semaphore creation with a cleanup strategy. Either have the creator process destroy it on exit, or provide a separate cleanup program. Use ipcs regularly to check for leaked IPC objects.
Which semctl() command destroys a semaphore set?

Chapter 8: Semaphore Simulator

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.

Mutex Semaphore (value=1)

Click "Request" to have a process try to enter the critical section. The semaphore only allows one at a time.

Semaphore: 1 | Queue: 0
A semaphore initialized to 3 allows how many processes to access the resource simultaneously?

Chapter 9: Beyond Semaphores

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.

FeatureFile LocksSystem V SemaphoresPOSIX Semaphores
Resource typeFiles onlyAnythingAnything
Read/Write distinctionYesNo (just counters)No
PersistenceReleased on closeKernel-persistentNamed: persistent
Crash recoveryAuto-releaseSEM_UNDO flagManual
API complexityLowHighMedium
Coming up: Now that you know how to synchronize, it's time for the fastest IPC mechanism: shared memory segments. Direct pointer access to memory shared between processes. But with great speed comes great responsibility — you'll need semaphores to keep things safe.

"Whenever you have multiple processes running through a critical section, you need semaphores." — Beej

Why might you use semaphores instead of file locking?