System V Semaphores – semop() Performing Semaphore Operations

 

System V Semaphores – semop()
Chapter 47 · Part 5 · Performing Semaphore Operations
semop()
Core API
sembuf
Operation struct
Atomic
All-or-nothing

What is semop()?

semop() is the Linux system call used to perform operations on System V semaphores. Once you have created a semaphore set with semget(), you use semop() to increment, decrement, or test the values of semaphores in that set.

The most important property of semop() is atomicity: when you pass multiple operations in one call, the kernel either performs ALL of them or NONE of them. This is what makes semaphores safe for process synchronization.

Think of a semaphore like a counter. Processes compete to acquire and release resources by incrementing or decrementing this counter. semop() is the gatekeeper.

Key Terms
semop() sembuf sem_num sem_op sem_flg IPC_NOWAIT SEM_UNDO EAGAIN Blocking Atomic Operations SEMNCNT SEMZCNT

1. semop() – Function Prototype

The semop() system call is defined in <sys/sem.h>:

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

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

Parameters:

  • semid — the semaphore set identifier returned by semget()
  • sops — pointer to an array of struct sembuf describing the operations to perform
  • nsops — the number of operations in the array (how many elements in sops)

Return value: Returns 0 on success, -1 on error (with errno set).

semop() Call Flow
Process
calls semop()
Kernel
checks all ops
Possible?
ALL succeed?
Execute ALL
atomically
If ANY operation cannot be performed, the kernel blocks the process (or returns EAGAIN with IPC_NOWAIT)

2. struct sembuf – The Operation Descriptor

Each element in the sops[] array is a struct sembuf. This structure tells the kernel which semaphore to operate on, what to do to it, and any flags to apply.

struct sembuf {
    unsigned short sem_num;  /* Semaphore number (index in the set) */
    short          sem_op;   /* Operation: +ve, -ve, or 0           */
    short          sem_flg;  /* Flags: IPC_NOWAIT, SEM_UNDO         */
};

Field Type Meaning Example
sem_num unsigned short Index (0-based) of the semaphore inside the set 0 = first semaphore
sem_op short >0 → add to semaphore (release / signal)
<0 → subtract (acquire / wait)
=0 → wait until value becomes 0
-1, +1, 0
sem_flg short Bitwise OR of flags: IPC_NOWAIT, SEM_UNDO, or 0 IPC_NOWAIT

3. How sem_op Works – The Three Cases

Understanding what sem_op does in each case is the heart of using semaphores correctly.

Case 1: sem_op > 0 (Positive)

The kernel adds sem_op to the semaphore’s current value.

This is a release or signal operation. It increases the available resource count. This operation never blocks.

/* Release: semval += sem_op */
sops[0].sem_op = +2; /* Add 2 */
Case 2: sem_op < 0 (Negative)

The kernel subtracts |sem_op| from the semaphore.

If semval >= |sem_op|: subtract immediately.
If semval < |sem_op|: block the calling process until the value is large enough.

/* Acquire: blocks if semval < 1 */
sops[0].sem_op = -1;
Case 3: sem_op == 0 (Zero Test)

The process waits until the semaphore’s value equals 0.

If semval == 0 already: returns immediately.
If semval != 0: blocks until another process reduces it to 0.

/* Wait for zero (barrier sync) */
sops[0].sem_op = 0;

Practical example – mutual exclusion (binary semaphore):

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

/* Acquire the semaphore (P operation / lock) */
void sem_lock(int semid) {
    struct sembuf sop;
    sop.sem_num = 0;   /* Semaphore 0 in the set */
    sop.sem_op  = -1;  /* Subtract 1 */
    sop.sem_flg = 0;   /* Block if necessary */
    if (semop(semid, &sop, 1) == -1)
        perror("semop lock");
}

/* Release the semaphore (V operation / unlock) */
void sem_unlock(int semid) {
    struct sembuf sop;
    sop.sem_num = 0;
    sop.sem_op  = +1;  /* Add 1 back */
    sop.sem_flg = 0;
    if (semop(semid, &sop, 1) == -1)
        perror("semop unlock");
}

4. IPC_NOWAIT Flag – Non-blocking semop()

By default, if a semaphore operation cannot be completed immediately (e.g., trying to subtract 1 from a semaphore that is already 0), semop() blocks the calling process until the operation can be satisfied.

If you set IPC_NOWAIT in sem_flg, the kernel will not block. Instead, if the operation would have blocked, semop() returns immediately with -1 and sets errno to EAGAIN.

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

/* Try to acquire semaphore; don't block if unavailable */
int sem_try_lock(int semid) {
    struct sembuf sop;
    sop.sem_num = 0;
    sop.sem_op  = -1;
    sop.sem_flg = IPC_NOWAIT;  /* Non-blocking */

    if (semop(semid, &sop, 1) == -1) {
        if (errno == EAGAIN) {
            printf("Semaphore busy, not blocking\n");
            return 0;  /* Could not acquire */
        }
        perror("semop");
        return -1;
    }
    return 1;  /* Acquired successfully */
}

