System V Semaphores Binary Semaphores: Concepts, Implementation & Interview Prep

 

System V Semaphores โ€“ Part 7
Binary Semaphores: Concepts, Implementation & Interview Prep
๐Ÿ“˜ TLPI Chapter 47
๐Ÿ”’ Binary Semaphores
๐Ÿ’ป C Code Examples
๐ŸŽฏ Interview Q&A

What is a Binary Semaphore?

A binary semaphore is the simplest form of a semaphore. It can hold only two values: 1 (free / available) and 0 (reserved / in use). Think of it like a toilet key at a petrol station โ€” there is exactly one key. If it is hanging on the hook (value = 1), you can take it (reserve it). If someone else has it (value = 0), you must wait until they return it.

In Linux/UNIX programming, binary semaphores are used to protect shared resources so that only one process or thread can use the resource at a time. This is called mutual exclusion (mutex).

Keywords in This Chapter

Binary Semaphore P and V Operations wait / post reserve / release SEM_UNDO semop() semctl() SETVAL EINTR mutual exclusion

1. The Three Core Operations on a Semaphore

Every semaphore โ€” whether binary or counting โ€” supports these fundamental operations:

Operation Dijkstra Name POSIX Name What It Does
Reserve P (Dutch: proberen) wait / down Decrement semaphore by 1. If already 0, block until it becomes 1.
Release V (Dutch: verhogen) post / up Increment semaphore by 1. Wakes up any process waiting on it.
Conditional Reserve โ€” trywait Non-blocking attempt. Returns error immediately if semaphore is 0.

The P and V naming comes from the Dutch computer scientist Edsger Dijkstra who invented semaphores. “P” stands for proberen (to test/try) and “V” for verhogen (to increment).

2. How Binary Semaphore States Are Represented

In a binary semaphore implemented with System V semaphores:

1
FREE
Available to use
reserve โ†’ decrement by 1
โ‡„
release โ†’ increment by 1
0
RESERVED
In use โ€” others must wait

The convention is: value 1 = free, value 0 = reserved. This is the opposite of what beginners might expect โ€” remember it this way: “1 is good to go, 0 means stop.”

3. The Binary Semaphore Header File (binary_sems.h)

The header file declares the API and exposes two global control variables:

  • bsUseSemUndo โ€” if TRUE, adds SEM_UNDO flag so the kernel automatically undoes the semaphore operation if the process dies unexpectedly
  • bsRetryOnEintr โ€” if TRUE, automatically retries a semop() call that was interrupted by a signal
/* binary_sems.h โ€” Header for binary semaphore implementation */

#ifndef BINARY_SEMS_H
#define BINARY_SEMS_H

#include "tlpi_hdr.h"   /* Boolean type, error helpers */

/* Controls whether SEM_UNDO is used in semop() calls.
   SEM_UNDO = kernel undoes semaphore on process exit.
   Default: FALSE */
extern Boolean bsUseSemUndo;

/* Controls whether semop() is retried on EINTR (signal interrupt).
   Default: TRUE */
extern Boolean bsRetryOnEintr;

/* Initialize semaphore to 1 โ€” "available" state */
int initSemAvailable(int semId, int semNum);

/* Initialize semaphore to 0 โ€” "in use" state */
int initSemInUse(int semId, int semNum);

/* Decrement (lock) the semaphore โ€” blocks if already 0 */
int reserveSem(int semId, int semNum);

/* Increment (unlock) the semaphore */
int releaseSem(int semId, int semNum);

#endif /* BINARY_SEMS_H */

Notice that the header does NOT include functions for creating or deleting semaphore sets. It assumes the semaphore set already exists โ€” creation/deletion is the caller’s responsibility.

4. Full Implementation: binary_sems.c

Below is the complete implementation with detailed explanations for each function:

4.1 Global Variables and Includes

#include <sys/types.h>
#include <sys/sem.h>
#include "semun.h"         /* Definition of union semun */
#include "binary_sems.h"

/* Default: do NOT use SEM_UNDO */
Boolean bsUseSemUndo = FALSE;

/* Default: DO retry if interrupted by a signal */
Boolean bsRetryOnEintr = TRUE;

4.2 initSemAvailable() โ€” Set to 1 (Free)

/* Initialize semaphore to 1 = "available" = can be reserved.
   semId  : ID of the semaphore set
   semNum : Index of semaphore within the set (0-based)
   Returns 0 on success, -1 on error */
int initSemAvailable(int semId, int semNum)
{
    union semun arg;
    arg.val = 1;        /* Set value to 1 = free */
    return semctl(semId, semNum, SETVAL, arg);
}

4.3 initSemInUse() โ€” Set to 0 (In Use)

/* Initialize semaphore to 0 = "in use" = must wait before using.
   Use this when a resource starts in a locked/busy state. */
