System V Semaphores Part 2: The semop() System Call

 

System V Semaphores
Chapter 47 – Part 2: The semop() System Call
47.6
TLPI Section
semop
Core Syscall
Atomic
Key Property

What you will learn in this file

semop() is the heart of System V semaphore usage. It lets you increment, decrement, or wait-for-zero on one or more semaphores in a single atomic operation. You will understand the sembuf structure, what each field does, what happens when an operation blocks, and how to use IPC_NOWAIT for non-blocking behavior.

Key Terms

semop() struct sembuf sem_num sem_op sem_flg IPC_NOWAIT SEM_UNDO EAGAIN EINTR EIDRM Atomic Operations

1. semop() – Function Signature
#include <sys/types.h>   /* For portability */
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned int nsops);
/* Returns 0 on success, -1 on error */
Parameter Type Meaning
semid int Semaphore set ID from semget()
sops struct sembuf * Array of operations to perform
nsops unsigned int Number of elements in sops array (≥ 1)

The key feature: if you pass multiple operations in the array, the kernel performs them all or nothing — atomically. Either all operations succeed immediately, or none of them are applied and the call blocks (or fails with EAGAIN if IPC_NOWAIT is set).

2. struct sembuf – The Operation Descriptor

Each element in the sops array is a struct sembuf:

struct sembuf {
    unsigned short sem_num;  /* Which semaphore in the set (0-based index) */
    short          sem_op;   /* What to do: >0 release, <0 acquire, ==0 wait */
    short          sem_flg;  /* Flags: IPC_NOWAIT, SEM_UNDO */
};

Field Value Behavior Permission needed
sem_op > 0 Add sem_op to semaphore value. Wakes blocked decrementers. Write (alter)
== 0 Block until semaphore value reaches 0. If already 0, returns immediately. Read
< 0 Subtract |sem_op| from semaphore value. Blocks if value < |sem_op|. Write (alter)
sem_flg IPC_NOWAIT Never block. If operation can’t complete, fail with EAGAIN.
SEM_UNDO Kernel auto-reverses this operation if the process exits. Prevents stuck semaphores.

sem_op > 0
Release / Post
Resource freed
Value increases
sem_op == 0
Wait for Zero
Synchronization point
Read-only semantics
sem_op < 0
Acquire / Wait
Resource claimed
Value decreases

3. Blocking Behavior – When semop() Waits

When semop() cannot complete immediately (e.g., trying to decrement a semaphore that’s already 0), the calling process is blocked by the kernel until one of three events occurs:

Wake-up Event Result errno
Another process modifies the semaphore so the operation can now proceed semop() completes, returns 0
A signal is delivered to the blocked process semop() fails with -1 EINTR
Another process deletes the semaphore set semop() fails with -1 EIDRM

Important: Unlike many system calls, semop() is never automatically restarted after being interrupted by a signal handler (the SA_RESTART flag has no effect on it). If you need retry-on-signal behavior you must implement it yourself with a loop.

4. Code Example – Binary Semaphore as a Mutex

A semaphore initialized to 1 acts as a mutual exclusion lock. Decrementing by 1 acquires the lock; incrementing by 1 releases it.

/* svsem_mutex.c
 * Uses a System V semaphore as a binary mutex (lock/unlock).
 * Two helper functions: sem_lock() and sem_unlock().
 * Compile: gcc svsem_mutex.c -o svsem_mutex
 */

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

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

/* Acquire (lock) semaphore — decrement by 1 */
int sem_lock(int semid)
{
    struct sembuf sop;
    sop.sem_num = 0;
    sop.sem_op  = -1;          /* Decrement by 1 */
    sop.sem_flg = SEM_UNDO;    /* Auto-release if process dies */
    return semop(semid, &sop, 1);
}

/* Release (unlock) semaphore — increment by 1 */
int sem_unlock(int semid)
{
    struct sembuf sop;
    sop.sem_num = 0;
    sop.sem_op  = 1;           /* Increment by 1 */
    sop.sem_flg = SEM_UNDO;
    return semop(semid, &sop, 1);
}

/* Get current semaphore value (for debugging) */
int sem_getval(int semid)
{
    return semctl(semid, 0, GETVAL);
}