Blocking vs Non-blocking Behavior
Scenario Default (no flag) IPC_NOWAIT
sem_op=-1, semval=1 ✔ Succeeds immediately ✔ Succeeds immediately
sem_op=-1, semval=0 Blocks until semval > 0 Returns -1, errno=EAGAIN
sem_op=0, semval=0 ✔ Succeeds immediately ✔ Succeeds immediately
sem_op=0, semval=5 Blocks until semval == 0 Returns -1, errno=EAGAIN

5. Multiple Operations in One Call – Atomicity

You can pass an array of struct sembuf to perform multiple operations atomically in a single semop() call. This is one of the key advantages of System V semaphores.

The rule: Either ALL operations succeed, or NONE are applied. If any one operation would block (or fail with IPC_NOWAIT), the kernel suspends the entire group.

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

int main(void) {
    int semid = 0; /* Assume valid semaphore set ID */

    struct sembuf sops[3];

    /* Operation 1: Subtract 1 from semaphore 0 (acquire resource A) */
    sops[0].sem_num = 0;
    sops[0].sem_op  = -1;
    sops[0].sem_flg = 0;

    /* Operation 2: Add 2 to semaphore 1 (release two units of resource B) */
    sops[1].sem_num = 1;
    sops[1].sem_op  = 2;
    sops[1].sem_flg = 0;

    /* Operation 3: Test semaphore 2 equals 0 (wait for condition C)
       but with IPC_NOWAIT so we don't block on this one specifically */
    sops[2].sem_num = 2;
    sops[2].sem_op  = 0;
    sops[2].sem_flg = IPC_NOWAIT;

    if (semop(semid, sops, 3) == -1) {
        if (errno == EAGAIN)
            printf("Operation 3 would have blocked – entire call skipped\n");
        else
            perror("semop");
    } else {
        printf("All 3 operations completed atomically\n");
    }
    return 0;
}

Why atomicity matters:

Without atomicity, a process might acquire resource A but fail to acquire resource B, leading to partial states and deadlock. System V semaphores solve this by making multi-resource acquisition all-or-nothing.

6. SEMNCNT and SEMZCNT – Waiting Process Counts

Each semaphore in a set has two associated kernel counters that track how many processes are waiting on it. These are visible via semctl() with GETNCNT and GETZCNT.

Counter Meaning semctl() command
SEMNCNT Number of processes blocked waiting for semaphore value to increase (i.e., doing a negative sem_op that can’t be satisfied yet) GETNCNT
SEMZCNT Number of processes blocked waiting for semaphore value to become zero (doing a zero-test sem_op = 0 that’s not yet satisfied) GETZCNT
#include <sys/sem.h>
#include <stdio.h>

void print_wait_counts(int semid, int semnum) {
    int ncnt = semctl(semid, semnum, GETNCNT);
    int zcnt = semctl(semid, semnum, GETZCNT);
    printf("Sem[%d]: SEMNCNT=%d, SEMZCNT=%d\n", semnum, ncnt, zcnt);
}

In the shell session from the book, after 3 background processes block on a 2-semaphore set, you see:

$ ./svsem_mon 32769
Sem # Value SEMPID SEMNCNT SEMZCNT
 0     1     0       1       1
 1     0     0       2       0

Semaphore 0 has value 1 but one process is waiting to decrement it further (SEMNCNT=1) and one is waiting for it to become zero (SEMZCNT=1). Semaphore 1 has value 0 and two processes are waiting to decrement it (SEMNCNT=2).

7. Complete Example – Producer/Consumer with semop()

A producer increments a semaphore to signal data is ready; a consumer decrements it to consume. This demonstrates the positive/negative sem_op pattern in a realistic context.

/* producer_consumer_sem.c
   Compile: gcc producer_consumer_sem.c -o pc_sem
   Usage: run two instances: ./pc_sem producer  and  ./pc_sem consumer
*/
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SEM_KEY  0x1234
#define SEM_PERMS 0600

/* semop wrapper for a single operation */
static void do_semop(int semid, int num, int op, int flags) {
    struct sembuf sop;
    sop.sem_num = (unsigned short) num;
    sop.sem_op  = (short) op;
    sop.sem_flg = (short) flags;
    if (semop(semid, &sop, 1) == -1) {
        perror("semop");
        exit(EXIT_FAILURE);
    }
}

