System V Semaphores inary Semaphores — Implementation & Patterns

System V Semaphores — Part 4
Binary Semaphores — Implementation & Patterns | TLPI Chapter 47
Pattern
Binary Semaphore
Values
0 or 1 only
Level
Intermediate
What Is a Binary Semaphore?

A binary semaphore takes only two values: 0 (locked/unavailable) and 1 (unlocked/available). It is the most common semaphore usage pattern — protecting a critical section.

While System V semaphores can hold any non-negative integer up to SEMVMX, most real programs only need binary behavior. The TLPI book provides a clean wrapper library (binary_sems.c) that abstracts this. Understanding it is a key interview topic.

The fundamental question when implementing binary semaphores is: should 0 mean “available” or 1 mean “available”? The answer affects how the wait-for-zero operation is used.

Key Terms

binary semaphore reserveSem() releaseSem() initSemAvailable() initSemInUse() mutex critical section 1 = available

Convention: 1 = Available, 0 = In Use

TLPI’s binary semaphore library uses 1 = available (resource free) and 0 = in use (resource held).

Operation sem_op Logic
reserveSem (acquire) -1 Wait until value is 1, then decrement to 0 (locks it)
releaseSem (release) +1 Increment from 0 to 1 (unlocks it, wakes a waiter)

Why not use 0 = available and test-for-zero? Because the wait-for-zero operation wakes up when value == 0, but multiple waiters would all wake simultaneously. With 1 = available, the decrement (-1) naturally serializes access — only one waiter can decrement from 1 to 0.

Code — binary_sems.h Header
/* binary_sems.h - Binary semaphore library interface */
#ifndef BINARY_SEMS_H
#define BINARY_SEMS_H

#include <sys/sem.h>

/* union semun must be defined by application */
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

/*
 * Initialize semaphore 'semNum' in set 'semId' to "available" state (1).
 * Call this after creating the semaphore set with semget().
 */
int initSemAvailable(int semId, int semNum);

/*
 * Initialize semaphore 'semNum' to "in use" state (0).
 * Use this when the resource is already occupied at startup.
 */
int initSemInUse(int semId, int semNum);

/*
 * Reserve (acquire) the semaphore: block until value is 1, decrement to 0.
 * Returns 0 on success, -1 on error.
 */
int reserveSem(int semId, int semNum);

/*
 * Release the semaphore: increment from 0 to 1.
 * Returns 0 on success, -1 on error.
 */
int releaseSem(int semId, int semNum);

#endif /* BINARY_SEMS_H */

Code — binary_sems.c Implementation
/* binary_sems.c - Binary semaphore library implementation */
#include "binary_sems.h"
#include <sys/sem.h>
#include <errno.h>

/*
 * Set semaphore to 1 (available).
 */
int initSemAvailable(int semId, int semNum)
{
    union semun arg;
    arg.val = 1;
    return semctl(semId, semNum, SETVAL, arg);
}

/*
 * Set semaphore to 0 (in use / locked).
 */
int initSemInUse(int semId, int semNum)
{
    union semun arg;
    arg.val = 0;
    return semctl(semId, semNum, SETVAL, arg);
}

/*
 * Decrement semaphore by 1.
 * Blocks until semaphore value >= 1 (i.e., resource is available).
 * After decrement, value == 0, signifying resource is held.
 */
int reserveSem(int semId, int semNum)
{
    struct sembuf sop;
    sop.sem_num = semNum;
    sop.sem_op  = -1;       /* decrement by 1 */
    sop.sem_flg = SEM_UNDO; /* auto-release if we crash */

    /* Restart if interrupted by a signal */
    while (semop(semId, &sop, 1) == -1) {
        if (errno != EINTR)
            return -1;
        /* EINTR: signal interrupted semop, retry */
    }
    return 0;
}

/*
 * Increment semaphore by 1.
 * Releases the resource; wakes one waiting process.
 */
int releaseSem(int semId, int semNum)
{
    struct sembuf sop;
    sop.sem_num = semNum;
    sop.sem_op  = +1;       /* increment by 1 */
    sop.sem_flg = SEM_UNDO;

    return semop(semId, &sop, 1);
}

Note on EINTR handling: if a signal arrives while semop() is blocked, it returns -1 with errno = EINTR. The correct response is to retry — this is called a restart loop.

Code Example — Using the Binary Semaphore Library
/* main_binary_sem.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include "binary_sems.h"

#define SEM_KEY_PATH "/tmp/mysem"
#define SEM_PROJ_ID  1

int shared_counter = 0;  /* illustrative — in real use, this would be in shared memory */

int main(void)
{
    key_t key;
    int semid;
    pid_t pid;

    key = ftok(SEM_KEY_PATH, SEM_PROJ_ID);
    /* Create file if it doesn't exist */
    fclose(fopen(SEM_KEY_PATH, "a"));

    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0600);
    if (semid == -1 && errno == EEXIST)
        semid = semget(key, 0, 0600);
    if (semid == -1) { perror("semget"); exit(1); }

    /* Initialize to available */
    if (initSemAvailable(semid, 0) == -1) {
        perror("initSemAvailable"); exit(1);
    }

    /* Spawn two child processes */
    for (int i = 0; i < 2; i++) {
        pid = fork();
        if (pid == 0) {
            int child_id = i + 1;
            for (int j = 0; j < 3; j++) {
                /* Acquire lock */
                if (reserveSem(semid, 0) == -1) {
                    perror("reserveSem"); exit(1);
                }

                /* Critical section */
                printf("Child %d: entering critical section\n", child_id);
                shared_counter++;
                printf("Child %d: counter = %d\n", child_id, shared_counter);
                usleep(100000);  /* simulate work */
                printf("Child %d: leaving critical section\n", child_id);

                /* Release lock */
                if (releaseSem(semid, 0) == -1) {
                    perror("releaseSem"); exit(1);
                }
                usleep(50000);
            }
            exit(0);
        }
    }

    /* Parent waits for children */
    wait(NULL);
    wait(NULL);

    /* Cleanup */
    semctl(semid, 0, IPC_RMID);
    printf("Done. Semaphore deleted.\n");
    return 0;
}

