What you will learn in this file
semop() is the heart of System V semaphore usage. It lets you increment, decrement, or wait-for-zero on one or more semaphores in a single atomic operation. You will understand the sembuf structure, what each field does, what happens when an operation blocks, and how to use IPC_NOWAIT for non-blocking behavior.
Key Terms
#include <sys/types.h> /* For portability */
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned int nsops);
/* Returns 0 on success, -1 on error */
| Parameter | Type | Meaning |
|---|---|---|
semid |
int |
Semaphore set ID from semget() |
sops |
struct sembuf * |
Array of operations to perform |
nsops |
unsigned int |
Number of elements in sops array (≥ 1) |
The key feature: if you pass multiple operations in the array, the kernel performs them all or nothing — atomically. Either all operations succeed immediately, or none of them are applied and the call blocks (or fails with EAGAIN if IPC_NOWAIT is set).
Each element in the sops array is a struct sembuf:
struct sembuf {
unsigned short sem_num; /* Which semaphore in the set (0-based index) */
short sem_op; /* What to do: >0 release, <0 acquire, ==0 wait */
short sem_flg; /* Flags: IPC_NOWAIT, SEM_UNDO */
};
| Field | Value | Behavior | Permission needed |
|---|---|---|---|
sem_op |
> 0 |
Add sem_op to semaphore value. Wakes blocked decrementers. | Write (alter) |
== 0 |
Block until semaphore value reaches 0. If already 0, returns immediately. | Read | |
< 0 |
Subtract |sem_op| from semaphore value. Blocks if value < |sem_op|. | Write (alter) | |
sem_flg |
IPC_NOWAIT |
Never block. If operation can’t complete, fail with EAGAIN. | — |
SEM_UNDO |
Kernel auto-reverses this operation if the process exits. Prevents stuck semaphores. | — |
Resource freed
Value increases
Synchronization point
Read-only semantics
Resource claimed
Value decreases
When semop() cannot complete immediately (e.g., trying to decrement a semaphore that’s already 0), the calling process is blocked by the kernel until one of three events occurs:
| Wake-up Event | Result | errno |
|---|---|---|
| Another process modifies the semaphore so the operation can now proceed | semop() completes, returns 0 | — |
| A signal is delivered to the blocked process | semop() fails with -1 | EINTR |
| Another process deletes the semaphore set | semop() fails with -1 | EIDRM |
Important: Unlike many system calls, semop() is never automatically restarted after being interrupted by a signal handler (the SA_RESTART flag has no effect on it). If you need retry-on-signal behavior you must implement it yourself with a loop.
A semaphore initialized to 1 acts as a mutual exclusion lock. Decrementing by 1 acquires the lock; incrementing by 1 releases it.
/* svsem_mutex.c
* Uses a System V semaphore as a binary mutex (lock/unlock).
* Two helper functions: sem_lock() and sem_unlock().
* Compile: gcc svsem_mutex.c -o svsem_mutex
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
/* Acquire (lock) semaphore — decrement by 1 */
int sem_lock(int semid)
{
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = -1; /* Decrement by 1 */
sop.sem_flg = SEM_UNDO; /* Auto-release if process dies */
return semop(semid, &sop, 1);
}
/* Release (unlock) semaphore — increment by 1 */
int sem_unlock(int semid)
{
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = 1; /* Increment by 1 */
sop.sem_flg = SEM_UNDO;
return semop(semid, &sop, 1);
}
/* Get current semaphore value (for debugging) */
int sem_getval(int semid)
{
return semctl(semid, 0, GETVAL);
}
int main(void)
{
int semid;
union semun arg;
/* Create a private semaphore set with 1 semaphore */
semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
/* Initialize to 1 — mutex is unlocked */
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
printf("Initial semaphore value: %d\n", sem_getval(semid));
pid_t pid = fork();
if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid == 0) {
/* --- CHILD --- */
printf("Child [%d]: trying to acquire mutex...\n", getpid());
if (sem_lock(semid) == -1) { perror("sem_lock"); exit(EXIT_FAILURE); }
printf("Child [%d]: acquired! value=%d\n", getpid(), sem_getval(semid));
printf("Child [%d]: working for 2 seconds...\n", getpid());
sleep(2);
printf("Child [%d]: releasing mutex\n", getpid());
if (sem_unlock(semid) == -1) { perror("sem_unlock"); exit(EXIT_FAILURE); }
printf("Child [%d]: done, value=%d\n", getpid(), sem_getval(semid));
exit(0);
} else {
/* --- PARENT --- */
sleep(0); /* Let child get scheduled first */
printf("Parent [%d]: trying to acquire mutex...\n", getpid());
if (sem_lock(semid) == -1) { perror("sem_lock"); exit(EXIT_FAILURE); }
printf("Parent [%d]: acquired! value=%d\n", getpid(), sem_getval(semid));
printf("Parent [%d]: working for 1 second...\n", getpid());
sleep(1);
printf("Parent [%d]: releasing mutex\n", getpid());
if (sem_unlock(semid) == -1) { perror("sem_unlock"); exit(EXIT_FAILURE); }
printf("Parent [%d]: done, value=%d\n", getpid(), sem_getval(semid));
wait(NULL);
semctl(semid, 0, IPC_RMID); /* Clean up */
}
return 0;
}
Note: SEM_UNDO is used so that if a process crashes while holding the lock, the kernel automatically undoes the decrement and restores the value to 1, preventing a permanent deadlock.
A counting semaphore initialized to N controls access to N identical resources (like a connection pool). Each acquisition decrements by 1; each release increments by 1.
/* svsem_counting.c
* Counting semaphore — controls access to a pool of N resources.
* Demonstrates multiple processes competing for limited resources.
* Compile: gcc svsem_counting.c -o svsem_counting
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#define POOL_SIZE 3 /* Max concurrent resource users */
#define NUM_WORKERS 6 /* Total worker processes */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main(void)
{
int semid;
union semun arg;
int i;
semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
arg.val = POOL_SIZE; /* 3 resources available */
semctl(semid, 0, SETVAL, arg);
printf("Resource pool size: %d\n", POOL_SIZE);
printf("Spawning %d workers...\n\n", NUM_WORKERS);
for (i = 0; i < NUM_WORKERS; i++) {
pid_t pid = fork();
if (pid == 0) {
/* --- Worker child --- */
struct sembuf acquire = {0, -1, SEM_UNDO}; /* Decrement */
struct sembuf release = {0, 1, SEM_UNDO}; /* Increment */
printf("Worker %d [%d]: waiting for resource...\n", i, getpid());
if (semop(semid, &acquire, 1) == -1) {
perror("semop acquire"); exit(EXIT_FAILURE);
}
printf("Worker %d [%d]: GOT resource (pool remaining: %d)\n",
i, getpid(), semctl(semid, 0, GETVAL));
sleep(2); /* Use the resource */
if (semop(semid, &release, 1) == -1) {
perror("semop release"); exit(EXIT_FAILURE);
}
printf("Worker %d [%d]: RELEASED resource\n", i, getpid());
exit(0);
}
}
/* Parent waits for all workers */
for (i = 0; i < NUM_WORKERS; i++)
wait(NULL);
printf("\nAll workers done. Final pool value: %d\n",
semctl(semid, 0, GETVAL));
semctl(semid, 0, IPC_RMID);
return 0;
}
Use IPC_NOWAIT when you want to try acquiring a semaphore but not block if it’s unavailable. If the operation would block, semop() returns -1 with errno == EAGAIN.
/* svsem_nowait.c
* Demonstrates IPC_NOWAIT for non-blocking semaphore acquisition.
* The process tries to acquire the semaphore. If busy, it does
* other work and retries.
* Compile: gcc svsem_nowait.c -o svsem_nowait
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int try_acquire(int semid)
{
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = -1;
sop.sem_flg = IPC_NOWAIT | SEM_UNDO; /* Non-blocking */
if (semop(semid, &sop, 1) == 0)
return 1; /* Acquired */
if (errno == EAGAIN)
return 0; /* Resource busy — try later */
perror("semop");
return -1; /* Real error */
}
int main(void)
{
int semid;
union semun arg;
int attempts = 0;
int result;
semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
/* Initialize to 0 — resource is currently locked */
arg.val = 0;
semctl(semid, 0, SETVAL, arg);
printf("Semaphore value: %d (resource locked)\n",
semctl(semid, 0, GETVAL));
/* Simulate a lock-holder releasing after 3 seconds */
pid_t pid = fork();
if (pid == 0) {
sleep(3);
struct sembuf release = {0, 1, 0};
semop(semid, &release, 1);
printf("Lock-holder: released the lock\n");
exit(0);
}
/* Main process: try non-blocking acquire */
printf("Main: trying non-blocking acquire...\n");
while (1) {
attempts++;
result = try_acquire(semid);
if (result == 1) {
printf("Main: acquired on attempt %d!\n", attempts);
break;
} else if (result == 0) {
printf("Main: attempt %d failed (EAGAIN), doing other work...\n",
attempts);
sleep(1); /* Do something else instead of blocking */
} else {
exit(EXIT_FAILURE);
}
}
/* Use resource */
printf("Main: using resource...\n");
/* Release */
struct sembuf release = {0, 1, SEM_UNDO};
semop(semid, &release, 1);
printf("Main: released\n");
wait(NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
One of the most powerful features of semop() is performing operations on multiple semaphores atomically. Consider a producer/consumer system with two semaphores: empty (slots available) and full (items available).
| Role | sem[0] = empty (slots free) | sem[1] = full (items ready) |
|---|---|---|
| Producer | Decrement (wait for free slot) | Increment (signal item added) |
| Consumer | Increment (signal slot freed) | Decrement (wait for item) |
/* svsem_producer_consumer.c
* Classic bounded buffer with two semaphores:
* sem[0] = empty slots (initially BUFFER_SIZE)
* sem[1] = filled slots (initially 0)
* Compile: gcc svsem_producer_consumer.c -o svsem_pc
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/wait.h>
#define BUFFER_SIZE 4
#define NUM_ITEMS 10
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main(void)
{
int semid;
union semun arg;
unsigned short init_vals[2];
/* Create semaphore set with 2 semaphores */
semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
/* sem[0] = empty = BUFFER_SIZE (all slots free) */
/* sem[1] = full = 0 (no items yet) */
init_vals[0] = BUFFER_SIZE;
init_vals[1] = 0;
arg.array = init_vals;
semctl(semid, 0, SETALL, arg);
printf("Buffer size: %d\n", BUFFER_SIZE);
pid_t pid = fork();
if (pid == 0) {
/* --- CONSUMER --- */
int i;
struct sembuf consume[2];
for (i = 0; i < NUM_ITEMS; i++) {
/* Wait for a full slot, then signal an empty slot.
* These two operations happen atomically. */
consume[0].sem_num = 1; consume[0].sem_op = -1; consume[0].sem_flg = SEM_UNDO; /* full-- */
consume[1].sem_num = 0; consume[1].sem_op = 1; consume[1].sem_flg = SEM_UNDO; /* empty++ */
semop(semid, consume, 2); /* Atomic pair */
printf("Consumer: consumed item %d (full=%d empty=%d)\n",
i, semctl(semid,1,GETVAL), semctl(semid,0,GETVAL));
usleep(150000); /* 150ms */
}
exit(0);
} else {
/* --- PRODUCER --- */
int i;
struct sembuf produce[2];
for (i = 0; i < NUM_ITEMS; i++) {
/* Wait for an empty slot, then signal a full slot.
* These two operations happen atomically. */
produce[0].sem_num = 0; produce[0].sem_op = -1; produce[0].sem_flg = SEM_UNDO; /* empty-- */
produce[1].sem_num = 1; produce[1].sem_op = 1; produce[1].sem_flg = SEM_UNDO; /* full++ */
semop(semid, produce, 2); /* Atomic pair */
printf("Producer: produced item %d (full=%d empty=%d)\n",
i, semctl(semid,1,GETVAL), semctl(semid,0,GETVAL));
usleep(100000); /* 100ms */
}
wait(NULL);
semctl(semid, 0, IPC_RMID);
}
return 0;
}
Why atomic? If the consumer’s decrement of full and increment of empty were two separate semop() calls, a crash between them could permanently lose a buffer slot. By passing both in one call, the kernel guarantees both happen or neither does.
Because semop() is never auto-restarted after a signal, production code should wrap it in a retry loop:
/* Robust semop() wrapper that retries on EINTR */
#include <errno.h>
#include <sys/sem.h>
int robust_semop(int semid, struct sembuf *sops, int nsops)
{
int ret;
while (1) {
ret = semop(semid, sops, nsops);
if (ret == 0)
return 0; /* Success */
if (errno == EINTR) {
/* Interrupted by signal — retry */
continue;
}
/* EAGAIN: would block and IPC_NOWAIT was set */
/* EIDRM: semaphore set was deleted */
/* Other errors are fatal */
return -1;
}
}
It means “wait until the semaphore value reaches zero.” If the value is already 0, semop() returns immediately. Otherwise, the process blocks. This is used as a synchronization point — for example, waiting for all worker processes to finish (each worker decrements a shared semaphore, and the coordinator waits for it to reach 0).
SEM_UNDO tells the kernel to remember what change this operation made to the semaphore and reverse it if the process exits (normally or abnormally). This prevents a process from dying while holding a semaphore lock, which would leave other processes blocked forever. It should generally always be set for acquire operations on mutex-style semaphores.
When you pass an array of N operations to semop(), the kernel either performs all of them or none of them. It never performs a partial subset. If any one operation would block, the entire call blocks (unless IPC_NOWAIT is set, in which case the entire call fails with EAGAIN). This guarantees consistency — you cannot leave the system in a half-applied state.
(1) Another process changes the semaphore value so the blocked operation can now complete. (2) A signal is delivered to the blocking process — semop() returns -1 with errno == EINTR. (3) The semaphore set is deleted by another process — semop() returns -1 with errno == EIDRM.
On semget(), IPC_NOWAIT is not a valid flag (it uses IPC_CREAT and IPC_EXCL). On semop(), IPC_NOWAIT in sem_flg means: if this specific operation would cause the call to block, instead fail immediately with EAGAIN. If multiple operations are given, and any one of them has IPC_NOWAIT and would block, the entire call fails with EAGAIN and no operations are performed.
POSIX and Linux classify semop() as a “slow” system call that cannot be restarted via SA_RESTART because restarting it could cause subtle race conditions in semaphore-based synchronization protocols. The caller is expected to handle EINTR explicitly and decide whether to retry based on the application’s logic. This gives the application more control.
For sem_op > 0 (increment) or sem_op < 0 (decrement), the process needs write (alter) permission on the semaphore set. For sem_op == 0 (wait-for-zero), the process only needs read permission. Permissions are set when the semaphore set is created via semget() and can be changed with semctl(IPC_SET).
The call fails immediately with errno == EINVAL because the specification requires the array to contain at least one element (nsops ≥ 1). Passing zero operations is meaningless and not allowed.
Next: semtimedop() — blocking semaphore operations with a timeout
