semctl() — Semaphore Control Operations

 

semctl() — Semaphore Control Operations
Chapter 47 · The Linux Programming Interface · Part 3
🎛️ Control API
📊 semid_ds Structure
🗑️ Deletion

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

int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);

/* Returns: non-negative value on success (depends on cmd), -1 on error */

/* The semun union — MUST be declared by the programmer */
union semun {
    int              val;    /* used with SETVAL */
    struct semid_ds *buf;    /* used with IPC_STAT, IPC_SET */
    unsigned short  *array;  /* used with GETALL, SETALL */
    struct seminfo  *__buf;  /* used with IPC_INFO (Linux only) */
};

semctl() is the Swiss Army knife of semaphore management. It handles initialization, querying, modification, and deletion of semaphore sets depending on the cmd argument passed.

All semctl() Commands at a Glance
Command semnum arg used What it does Returns
SETVAL sem index arg.val Set one semaphore to a specific value 0
GETVAL sem index not needed Get current value of one semaphore semaphore value
SETALL ignored arg.array Set ALL semaphores in the set at once 0
GETALL ignored arg.array Get ALL semaphore values into array 0
IPC_RMID ignored not needed Delete the entire semaphore set 0
IPC_STAT ignored arg.buf Copy metadata into semid_ds struct 0
IPC_SET ignored arg.buf Change ownership/permissions from semid_ds 0
GETPID sem index not needed Get PID of last process to call semop() PID
GETNCNT sem index not needed Get count of processes waiting to decrease sem count
GETZCNT sem index not needed Get count of processes waiting for value = 0 count

SETVAL and GETVAL — Single Semaphore Operations
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

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

/* Set semaphore[semnum] to a value */
int sem_set_value(int semid, int semnum, int value) {
    union semun arg;
    arg.val = value;
    return semctl(semid, semnum, SETVAL, arg);
}

/* Get current value of semaphore[semnum] */
int sem_get_value(int semid, int semnum) {
    /* No arg needed for GETVAL */
    return semctl(semid, semnum, GETVAL);
}

/* Demonstrate SETVAL and GETVAL */
void demo_setval_getval(int semid) {
    int val;

    /* Set semaphore 0 to 5 */
    if (sem_set_value(semid, 0, 5) == -1) {
        perror("SETVAL"); return;
    }
    printf("Set semaphore[0] = 5\n");

    /* Read it back */
    val = sem_get_value(semid, 0);
    if (val == -1) { perror("GETVAL"); return; }
    printf("GETVAL semaphore[0] = %d\n", val);

    /* Set to 0 (mark resource as unavailable) */
    if (sem_set_value(semid, 0, 0) == -1) {
        perror("SETVAL to 0"); return;
    }
    printf("Set semaphore[0] = 0 (all processes waiting will stay blocked)\n");

    /* WARNING: setting value to 0 while others are waiting */
    /* Use GETNCNT to check before setting */
    int waiters = semctl(semid, 0, GETNCNT);
    printf("Processes waiting to decrement: %d\n", waiters);
}
Side effect of SETVAL/SETALL: When you change a semaphore value with SETVAL or SETALL, the kernel automatically clears the semadj values (used for undo operations) for all processes in the system. This prevents stale undo adjustments from interfering.

SETALL and GETALL — Bulk Operations on All Semaphores

When you have a multi-semaphore set, SETALL and GETALL are more efficient than calling SETVAL/GETVAL for each semaphore individually:

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

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

#define NUM_SEMS 4

/* Initialize all semaphores in one call */
int sem_init_all(int semid, unsigned short values[], int count) {
    union semun arg;
    arg.array = values;
    /* semnum is ignored for SETALL */
    return semctl(semid, 0, SETALL, arg);
}

/* Read all semaphore values */
int sem_get_all(int semid, unsigned short values[], int count) {
    union semun arg;
    arg.array = values;
    return semctl(semid, 0, GETALL, arg);
}