int initSemInUse(int semId, int semNum)
{
    union semun arg;
    arg.val = 0;        /* Set value to 0 = in use */
    return semctl(semId, semNum, SETVAL, arg);
}

4.4 reserveSem() โ€” Lock / P / wait (Decrement)

/* Reserve the semaphore โ€” decrements it by 1.
   If the semaphore is 0, this call BLOCKS until another process
   releases it (increments it back to 1).
   Returns 0 on success, -1 with errno=EINTR if interrupted
   and bsRetryOnEintr is FALSE. */
int reserveSem(int semId, int semNum)
{
    struct sembuf sops;

    sops.sem_num = semNum;  /* Which semaphore in the set */
    sops.sem_op  = -1;      /* Decrement by 1 (the P / wait operation) */
    sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;
    /*
     * SEM_UNDO: If this process crashes or exits without calling
     * releaseSem(), the kernel automatically undoes this operation.
     * This prevents deadlocks caused by crashed processes.
     */

    /* Keep retrying if interrupted by a signal (unless told not to) */
    while (semop(semId, &sops, 1) == -1) {
        if (errno != EINTR || !bsRetryOnEintr)
            return -1;
        /* errno == EINTR and bsRetryOnEintr is TRUE: loop and retry */
    }

    return 0;   /* Semaphore successfully decremented (reserved) */
}

4.5 releaseSem() โ€” Unlock / V / post (Increment)

/* Release the semaphore โ€” increments it by 1.
   This wakes up any process that is blocked in reserveSem().
   Returns 0 on success, -1 on error. */
int releaseSem(int semId, int semNum)
{
    struct sembuf sops;

    sops.sem_num = semNum;
    sops.sem_op  = 1;       /* Increment by 1 (the V / post operation) */
    sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;

    return semop(semId, &sops, 1);
    /*
     * No retry loop here โ€” release cannot block (incrementing
     * a semaphore never blocks), so EINTR just means "return error".
     */
}

5. Complete Working Example: Protecting a Shared Counter

This example creates a semaphore set, forks a child process, and uses binary semaphores to protect a shared memory counter so both parent and child can update it safely without race conditions.

/* bsem_counter.c โ€” Protect a shared counter with a binary semaphore
   Compile: gcc bsem_counter.c binary_sems.c -o bsem_counter
   Run:     ./bsem_counter
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include "binary_sems.h"

#define SEM_KEY  0xABCD1234
#define SHM_KEY  0xABCD5678
#define LOOPS    10000

int main(void)
{
    int semId, shmId;
    long *counter;

    /* ---- Create semaphore set with 1 semaphore ---- */
    semId = semget(SEM_KEY, 1, IPC_CREAT | IPC_EXCL | 0600);
    if (semId == -1) { perror("semget"); exit(EXIT_FAILURE); }

    /* Initialize the single semaphore to 1 (available) */
    if (initSemAvailable(semId, 0) == -1) {
        perror("initSemAvailable"); exit(EXIT_FAILURE);
    }

    /* ---- Create shared memory for the counter ---- */
    shmId = shmget(SHM_KEY, sizeof(long), IPC_CREAT | IPC_EXCL | 0600);
    if (shmId == -1) { perror("shmget"); exit(EXIT_FAILURE); }

    counter = (long *) shmat(shmId, NULL, 0);
    if (counter == (void *) -1) { perror("shmat"); exit(EXIT_FAILURE); }
    *counter = 0;

    /* ---- Fork child ---- */
    switch (fork()) {
    case -1:
        perror("fork"); exit(EXIT_FAILURE);

    case 0: /* Child process */
        for (int i = 0; i < LOOPS; i++) {
            reserveSem(semId, 0);   /* Lock */
            (*counter)++;           /* Critical section */
            releaseSem(semId, 0);   /* Unlock */
        }
        exit(EXIT_SUCCESS);

    default: /* Parent process */
        for (int i = 0; i < LOOPS; i++) {
            reserveSem(semId, 0);   /* Lock */
            (*counter)++;           /* Critical section */
            releaseSem(semId, 0);   /* Unlock */
        }
        wait(NULL); /* Wait for child */
        break;
    }

    printf("Final counter = %ld (expected %d)\n", *counter, 2 * LOOPS);
    /* Without semaphore protection, this would be less than 20000
       due to race conditions. With protection, it is always 20000. */

    /* ---- Cleanup ---- */
    semctl(semId, 0, IPC_RMID);
    shmdt(counter);
    shmctl(shmId, 0, IPC_RMID);

    return 0;
}
Expected Output:
Final counter = 20000 (expected 20000)

Without the semaphore, the counter would often be less than 20000 because both processes read-modify-write the shared memory simultaneously (race condition).

6. Deep Dive: SEM_UNDO Flag

