System V Semaphores Part 2: semctl() and the semun Union

 

System V Semaphores
Chapter 47 — Part 2: semctl() and the semun Union
47.3
TLPI Chapter Section
semctl
Control Operations
semun
The Union Type

What does semctl() do?

semctl() is the Swiss Army knife for semaphore management. After creating a semaphore set with semget(), you use semctl() to perform control operations such as initializing semaphore values, reading semaphore values, querying metadata, and removing the semaphore set entirely.

It operates either on the entire set or on a single semaphore within the set depending on the operation (cmd) you choose.

semctl() — Function Signature
#include <sys/types.h>
#include <sys/sem.h>

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

/*
 * semid   — semaphore set identifier from semget()
 * semnum  — index of semaphore within the set (0-based)
 *           Ignored for set-wide operations
 * cmd     — operation to perform (IPC_RMID, SETVAL, GETVAL, etc.)
 * arg     — optional 4th argument; type is union semun
 *           Required only by some commands
 *
 * Returns: nonneg integer on success (meaning depends on cmd)
 *          -1 on error
 */
Important: The 4th argument (arg) is optional — only certain commands need it. For portability with older implementations that require it, always pass a dummy arg even when not needed. The type of arg is union semun which you must define yourself.

The semun Union — You Must Define It Yourself

SUSv3 (the POSIX standard) deliberately does NOT include the definition of union semun in any header file. Every program that uses semctl() must define it. This is one of the most common gotchas for beginners.

/* semun.h — include this in every program that uses semctl() */
#ifndef SEMUN_H
#define SEMUN_H

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

union semun {
    int              val;    /* Used by SETVAL */
    struct semid_ds *buf;    /* Used by IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Used by GETALL, SETALL */
#if defined(__linux__)
    struct seminfo  *__buf;  /* Linux-specific: used by IPC_INFO */
#endif
};

#endif /* SEMUN_H */

Which semun field is used by which command?
semun field Type Used by cmd
val int SETVAL — set one semaphore to this integer
buf struct semid_ds * IPC_STAT (read metadata), IPC_SET (write metadata)
array unsigned short * GETALL (read all values), SETALL (write all values)
__buf struct seminfo * IPC_INFO (Linux only — system limits)
/* How to use the union: always set the correct field */
union semun arg;

/* For SETVAL: set sem[0] = 1 */
arg.val = 1;
semctl(semid, 0, SETVAL, arg);

/* For SETALL: set all semaphores in the set */
unsigned short vals[3] = {1, 0, 3};
arg.array = vals;
semctl(semid, 0, SETALL, arg);  /* semnum ignored for SETALL */

/* For IPC_STAT: read metadata */
struct semid_ds ds;
arg.buf = &ds;
semctl(semid, 0, IPC_STAT, arg);

IPC_RMID — Remove the Semaphore Set

IPC_RMID immediately removes the semaphore set from the kernel. Any process blocked in semop() on a semaphore in this set is unblocked and semop() returns EIDRM error.

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

void remove_semaphore(int semid)
{
    /*
     * IPC_RMID does not need a semnum or arg.
     * Convention: pass semnum=0, no arg needed
     * For portability, some code passes a dummy union semun
     */
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        exit(EXIT_FAILURE);
    }
    printf("Semaphore set %d removed.\n", semid);
}

int main(void)
{
    key_t key = ftok("/tmp/demo", 'X');
    int semid = semget(key, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(1); }

    printf("Created semid=%d\n", semid);

    /* ... use the semaphore ... */

    /* Always clean up */
    remove_semaphore(semid);

    return 0;
}
Critical: System V semaphores are NOT automatically removed when the creating process exits. Always call IPC_RMID in cleanup code or install a signal handler to do so. Leftover semaphores accumulate and eventually hit the system limit (ENOSPC on next semget).

IPC_STAT and IPC_SET — Reading and Writing Metadata
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <time.h>

/* Include your semun definition */
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

void print_sem_info(int semid)
{
    union semun arg;
    struct semid_ds ds;
    arg.buf = &ds;

    /* IPC_STAT: copies kernel's semid_ds into ds */
    if (semctl(semid, 0, IPC_STAT, arg) == -1) {
        perror("semctl IPC_STAT");
        return;
    }

    printf("=== Semaphore Set Info ===\n");
    printf("Owner UID         : %d\n", ds.sem_perm.uid);
    printf("Owner GID         : %d\n", ds.sem_perm.gid);
    printf("Creator UID       : %d\n", ds.sem_perm.cuid);
    printf("Permissions (octal): %03o\n", ds.sem_perm.mode & 0777);
    printf("Number of sems    : %lu\n", (unsigned long)ds.sem_nsems);
    printf("Last semop() time : %s", ctime(&ds.sem_otime));
    printf("Last change time  : %s", ctime(&ds.sem_ctime));
}

void change_permissions(int semid)
{
    union semun arg;
    struct semid_ds ds;
    arg.buf = &ds;

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

    /* Modify the permission bits */
    ds.sem_perm.mode = (ds.sem_perm.mode & ~0777) | 0640;

    /* Write back with IPC_SET */
    if (semctl(semid, 0, IPC_SET, arg) == -1) {
        perror("IPC_SET");
        return;
    }
    printf("Permissions changed to 0640\n");
}

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

    print_sem_info(semid);
    change_permissions(semid);
    print_sem_info(semid);

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

