System V Semaphores locking semop() Operations, EAGAIN, and semncnt / semzcnt

 

System V Semaphores
Part 1 โ€“ Blocking semop() Operations, EAGAIN, and semncnt / semzcnt
๐Ÿ“˜ Chapter 47
๐Ÿ”’ IPC / Semaphores
๐ŸŽฏ Interview Ready

What You Will Learn

When you call semop() to decrease a semaphore value and the value is already 0, the kernel blocks your process until the operation can be satisfied. This file explains how blocking works, what the kernel tracks in semncnt and semzcnt, how IPC_NOWAIT and EAGAIN fit in, and how multiple blocked processes get unblocked when another process finally adds to the semaphore.

Key Terms
semop() IPC_NOWAIT EAGAIN semncnt semzcnt sem_otime sempid semget() semid_ds EIDRM

1. Quick Recap โ€“ What is semop()?

semop() is the system call used to atomically perform one or more operations on the semaphores in a set. Each operation says: “add N to semaphore X”, “subtract N from semaphore X”, or “wait until semaphore X equals zero”.

The prototype is:

#include <sys/sem.h>

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

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

Each struct sembuf describes one operation:

struct sembuf {
    unsigned short sem_num;   /* semaphore number in the set (0-based) */
    short          sem_op;    /* operation: positive=add, negative=subtract, 0=wait-for-zero */
    short          sem_flg;   /* IPC_NOWAIT, SEM_UNDO, or 0 */
};

Rules the kernel applies for each operation in the array:

sem_op value Meaning Blocks if โ€ฆ
Positive Add sem_op to semaphore value Never blocks
Negative Subtract |sem_op| from semaphore value semval < |sem_op|
Zero Wait until semaphore value equals 0 semval != 0

2. How Blocking Works Inside the Kernel

When a process calls semop() with a negative sem_op and the semaphore value is too low, the kernel does NOT return an error. Instead it puts the calling process to sleep on a wait queue inside the kernel. The process stays there until:

  • Another process increases the semaphore value enough for this operation to proceed
  • Another process deletes the semaphore set (returns EIDRM)
  • A signal interrupts the wait (returns EINTR)

Process calls semop(sem_op = -1)
โ†“
Kernel checks: semval >= 1 ?
YES
โ†“
Decrement semval, return 0
NO
โ†“
Block process (add to wait queue)
increment semncnt

semncnt is the kernel counter tracking how many processes are blocked waiting to decrease this semaphore.

semzcnt tracks how many processes are blocked waiting for the semaphore to reach zero.

3. Complete Code Example โ€“ Blocking semop()

The following program creates a semaphore set with 2 semaphores, both initialized to 0. It then forks a child that tries to decrease semaphore 0 โ€” which blocks immediately. The parent waits 2 seconds then increases the value so the child can proceed.

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

/* Helper: get current time as string */
static void print_time(const char *msg) {
    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);
    char buf[64];
    strftime(buf, sizeof(buf), "%H:%M:%S", tm_info);
    printf("[%s] PID=%d %s\n", buf, getpid(), msg);
}