int main(void)
{
    int semid;
    union semun arg;

    /* Create a private semaphore set with 1 semaphore */
    semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

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

    printf("Initial semaphore value: %d\n", sem_getval(semid));

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid == 0) {
        /* --- CHILD --- */
        printf("Child [%d]: trying to acquire mutex...\n", getpid());
        if (sem_lock(semid) == -1) { perror("sem_lock"); exit(EXIT_FAILURE); }
        printf("Child [%d]: acquired! value=%d\n", getpid(), sem_getval(semid));
        printf("Child [%d]: working for 2 seconds...\n", getpid());
        sleep(2);
        printf("Child [%d]: releasing mutex\n", getpid());
        if (sem_unlock(semid) == -1) { perror("sem_unlock"); exit(EXIT_FAILURE); }
        printf("Child [%d]: done, value=%d\n", getpid(), sem_getval(semid));
        exit(0);
    } else {
        /* --- PARENT --- */
        sleep(0);  /* Let child get scheduled first */
        printf("Parent [%d]: trying to acquire mutex...\n", getpid());
        if (sem_lock(semid) == -1) { perror("sem_lock"); exit(EXIT_FAILURE); }
        printf("Parent [%d]: acquired! value=%d\n", getpid(), sem_getval(semid));
        printf("Parent [%d]: working for 1 second...\n", getpid());
        sleep(1);
        printf("Parent [%d]: releasing mutex\n", getpid());
        if (sem_unlock(semid) == -1) { perror("sem_unlock"); exit(EXIT_FAILURE); }
        printf("Parent [%d]: done, value=%d\n", getpid(), sem_getval(semid));

        wait(NULL);
        semctl(semid, 0, IPC_RMID);  /* Clean up */
    }

    return 0;
}

Note: SEM_UNDO is used so that if a process crashes while holding the lock, the kernel automatically undoes the decrement and restores the value to 1, preventing a permanent deadlock.

5. Code Example – Counting Semaphore (Resource Pool)

A counting semaphore initialized to N controls access to N identical resources (like a connection pool). Each acquisition decrements by 1; each release increments by 1.

/* svsem_counting.c
 * Counting semaphore — controls access to a pool of N resources.
 * Demonstrates multiple processes competing for limited resources.
 * Compile: gcc svsem_counting.c -o svsem_counting
 */

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

#define POOL_SIZE   3   /* Max concurrent resource users */
#define NUM_WORKERS 6   /* Total worker processes */

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

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

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

    arg.val = POOL_SIZE;  /* 3 resources available */
    semctl(semid, 0, SETVAL, arg);

    printf("Resource pool size: %d\n", POOL_SIZE);
    printf("Spawning %d workers...\n\n", NUM_WORKERS);

    for (i = 0; i < NUM_WORKERS; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            /* --- Worker child --- */
            struct sembuf acquire = {0, -1, SEM_UNDO};  /* Decrement */
            struct sembuf release = {0,  1, SEM_UNDO};  /* Increment */

            printf("Worker %d [%d]: waiting for resource...\n", i, getpid());

            if (semop(semid, &acquire, 1) == -1) {
                perror("semop acquire"); exit(EXIT_FAILURE);
            }

            printf("Worker %d [%d]: GOT resource (pool remaining: %d)\n",
                   i, getpid(), semctl(semid, 0, GETVAL));

            sleep(2);  /* Use the resource */

            if (semop(semid, &release, 1) == -1) {
                perror("semop release"); exit(EXIT_FAILURE);
            }

            printf("Worker %d [%d]: RELEASED resource\n", i, getpid());
            exit(0);
        }
    }

    /* Parent waits for all workers */
    for (i = 0; i < NUM_WORKERS; i++)
        wait(NULL);

    printf("\nAll workers done. Final pool value: %d\n",
           semctl(semid, 0, GETVAL));

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

6. Code Example – Non-Blocking with IPC_NOWAIT

Use IPC_NOWAIT when you want to try acquiring a semaphore but not block if it’s unavailable. If the operation would block, semop() returns -1 with errno == EAGAIN.

/* svsem_nowait.c
 * Demonstrates IPC_NOWAIT for non-blocking semaphore acquisition.
 * The process tries to acquire the semaphore. If busy, it does
 * other work and retries.
 * Compile: gcc svsem_nowait.c -o svsem_nowait
 */

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

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

