System V Semaphores semctl() — Control, Initialization, Limits

System V Semaphores — Part 3
semctl() — Control, Initialization, Limits | TLPI Chapter 47
API
semctl()
Union
semun
Level
Intermediate
semctl() Overview

semctl() is the Swiss Army knife of System V semaphores. It performs all control operations: initializing semaphore values, querying values, changing ownership, and deleting sets.

#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, .../* union semun arg */);
/* Returns value depending on cmd; -1 on error */

The fourth argument is a union semun whose member you fill depending on the command used. Important: the application must define this union — it is not defined in any header on Linux.

/* Must be defined by the application — not in any header */
union semun {
    int              val;    /* for SETVAL */
    struct semid_ds *buf;    /* for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* for GETALL, SETALL */
    struct seminfo  *__buf;  /* for IPC_INFO, SEM_INFO */
};
Key Terms

semctl() union semun SETVAL GETVAL SETALL GETALL IPC_RMID IPC_STAT semid_ds seminfo

semctl() Commands Reference
Command arg Used Description Return Value
SETVAL arg.val Set value of semaphore semnum 0
GETVAL none Get current value of semaphore semnum semval
SETALL arg.array Set all semaphores from array 0
GETALL arg.array Get all semaphore values into array 0
GETPID none Get PID of last process to do semop() on semnum PID
GETNCNT none Count of processes waiting for semval to increase count
GETZCNT none Count of processes waiting for semval == 0 count
IPC_STAT arg.buf Retrieve semid_ds metadata into arg.buf 0
IPC_SET arg.buf Set permissions/ownership from arg.buf 0
IPC_RMID none Delete the semaphore set immediately 0
IPC_INFO arg.__buf Get system-wide semaphore limits max index
SEM_INFO arg.__buf Get actual resource usage (Linux-specific) max index
SEM_STAT arg.buf Like IPC_STAT but uses kernel index not semid semid

Code Example 1 — SETVAL and GETVAL
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

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

