semop() — Performing Semaphore Operations

 

semop() — Performing Semaphore Operations
Chapter 47 · The Linux Programming Interface · Part 4
⚙️ Acquire & Release
⚛️ Atomic Operations
🚩 IPC_NOWAIT & SEM_UNDO

Function Signature
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

/* Returns: 0 on success, -1 on error */

/* Each operation in the sops array is described by this struct */
struct sembuf {
    unsigned short sem_num;  /* which semaphore in the set (0-based index) */
    short          sem_op;   /* operation: positive=add, negative=subtract, 0=wait-for-zero */
    short          sem_flg;  /* flags: 0, IPC_NOWAIT, SEM_UNDO, or combination */
};

semop() is the workhorse — it actually performs operations on semaphore values. It can operate on one or multiple semaphores in a set atomically in a single system call.

The sem_op Field — Three Types of Operations

sem_op > 0 (positive) — Add / Release / Signal

The semaphore value is increased by sem_op. This never blocks. Used to release a resource or signal another process.

Example: After finishing with a shared resource, you add 1 back. Processes waiting to acquire will be woken up.

sem_op < 0 (negative) — Subtract / Acquire / Wait

The kernel checks if semaphore_value + sem_op ≥ 0. If yes, the value is decreased and the call returns immediately. If no, the process is blocked until the condition can be satisfied.

Example: Trying to subtract 1 from a semaphore = 0 blocks. Trying to subtract 3 from a semaphore = 5 succeeds immediately (result = 2).

sem_op == 0 — Wait for Zero

The call blocks until the semaphore value becomes exactly 0. If the value is already 0, it returns immediately.

Use case: A supervisor process waits for all workers to finish (workers decrement a counter; when it reaches 0, all are done).

semop() Decision Logic (kernel view)
semop() called with sem_op = X

X > 0
Add X to value
Never blocks

X < 0
value + X ≥ 0?
YES
value += X
return 0
NO
BLOCK
wait…

X == 0
value == 0?
YES
return 0
NO
BLOCK
wait…

The sem_flg Field — Flags
Flag Meaning Use case
0 No flags — default blocking behaviour Most common; block if operation can’t complete now
IPC_NOWAIT Non-blocking. If operation can’t complete immediately, return -1 with EAGAIN Try-lock pattern. Avoid blocking in time-critical sections.
SEM_UNDO Auto-reverse this operation when the process exits Prevent semaphore from being left locked if process crashes

IPC_NOWAIT — Non-blocking Try-Lock
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>

/*
 * Try to acquire semaphore without blocking.
 * Returns: 0 if acquired, -1 if not available (EAGAIN), -2 on error.
 */
int sem_trywait(int semid, int semnum) {
    struct sembuf sop;
    sop.sem_num = semnum;
    sop.sem_op  = -1;             /* try to subtract 1 */
    sop.sem_flg = IPC_NOWAIT;     /* don't block — return EAGAIN if can't */

    if (semop(semid, &sop, 1) == -1) {
        if (errno == EAGAIN) {
            /* Resource not available right now */
            return -1;
        }
        perror("semop trywait");
        return -2;
    }
    return 0;  /* acquired! */
}

/* Usage: try to acquire, do something else if not available */
void process_with_trywait(int semid) {
    int result = sem_trywait(semid, 0);

    if (result == 0) {
        printf("Acquired semaphore — entering critical section\n");

        /* ... do protected work ... */

        /* Release: add 1 back */
        struct sembuf rel = {0, 1, 0};
        semop(semid, &rel, 1);
        printf("Released semaphore\n");

    } else if (result == -1) {
        printf("Resource busy — will try again later or do other work\n");
        /* ... do something else ... */
    }
}

/* Spin-lock pattern using IPC_NOWAIT (not recommended for production) */
void spin_acquire(int semid, int semnum) {
    struct sembuf sop = {semnum, -1, IPC_NOWAIT};
    int attempts = 0;

    while (semop(semid, &sop, 1) == -1) {
        if (errno != EAGAIN) { perror("semop spin"); return; }
        attempts++;
        usleep(1000);  /* 1ms backoff */
        if (attempts > 1000) {
            fprintf(stderr, "Spin timeout after %d attempts\n", attempts);
            return;
        }
    }
    printf("Acquired after %d spin attempts\n", attempts);
}