int try_acquire(int semid)
{
    struct sembuf sop;
    sop.sem_num = 0;
    sop.sem_op  = -1;
    sop.sem_flg = IPC_NOWAIT | SEM_UNDO;  /* Non-blocking */

    if (semop(semid, &sop, 1) == 0)
        return 1;   /* Acquired */

    if (errno == EAGAIN)
        return 0;   /* Resource busy — try later */

    perror("semop");
    return -1;      /* Real error */
}

int main(void)
{
    int semid;
    union semun arg;
    int attempts = 0;
    int result;

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

    /* Initialize to 0 — resource is currently locked */
    arg.val = 0;
    semctl(semid, 0, SETVAL, arg);

    printf("Semaphore value: %d (resource locked)\n",
           semctl(semid, 0, GETVAL));

    /* Simulate a lock-holder releasing after 3 seconds */
    pid_t pid = fork();
    if (pid == 0) {
        sleep(3);
        struct sembuf release = {0, 1, 0};
        semop(semid, &release, 1);
        printf("Lock-holder: released the lock\n");
        exit(0);
    }

    /* Main process: try non-blocking acquire */
    printf("Main: trying non-blocking acquire...\n");
    while (1) {
        attempts++;
        result = try_acquire(semid);
        if (result == 1) {
            printf("Main: acquired on attempt %d!\n", attempts);
            break;
        } else if (result == 0) {
            printf("Main: attempt %d failed (EAGAIN), doing other work...\n",
                   attempts);
            sleep(1);  /* Do something else instead of blocking */
        } else {
            exit(EXIT_FAILURE);
        }
    }

    /* Use resource */
    printf("Main: using resource...\n");

    /* Release */
    struct sembuf release = {0, 1, SEM_UNDO};
    semop(semid, &release, 1);
    printf("Main: released\n");

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

7. Code Example – Atomically Operating on Multiple Semaphores

One of the most powerful features of semop() is performing operations on multiple semaphores atomically. Consider a producer/consumer system with two semaphores: empty (slots available) and full (items available).

Role sem[0] = empty (slots free) sem[1] = full (items ready)
Producer Decrement (wait for free slot) Increment (signal item added)
Consumer Increment (signal slot freed) Decrement (wait for item)
/* svsem_producer_consumer.c
 * Classic bounded buffer with two semaphores:
 *   sem[0] = empty slots (initially BUFFER_SIZE)
 *   sem[1] = filled slots (initially 0)
 * Compile: gcc svsem_producer_consumer.c -o svsem_pc
 */

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

#define BUFFER_SIZE  4
#define NUM_ITEMS   10

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

int main(void)
{
    int semid;
    union semun arg;
    unsigned short init_vals[2];

    /* Create semaphore set with 2 semaphores */
    semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    /* sem[0] = empty = BUFFER_SIZE (all slots free) */
    /* sem[1] = full  = 0          (no items yet)    */
    init_vals[0] = BUFFER_SIZE;
    init_vals[1] = 0;
    arg.array = init_vals;
    semctl(semid, 0, SETALL, arg);

    printf("Buffer size: %d\n", BUFFER_SIZE);

    pid_t pid = fork();
    if (pid == 0) {
        /* --- CONSUMER --- */
        int i;
        struct sembuf consume[2];

        for (i = 0; i < NUM_ITEMS; i++) {
            /* Wait for a full slot, then signal an empty slot.
             * These two operations happen atomically. */
            consume[0].sem_num = 1; consume[0].sem_op = -1; consume[0].sem_flg = SEM_UNDO; /* full-- */
            consume[1].sem_num = 0; consume[1].sem_op =  1; consume[1].sem_flg = SEM_UNDO; /* empty++ */

            semop(semid, consume, 2);  /* Atomic pair */
            printf("Consumer: consumed item %d (full=%d empty=%d)\n",
                   i, semctl(semid,1,GETVAL), semctl(semid,0,GETVAL));
            usleep(150000);  /* 150ms */
        }
        exit(0);
    } else {
        /* --- PRODUCER --- */
        int i;
        struct sembuf produce[2];

        for (i = 0; i < NUM_ITEMS; i++) {
            /* Wait for an empty slot, then signal a full slot.
             * These two operations happen atomically. */
            produce[0].sem_num = 0; produce[0].sem_op = -1; produce[0].sem_flg = SEM_UNDO; /* empty-- */
            produce[1].sem_num = 1; produce[1].sem_op =  1; produce[1].sem_flg = SEM_UNDO; /* full++ */

            semop(semid, produce, 2);  /* Atomic pair */
            printf("Producer: produced item %d (full=%d empty=%d)\n",
                   i, semctl(semid,1,GETVAL), semctl(semid,0,GETVAL));
            usleep(100000);  /* 100ms */
        }
        wait(NULL);
        semctl(semid, 0, IPC_RMID);
    }

    return 0;
}

Why atomic? If the consumer’s decrement of full and increment of empty were two separate semop() calls, a crash between them could permanently lose a buffer slot. By passing both in one call, the kernel guarantees both happen or neither does.

8. Code Example – Handling EINTR (Signal Interruption)

Because semop() is never auto-restarted after a signal, production code should wrap it in a retry loop:

/* Robust semop() wrapper that retries on EINTR */
#include <errno.h>
#include <sys/sem.h>

int robust_semop(int semid, struct sembuf *sops, int nsops)
{
    int ret;
    while (1) {
        ret = semop(semid, sops, nsops);
        if (ret == 0)
            return 0;   /* Success */

        if (errno == EINTR) {
            /* Interrupted by signal — retry */
            continue;
        }

        /* EAGAIN: would block and IPC_NOWAIT was set */
        /* EIDRM:  semaphore set was deleted          */
        /* Other errors are fatal                     */
        return -1;
    }
}

9. Interview Questions & Answers
Q1. What does sem_op = 0 mean in struct sembuf?

It means “wait until the semaphore value reaches zero.” If the value is already 0, semop() returns immediately. Otherwise, the process blocks. This is used as a synchronization point — for example, waiting for all worker processes to finish (each worker decrements a shared semaphore, and the coordinator waits for it to reach 0).

Q2. What is SEM_UNDO and when should you use it?

SEM_UNDO tells the kernel to remember what change this operation made to the semaphore and reverse it if the process exits (normally or abnormally). This prevents a process from dying while holding a semaphore lock, which would leave other processes blocked forever. It should generally always be set for acquire operations on mutex-style semaphores.

Q3. What does “atomically” mean in the context of semop() with multiple operations?

When you pass an array of N operations to semop(), the kernel either performs all of them or none of them. It never performs a partial subset. If any one operation would block, the entire call blocks (unless IPC_NOWAIT is set, in which case the entire call fails with EAGAIN). This guarantees consistency — you cannot leave the system in a half-applied state.

Q4. What are the three reasons a blocked semop() can be unblocked?

(1) Another process changes the semaphore value so the blocked operation can now complete. (2) A signal is delivered to the blocking process — semop() returns -1 with errno == EINTR. (3) The semaphore set is deleted by another process — semop() returns -1 with errno == EIDRM.

Q5. What is the difference between IPC_NOWAIT on semop() vs. on semget()?

On semget(), IPC_NOWAIT is not a valid flag (it uses IPC_CREAT and IPC_EXCL). On semop(), IPC_NOWAIT in sem_flg means: if this specific operation would cause the call to block, instead fail immediately with EAGAIN. If multiple operations are given, and any one of them has IPC_NOWAIT and would block, the entire call fails with EAGAIN and no operations are performed.

Q6. Why is semop() not restarted after a signal, unlike read() or write()?

POSIX and Linux classify semop() as a “slow” system call that cannot be restarted via SA_RESTART because restarting it could cause subtle race conditions in semaphore-based synchronization protocols. The caller is expected to handle EINTR explicitly and decide whether to retry based on the application’s logic. This gives the application more control.

Q7. What permission does a process need to perform various semop() operations?

For sem_op > 0 (increment) or sem_op < 0 (decrement), the process needs write (alter) permission on the semaphore set. For sem_op == 0 (wait-for-zero), the process only needs read permission. Permissions are set when the semaphore set is created via semget() and can be changed with semctl(IPC_SET).

Q8. What happens if a process calls semop() with nsops = 0?

The call fails immediately with errno == EINVAL because the specification requires the array to contain at least one element (nsops ≥ 1). Passing zero operations is meaningless and not allowed.

Continue Learning

Next: semtimedop() — blocking semaphore operations with a timeout

Part 3: semtimedop() → ← Part 1: Initialization

Leave a Reply

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