SETVAL and GETVAL — Single Semaphore Value

These operate on a single semaphore identified by semnum within the set.

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

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

/* Initialize one semaphore to a specific value */
int sem_setval(int semid, int semnum, int value)
{
    union semun arg;
    arg.val = value;
    return semctl(semid, semnum, SETVAL, arg);
}

/* Read the current value of one semaphore */
int sem_getval(int semid, int semnum)
{
    /* GETVAL returns the value directly as the function result */
    /* No arg needed */
    return semctl(semid, semnum, GETVAL);
}

int main(void)
{
    int semid;
    int val;

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

    /* Initialize each semaphore */
    if (sem_setval(semid, 0, 1) == -1) { perror("SETVAL 0"); exit(1); }
    if (sem_setval(semid, 1, 3) == -1) { perror("SETVAL 1"); exit(1); }
    if (sem_setval(semid, 2, 0) == -1) { perror("SETVAL 2"); exit(1); }

    /* Read them back */
    for (int i = 0; i < 3; i++) {
        val = sem_getval(semid, i);
        if (val == -1) { perror("GETVAL"); exit(1); }
        printf("sem[%d] = %d\n", i, val);
    }
    /* Output:
     * sem[0] = 1
     * sem[1] = 3
     * sem[2] = 0
     */

    semctl(semid, 0, IPC_RMID);
    return 0;
}
Race Condition Warning: The value returned by GETVAL may already be stale by the time you use it. Any design that reads a semaphore value and makes decisions based on it is potentially subject to time-of-check/time-of-use (TOCTOU) races.

SETALL and GETALL — All Semaphores at Once

When you have a multi-semaphore set, SETALL and GETALL are more efficient than calling SETVAL/GETVAL in a loop. For SETALL, if any process is waiting and the change unblocks it, the kernel wakes it.

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

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

/* Get number of semaphores in the set */
int sem_nsems(int semid)
{
    union semun arg;
    struct semid_ds ds;
    arg.buf = &ds;
    if (semctl(semid, 0, IPC_STAT, arg) == -1)
        return -1;
    return (int)ds.sem_nsems;
}

/* Initialize all semaphores using SETALL */
int sem_setall(int semid, unsigned short *values)
{
    union semun arg;
    arg.array = values;
    return semctl(semid, 0, SETALL, arg);
    /* semnum (2nd arg) is ignored for SETALL */
}

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