SEM_UNDO — Automatic Cleanup on Process Exit

When a process acquires a semaphore with SEM_UNDO and then crashes (or exits abnormally), the kernel automatically reverses the operation. This prevents semaphores from being permanently locked.

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

/*
 * Acquire semaphore with SEM_UNDO.
 * If this process exits/crashes without releasing, kernel will add 1 back.
 */
int sem_acquire_with_undo(int semid, int semnum) {
    struct sembuf sop;
    sop.sem_num = semnum;
    sop.sem_op  = -1;
    sop.sem_flg = SEM_UNDO;  /* track this operation for automatic undo */
    return semop(semid, &sop, 1);
}

/*
 * Release semaphore with SEM_UNDO.
 * The undo adjustment is cancelled (adjusted back to 0).
 */
int sem_release_with_undo(int semid, int semnum) {
    struct sembuf sop;
    sop.sem_num = semnum;
    sop.sem_op  = +1;
    sop.sem_flg = SEM_UNDO;  /* cancel the earlier undo adjustment */
    return semop(semid, &sop, 1);
}

/* Demonstrate SEM_UNDO: child crashes, parent sees semaphore released */
int main(void) {
    int semid;
    union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;

    semid = semget(IPC_PRIVATE, 1, 0600);
    if (semid == -1) { perror("semget"); exit(1); }
    arg.val = 1;
    semctl(semid, 0, SETVAL, arg);

    pid_t pid = fork();
    if (pid == 0) {
        /* CHILD: acquire with SEM_UNDO, then crash */
        printf("Child: acquiring semaphore with SEM_UNDO\n");
        if (sem_acquire_with_undo(semid, 0) == -1) {
            perror("acquire"); exit(1);
        }
        int val = semctl(semid, 0, GETVAL);
        printf("Child: acquired! semaphore value = %d\n", val);

        printf("Child: simulating crash (exit without releasing)\n");
        _exit(1);  /* crash — no cleanup */

    } else {
        /* PARENT: wait for child, then check semaphore */
        wait(NULL);
        int val = semctl(semid, 0, GETVAL);
        printf("Parent: semaphore value after child crash = %d\n", val);
        /* value = 1 again! Kernel auto-reversed the child's acquire */
        /* WITHOUT SEM_UNDO this would be 0 — permanently locked */

        semctl(semid, 0, IPC_RMID);
    }
    return 0;
}
How SEM_UNDO works internally: Each process has a per-semaphore semadj adjustment value. When you acquire with SEM_UNDO (subtract 1), the kernel records +1 in the process’s semadj for that semaphore. When the process exits, the kernel adds semadj to the semaphore value, effectively reversing the operation.

Atomic Operations on Multiple Semaphores

The most powerful feature of semop() is the ability to operate on multiple semaphores in a single atomic step. This prevents deadlocks that would occur with individual operations.

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

/*
 * Classic deadlock scenario WITHOUT atomic operations:
 *
 * Process A: acquire sem[0], then acquire sem[1]
 * Process B: acquire sem[1], then acquire sem[0]
 *
 * Both can block waiting for the other → DEADLOCK
 *
 * Solution: acquire BOTH atomically in one semop() call
 */

/* Atomically acquire two resources at once */
int acquire_two_resources(int semid) {
    struct sembuf ops[2];

    ops[0].sem_num = 0;   /* resource 1 */
    ops[0].sem_op  = -1;
    ops[0].sem_flg = SEM_UNDO;

    ops[1].sem_num = 1;   /* resource 2 */
    ops[1].sem_op  = -1;
    ops[1].sem_flg = SEM_UNDO;

    /*
     * ATOMIC: either BOTH operations succeed, or NEITHER does.
     * If sem[1] is not available, the kernel will NOT decrement sem[0].
     * The process blocks until BOTH can be acquired simultaneously.
     */
    return semop(semid, ops, 2);
}

/* Atomically release both resources */
int release_two_resources(int semid) {
    struct sembuf ops[2];

    ops[0].sem_num = 0;
    ops[0].sem_op  = +1;
    ops[0].sem_flg = SEM_UNDO;

    ops[1].sem_num = 1;
    ops[1].sem_op  = +1;
    ops[1].sem_flg = SEM_UNDO;

    return semop(semid, ops, 2);
}