int main(void)
{
    int semid;
    struct sembuf sop;

    /* Create a set with 2 semaphores, IPC_PRIVATE = visible only to related processes */
    semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    /* Initialize both semaphores to 0 */
    unsigned short vals[2] = {0, 0};
    if (semctl(semid, 0, SETALL, vals) == -1) { perror("semctl SETALL"); exit(EXIT_FAILURE); }

    printf("Semaphore set created: semid=%d\n", semid);
    printf("Both semaphores initialized to 0\n\n");

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

    if (child == 0) {
        /* CHILD: try to subtract 1 from semaphore 0 -- will BLOCK */
        print_time("Child: calling semop(sem0 - 1) -- will block");

        sop.sem_num = 0;
        sop.sem_op  = -1;   /* decrease by 1 */
        sop.sem_flg = 0;    /* blocking (no IPC_NOWAIT) */

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

        print_time("Child: semop() returned -- semaphore decreased successfully!");
        exit(EXIT_SUCCESS);

    } else {
        /* PARENT: sleep 2 seconds, then add 1 to semaphore 0 to unblock child */
        print_time("Parent: sleeping 2 seconds before unblocking child...");
        sleep(2);

        sop.sem_num = 0;
        sop.sem_op  = +1;   /* increase by 1 */
        sop.sem_flg = 0;

        print_time("Parent: calling semop(sem0 + 1) to unblock child");
        if (semop(semid, &sop, 1) == -1) { perror("semop in parent"); exit(EXIT_FAILURE); }
        print_time("Parent: semop() done, child should be unblocked now");

        /* Wait for child */
        wait(NULL);

        /* Clean up */
        if (semctl(semid, 0, IPC_RMID) == -1) { perror("semctl IPC_RMID"); }
        printf("\nSemaphore set removed.\n");
    }

    return 0;
}

Expected output:

Semaphore set created: semid=131073
Both semaphores initialized to 0

[10:00:00] PID=1234 Parent: sleeping 2 seconds before unblocking child...
[10:00:00] PID=1235 Child: calling semop(sem0 - 1) -- will block
[10:00:02] PID=1234 Parent: calling semop(sem0 + 1) to unblock child
[10:00:02] PID=1234 Parent: semop() done, child should be unblocked now
[10:00:02] PID=1235 Child: semop() returned -- semaphore decreased successfully!

Semaphore set removed.

4. Non-blocking Attempt โ€“ IPC_NOWAIT and EAGAIN

Sometimes you don’t want to block. You want to try the operation and immediately know if it can’t be done right now. Use the IPC_NOWAIT flag in sem_flg. If the operation cannot be performed immediately, semop() returns -1 with errno set to EAGAIN (also known as EWOULDBLOCK).

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

int try_acquire_semaphore(int semid, int sem_num)
{
    struct sembuf sop;
    sop.sem_num = sem_num;
    sop.sem_op  = -1;           /* try to subtract 1 */
    sop.sem_flg = IPC_NOWAIT;   /* do NOT block */

    if (semop(semid, &sop, 1) == -1) {
        if (errno == EAGAIN) {
            printf("Semaphore %d is not available right now (EAGAIN)\n", sem_num);
            return 0;   /* not acquired */
        }
        perror("semop IPC_NOWAIT");
        return -1;  /* real error */
    }

    printf("Semaphore %d acquired successfully\n", sem_num);
    return 1;   /* acquired */
}