int main(void)
{
    int semid;
    int nsems;
    unsigned short *vals;

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

    nsems = sem_nsems(semid);
    printf("Set has %d semaphores\n", nsems);

    /* Allocate array on the heap for portability */
    vals = calloc(nsems, sizeof(unsigned short));
    if (!vals) { perror("calloc"); exit(1); }

    /* Set values: 1, 2, 3, 4 */
    for (int i = 0; i < nsems; i++)
        vals[i] = (unsigned short)(i + 1);

    if (sem_setall(semid, vals) == -1) {
        perror("SETALL"); exit(1);
    }
    printf("All semaphores initialized.\n");

    /* Zero out array and read back */
    for (int i = 0; i < nsems; i++)
        vals[i] = 0;

    if (sem_getall(semid, vals) == -1) {
        perror("GETALL"); exit(1);
    }

    printf("Current values:\n");
    for (int i = 0; i < nsems; i++)
        printf("  sem[%d] = %u\n", i, vals[i]);

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

GETPID, GETNCNT, GETZCNT — Per-Semaphore Status

These three operations return diagnostic information about a single semaphore. They are read-only (require read permission only) and the arg is not needed.

What each returns (for sem[semnum]):
cmd Returns Kernel field
GETPID PID of last process that called semop() on this semaphore. 0 if no semop() yet. sempid
GETNCNT Number of processes currently blocked waiting for this semaphore’s value to increase. semncnt
GETZCNT Number of processes currently waiting for this semaphore’s value to become 0. semzcnt
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>

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

/* Print diagnostic info for each semaphore in the set */
void print_sem_status(int semid)
{
    union semun arg;
    struct semid_ds ds;
    int nsems, val, pid, ncnt, zcnt;

    /* Get number of semaphores */
    arg.buf = &ds;
    if (semctl(semid, 0, IPC_STAT, arg) == -1) {
        perror("IPC_STAT"); return;
    }
    nsems = (int)ds.sem_nsems;

    printf("%-6s %-8s %-8s %-8s %-8s\n",
           "SEM#", "VALUE", "LASTPID", "NCNT", "ZCNT");

    for (int i = 0; i < nsems; i++) {
        val  = semctl(semid, i, GETVAL);
        pid  = semctl(semid, i, GETPID);
        ncnt = semctl(semid, i, GETNCNT);
        zcnt = semctl(semid, i, GETZCNT);

        if (val == -1 || ncnt == -1 || zcnt == -1) {
            perror("semctl"); return;
        }

        printf("%-6d %-8d %-8d %-8d %-8d\n",
               i, val, pid, ncnt, zcnt);
    }
}

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

    /* Initialize semaphores */
    union semun arg;
    unsigned short vals[3] = {1, 0, 5};
    arg.array = vals;
    semctl(semid, 0, SETALL, arg);

    printf("=== Semaphore Status ===\n");
    print_sem_status(semid);
    /*
     * Example output (no processes waiting):
     * SEM#   VALUE    LASTPID  NCNT     ZCNT
     * 0      1        0        0        0
     * 1      0        0        0        1   (1 process might wait for value increase)
     * 2      5        0        0        0
     */

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

Quick Reference — All semctl() Commands
cmd Scope arg needed? Permission Return value
IPC_RMID Set No Owner/root 0
IPC_STAT Set arg.buf Read 0
IPC_SET Set arg.buf Owner/root 0
GETVAL Single No Read semaphore value
SETVAL Single arg.val Alter 0
GETALL Set arg.array Read 0
SETALL Set arg.array Alter 0
GETPID Single No Read PID (sempid)
GETNCNT Single No Read wait count (semncnt)
GETZCNT Single No Read zero-wait count (semzcnt)

Reusable Helper Library for semctl()
/* sem_helpers.h */
#ifndef SEM_HELPERS_H
#define SEM_HELPERS_H

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

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

/* Set one semaphore value */
static inline int semctl_setval(int semid, int semnum, int val)
{
    union semun arg;
    arg.val = val;
    return semctl(semid, semnum, SETVAL, arg);
}

/* Get one semaphore value */
static inline int semctl_getval(int semid, int semnum)
{
    return semctl(semid, semnum, GETVAL);
}

/* Remove the semaphore set */
static inline int semctl_rmid(int semid)
{
    return semctl(semid, 0, IPC_RMID);
}

/* Get number of semaphores in set */
static inline int semctl_nsems(int semid)
{
    union semun arg;
    struct semid_ds ds;
    arg.buf = &ds;
    if (semctl(semid, 0, IPC_STAT, arg) == -1)
        return -1;
    return (int)ds.sem_nsems;
}

/* Initialize all semaphores using an array */
static inline int semctl_setall(int semid, unsigned short *values)
{
    union semun arg;
    arg.array = values;
    return semctl(semid, 0, SETALL, arg);
}

/* Read all semaphore values into a caller-allocated array */
static inline int semctl_getall(int semid, unsigned short *values)
{
    union semun arg;
    arg.array = values;
    return semctl(semid, 0, GETALL, arg);
}

#endif /* SEM_HELPERS_H */

Interview Questions — semctl() and semun

Q1. Why is the semun union not defined in any standard header file?

SUSv3 explicitly requires the programmer to define it themselves. The standard specifies that it must contain at least val, buf, and array, but the exact definition can vary between platforms. Leaving it to the programmer ensures portability — some implementations may add extra fields (like __buf for Linux’s IPC_INFO). Note: some older glibc versions did define it in <sys/sem.h> but glibc 2.1+ removed it for POSIX compliance.
Q2. What is the difference between SETVAL and SETALL?

SETVAL sets a single semaphore (identified by semnum) to the value in arg.val. SETALL initializes ALL semaphores in the set using values from arg.array, and semnum is ignored. SETALL is atomic across all semaphores in the set which is important for multi-resource initialization.
Q3. What happens to processes blocked in semop() when IPC_RMID is called?

They are immediately unblocked. The semop() call in those blocked processes returns -1 with errno set to EIDRM (identifier removed). This is an important design consideration — your semop() error handling code should check for EIDRM and handle it gracefully.
Q4. What does GETNCNT tell you?

GETNCNT returns the number of processes currently blocked waiting for a semaphore’s value to become greater than some threshold (i.e., processes trying to decrement the semaphore but blocked because the current value is too low). This is useful for debugging deadlocks — if GETNCNT is non-zero and never decreasing, something is wrong.
Q5. Why might you need to check for EIDRM error in semop() code?

Any other process (with sufficient privileges) can remove the semaphore set at any time with IPC_RMID. If your code is blocked in semop() and someone deletes the set, you’ll get EIDRM. Robust code must handle this instead of treating it as a generic error and retrying.
Q6. In semctl(), when does semnum matter and when is it ignored?

semnum specifies which individual semaphore within the set to operate on. It is used by: GETVAL, SETVAL, GETPID, GETNCNT, GETZCNT. It is IGNORED by set-wide operations: IPC_RMID, IPC_STAT, IPC_SET, GETALL, SETALL. Convention is to pass 0 when it is ignored.
Q7. How do you safely remove a semaphore set in a signal handler during program exit?

Store the semid in a global variable. In atexit() or signal handlers for SIGTERM/SIGINT, call semctl(semid, 0, IPC_RMID). Note that semctl() is not async-signal-safe so strictly it should only be called from atexit(); in a signal handler you should set a flag and clean up in the main program flow, or use a self-pipe pattern.

Next: semid_ds — The Associated Data Structure

Understand the kernel data structure behind every semaphore set and its fields.

Part 3: semid_ds → ← Part 1: semget()

Leave a Reply

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