System V Semaphores semop() Operations, Blocking, SEM_UNDO

System V Semaphores — Part 2
semop() Operations, Blocking, SEM_UNDO | TLPI Chapter 47
API
semop()
Flag
SEM_UNDO
Level
Intermediate
The sembuf Structure — Describing an Operation

Every semaphore operation is described by a struct sembuf. You build an array of these structures and pass it to semop(), which performs all operations atomically.

struct sembuf {
    unsigned short sem_num;  /* Index of semaphore in the set (0-based) */
    short          sem_op;   /* Operation: negative, positive, or zero  */
    short          sem_flg;  /* Flags: IPC_NOWAIT, SEM_UNDO             */
};

The meaning of sem_op:

sem_op value Operation Name Effect
negative (e.g. -1) Decrement / Wait / P() Block if semval < abs(sem_op); otherwise subtract.
positive (e.g. +1) Increment / Signal / V() Always succeeds; adds sem_op to semval, may wake waiters.
zero (0) Wait-for-zero Block until semval == 0.
Key Terms

sembuf sem_op sem_num sem_flg IPC_NOWAIT SEM_UNDO semadj blocking starvation

semop() — The System Call
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
/* Returns 0 on success, -1 on error */

Parameters:

Parameter Description
semid Semaphore set identifier from semget()
sops Pointer to array of sembuf structures
nsops Number of operations in the array

Atomicity guarantee: all operations in the sops array are performed as a single atomic unit. Either all succeed, or none are applied and the call blocks (or returns EAGAIN with IPC_NOWAIT).

What Happens When semop() Blocks
Process calls
semop(-1)
Kernel checks:
semval >= 1?
YES: subtract 1,
return 0
↓ NO
Process added to semaphore’s wait queue
(TASK_INTERRUPTIBLE sleep)
Woken when another
process does semop(+1)

Code Example 1 — Basic Wait (P) and Signal (V)
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

/* Decrement semaphore by 1 — wait/acquire/P */
int sem_wait(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = -1;       /* subtract 1 */
    sop.sem_flg = 0;        /* block if semval < 1 */
    return semop(semid, &sop, 1);
}

/* Increment semaphore by 1 — signal/release/V */
int sem_signal(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = +1;       /* add 1 */
    sop.sem_flg = 0;
    return semop(semid, &sop, 1);
}

int main(void)
{
    /* Assume semid is already created and initialized to 1 */
    int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);

    /* Initialize to 1 using semctl SETVAL */
    union semun { int val; } arg;
    arg.val = 1;
    semctl(semid, 0, SETVAL, arg);

    printf("Acquiring semaphore...\n");
    if (sem_wait(semid, 0) == -1) { perror("sem_wait"); exit(1); }

    printf("Inside critical section\n");
    /* ... do work with shared resource ... */

    if (sem_signal(semid, 0) == -1) { perror("sem_signal"); exit(1); }
    printf("Released semaphore\n");

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

Code Example 2 — Non-Blocking Operation (IPC_NOWAIT)

Adding IPC_NOWAIT to sem_flg makes the operation return immediately with errno = EAGAIN instead of blocking when the operation cannot be performed.

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

/* Try to acquire semaphore without blocking */
int sem_trywait(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = -1;
    sop.sem_flg = IPC_NOWAIT;  /* non-blocking */

    if (semop(semid, &sop, 1) == -1) {
        if (errno == EAGAIN) {
            printf("Semaphore not available right now\n");
            return -1;  /* resource busy */
        }
        perror("semop");
        exit(EXIT_FAILURE);
    }
    return 0;  /* acquired */
}

int main(void)
{
    int semid = /* obtained from semget */ 0;
    /* ... */

    if (sem_trywait(semid, 0) == 0) {
        printf("Got the semaphore, doing work\n");
        /* ... critical section ... */
        /* release */
    } else {
        printf("Semaphore busy — doing something else\n");
    }
    return 0;
}

Code Example 3 — Wait for Zero (sem_op = 0)

A zero operation blocks until the semaphore value reaches 0. This is useful for waiting until all workers have finished (like a barrier or join).

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

/*
 * Use case: semaphore is initialized to N (number of workers).
 * Each worker decrements it when done.
 * Monitor waits for semval == 0 (all workers done).
 */
int wait_for_zero(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = 0;   /* wait until semval == 0 */
    sop.sem_flg = 0;
    return semop(semid, &sop, 1);
}

/* Usage example */
void monitor(int semid)
{
    printf("Waiting for all workers to finish...\n");
    if (wait_for_zero(semid, 0) == -1) {
        perror("wait_for_zero");
        return;
    }
    printf("All workers done!\n");
}

Code Example 4 — Atomic Operations on Multiple Semaphores

This is the power of System V semaphores: atomically acquire multiple resources to avoid deadlock.

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

/*
 * Atomically decrement sem[0] AND sem[1].
 * Both must be available (>= 1). If only one is available,
 * the call blocks until both can be decremented together.
 * This prevents the "partial lock" deadlock pattern.
 */
int acquire_two_resources(int semid)
{
    struct sembuf sops[2];

    sops[0].sem_num = 0;
    sops[0].sem_op  = -1;
    sops[0].sem_flg = 0;

    sops[1].sem_num = 1;
    sops[1].sem_op  = -1;
    sops[1].sem_flg = 0;

    /* Atomic: either both are decremented, or neither is */
    return semop(semid, sops, 2);
}