The SEM_UNDO flag is one of the most important safety features in System V semaphores. Here is what it does and when to use it:

Scenario Without SEM_UNDO With SEM_UNDO
Process reserves semaphore then crashes Semaphore stays at 0 forever โ€” deadlock! Kernel automatically restores semaphore to 1 โ€” safe
Process reserves semaphore, finishes normally Normal release by releaseSem() Normal release by releaseSem() (undo adjustment cancels)
/* Demonstrating SEM_UNDO behavior */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include "binary_sems.h"

int main(void)
{
    int semId;
    semId = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semId == -1) { perror("semget"); exit(1); }

    initSemAvailable(semId, 0);

    /* Enable SEM_UNDO so kernel cleans up on exit */
    bsUseSemUndo = TRUE;

    /* Reserve the semaphore */
    if (reserveSem(semId, 0) == -1) { perror("reserveSem"); exit(1); }

    printf("Semaphore reserved (value=0). Exiting WITHOUT releasing...\n");
    printf("If SEM_UNDO works, the semaphore will be restored to 1\n");
    printf("by the kernel when this process exits.\n");

    /* We exit WITHOUT calling releaseSem().
       With SEM_UNDO, the kernel will increment the semaphore by 1
       (reversing our -1 operation), restoring it to 1 (free). */

    /* Other processes waiting on this semaphore will be woken up */
    semctl(semId, 0, IPC_RMID); /* cleanup */
    return 0;
}

Rule of thumb: Always set bsUseSemUndo = TRUE in production code unless you have a specific reason not to. It prevents semaphore leaks if processes crash.

7. Signal Interruption and EINTR Handling

When a process is blocked in semop() waiting for a semaphore, and a signal arrives, the kernel wakes up the process and semop() returns -1 with errno = EINTR. The implementation handles this with a retry loop:

/* The retry loop in reserveSem() explained: */

while (semop(semId, &sops, 1) == -1) {
    if (errno != EINTR || !bsRetryOnEintr)
        return -1;   /* Real error OR caller wants no retry */
    /*
     * errno == EINTR means we were interrupted by a signal.
     * bsRetryOnEintr == TRUE means "just try again".
     * So we loop back and call semop() again.
     */
}

The table below clarifies the two-variable decision:

errno bsRetryOnEintr Action
EINTR TRUE Retry semop() automatically
EINTR FALSE Return -1 (caller handles)
Other error (e.g. EINVAL) Any Return -1 immediately
/* Example: Using bsRetryOnEintr = FALSE for signal-aware code */

#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include "binary_sems.h"

volatile sig_atomic_t got_signal = 0;

void sig_handler(int sig) {
    got_signal = 1;
}

int main(void)
{
    int semId;
    struct sigaction sa;

    /* Set up SIGINT handler */
    sa.sa_handler = sig_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  /* No SA_RESTART โ€” let semop() be interrupted */
    sigaction(SIGINT, &sa, NULL);

    semId = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    initSemInUse(semId, 0); /* Start locked so we will block */

    /* Tell the library: return EINTR to us, don't retry */
    bsRetryOnEintr = FALSE;

    printf("Waiting for semaphore... Press Ctrl+C to interrupt\n");

    if (reserveSem(semId, 0) == -1) {
        if (errno == EINTR && got_signal) {
            printf("Interrupted by signal โ€” handling gracefully\n");
        } else {
            perror("reserveSem");
        }
    }

    semctl(semId, 0, IPC_RMID);
    return 0;
}

8. Practical Usage Pattern: Protecting a Critical Section

Here is the standard pattern used everywhere in real embedded / Linux system code:

/*
 * STANDARD BINARY SEMAPHORE USAGE PATTERN
 *
 *  1. Create and initialize semaphore (once, at startup)
 *  2. Before critical section: reserveSem()   [blocks if busy]
 *  3. Do critical section work
 *  4. After critical section: releaseSem()    [wakes next waiter]
 */

/* ---- Startup (typically in server/init process) ---- */
int semId = semget(MY_KEY, 1, IPC_CREAT | 0600);
initSemAvailable(semId, 0);   /* value = 1, ready to use */
bsUseSemUndo = TRUE;          /* Safety: kernel cleans up on crash */

/* ---- In every process/thread that uses shared resource ---- */

/* Acquire the lock */
if (reserveSem(semId, 0) == -1) {
    perror("reserveSem");
    /* Handle error โ€” do NOT proceed to critical section */
    exit(EXIT_FAILURE);
}

/* === CRITICAL SECTION START === */
/* Only one process is here at a time */
read_from_shared_memory();
write_to_shared_memory();
update_shared_file();
/* === CRITICAL SECTION END === */