void demo_setall_getall(int semid) {
    unsigned short init_vals[NUM_SEMS] = {1, 0, 5, 10};
    unsigned short read_vals[NUM_SEMS];
    int i;

    /* Initialize all 4 semaphores at once */
    if (sem_init_all(semid, init_vals, NUM_SEMS) == -1) {
        perror("SETALL");
        return;
    }
    printf("Initialized all %d semaphores\n", NUM_SEMS);

    /* Read them all back */
    if (sem_get_all(semid, read_vals, NUM_SEMS) == -1) {
        perror("GETALL");
        return;
    }

    printf("Current semaphore values:\n");
    for (i = 0; i < NUM_SEMS; i++) {
        printf("  sem[%d] = %u\n", i, read_vals[i]);
    }
}

IPC_STAT — The semid_ds Structure

Each semaphore set has an associated semid_ds structure maintained by the kernel. IPC_STAT copies it into a user-space buffer so you can inspect metadata about the set:

/* The semid_ds structure (simplified) */
struct semid_ds {
    struct ipc_perm sem_perm;  /* ownership and permissions */
    time_t          sem_otime; /* time of last semop() call */
    time_t          sem_ctime; /* time of last semctl() change */
    unsigned short  sem_nsems; /* number of semaphores in this set */
};

/* The ipc_perm structure (for sem_perm field) */
struct ipc_perm {
    key_t          __key;  /* key passed to semget() */
    uid_t          uid;    /* effective UID of owner */
    gid_t          gid;    /* effective GID of owner */
    uid_t          cuid;   /* effective UID of creator */
    gid_t          cgid;   /* effective GID of creator */
    unsigned short mode;   /* permissions (lower 9 bits) */
    unsigned short __seq;  /* sequence number */
};
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

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

/* Print metadata about a semaphore set */
void print_sem_info(int semid) {
    union semun arg;
    struct semid_ds ds;
    char timebuf[32];

    arg.buf = &ds;

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

    printf("=== Semaphore Set Info (semid=%d) ===\n", semid);
    printf("  Number of semaphores : %d\n", ds.sem_nsems);
    printf("  Owner UID            : %d\n", ds.sem_perm.uid);
    printf("  Owner GID            : %d\n", ds.sem_perm.gid);
    printf("  Permissions (octal)  : %04o\n", ds.sem_perm.mode & 0777);

    if (ds.sem_otime != 0) {
        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
                 localtime(&ds.sem_otime));
        printf("  Last semop()         : %s\n", timebuf);
    } else {
        printf("  Last semop()         : never\n");
    }

    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
             localtime(&ds.sem_ctime));
    printf("  Last semctl() change : %s\n", timebuf);
}

/* KEY USE OF sem_otime: detect if initialization is done */
/* sem_otime = 0 means no semop() has ever been called    */
/* A pattern: creator marks init complete by doing semop  */
/* Other processes poll IPC_STAT waiting for sem_otime != 0 */
int wait_for_init(int semid) {
    union semun arg;
    struct semid_ds ds;
    int retries = 0;

    arg.buf = &ds;

    /* Poll until sem_otime becomes non-zero (creator finished init) */
    while (retries < 100) {
        if (semctl(semid, 0, IPC_STAT, arg) == -1) return -1;
        if (ds.sem_otime != 0) {
            printf("Semaphore initialized (detected via sem_otime)\n");
            return 0;
        }
        usleep(10000);  /* sleep 10ms between polls */
        retries++;
    }
    return -1;  /* timed out */
}
Key trick with sem_otime: The kernel sets sem_otime to the current time every time semop() is called. Initially it is 0. This fact is used as a synchronization mechanism: the creator calls semop() as its last initialization step, and other processes poll IPC_STAT waiting for sem_otime to become non-zero before proceeding.

IPC_SET — Changing Ownership and Permissions
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

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

/* Change permissions of an existing semaphore set */
int sem_change_perm(int semid, unsigned short new_mode) {
    union semun arg;
    struct semid_ds ds;

    arg.buf = &ds;

    /* First read current settings */
    if (semctl(semid, 0, IPC_STAT, arg) == -1) {
        perror("IPC_STAT"); return -1;
    }

    /* Modify only the permission bits */
    ds.sem_perm.mode = new_mode;

    /* Write back — only uid, gid, and mode can be changed */
    if (semctl(semid, 0, IPC_SET, arg) == -1) {
        perror("IPC_SET"); return -1;
    }

    printf("Changed semaphore set permissions to %04o\n", new_mode);
    return 0;
}