void set_semval(int semid, int semnum, int val)
{
    union semun arg;
    arg.val = val;
    if (semctl(semid, semnum, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        exit(EXIT_FAILURE);
    }
    printf("Set sem[%d] = %d\n", semnum, val);
}

int get_semval(int semid, int semnum)
{
    int val = semctl(semid, semnum, GETVAL);
    if (val == -1) {
        perror("semctl GETVAL");
        exit(EXIT_FAILURE);
    }
    printf("sem[%d] current value = %d\n", semnum, val);
    return val;
}

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

    set_semval(semid, 0, 1);   /* binary semaphore */
    set_semval(semid, 1, 5);   /* counting semaphore */
    set_semval(semid, 2, 0);   /* starts locked */

    get_semval(semid, 1);      /* prints 5 */

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

Code Example 2 — SETALL and GETALL
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

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

int main(void)
{
    int semid;
    union semun arg;
    unsigned short init_vals[4] = {1, 1, 3, 0};
    unsigned short read_vals[4];
    int i;

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

    /* Initialize all 4 semaphores at once */
    arg.array = init_vals;
    if (semctl(semid, 0, SETALL, arg) == -1) {
        perror("SETALL");
        exit(EXIT_FAILURE);
    }

    /* Read all values back */
    arg.array = read_vals;
    if (semctl(semid, 0, GETALL, arg) == -1) {
        perror("GETALL");
        exit(EXIT_FAILURE);
    }

    for (i = 0; i < 4; i++)
        printf("sem[%d] = %u\n", i, read_vals[i]);

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

Code Example 3 — IPC_STAT: Querying Metadata

The semid_ds structure holds metadata about a semaphore set:

struct semid_ds {
    struct ipc_perm sem_perm;  /* Permissions */
    time_t          sem_otime; /* Time of last semop() */
    time_t          sem_ctime; /* Time of last change */
    unsigned long   sem_nsems; /* Number of semaphores in set */
};
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

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

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

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

    printf("Semaphore set info:\n");
    printf("  Number of semaphores : %lu\n", ds.sem_nsems);
    printf("  Owner UID            : %d\n",  ds.sem_perm.uid);
    printf("  Permissions (octal)  : %04o\n", ds.sem_perm.mode & 0777);

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

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

The Initialization Race Condition

This is one of the most important and tricky aspects of System V semaphores. semget() (create) and the initialization via semctl(SETVAL) are two separate system calls. Between them, another process may already start using the semaphore with its default value of 0.

Process A Process B Problem
semget(key, 1, IPC_CREAT|0600) → creates, semval=0
semget(key, 0, 0) → opens existing B has semid before it’s initialized!
semop(-1) → blocks on semval=0 B is stuck waiting!
semctl(SETVAL, 1) → sets to 1 B wakes up — but was it safe?

The standard fix: use the sem_otime field. Right after semget(), check if sem_otime is non-zero. If it is, another process already initialized it. If zero, we need to initialize it ourselves.

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

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

/*
 * Create-or-open a semaphore set with one semaphore,
 * initializing it to init_val if we create it.
 * Uses sem_otime trick to avoid init race.
 */
int create_or_open_sem(key_t key, int init_val)
{
    int semid;
    union semun arg;
    struct semid_ds ds;

    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0600);

    if (semid != -1) {
        /* We created it — initialize now */
        arg.val = init_val;
        if (semctl(semid, 0, SETVAL, arg) == -1) {
            perror("semctl SETVAL");
            exit(EXIT_FAILURE);
        }
        printf("Created and initialized semaphore %d\n", semid);
        return semid;
    }

    if (errno == EEXIST) {
        /* Another process created it — open and wait for init */
        semid = semget(key, 0, 0600);
        if (semid == -1) { perror("semget open"); exit(EXIT_FAILURE); }

        /* Poll sem_otime until non-zero (init complete) */
        for (;;) {
            arg.buf = &ds;
            if (semctl(semid, 0, IPC_STAT, arg) == -1) {
                perror("IPC_STAT"); exit(EXIT_FAILURE);
            }
            if (ds.sem_otime != 0) break;  /* initialized */
            /* small sleep to avoid busy-wait */
            usleep(10000);  /* 10ms */
        }
        printf("Opened existing semaphore %d\n", semid);
        return semid;
    }

    perror("semget unexpected");
    exit(EXIT_FAILURE);
}

Why does sem_otime work? The kernel only sets sem_otime when the first semop() completes. But we rely on the fact that after SETVAL, a subsequent semop() by the creator will set sem_otime. A cleaner but equivalent approach: do a dummy semop() (add 0) immediately after SETVAL so sem_otime gets set.

System V Semaphore Limits

The kernel enforces limits on semaphores. These can be queried with semctl(0, 0, IPC_INFO, arg) and configured via /proc/sys/kernel/sem.

Limit Symbol Description Default (Linux)
Max semaphores per set SEMMSL Max value for nsems in semget() 32000
Max semaphore sets SEMMNI System-wide max number of semaphore sets 32000
Max total semaphores SEMMNS System-wide max total semaphores across all sets 1024000000
Max semaphore value SEMVMX Maximum value a semaphore can hold 32767
Max ops per semop() SEMOPM Max semaphores per semop() call 500
Max undo entries SEMMNU System-wide max SEM_UNDO structures SEMMNS
/* Check current limits on Linux: */
/* cat /proc/sys/kernel/sem
   Output: SEMMSL  SEMMNS  SEMOPM  SEMMNI
   e.g.:   32000   1024000000  500  32000
*/

/* Change limits (as root): */
/* echo "32000 1024000000 500 32000" > /proc/sys/kernel/sem */

Code Example 4 — Querying System Limits with IPC_INFO
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

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

int main(void)
{
    union semun arg;
    struct seminfo si;
    arg.__buf = &si;

    if (semctl(0, 0, IPC_INFO, arg) == -1) {
        perror("semctl IPC_INFO");
        exit(EXIT_FAILURE);
    }

    printf("System V Semaphore Limits:\n");
    printf("  SEMMSL (max sems per set)     : %d\n", si.semmsl);
    printf("  SEMMNS (max sems total)       : %d\n", si.semmns);
    printf("  SEMOPM (max ops per semop)    : %d\n", si.semopm);
    printf("  SEMMNI (max sem sets)         : %d\n", si.semmni);
    printf("  SEMVMX (max semaphore value)  : %d\n", si.semvmx);

    return 0;
}

Code Example 5 — Monitoring Waiting Processes
#include <sys/sem.h>
#include <stdio.h>

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

    val  = semctl(semid, semnum, GETVAL);
    ncnt = semctl(semid, semnum, GETNCNT);  /* waiting to decrement */
    zcnt = semctl(semid, semnum, GETZCNT);  /* waiting for zero     */
    pid  = semctl(semid, semnum, GETPID);   /* last semop() process */

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

    printf("sem[%d]: value=%d, waiting_for_nonzero=%d, "
           "waiting_for_zero=%d, last_pid=%d\n",
           semnum, val, ncnt, zcnt, pid);
}

Interview Questions — semctl()

Q1. Why must the application define union semun rather than including it from a header?

POSIX specifies that the application is responsible for defining union semun. The union varies across implementations (different members may exist on different systems), so no standard header defines it. On Linux, it is mentioned in the semctl(2) man page as an example the programmer must copy.

Q2. Why is there a race condition between semget() and semctl(SETVAL)?

These are two separate system calls. Between the call to semget() (which creates the set with all semaphores at 0) and the call to semctl() (which sets the initial value), another process can obtain the semid and try to use the semaphore with an incorrect initial value of 0.

Q3. How does the sem_otime trick solve the initialization race?

sem_otime is set by the kernel only when a semop() is performed. If a process opens an existing semaphore set and finds sem_otime == 0, it knows the creator has not yet performed any semop() and may still be in the middle of initialization. It spins (with sleep) until sem_otime becomes non-zero, at which point initialization is guaranteed complete.

Q4. What does IPC_RMID do if processes are currently blocked on the semaphore?

It removes the semaphore set immediately. All processes blocked in semop() on that semaphore set are woken up and semop() returns -1 with errno set to EIDRM.

Q5. What is SEMVMX and why does it matter for SEM_UNDO?

SEMVMX is the maximum allowed semaphore value (32767 on Linux). If a SEM_UNDO adjustment on process exit would push a semaphore value above SEMVMX, the kernel clamps it at SEMVMX instead of failing — so cleanup may not be perfect in extreme cases.

Q6. How do you list all existing semaphore sets on a Linux system?

From the shell: ipcs -s lists all semaphore sets with their keys, IDs, owners, and permissions. Programmatically, use semctl() with IPC_INFO to get the max index, then loop calling semctl() with SEM_STAT to retrieve each active set.

Next: Binary Semaphores

See how to implement clean binary semaphore primitives on top of System V semaphores.

Leave a Reply

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