/* Producer-consumer: atomically check buffer state */
int producer_deposit(int semid) {
    struct sembuf ops[2];

    /* Atomically: decrement empty-slot count AND acquire mutex */
    ops[0].sem_num = 0;   /* empty slots counter */
    ops[0].sem_op  = -1;  /* take one empty slot */
    ops[0].sem_flg = SEM_UNDO;

    ops[1].sem_num = 1;   /* mutex */
    ops[1].sem_op  = -1;  /* lock mutex */
    ops[1].sem_flg = SEM_UNDO;

    if (semop(semid, ops, 2) == -1) { perror("producer deposit"); return -1; }
    printf("Producer: locked mutex and took empty slot\n");
    return 0;
}
Atomicity guarantee: When you pass multiple sembuf entries to semop(), the kernel applies them as a single atomic unit. If any individual operation would block, none of the operations are applied and the whole call blocks. This eliminates the classic “partial acquisition” deadlock.

Wait-for-Zero Pattern — Barrier Synchronization
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/*
 * Wait-for-zero pattern:
 * - Initialize semaphore to N (number of workers)
 * - Each worker decrements by 1 when done
 * - Supervisor waits for value to reach 0 (all done)
 */

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

void worker(int semid, int worker_id) {
    printf("Worker %d: starting task\n", worker_id);
    sleep(worker_id);  /* simulate variable work time */
    printf("Worker %d: done, decrementing semaphore\n", worker_id);

    struct sembuf done = {0, -1, 0};  /* decrement by 1 */
    if (semop(semid, &done, 1) == -1) { perror("worker decrement"); }
}

void supervisor(int semid) {
    printf("Supervisor: waiting for all workers to complete...\n");

    /* Wait until semaphore value == 0 */
    struct sembuf wait_zero = {0, 0, 0};  /* sem_op = 0 means wait-for-zero */
    if (semop(semid, &wait_zero, 1) == -1) {
        perror("supervisor wait-for-zero");
        return;
    }

    printf("Supervisor: all workers done!\n");
}

int main(void) {
    int semid;
    union semun arg;
    int num_workers = 3;
    int i;

    /* Create semaphore set */
    semid = semget(IPC_PRIVATE, 1, 0600);
    if (semid == -1) { perror("semget"); exit(1); }

    /* Initialize to num_workers */
    arg.val = num_workers;
    if (semctl(semid, 0, SETVAL, arg) == -1) { perror("SETVAL"); exit(1); }

    printf("Semaphore initialized to %d (number of workers)\n", num_workers);

    /* Fork workers */
    for (i = 1; i <= num_workers; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            worker(semid, i);
            exit(0);
        }
    }

    /* Supervisor: wait for all to finish */
    supervisor(semid);

    /* Wait for all children */
    for (i = 0; i < num_workers; i++) wait(NULL);

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

Binary Semaphore as Mutex — Complete Pattern
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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

/* Create a binary semaphore (mutex) initialized to 1 (unlocked) */
int create_mutex(void) {
    int semid;
    union semun arg;

    semid = semget(IPC_PRIVATE, 1, 0600);
    if (semid == -1) { perror("semget"); return -1; }

    arg.val = 1;  /* 1 = unlocked, 0 = locked */
    if (semctl(semid, 0, SETVAL, arg) == -1) { perror("SETVAL"); return -1; }

    return semid;
}

/* Lock the mutex (blocks if already locked) */
int mutex_lock(int semid) {
    struct sembuf sop = {0, -1, SEM_UNDO};  /* subtract 1 */
    return semop(semid, &sop, 1);
}

/* Unlock the mutex */
int mutex_unlock(int semid) {
    struct sembuf sop = {0, +1, SEM_UNDO};  /* add 1 */
    return semop(semid, &sop, 1);
}

/* Try to lock without blocking */
int mutex_trylock(int semid) {
    struct sembuf sop = {0, -1, IPC_NOWAIT | SEM_UNDO};
    if (semop(semid, &sop, 1) == -1) {
        if (errno == EAGAIN) return 1;  /* locked by another */
        return -1;  /* error */
    }
    return 0;  /* successfully locked */
}