/* Release the lock */
if (releaseSem(semId, 0) == -1) {
    perror("releaseSem");
    /* Log the error; resource may now be permanently locked */
}
โš  Common Mistake: Forgetting to call releaseSem() on every error path inside the critical section. Always use a cleanup label or wrapper to guarantee the semaphore is always released:
/* Safe pattern: always release even on error */
if (reserveSem(semId, 0) == -1) { perror("reserve"); return -1; }

int rc = do_critical_work();   /* may fail */

releaseSem(semId, 0);          /* always runs, regardless of rc */
return rc;

9. Interview Questions and Answers
Q1. What is a binary semaphore and how does it differ from a mutex?

A binary semaphore is a semaphore with only two states: 1 (free) and 0 (in use). A mutex (mutual exclusion lock) is conceptually similar but with a key difference: a mutex has ownership โ€” only the thread that locked it can unlock it. A binary semaphore has no ownership โ€” one process can reserve it and a different process can release it. System V binary semaphores are closer in nature to counting semaphores restricted to [0,1] values.

Q2. What are the P and V operations? Who invented them?

P (proberen = to test) decrements the semaphore โ€” equivalent to “wait” or “lock”. V (verhogen = to increment) increments the semaphore โ€” equivalent to “post” or “unlock”. They were invented by Dutch computer scientist Edsger Dijkstra, who also invented shortest-path algorithms (Dijkstra’s algorithm). In POSIX, these are called sem_wait() and sem_post().

Q3. What is SEM_UNDO and why is it important?

SEM_UNDO is a flag for semop() that tells the kernel to record the operation in the process’s semaphore adjustment list. When the process exits (for any reason โ€” normal exit, crash, signal), the kernel reverses all pending semaphore adjustments. Without SEM_UNDO, if a process holds a semaphore and crashes, the semaphore stays locked forever โ€” causing a deadlock for all other processes waiting on it.

Q4. Why does reserveSem() have a while loop but releaseSem() does not?

reserveSem() decrements (P operation) and can block if the semaphore is 0. While blocked, a signal can arrive, interrupting the semop() call with errno = EINTR. The while loop retries the call in that case. releaseSem() increments (V operation) and never blocks โ€” incrementing a semaphore always succeeds immediately, so there is nothing to retry.

Q5. Why is the binary semaphore initialized to 1 for “free” rather than 0?

Because the reserve operation subtracts 1 from the current value. If the semaphore starts at 1 (free), the first process to call reserveSem() subtracts 1, bringing it to 0. Any subsequent process trying to reserve will find 0 and block (because decrementing would go negative, which semop() prevents). When the first process calls releaseSem(), it adds 1 back to 0, making it 1 again and waking up any waiters. Starting at 0 would mean the semaphore is immediately locked with nobody holding it.

Q6. What is the difference between IPC_PRIVATE and a user-defined key in semget()?

IPC_PRIVATE (value 0) creates a private semaphore set guaranteed to have a unique ID. It is used when the parent and child processes share the semaphore via fork() โ€” the ID is passed down through the process relationship. A user-defined key (like 0xABCD1234) allows unrelated processes to access the same semaphore by agreeing on the key value in advance (like a shared password).

Q7. Write code to implement a non-blocking (try-lock) binary semaphore.

/* Non-blocking reserveSem using IPC_NOWAIT */
int tryReserveSem(int semId, int semNum)
{
    struct sembuf sops;
    sops.sem_num = semNum;
    sops.sem_op  = -1;
    sops.sem_flg = IPC_NOWAIT;  /* Return immediately if semaphore is 0 */

    if (semop(semId, &sops, 1) == -1) {
        if (errno == EAGAIN) {
            /* Semaphore is 0 โ€” currently reserved by someone else */
            return -1;
        }
        perror("semop tryReserve");
        return -1;
    }
    return 0; /* Successfully reserved */
}
Q8. What happens when two processes call reserveSem() simultaneously on a free semaphore?

The kernel guarantees atomicity of semop(). Only one of the two processes will successfully decrement the semaphore from 1 to 0. The other will find the semaphore already at 0 and will block until the first process calls releaseSem(). This is the fundamental guarantee that makes semaphores useful for mutual exclusion โ€” no race condition can occur in the decrement itself.

10. Quick Reference: Binary Semaphore API Summary
Function sem_op Blocks? Purpose
initSemAvailable() SETVAL to 1 No Set semaphore to 1 (unlocked)
initSemInUse() SETVAL to 0 No Set semaphore to 0 (locked)
reserveSem() -1 Yes (if value = 0) Lock / acquire (P operation)
releaseSem() +1 Never Unlock / release (V operation)

Next Up: Semaphore Limits
Learn about SEMMNI, SEMMSL, SEMMNS, SEMOPM, SEMVMX and how to tune them

Next: Semaphore Limits โ†’ Back to Home

Leave a Reply

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