What is semop()?
semop() is the Linux system call used to perform operations on System V semaphores. Once you have created a semaphore set with semget(), you use semop() to increment, decrement, or test the values of semaphores in that set.
The most important property of semop() is atomicity: when you pass multiple operations in one call, the kernel either performs ALL of them or NONE of them. This is what makes semaphores safe for process synchronization.
Think of a semaphore like a counter. Processes compete to acquire and release resources by incrementing or decrementing this counter. semop() is the gatekeeper.
The semop() system call is defined in <sys/sem.h>:
#include <sys/types.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
Parameters:
semid— the semaphore set identifier returned bysemget()sops— pointer to an array ofstruct sembufdescribing the operations to performnsops— the number of operations in the array (how many elements insops)
Return value: Returns 0 on success, -1 on error (with errno set).
Each element in the sops[] array is a struct sembuf. This structure tells the kernel which semaphore to operate on, what to do to it, and any flags to apply.
struct sembuf {
unsigned short sem_num; /* Semaphore number (index in the set) */
short sem_op; /* Operation: +ve, -ve, or 0 */
short sem_flg; /* Flags: IPC_NOWAIT, SEM_UNDO */
};
| Field | Type | Meaning | Example |
|---|---|---|---|
sem_num |
unsigned short | Index (0-based) of the semaphore inside the set | 0 = first semaphore |
sem_op |
short | >0 → add to semaphore (release / signal) <0 → subtract (acquire / wait) =0 → wait until value becomes 0 |
-1, +1, 0 |
sem_flg |
short | Bitwise OR of flags: IPC_NOWAIT, SEM_UNDO, or 0 |
IPC_NOWAIT |
Understanding what sem_op does in each case is the heart of using semaphores correctly.
The kernel adds sem_op to the semaphore’s current value.
This is a release or signal operation. It increases the available resource count. This operation never blocks.
/* Release: semval += sem_op */
sops[0].sem_op = +2; /* Add 2 */
The kernel subtracts |sem_op| from the semaphore.
If semval >= |sem_op|: subtract immediately.
If semval < |sem_op|: block the calling process until the value is large enough.
/* Acquire: blocks if semval < 1 */
sops[0].sem_op = -1;
The process waits until the semaphore’s value equals 0.
If semval == 0 already: returns immediately.
If semval != 0: blocks until another process reduces it to 0.
/* Wait for zero (barrier sync) */
sops[0].sem_op = 0;
Practical example – mutual exclusion (binary semaphore):
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
/* Acquire the semaphore (P operation / lock) */
void sem_lock(int semid) {
struct sembuf sop;
sop.sem_num = 0; /* Semaphore 0 in the set */
sop.sem_op = -1; /* Subtract 1 */
sop.sem_flg = 0; /* Block if necessary */
if (semop(semid, &sop, 1) == -1)
perror("semop lock");
}
/* Release the semaphore (V operation / unlock) */
void sem_unlock(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = +1; /* Add 1 back */
sop.sem_flg = 0;
if (semop(semid, &sop, 1) == -1)
perror("semop unlock");
}
By default, if a semaphore operation cannot be completed immediately (e.g., trying to subtract 1 from a semaphore that is already 0), semop() blocks the calling process until the operation can be satisfied.
If you set IPC_NOWAIT in sem_flg, the kernel will not block. Instead, if the operation would have blocked, semop() returns immediately with -1 and sets errno to EAGAIN.
#include <sys/types.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>
/* Try to acquire semaphore; don't block if unavailable */
int sem_try_lock(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = -1;
sop.sem_flg = IPC_NOWAIT; /* Non-blocking */
if (semop(semid, &sop, 1) == -1) {
if (errno == EAGAIN) {
printf("Semaphore busy, not blocking\n");
return 0; /* Could not acquire */
}
perror("semop");
return -1;
}
return 1; /* Acquired successfully */
}
You can pass an array of struct sembuf to perform multiple operations atomically in a single semop() call. This is one of the key advantages of System V semaphores.
The rule: Either ALL operations succeed, or NONE are applied. If any one operation would block (or fail with IPC_NOWAIT), the kernel suspends the entire group.
#include <sys/types.h>
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
int main(void) {
int semid = 0; /* Assume valid semaphore set ID */
struct sembuf sops[3];
/* Operation 1: Subtract 1 from semaphore 0 (acquire resource A) */
sops[0].sem_num = 0;
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
/* Operation 2: Add 2 to semaphore 1 (release two units of resource B) */
sops[1].sem_num = 1;
sops[1].sem_op = 2;
sops[1].sem_flg = 0;
/* Operation 3: Test semaphore 2 equals 0 (wait for condition C)
but with IPC_NOWAIT so we don't block on this one specifically */
sops[2].sem_num = 2;
sops[2].sem_op = 0;
sops[2].sem_flg = IPC_NOWAIT;
if (semop(semid, sops, 3) == -1) {
if (errno == EAGAIN)
printf("Operation 3 would have blocked – entire call skipped\n");
else
perror("semop");
} else {
printf("All 3 operations completed atomically\n");
}
return 0;
}
Without atomicity, a process might acquire resource A but fail to acquire resource B, leading to partial states and deadlock. System V semaphores solve this by making multi-resource acquisition all-or-nothing.
Each semaphore in a set has two associated kernel counters that track how many processes are waiting on it. These are visible via semctl() with GETNCNT and GETZCNT.
| Counter | Meaning | semctl() command |
|---|---|---|
SEMNCNT |
Number of processes blocked waiting for semaphore value to increase (i.e., doing a negative sem_op that can’t be satisfied yet) |
GETNCNT |
SEMZCNT |
Number of processes blocked waiting for semaphore value to become zero (doing a zero-test sem_op = 0 that’s not yet satisfied) |
GETZCNT |
#include <sys/sem.h>
#include <stdio.h>
void print_wait_counts(int semid, int semnum) {
int ncnt = semctl(semid, semnum, GETNCNT);
int zcnt = semctl(semid, semnum, GETZCNT);
printf("Sem[%d]: SEMNCNT=%d, SEMZCNT=%d\n", semnum, ncnt, zcnt);
}
In the shell session from the book, after 3 background processes block on a 2-semaphore set, you see:
$ ./svsem_mon 32769
Sem # Value SEMPID SEMNCNT SEMZCNT
0 1 0 1 1
1 0 0 2 0
Semaphore 0 has value 1 but one process is waiting to decrement it further (SEMNCNT=1) and one is waiting for it to become zero (SEMZCNT=1). Semaphore 1 has value 0 and two processes are waiting to decrement it (SEMNCNT=2).
A producer increments a semaphore to signal data is ready; a consumer decrements it to consume. This demonstrates the positive/negative sem_op pattern in a realistic context.
/* producer_consumer_sem.c
Compile: gcc producer_consumer_sem.c -o pc_sem
Usage: run two instances: ./pc_sem producer and ./pc_sem consumer
*/
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SEM_KEY 0x1234
#define SEM_PERMS 0600
/* semop wrapper for a single operation */
static void do_semop(int semid, int num, int op, int flags) {
struct sembuf sop;
sop.sem_num = (unsigned short) num;
sop.sem_op = (short) op;
sop.sem_flg = (short) flags;
if (semop(semid, &sop, 1) == -1) {
perror("semop");
exit(EXIT_FAILURE);
}
}
/* Get or create a semaphore set of 1 semaphore, initialized to 0 */
static int get_semaphore(void) {
int semid = semget(SEM_KEY, 1, IPC_CREAT | SEM_PERMS);
if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }
return semid;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s producer|consumer\n", argv[0]);
return 1;
}
int semid = get_semaphore();
if (strcmp(argv[1], "producer") == 0) {
printf("[Producer] Producing item...\n");
sleep(2); /* Simulate production time */
printf("[Producer] Item ready. Signaling consumer.\n");
do_semop(semid, 0, +1, 0); /* Signal: semval becomes 1 */
} else if (strcmp(argv[1], "consumer") == 0) {
printf("[Consumer] Waiting for item...\n");
do_semop(semid, 0, -1, 0); /* Wait: blocks until semval >= 1 */
printf("[Consumer] Got item! Processing.\n");
/* Clean up semaphore set */
semctl(semid, 0, IPC_RMID);
printf("[Consumer] Semaphore removed.\n");
}
return 0;
}
Run in two terminals: first start consumer (it blocks), then start producer. The consumer unblocks as soon as the producer signals.
A counting semaphore controls access to a pool of N identical resources (e.g., N database connections, N worker threads). Initialize to N; each acquisition subtracts 1; each release adds 1.
/* counting_semaphore.c – Manage a pool of 3 resources */
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define POOL_SIZE 3 /* 3 resources in the pool */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int create_counting_sem(int n) {
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(1); }
union semun arg;
arg.val = n; /* Initialize semaphore to pool size */
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(1);
}
printf("Counting semaphore created, initial value = %d\n", n);
return semid;
}
void acquire_resource(int semid) {
struct sembuf sop = { 0, -1, 0 }; /* Decrement by 1, blocking */
printf("[PID %d] Acquiring resource...\n", getpid());
semop(semid, &sop, 1);
printf("[PID %d] Resource acquired!\n", getpid());
}
void release_resource(int semid) {
struct sembuf sop = { 0, +1, 0 }; /* Increment by 1 */
semop(semid, &sop, 1);
printf("[PID %d] Resource released.\n", getpid());
}
int get_available(int semid) {
return semctl(semid, 0, GETVAL);
}
int main(void) {
int semid = create_counting_sem(POOL_SIZE);
int i;
printf("Available resources: %d\n", get_available(semid));
/* Simulate 3 processes acquiring resources */
for (i = 0; i < POOL_SIZE; i++) {
if (fork() == 0) {
acquire_resource(semid);
sleep(2); /* Use the resource */
release_resource(semid);
exit(0);
}
}
/* Wait for all children */
for (i = 0; i < POOL_SIZE; i++) wait(NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
semop(int semid, struct sembuf *sops, size_t nsops) performs one or more semaphore operations on the semaphore set identified by semid. The sops array describes each operation (which semaphore, what value change, what flags). nsops is the count of operations. The key feature is that all operations in one call are applied atomically.
When multiple operations are passed to semop(), the kernel either executes ALL of them or NONE. This prevents partial states. For example, if a process needs to acquire two resources simultaneously, atomicity ensures it won’t hold one while waiting for the other — preventing a class of deadlock scenarios.
Positive (sem_op > 0): Adds the value to the semaphore. Never blocks. Used to signal/release.
Negative (sem_op < 0): Subtracts |sem_op| from semaphore. Blocks if semval < |sem_op|. Used to acquire/wait.
Zero (sem_op == 0): Waits until the semaphore reaches exactly 0. Used as a barrier or completion signal.
IPC_NOWAIT in sem_flg makes the operation non-blocking. If the operation would block, semop() immediately returns -1 with errno = EAGAIN. Use it when a process should try to acquire a resource but must remain responsive (e.g., timeout loops, try-lock patterns, real-time systems where blocking is unacceptable).
SEMNCNT is the count of processes blocked waiting to decrement the semaphore (waiting for it to increase enough). SEMZCNT is the count of processes blocked waiting for the semaphore value to become zero. Both are accessible via semctl() with GETNCNT and GETZCNT commands.
Yes. IPC_NOWAIT is per-operation (in sem_flg of each struct sembuf). If any operation with IPC_NOWAIT would block, the entire semop() call fails with EAGAIN — none of the operations are applied. This maintains atomicity.
A blocking semop() can be interrupted by a signal, in which case it returns -1 with errno = EINTR. The operation is NOT applied in that case. Robust code must handle EINTR by restarting the call (often using SA_RESTART in signal handlers or a retry loop).
A binary semaphore has only values 0 or 1 and is used for mutual exclusion (like a mutex). A counting semaphore starts at N and controls access to a pool of N identical resources. System V semaphores natively support counting semaphores — the value can be any non-negative integer up to SEMVMX (typically 32767).
Next: svsem_op Example Program & Shell Session Demo
