System V Semaphores Safe Semaphore Initialization & Race Conditions

 

System V Semaphores
Chapter 47 – Part 1: Safe Semaphore Initialization & Race Conditions
47.5
TLPI Section
IPC
Category
Linux
Platform

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

semget() semctl() SETVAL semop() IPC_CREAT IPC_EXCL sem_otime Race Condition Atomic Init union semun

1. The Core Problem – Why Initialization Races Happen

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:

  1. Call semget() to create the semaphore set
  2. Call semctl(SETVAL) or semctl(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.

2. The Standard Fix – Using sem_otime as a Signal

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

3. The union semun – What It Is and Why You Need It

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().

4. Complete Working Code – Safe Semaphore Initialization

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.

5. When You Do NOT Need This Pattern

The safe initialization pattern is only needed when multiple unrelated processes might race to create the same semaphore. You can skip it if:

✔ Safe — No pattern needed

Parent creates & initializes the semaphore before calling fork(). All children inherit an already-initialized semaphore.

✔ Safe — No pattern needed

One known init process runs first, finishes all semaphore setup, then starts worker processes via exec().

✘ Not safe — Pattern required

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;
}

6. Variations – Multiple Semaphores & Nonzero Initial Values

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;
}

7. Interview Questions & Answers
Q1. Why is System V semaphore initialization a two-step process?

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

Q2. What is sem_otime and how is it used to solve the race condition?

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.

Q3. Why do you use IPC_CREAT | IPC_EXCL together?

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.

Q4. What is the “no-op” semaphore operation and why is it needed?

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.

Q5. What happens if you don’t define union semun yourself on Linux?

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().

Q6. In what scenario can you safely skip the sem_otime polling pattern?

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.

Q7. What is the difference between IPC_PRIVATE and a key from ftok()?

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.

Continue Learning

Next: Learn the semop() system call in detail — operations, blocking, and flags

Part 2: semop() Operations → Home

Leave a Reply

Your email address will not be published. Required fields are marked *