int main(void)
{
    /* Create semaphore set: 1 semaphore, value = 0 */
    int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    semctl(semid, 0, SETVAL, 0);  /* value = 0 */

    printf("=== Attempt 1: semaphore value is 0 ===\n");
    int result = try_acquire_semaphore(semid, 0);
    printf("Result: %d\n\n", result);

    /* Now set value to 1 */
    semctl(semid, 0, SETVAL, 1);

    printf("=== Attempt 2: semaphore value is now 1 ===\n");
    result = try_acquire_semaphore(semid, 0);
    printf("Result: %d\n\n", result);

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

Expected output:

=== Attempt 1: semaphore value is 0 ===
Semaphore 0 is not available right now (EAGAIN)
Result: 0

=== Attempt 2: semaphore value is now 1 ===
Semaphore 0 acquired successfully
Result: 1

sem_flg = 0
๐Ÿ˜ด
Process sleeps
until condition met
sem_flg = IPC_NOWAIT
โšก
Returns immediately
errno = EAGAIN

5. Reading semncnt and semzcnt โ€“ Who is Waiting?

The kernel keeps two counters per semaphore inside the semid_ds structure. You can read them at any time using semctl() with GETNCNT and GETZCNT.

Field semctl command What it counts
semncnt GETNCNT Processes blocked waiting to decrease the semaphore
semzcnt GETZCNT Processes blocked waiting for the semaphore to reach zero
sempid GETPID PID of the last process that called semop() on this semaphore
sem_otime via IPC_STAT Timestamp of last successful semop()
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <time.h>

void print_semaphore_status(int semid, int nsems)
{
    struct semid_ds ds;
    union semun {
        int              val;
        struct semid_ds *buf;
        unsigned short  *array;
    } arg;

    arg.buf = &ds;
    if (semctl(semid, 0, IPC_STAT, arg) == -1) {
        perror("semctl IPC_STAT");
        return;
    }

    /* Print last semop time */
    if (ds.sem_otime != 0)
        printf("Last semop():  %s", ctime(&ds.sem_otime));
    else
        printf("Last semop():  never\n");

    printf("%-6s %-8s %-8s %-10s %-10s\n",
           "Sem#", "Value", "SEMPID", "SEMNCNT", "SEMZCNT");

    for (int i = 0; i < nsems; i++) {
        int val    = semctl(semid, i, GETVAL);
        int pid    = semctl(semid, i, GETPID);
        int semncnt = semctl(semid, i, GETNCNT);
        int semzcnt = semctl(semid, i, GETZCNT);
        printf("%-6d %-8d %-8d %-10d %-10d\n",
               i, val, pid, semncnt, semzcnt);
    }
    printf("\n");
}

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

    /* Set semaphore 0 = 1, semaphore 1 = 0 */
    unsigned short vals[2] = {1, 0};
    union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;
    arg.array = vals;
    semctl(semid, 0, SETALL, arg);

    printf("=== Initial state ===\n");
    print_semaphore_status(semid, 2);

    /* Perform one operation to update sempid and sem_otime */
    struct sembuf sop = {0, +1, 0};   /* add 1 to sem 0 */
    semop(semid, &sop, 1);

    printf("=== After semop(sem0 + 1) ===\n");
    print_semaphore_status(semid, 2);

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

6. EIDRM โ€“ What Happens When the Set is Deleted While Processes are Blocked

If a process is blocked in semop() and another process removes the semaphore set using semctl(semid, 0, IPC_RMID), the blocked process is woken up and semop() returns -1 with errno set to EIDRM (“Identifier removed”).

Always handle EIDRM in production code.

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

int main(void)
{
    int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    semctl(semid, 0, SETVAL, 0);    /* value = 0, any decrease will block */

    pid_t child = fork();
    if (child == 0) {
        /* CHILD: block on semaphore */
        struct sembuf sop = {0, -1, 0};
        printf("Child PID=%d: blocking on semop()...\n", getpid());
        int ret = semop(semid, &sop, 1);
        if (ret == -1) {
            if (errno == EIDRM)
                printf("Child PID=%d: semaphore set was deleted -- EIDRM\n", getpid());
            else
                perror("semop child");
        }
        exit(EXIT_SUCCESS);
    }

    /* PARENT: sleep then delete semaphore set */
    sleep(1);
    printf("Parent: removing semaphore set...\n");
    semctl(semid, 0, IPC_RMID);   /* this wakes the child with EIDRM */

    wait(NULL);
    return 0;
}

Expected output:

Child PID=5678: blocking on semop()...
Parent: removing semaphore set...
Child PID=5678: semaphore set was deleted -- EIDRM

7. Wait-for-Zero Operation (sem_op = 0)

A sem_op of 0 means “block until this semaphore’s value equals zero”. This is useful for barrier-style synchronization โ€” wait until all workers have released a shared resource.

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

int main(void)
{
    int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    semctl(semid, 0, SETVAL, 3);   /* 3 workers using resource */

    pid_t child = fork();
    if (child == 0) {
        /* CHILD: wait until semaphore reaches zero */
        struct sembuf wait_zero = {0, 0, 0};   /* sem_op = 0 */
        printf("Child: waiting for semaphore to reach zero...\n");
        semop(semid, &wait_zero, 1);
        printf("Child: semaphore is zero, all workers done!\n");
        exit(EXIT_SUCCESS);
    }

    /* PARENT: simulate 3 workers finishing one by one */
    for (int i = 0; i < 3; i++) {
        sleep(1);
        struct sembuf release = {0, -1, 0};
        semop(semid, &release, 1);
        int val = semctl(semid, 0, GETVAL);
        printf("Parent: worker %d done, semaphore = %d\n", i+1, val);
    }

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

Expected output:

Child: waiting for semaphore to reach zero...
Parent: worker 1 done, semaphore = 2
Parent: worker 2 done, semaphore = 1
Parent: worker 3 done, semaphore = 0
Child: semaphore is zero, all workers done!

8. Atomically Operating on Multiple Semaphores

You can pass an array of struct sembuf to a single semop() call. The kernel performs ALL operations atomically โ€” either all succeed or none do. This prevents deadlocks in dining-philosopher-style problems.

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

/* Atomically acquire two resources (semaphore 0 AND semaphore 1) */
int acquire_both(int semid)
{
    struct sembuf ops[2];

    ops[0].sem_num = 0;
    ops[0].sem_op  = -1;   /* subtract 1 from sem 0 */
    ops[0].sem_flg = 0;

    ops[1].sem_num = 1;
    ops[1].sem_op  = -1;   /* subtract 1 from sem 1 */
    ops[1].sem_flg = 0;

    /* Both operations happen atomically */
    return semop(semid, ops, 2);
}

/* Release both resources atomically */
int release_both(int semid)
{
    struct sembuf ops[2];

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

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

    return semop(semid, ops, 2);
}

int main(void)
{
    int semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);

    /* Both semaphores start at 1 (both resources available) */
    unsigned short vals[2] = {1, 1};
    union semun { int v; struct semid_ds *b; unsigned short *a; } arg;
    arg.array = vals;
    semctl(semid, 0, SETALL, arg);

    printf("Acquiring both resources atomically...\n");
    if (acquire_both(semid) == 0)
        printf("Both acquired!\n");

    printf("Doing work with both resources...\n");

    if (release_both(semid) == 0)
        printf("Both released!\n");

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

Interview Questions โ€“ Blocking semop()

Q1: What happens when you call semop() with sem_op = -1 and the semaphore value is 0?

The calling process blocks (sleeps) until another process increases the semaphore value to at least 1. The kernel increments semncnt for that semaphore while the process is waiting.

Q2: What is the difference between EAGAIN and EINTR in semop()?

EAGAIN means the operation couldn’t be performed immediately and IPC_NOWAIT was set (the process didn’t want to block). EINTR means the process WAS blocked and a signal arrived before the operation could complete.

Q3: What is semncnt and how does it differ from semzcnt?

semncnt counts processes waiting to decrease the semaphore (sem_op < 0). semzcnt counts processes waiting for the semaphore value to reach zero (sem_op = 0).

Q4: Why is the atomic multi-semaphore operation useful for deadlock prevention?

If Process A holds resource 1 and waits for resource 2, and Process B holds resource 2 and waits for resource 1, a deadlock occurs. By atomically acquiring both resources in a single semop() call, a process either gets both or blocks without holding any, preventing the circular wait.

Q5: What error is returned when a process is blocked on semop() and the semaphore set is deleted?

EIDRM โ€“ “Identifier removed”. The kernel wakes all blocked processes and sets errno to EIDRM.

Q6: What does sem_otime represent and what value does it have when no semop() has been called yet?

sem_otime stores the time of the last successful semop() as a Unix timestamp. When no semop() has been called yet (for example, right after semget()), it is initialized to 0, which corresponds to the Epoch (1 January 1970 00:00:00 UTC).

Continue Learning

Next: Starvation and Ordering Behaviour in Semaphores

Part 2 โ†’ Chapter Index

Leave a Reply

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