/* Get or create a semaphore set of 1 semaphore, initialized to 0 */
static int get_semaphore(void) {
    int semid = semget(SEM_KEY, 1, IPC_CREAT | SEM_PERMS);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
    return semid;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s producer|consumer\n", argv[0]);
        return 1;
    }

    int semid = get_semaphore();

    if (strcmp(argv[1], "producer") == 0) {
        printf("[Producer] Producing item...\n");
        sleep(2);  /* Simulate production time */
        printf("[Producer] Item ready. Signaling consumer.\n");
        do_semop(semid, 0, +1, 0);  /* Signal: semval becomes 1 */

    } else if (strcmp(argv[1], "consumer") == 0) {
        printf("[Consumer] Waiting for item...\n");
        do_semop(semid, 0, -1, 0);  /* Wait: blocks until semval >= 1 */
        printf("[Consumer] Got item! Processing.\n");

        /* Clean up semaphore set */
        semctl(semid, 0, IPC_RMID);
        printf("[Consumer] Semaphore removed.\n");
    }

    return 0;
}

Run in two terminals: first start consumer (it blocks), then start producer. The consumer unblocks as soon as the producer signals.

8. Counting Semaphore – Managing N Resources

A counting semaphore controls access to a pool of N identical resources (e.g., N database connections, N worker threads). Initialize to N; each acquisition subtracts 1; each release adds 1.

/* counting_semaphore.c – Manage a pool of 3 resources */
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define POOL_SIZE 3  /* 3 resources in the pool */

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

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

    union semun arg;
    arg.val = n;  /* Initialize semaphore to pool size */
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(1);
    }
    printf("Counting semaphore created, initial value = %d\n", n);
    return semid;
}

void acquire_resource(int semid) {
    struct sembuf sop = { 0, -1, 0 };  /* Decrement by 1, blocking */
    printf("[PID %d] Acquiring resource...\n", getpid());
    semop(semid, &sop, 1);
    printf("[PID %d] Resource acquired!\n", getpid());
}

void release_resource(int semid) {
    struct sembuf sop = { 0, +1, 0 };  /* Increment by 1 */
    semop(semid, &sop, 1);
    printf("[PID %d] Resource released.\n", getpid());
}

int get_available(int semid) {
    return semctl(semid, 0, GETVAL);
}

int main(void) {
    int semid = create_counting_sem(POOL_SIZE);
    int i;

    printf("Available resources: %d\n", get_available(semid));

    /* Simulate 3 processes acquiring resources */
    for (i = 0; i < POOL_SIZE; i++) {
        if (fork() == 0) {
            acquire_resource(semid);
            sleep(2);  /* Use the resource */
            release_resource(semid);
            exit(0);
        }
    }

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

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

Interview Questions – semop()
Q1. What does semop() do and what are its parameters?

semop(int semid, struct sembuf *sops, size_t nsops) performs one or more semaphore operations on the semaphore set identified by semid. The sops array describes each operation (which semaphore, what value change, what flags). nsops is the count of operations. The key feature is that all operations in one call are applied atomically.

Q2. What is the significance of atomicity in semop()?

When multiple operations are passed to semop(), the kernel either executes ALL of them or NONE. This prevents partial states. For example, if a process needs to acquire two resources simultaneously, atomicity ensures it won’t hold one while waiting for the other — preventing a class of deadlock scenarios.

Q3. What are the three possible values of sem_op and what does each do?

Positive (sem_op > 0): Adds the value to the semaphore. Never blocks. Used to signal/release.
Negative (sem_op < 0): Subtracts |sem_op| from semaphore. Blocks if semval < |sem_op|. Used to acquire/wait.
Zero (sem_op == 0): Waits until the semaphore reaches exactly 0. Used as a barrier or completion signal.

Q4. What is IPC_NOWAIT and when would you use it?

IPC_NOWAIT in sem_flg makes the operation non-blocking. If the operation would block, semop() immediately returns -1 with errno = EAGAIN. Use it when a process should try to acquire a resource but must remain responsive (e.g., timeout loops, try-lock patterns, real-time systems where blocking is unacceptable).

Q5. What is SEMNCNT vs SEMZCNT?

SEMNCNT is the count of processes blocked waiting to decrement the semaphore (waiting for it to increase enough). SEMZCNT is the count of processes blocked waiting for the semaphore value to become zero. Both are accessible via semctl() with GETNCNT and GETZCNT commands.

Q6. Can IPC_NOWAIT apply to only some operations in a multi-operation semop() call?

Yes. IPC_NOWAIT is per-operation (in sem_flg of each struct sembuf). If any operation with IPC_NOWAIT would block, the entire semop() call fails with EAGAIN — none of the operations are applied. This maintains atomicity.

Q7. What signals can interrupt a blocking semop() call?

A blocking semop() can be interrupted by a signal, in which case it returns -1 with errno = EINTR. The operation is NOT applied in that case. Robust code must handle EINTR by restarting the call (often using SA_RESTART in signal handlers or a retry loop).

Q8. What is the difference between a binary semaphore and a counting semaphore?

A binary semaphore has only values 0 or 1 and is used for mutual exclusion (like a mutex). A counting semaphore starts at N and controls access to a pool of N identical resources. System V semaphores natively support counting semaphores — the value can be any non-negative integer up to SEMVMX (typically 32767).

Continue the Series

Next: svsem_op Example Program & Shell Session Demo

Next Part → EmbeddedPathashala Home

Leave a Reply

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