/* Usage example */
void example_ipc_set(int semid) {
    /* Restrict: only owner can access */
    sem_change_perm(semid, 0600);

    /* Open up to group */
    sem_change_perm(semid, 0660);

    /* Note: only the creator/owner or superuser can call IPC_SET */
}

IPC_RMID — Deleting a Semaphore Set

IPC_RMID removes the entire semaphore set from the kernel. Any processes blocked in semop() on this set will be immediately woken up with an error (EIDRM).

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

static int g_semid = -1;

/* Signal handler — clean up semaphore on Ctrl+C */
void cleanup_handler(int sig) {
    if (g_semid != -1) {
        /* semnum is ignored, arg is not needed */
        if (semctl(g_semid, 0, IPC_RMID) == -1)
            perror("semctl IPC_RMID in handler");
        else
            printf("\nSemaphore set %d deleted\n", g_semid);
    }
    exit(0);
}

int main(void) {
    union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;

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

    arg.val = 1;
    semctl(g_semid, 0, SETVAL, arg);

    /* Register cleanup handler */
    signal(SIGINT,  cleanup_handler);
    signal(SIGTERM, cleanup_handler);

    printf("Semaphore set created: semid=%d\n", g_semid);
    printf("Press Ctrl+C to delete and exit\n");

    /* ... application logic ... */
    pause();  /* wait for signal */

    return 0;
}
What happens to blocked processes on IPC_RMID: Any process currently blocked in semop() waiting on a deleted semaphore set is immediately woken up and semop() returns -1 with errno = EIDRM (identifier removed). Always check for this error in robust code.

GETNCNT, GETZCNT, GETPID — Diagnostic Commands

These commands let you inspect the state of individual semaphores:

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

void diagnose_semaphore(int semid, int semnum) {
    int val, ncnt, zcnt, pid;

    /* Current value */
    val = semctl(semid, semnum, GETVAL);

    /* Processes waiting to DECREASE (blocked in semop with negative sem_op) */
    ncnt = semctl(semid, semnum, GETNCNT);

    /* Processes waiting for value to BECOME 0 (blocked in semop with sem_op=0) */
    zcnt = semctl(semid, semnum, GETZCNT);

    /* PID of last process to call semop() on this semaphore */
    pid  = semctl(semid, semnum, GETPID);

    printf("=== semaphore[%d] diagnostics ===\n", semnum);
    printf("  Current value        : %d\n", val);
    printf("  Waiting to decrement : %d processes\n", ncnt);
    printf("  Waiting for zero     : %d processes\n", zcnt);
    printf("  Last semop() caller  : PID %d\n", pid);

    if (ncnt > 0)
        printf("  WARNING: %d process(es) are blocked waiting to acquire!\n", ncnt);
    if (val == 0 && ncnt == 0)
        printf("  Status: semaphore is free (value=0, no one waiting)\n");
    if (val > 0 && ncnt == 0)
        printf("  Status: resource available (value=%d)\n", val);
}

/* Example: print all semaphores in a set */
void diagnose_all(int semid, int nsems) {
    int i;
    printf("\n=== Full semaphore set diagnostic (semid=%d) ===\n", semid);
    for (i = 0; i < nsems; i++) {
        diagnose_semaphore(semid, i);
        printf("\n");
    }
}

Robust Initialization Pattern Using sem_otime

The most reliable pattern for initializing semaphores in a multi-process environment uses sem_otime as a “ready” flag:

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

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

#define KEY_PATH "/tmp/robust_sem"
#define KEY_ID   1
#define NUM_SEMS 2

/*
 * Open or create and initialize a semaphore set robustly.
 * Returns semid on success, -1 on error.
 *
 * Algorithm:
 *  1. Try IPC_CREAT | IPC_EXCL — if we succeed, we are the creator
 *  2. If we are creator: initialize, then do a semop() to set sem_otime
 *  3. If EEXIST: just open, then poll sem_otime != 0 to confirm init done
 */