/* Shared counter protected by mutex */
static int shared_counter = 0;  /* in real code, this would be in shared mem */

void increment_safely(int semid, int n) {
    int i;
    for (i = 0; i < n; i++) {
        mutex_lock(semid);
        shared_counter++;  /* critical section */
        mutex_unlock(semid);
    }
}

int main(void) {
    int semid = create_mutex();
    if (semid == -1) exit(1);

    /* Fork two processes that increment the counter */
    for (int p = 0; p < 2; p++) {
        if (fork() == 0) {
            increment_safely(semid, 1000);
            printf("Child done\n");
            exit(0);
        }
    }

    wait(NULL); wait(NULL);

    printf("Final counter = %d (expected: in shared mem scenario)\n", shared_counter);
    semctl(semid, 0, IPC_RMID);
    return 0;
}

semop() Error Handling — All errno Values
errno Cause Recovery
EAGAIN IPC_NOWAIT specified and operation would block Try again later or do alternative work
EINTR Blocking semop() was interrupted by a signal Retry the call (loop until success or real error)
EIDRM Semaphore set was deleted while process was blocked Treat as fatal — the resource no longer exists
ERANGE Adding sem_op would make value exceed SEMVMX limit Logic error — check for value overflow
EACCES Process does not have write permission on the set Permission error — check semaphore creation flags
/* Robust semop with signal-safe retry */
int semop_retry(int semid, struct sembuf *sops, size_t nsops) {
    int ret;
    do {
        ret = semop(semid, sops, nsops);
    } while (ret == -1 && errno == EINTR);  /* retry if interrupted by signal */

    if (ret == -1) {
        if (errno == EIDRM) {
            fprintf(stderr, "semop: semaphore set was deleted\n");
        } else if (errno == EAGAIN) {
            /* Normal: non-blocking operation couldn't complete */
            return -1;
        } else {
            perror("semop");
        }
    }
    return ret;
}

Interview Questions — semop()
Q1: What does sem_op = 0 mean in struct sembuf, and when is it useful?
sem_op = 0 means “wait until the semaphore value equals zero.” It does not change the semaphore value — it just blocks until the condition is met. It is useful for barrier synchronization: initialize a semaphore to N (number of workers), have each worker decrement by 1 when done, and have a supervisor call semop with sem_op = 0 to wait until all N workers have finished (value reaches 0).
Q2: What is SEM_UNDO and why is it important?
SEM_UNDO tells the kernel to record a “semaphore adjustment” (semadj) for the operation. When the process exits for any reason (normal or crash), the kernel automatically reverses all operations done with SEM_UNDO. This prevents semaphores from being left in a locked state when a process crashes without releasing them — a common cause of system hangs.
Q3: How does passing multiple sembuf entries to semop() prevent deadlocks?
When you pass multiple sembuf entries, the kernel applies them atomically — either all succeed or none are applied. Consider two processes both needing semaphores A and B: if Process 1 acquires A then tries B, and Process 2 acquires B then tries A, both can deadlock. By requesting both A and B in a single semop() call, you guarantee that a process only proceeds when both are available simultaneously, eliminating the partial-acquisition deadlock.
Q4: What does EINTR mean for a blocked semop() and how should you handle it?
If a process is blocked waiting in semop() and receives a signal (even if the signal handler does nothing), semop() returns -1 with errno = EINTR. This is not a real error — it just means a signal interrupted the wait. The correct handling is to retry the call in a loop: while (semop(...) == -1 && errno == EINTR) continue;. This is especially important for programs that use signals (e.g., SIGCHLD for child monitoring).
Q5: What is the difference between IPC_NOWAIT and SEM_UNDO? Can you use them together?
IPC_NOWAIT changes the blocking behaviour: instead of waiting, return immediately with EAGAIN if the operation can’t proceed. SEM_UNDO affects cleanup: it registers the operation for automatic reversal when the process exits. They serve completely different purposes and can absolutely be used together: sop.sem_flg = IPC_NOWAIT | SEM_UNDO; means “try to acquire non-blocking, and if you do succeed, automatically release it when the process exits.”

Leave a Reply

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