int release_two_resources(int semid)
{
    struct sembuf sops[2];

    sops[0].sem_num = 0; sops[0].sem_op = +1; sops[0].sem_flg = 0;
    sops[1].sem_num = 1; sops[1].sem_op = +1; sops[1].sem_flg = 0;

    return semop(semid, sops, 2);
}

SEM_UNDO — Automatic Cleanup on Process Exit

If a process holding a semaphore crashes or is killed, the semaphore stays in its modified state — other processes may block forever. The SEM_UNDO flag tells the kernel to undo that semaphore operation when the process exits.

The kernel maintains a per-process semadj table. For each semaphore operation flagged with SEM_UNDO, the kernel records the inverse adjustment. On process exit (normal or abnormal), all recorded adjustments are applied.

Step With SEM_UNDO Without SEM_UNDO
Process does semop(-1) semval– AND semadj += 1 semval– only
Process crashes Kernel applies semadj: semval++ semval stays 0 — other processes block forever!
#include <sys/sem.h>

int sem_wait_undo(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = -1;
    sop.sem_flg = SEM_UNDO;   /* kernel will undo this if we die */
    return semop(semid, &sop, 1);
}

int sem_signal_undo(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = +1;
    sop.sem_flg = SEM_UNDO;   /* kernel adjusts semadj accordingly */
    return semop(semid, &sop, 1);
}

SEM_UNDO — Important Caveats

SEM_UNDO is useful but has limitations you must know for interviews:

Caveat Explanation
semadj is per-process Forked children do NOT inherit the parent’s semadj table
Adjustment may fail silently If applying semadj would violate limits (SEMVMX), the kernel adjusts as much as possible
Mixed use is dangerous Mixing SEM_UNDO and non-SEM_UNDO operations on the same semaphore can produce unpredictable values
Does not survive reboot On reboot, all semaphore sets (and semadj tables) are gone anyway

Code Example 5 — Producer/Consumer with semop()
/*
 * Two semaphores in one set:
 *   sem[0] = "slots available" (initialized to BUFFER_SIZE)
 *   sem[1] = "items available" (initialized to 0)
 *
 * Producer: wait for slot, produce, signal item
 * Consumer: wait for item, consume, signal slot
 */
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#define BUFFER_SIZE 5
#define SEM_SLOTS   0
#define SEM_ITEMS   1

union semun { int val; };

int semid;

void set_semval(int semid, int semnum, int val)
{
    union semun arg;
    arg.val = val;
    if (semctl(semid, semnum, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(EXIT_FAILURE);
    }
}

void do_semop(int semid, int semnum, int op, int flags)
{
    struct sembuf sop = { semnum, op, flags };
    if (semop(semid, &sop, 1) == -1) {
        perror("semop");
        exit(EXIT_FAILURE);
    }
}

void producer(void)
{
    for (int i = 0; i < 10; i++) {
        do_semop(semid, SEM_SLOTS, -1, SEM_UNDO); /* wait for slot */
        printf("Producer: produced item %d\n", i);
        do_semop(semid, SEM_ITEMS, +1, SEM_UNDO); /* signal item ready */
        usleep(100000);
    }
}

void consumer(void)
{
    for (int i = 0; i < 10; i++) {
        do_semop(semid, SEM_ITEMS, -1, SEM_UNDO); /* wait for item */
        printf("  Consumer: consumed item %d\n", i);
        do_semop(semid, SEM_SLOTS, +1, SEM_UNDO); /* signal slot free */
        usleep(200000);
    }
}

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

    set_semval(semid, SEM_SLOTS, BUFFER_SIZE);  /* 5 slots free */
    set_semval(semid, SEM_ITEMS, 0);            /* 0 items ready */

    if (fork() == 0) { producer(); exit(0); }
    if (fork() == 0) { consumer(); exit(0); }

    wait(NULL); wait(NULL);

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

Interview Questions — semop() & SEM_UNDO

Q1. What are the three types of semop() operations based on sem_op value?

Negative: decrement the semaphore (wait/acquire). Positive: increment the semaphore (signal/release). Zero: wait until the semaphore reaches 0.

Q2. What does atomicity mean in the context of semop()?

When you pass multiple sembuf operations to semop(), either all of them are applied together or none are. The kernel does not apply a partial set. This enables deadlock-free acquisition of multiple resources in one call.

Q3. What is the purpose of SEM_UNDO, and when would you NOT want to use it?

SEM_UNDO records the inverse of each operation in a per-process semadj table. On process exit, the kernel reverses all flagged operations. You would NOT use it when the semaphore value represents a persistent resource state that should survive the process — for example, a semaphore tracking a file state that another process will continue to manage.

Q4. Process A decrements a semaphore with SEM_UNDO and then calls exec(). What happens to the semadj entry?

exec() preserves semadj entries. The new program still has the pending undo adjustments. This is intentional — the kernel wants to ensure cleanup even if the process image changes.

Q5. What happens if two processes are waiting to decrement the same semaphore by different amounts, and the semaphore is incremented by 1?

Only the process whose required decrement can now be satisfied will be woken. Operations complete in the order they become possible, not in FIFO order of waiting. A process needing a large decrement may be starved if the semaphore keeps fluctuating near small values.

Q6. How does IPC_NOWAIT differ from a timed wait on a semaphore?

IPC_NOWAIT returns immediately with EAGAIN if the operation cannot proceed. System V semaphores do not have a built-in timed wait (semtimedop is a Linux extension). For timed waits, semtimedop() on Linux accepts a timespec argument.

Next: semctl() — Control Operations

Learn how to initialize, query, and delete semaphores using semctl().

Leave a Reply

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