int open_sem_robust(unsigned short init_vals[], int nsems) {
    key_t key;
    int semid;
    union semun arg;
    struct semid_ds ds;

    key = ftok(KEY_PATH, KEY_ID);
    if (key == -1) { perror("ftok"); return -1; }

    /* Try to create exclusively */
    semid = semget(key, nsems, IPC_CREAT | IPC_EXCL | 0600);

    if (semid != -1) {
        /* === WE ARE THE CREATOR === */
        printf("[%d] Created semaphore set semid=%d\n", getpid(), semid);

        /* Initialize all semaphore values */
        arg.array = init_vals;
        if (semctl(semid, 0, SETALL, arg) == -1) {
            perror("SETALL");
            semctl(semid, 0, IPC_RMID);
            return -1;
        }

        /*
         * Stamp sem_otime to signal other processes that init is complete.
         * We do this by performing a no-op semop (adding 0 to semaphore 0).
         * This sets sem_otime to a non-zero timestamp.
         */
        struct sembuf noop = {0, 0, 0};   /* wait-for-zero on sem[0]=0 */
        /* Actually just do +0 trick differently — add 0 to trigger otime */
        /* Better: do a real operation that immediately succeeds */
        /* If sem[0] = 1 (mutex), we can do nothing meaningful here */
        /* Instead just flag via IPC_STAT sem_ctime — or do a tiny semop */

        /* Simplest: set a special "initialized" marker semaphore to 1 */
        /* Here we use sem[nsems-1] as the ready flag */
        struct sembuf mark = {nsems - 1, 1, 0};   /* add 1 to last sem */
        if (semop(semid, &mark, 1) == -1) {
            perror("semop mark");
            semctl(semid, 0, IPC_RMID);
            return -1;
        }

        printf("[%d] Initialization complete (sem_otime stamped)\n", getpid());

    } else if (errno == EEXIST) {
        /* === ALREADY EXISTS — just open it === */
        semid = semget(key, 0, 0600);
        if (semid == -1) { perror("semget open"); return -1; }

        printf("[%d] Opened existing semid=%d, waiting for init...\n",
               getpid(), semid);

        /* Poll until creator has stamped sem_otime */
        arg.buf = &ds;
        int retries = 0;
        while (retries++ < 500) {
            if (semctl(semid, 0, IPC_STAT, arg) == -1) {
                if (errno == EINVAL) continue;  /* set was deleted */
                perror("IPC_STAT"); return -1;
            }
            if (ds.sem_otime != 0) {
                printf("[%d] Init detected via sem_otime\n", getpid());
                return semid;
            }
            usleep(5000);  /* 5ms */
        }

        fprintf(stderr, "[%d] Timed out waiting for init\n", getpid());
        return -1;
    } else {
        perror("semget");
        return -1;
    }

    return semid;
}

Interview Questions — semctl()
Q1: What is the fourth argument to semctl() and why is it unusual?
The fourth argument is a union semun that can be an int, a pointer to semid_ds, or a pointer to unsigned short array depending on the command. It is unusual because Linux does not define this union in any system header — the programmer must declare it in their own code. POSIX specifies its members but leaves the definition to the application.
Q2: What happens to processes blocked in semop() when another process calls semctl(IPC_RMID)?
They are immediately woken up and their semop() call returns -1 with errno set to EIDRM (identifier removed). Robust programs always check for this error and handle it gracefully.
Q3: How is sem_otime used as a synchronization mechanism?
sem_otime starts at 0 when a set is created. The kernel updates it whenever semop() is called. The creator performs a semop() as its last initialization step, which stamps sem_otime to a non-zero value. Other processes poll IPC_STAT in a loop checking for sem_otime != 0. When they see a non-zero timestamp, they know initialization is complete and it is safe to use the semaphore.
Q4: What is the difference between SETVAL and SETALL?
SETVAL sets a single semaphore (specified by semnum) to a specific integer value (from arg.val). SETALL sets all semaphores in the set simultaneously using an array of unsigned short values (from arg.array); the semnum argument is ignored. SETALL is more efficient when initializing multiple semaphores.
Q5: What do GETNCNT and GETZCNT return, and when would you use them?
GETNCNT returns the number of processes currently blocked waiting to decrease the semaphore value (i.e., blocked in semop() with a negative sem_op). GETZCNT returns the number waiting for the value to become zero. These are used for diagnostics, performance monitoring, and detecting deadlocks — if GETNCNT is very high, it indicates contention or a bug where a semaphore is never being released.

Leave a Reply

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