What you will learn in this file
When multiple processes start at the same time and all try to create and initialize the same semaphore, a classic race condition can occur. One process creates the semaphore but gets interrupted before initializing it, and another process picks it up in an uninitialized state. This tutorial explains the problem and the standard Linux solution step by step with working C code.
Key Terms
System V semaphore creation and initialization are two separate steps. Unlike POSIX semaphores where you can pass an initial value to sem_open(), with System V you must:
- Call
semget()to create the semaphore set - Call
semctl(SETVAL)orsemctl(SETALL)separately to set the value
Between steps 1 and 2, the kernel may schedule another process. That second process calls semget(), gets the existing (but not-yet-initialized) semaphore, and starts using it with value 0. This is a silent bug — no error is returned, but behavior is wrong.
| Process A (Creator) | Process B (Late Joiner) |
|---|---|
| semget(IPC_CREAT|IPC_EXCL) → SUCCESS ✔ | — waiting — |
| TIME SLICE EXPIRES — preempted before SETVAL | — waiting — |
| — waiting — | semget() → EEXIST → gets semid |
| — waiting — | Uses semaphore with value=0 — BUG! ❌ |
| semctl(SETVAL) → sets value (too late!) | already done wrong operations |
Figure: Race condition between semaphore creation and initialization
This race is particularly nasty because it only shows up sometimes — only when the OS scheduler happens to switch processes at exactly the wrong moment.
The key insight: the kernel tracks when semop() was last called on a semaphore set, storing this timestamp in the sem_otime field of the semid_ds structure. Immediately after semget() creates a new set, sem_otime == 0 because no semop() has ever been called on it.
The trick: after initializing with semctl(SETVAL), the creator performs a no-op semaphore operation — an operation that doesn’t change the value but does update sem_otime to a nonzero value. Late joiners poll sem_otime and wait until it becomes nonzero before proceeding.
| Process A (Creator) | Process B (Late Joiner) |
|---|---|
| semget(IPC_CREAT|IPC_EXCL) → SUCCESS | — waiting — |
| semctl(SETVAL, 0) → value set | — waiting — |
| semop(no-op) → sem_otime becomes nonzero ✔ | — waiting — |
| — proceeds with real work — | semget() → gets semid |
| — running — | polls IPC_STAT → sem_otime != 0 → safe to use ✔ |
Figure: Safe initialization using sem_otime handshake
semctl() uses a fourth argument that can be one of several types depending on the operation. This is represented as union semun. On Linux, you must define this union yourself — it is not provided by any header:
/* Must be defined by the application — not in any system header on Linux */
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT and IPC_SET */
unsigned short *array; /* Array for GETALL and SETALL */
};
If you forget to define it and try to use it, you get a compile error: “storage size of ‘arg’ isn’t known” or similar. Always define it before using semctl().
The code below implements the full safe initialization pattern. Both the creator path and the late-joiner path are shown:
/* svsem_safe_init.c
* Demonstrates safe System V semaphore initialization
* using sem_otime as a "ready" signal to avoid race conditions.
* Compile: gcc svsem_safe_init.c -o svsem_safe_init
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sem.h>
/* On Linux, union semun must be defined by the application */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
#define KEY_PATH "/tmp"
#define KEY_ID 42
#define SEM_PERMS 0600
#define MAX_TRIES 10
int main(void)
{
key_t key;
int semid;
/* Create a unique IPC key from a path + project ID */
key = ftok(KEY_PATH, KEY_ID);
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
/* --- ATTEMPT TO CREATE the semaphore set (1 semaphore) --- */
semid = semget(key, 1, IPC_CREAT | IPC_EXCL | SEM_PERMS);
if (semid != -1) {
/* ============================================================
* We are the CREATOR.
* The set exists but is not yet initialized.
* sem_otime == 0 at this point.
* ============================================================ */
union semun arg;
struct sembuf sop;
printf("Creator: semaphore created, semid=%d\n", semid);
/* Step 1: Initialize the semaphore value to 1 (binary mutex) */
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
/* Step 2: Perform a no-op operation (sem_op=0 means "wait for
* value to equal 0", but since value is 1 this would block.
* Instead use sem_op=0 on a 0-initialized semaphore, OR
* we do a "add 0" style trick. Below shows the standard
* approach: initialize to 0 first, do the no-op, then set
* the real value with SETALL.
*
* Simpler approach: just call semop with sem_op = 0 on a
* semaphore whose value we set to 0. This updates sem_otime. */
arg.val = 0;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL 0");
exit(EXIT_FAILURE);
}
sop.sem_num = 0; /* Operate on semaphore 0 */
sop.sem_op = 0; /* Wait for value == 0 (it is 0, so returns immediately) */
sop.sem_flg = 0;
if (semop(semid, &sop, 1) == -1) {
perror("semop no-op");
exit(EXIT_FAILURE);
}
/* Now set the real intended initial value (e.g., 1 for mutex) */
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL final");
exit(EXIT_FAILURE);
}
printf("Creator: semaphore initialized, sem_otime is now nonzero\n");
} else {
/* ============================================================
* We are a LATE JOINER.
* Someone else created the set. We must wait for them to
* finish initialization before we use it.
* ============================================================ */
union semun arg;
struct semid_ds ds;
int j;
if (errno != EEXIST) {
/* Unexpected error — not just "already exists" */
perror("semget unexpected error");
exit(EXIT_FAILURE);
}
/* Retrieve the ID of the already-existing semaphore set */
semid = semget(key, 1, SEM_PERMS);
if (semid == -1) {
perror("semget retrieve");
exit(EXIT_FAILURE);
}
printf("Joiner: semaphore exists, semid=%d, waiting for init...\n", semid);
/* Poll sem_otime — zero means creator hasn't called semop() yet */
arg.buf = &ds;
for (j = 0; j < MAX_TRIES; j++) {
if (semctl(semid, 0, IPC_STAT, arg) == -1) {
perror("semctl IPC_STAT");
exit(EXIT_FAILURE);
}
if (ds.sem_otime != 0) {
/* Creator finished initialization */
printf("Joiner: initialization confirmed after %d poll(s)\n", j + 1);
break;
}
printf("Joiner: poll %d, sem_otime still 0, sleeping 1s...\n", j + 1);
sleep(1);
}
if (ds.sem_otime == 0) {
fprintf(stderr, "Joiner: semaphore never initialized after %d tries!\n",
MAX_TRIES);
exit(EXIT_FAILURE);
}
}
printf("Both paths: semid=%d ready to use\n", semid);
/* --- Use the semaphore here (acquire / release) --- */
return 0;
}
Key insight: The late joiner does NOT use the semaphore until sem_otime != 0. The creator guarantees this by calling semop() after semctl(SETVAL). These two steps together form the “initialization complete” handshake.
The safe initialization pattern is only needed when multiple unrelated processes might race to create the same semaphore. You can skip it if:
Parent creates & initializes the semaphore before calling fork(). All children inherit an already-initialized semaphore.
One known init process runs first, finishes all semaphore setup, then starts worker processes via exec().
Multiple independent processes start concurrently (e.g., daemons started by init at boot), all sharing the same semaphore key.
Simple case — parent/child — no polling needed:
/* svsem_parent_child.c
* Safe WITHOUT the sem_otime pattern because parent initializes
* before forking children.
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main(void)
{
int semid;
union semun arg;
struct sembuf sop;
/* Parent creates and initializes BEFORE fork */
semid = semget(IPC_PRIVATE, 1, 0600);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
arg.val = 1; /* Binary mutex, initially unlocked */
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl"); exit(EXIT_FAILURE);
}
printf("Parent: semaphore %d created and set to 1\n", semid);
pid_t pid = fork();
if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid == 0) {
/* Child — acquire semaphore (decrement by 1) */
sop.sem_num = 0;
sop.sem_op = -1; /* Lock */
sop.sem_flg = 0;
semop(semid, &sop, 1);
printf("Child: inside critical section\n");
sleep(2);
sop.sem_op = 1; /* Unlock */
semop(semid, &sop, 1);
printf("Child: released semaphore\n");
exit(0);
} else {
/* Parent — also tries to acquire */
sop.sem_num = 0;
sop.sem_op = -1;
sop.sem_flg = 0;
semop(semid, &sop, 1);
printf("Parent: inside critical section\n");
sleep(1);
sop.sem_op = 1;
semop(semid, &sop, 1);
printf("Parent: released semaphore\n");
wait(NULL);
/* Clean up */
semctl(semid, 0, IPC_RMID);
}
return 0;
}
The same technique works for semaphore sets with multiple semaphores. Use SETALL to initialize all semaphores atomically, then do the no-op semop() to update sem_otime:
/* Initializing a set of 3 semaphores atomically */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void init_semaphore_set(int semid, int nsems)
{
union semun arg;
struct sembuf sop;
unsigned short values[3]; /* Adjust size for your nsems */
int i;
/* Set each semaphore's initial value */
for (i = 0; i < nsems; i++)
values[i] = 1; /* All start as "available" (mutex style) */
arg.array = values;
if (semctl(semid, 0, SETALL, arg) == -1) {
perror("semctl SETALL");
exit(EXIT_FAILURE);
}
/* Perform no-op on semaphore 0 to set sem_otime to nonzero.
* We temporarily set it to 0, do the wait-for-zero op, then
* SETALL again with real values. */
arg.val = 0;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
/* No-op: wait for semaphore 0 to be 0 (it is, so immediate) */
sop.sem_num = 0;
sop.sem_op = 0;
sop.sem_flg = 0;
if (semop(semid, &sop, 1) == -1) {
perror("semop no-op");
exit(EXIT_FAILURE);
}
/* Restore all semaphores to their real initial values */
for (i = 0; i < nsems; i++)
values[i] = 1;
arg.array = values;
if (semctl(semid, 0, SETALL, arg) == -1) {
perror("semctl SETALL restore");
exit(EXIT_FAILURE);
}
printf("Semaphore set of %d initialized, sem_otime is nonzero\n", nsems);
}
int main(void)
{
int semid = semget(IPC_PRIVATE, 3, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
init_semaphore_set(semid, 3);
/* Use the semaphores ... */
semctl(semid, 0, IPC_RMID);
return 0;
}
Because semget() only allocates the semaphore set — it does not accept an initial value. You must follow it with semctl(SETVAL) or semctl(SETALL). Between these two calls the kernel can schedule other processes, creating a window where the semaphore exists but has an undefined value (typically 0 after kernel zeroing, but that may not be the intended value).
sem_otime is a field in struct semid_ds that the kernel sets to the current time whenever semop() successfully completes on that semaphore set. A freshly created semaphore set has sem_otime == 0. The creator calls semop() (a no-op) after initialization, making sem_otime != 0. Late joiners poll IPC_STAT and wait until sem_otime != 0, guaranteeing the creator has finished.
IPC_CREAT alone creates the semaphore if it doesn’t exist, or returns the existing one if it does — making it impossible to know whether you are the creator. IPC_EXCL forces semget() to fail with EEXIST if the set already exists. This lets you distinguish the creator (success) from the joiner (EEXIST), which is essential for the safe init pattern.
A no-op is a semop() call with sem_op = 0 on a semaphore whose value is currently 0. This returns immediately without blocking and without changing the semaphore value, but it does update sem_otime in the kernel’s data structure. This is the minimal way to signal “initialization complete” to late joiners without actually changing semaphore state.
On Linux, union semun is not defined in any system header (unlike some older UNIX systems). If you try to use semctl() with a fourth argument without defining the union, the compiler will give an error such as “storage size of ‘arg’ isn’t known”. You must always define it yourself before any code that calls semctl().
When the creating process is guaranteed to complete initialization before any other process can possibly use the semaphore. The most common example is a parent process that creates and initializes the semaphore, then calls fork(). All child processes inherit an already-initialized semaphore descriptor and can start using it immediately without any polling.
IPC_PRIVATE creates a semaphore set that is only accessible to the creating process and its descendants (via inherited file descriptor). It cannot be accessed by unrelated processes. A key from ftok() generates a numeric key from a pathname and project ID, which any process on the system can use to get the same semaphore set via semget(), enabling semaphore sharing between unrelated processes.
Next: Learn the semop() system call in detail — operations, blocking, and flags