Code Example — Non-Blocking reserveSemNB()

This is one of the exercises from TLPI 47-4. Add a conditional (non-blocking) reserve function:

/*
 * reserveSemNB() — Try to acquire the semaphore without blocking.
 * Returns  0 if acquired successfully.
 * Returns -1 with errno=EAGAIN if semaphore is currently in use.
 * Returns -1 with other errno on genuine error.
 */
int reserveSemNB(int semId, int semNum)
{
    struct sembuf sop;
    sop.sem_num = semNum;
    sop.sem_op  = -1;
    sop.sem_flg = SEM_UNDO | IPC_NOWAIT;  /* don't block */

    while (semop(semId, &sop, 1) == -1) {
        if (errno == EAGAIN) {
            return -1;  /* unavailable — caller can retry later */
        }
        if (errno != EINTR)
            return -1;  /* real error */
        /* EINTR: retry the non-blocking attempt */
    }
    return 0;
}

/* Usage */
void try_access(int semid)
{
    if (reserveSemNB(semid, 0) == 0) {
        printf("Got the lock, doing work\n");
        /* ... work ... */
        releaseSem(semid, 0);
    } else if (errno == EAGAIN) {
        printf("Resource busy — will try again later\n");
    } else {
        perror("reserveSemNB");
    }
}

Event Flags Using Semaphores (TLPI Exercise 47-5)

An event flag has two states: set and clear. Processes can wait for it to be set. The trick is that for waitForEventFlag() to work using the wait-for-zero semop, we must choose: set = 0, clear = 1 (reversed from intuition).

/*
 * Event flag using System V semaphore.
 * Convention: semaphore value 0 = "flag set" (event occurred)
 *             semaphore value 1 = "flag clear" (not yet)
 *
 * This REVERSAL is needed because wait-for-zero blocks until value==0,
 * which is our "flag set" state.
 */

#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

union semun { int val; struct semid_ds *buf; unsigned short *array; };

/* Initialize event flag to "clear" state */
int initEventFlag(int semId, int semNum)
{
    union semun arg;
    arg.val = 1;  /* 1 = clear */
    return semctl(semId, semNum, SETVAL, arg);
}

/* Set the event flag (wake all waiting processes) */
int setEventFlag(int semId, int semNum)
{
    union semun arg;
    arg.val = 0;  /* 0 = set */
    return semctl(semId, semNum, SETVAL, arg);
}

/* Clear the event flag */
int clearEventFlag(int semId, int semNum)
{
    union semun arg;
    arg.val = 1;  /* back to "clear" */
    return semctl(semId, semNum, SETVAL, arg);
}

/* Block until the event flag is set (value == 0) */
int waitForEventFlag(int semId, int semNum)
{
    struct sembuf sop;
    sop.sem_num = semNum;
    sop.sem_op  = 0;    /* wait until value == 0 (flag set) */
    sop.sem_flg = 0;
    while (semop(semId, &sop, 1) == -1) {
        if (errno != EINTR) return -1;
    }
    return 0;
}

/* Get current flag state */
int getFlagState(int semId, int semNum)
{
    int val = semctl(semId, semNum, GETVAL);
    if (val == -1) return -1;
    return (val == 0) ? 1 : 0;  /* 1=set, 0=clear */
}

Interview Questions — Binary Semaphores

Q1. Why does TLPI’s binary semaphore library use 1 = available rather than 0 = available?

Using 1 = available and sem_op = -1 to acquire means only one process can decrement from 1 to 0 at a time — that process wins the lock. If 0 = available with wait-for-zero, all waiting processes would wake simultaneously when the value reaches 0, causing a thundering herd and requiring extra logic to pick one winner.

Q2. Why does reserveSem() have a retry loop for EINTR?

If a signal is delivered while the process is blocked in semop(), the system call is interrupted and returns -1 with errno = EINTR. This is not an error — the process just needs to retry the semop(). Without the loop, a signal would appear to the caller as a lock acquisition failure.

Q3. Is a binary semaphore equivalent to a mutex?

Almost, but with a key difference: a mutex has ownership — only the thread that locked it can unlock it. A semaphore has no ownership — any process can call releaseSem() regardless of who called reserveSem(). This makes semaphores useful for signaling between different processes, but inappropriate where ownership enforcement is needed.

Q4. What happens if a process calls releaseSem() when the semaphore is already at 1?

The value becomes 2, which violates the binary contract. The library does not guard against this. It is the caller’s responsibility to only call releaseSem() after a successful reserveSem(). This is a common bug source in semaphore-based code.

Q5. What does SEM_UNDO provide in the binary semaphore context?

If the process holding the semaphore (value = 0) crashes, SEM_UNDO causes the kernel to restore the value to 1 on process exit, effectively releasing the lock. Without SEM_UNDO, the semaphore stays at 0 and all waiting processes block forever.

Q6. Why is the event flag convention (set = 0, clear = 1) counterintuitive?

Because the only way to block waiting for a flag to become “set” using semop() is the wait-for-zero operation (sem_op = 0), which only wakes when the value is exactly 0. So “set” must map to the value 0 for this to work. “Clear” = 1 lets the wait-for-zero block correctly until the event fires.

Next: Disadvantages, Summary & Full Q&A

Understand the known limitations of System V semaphores and common interview questions.

Leave a Reply

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