What This Part Covers
TLPI provides a command-line program called svsem_op that lets you perform semop() operations directly from the shell without writing C code. This is incredibly useful for experimenting with semaphore blocking behavior and understanding how multiple processes interact around semaphores.
The shell session demo in the book is one of the best illustrations of how System V semaphore blocking, SEMNCNT, and SEMZCNT work in practice. We will walk through every line of that demo here and explain what is happening at the kernel level.
The svsem_op program provides a shell interface to semop(). It takes a semaphore set ID as its first argument, followed by one or more operation groups as subsequent arguments.
Command format:
./svsem_op <semid> <op-group> [<op-group> ...]
Each op-group is a comma-separated list of operations. Each operation has the form:
semnum+value /* Add value to semaphore semnum */
semnum-value /* Subtract value from semaphore semnum */
semnum=0 /* Test if semaphore semnum equals 0 */
You can also append n (IPC_NOWAIT) or u (SEM_UNDO) to any operation:
0-1n /* Subtract 1 from sem 0, non-blocking (IPC_NOWAIT) */
1+2u /* Add 2 to sem 1, with SEM_UNDO */
0=0n /* Test sem 0 for zero, non-blocking */
parseOps() is the heart of the argument parsing. It takes a string like "0-1,1-2n" and fills a struct sembuf array with the decoded operations.
Understanding this function is good practice for parsing structured strings in C using strtol(), pointer arithmetic, and strchr().
/* Simplified version of parseOps() to understand the logic */
#include <sys/sem.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define MAX_SEMOPS 1000
/* Parse a string like "0-1,1+2n,2=0" into struct sembuf array.
Returns the number of operations parsed. */
int parseOps(char *arg, struct sembuf sops[]) {
char *comma, *sign, *remaining, *flags;
int numOps;
for (numOps = 0, remaining = arg; ; numOps++) {
/* Must start with a digit (the semaphore number) */
if (!isdigit((unsigned char)*remaining)) {
fprintf(stderr, "Expected digit in: %s\n", arg);
return -1;
}
/* Read semaphore number */
sops[numOps].sem_num = (unsigned short) strtol(remaining, &sign, 10);
/* Next char must be +, -, or = */
if (*sign == '\0' || strchr("+-=", *sign) == NULL) {
fprintf(stderr, "Expected +, -, or = in: %s\n", arg);
return -1;
}
/* Read the operation value (the number after +/-/=) */
sops[numOps].sem_op = (short) strtol(sign + 1, &flags, 10);
/* If operator was '-', negate the value */
if (*sign == '-')
sops[numOps].sem_op = -sops[numOps].sem_op;
/* If operator was '=', value must be 0 */
if (*sign == '=' && sops[numOps].sem_op != 0) {
fprintf(stderr, "Only =0 is valid (not =%d)\n", sops[numOps].sem_op);
return -1;
}
/* Parse optional flags: 'n' = IPC_NOWAIT, 'u' = SEM_UNDO */
sops[numOps].sem_flg = 0;
for (; ; flags++) {
if (*flags == 'n')
sops[numOps].sem_flg |= IPC_NOWAIT;
else if (*flags == 'u')
sops[numOps].sem_flg |= SEM_UNDO;
else
break;
}
/* Check for comma: more ops follow; otherwise done */
comma = strchr(remaining, ',');
if (comma == NULL)
break;
else
remaining = comma + 1;
}
return numOps + 1;
}
Parsing example trace for "0-1,1-2n":
| Input token | sem_num | sem_op | sem_flg |
|---|---|---|---|
0-1 |
0 | -1 | 0 |
1-2n |
1 | -2 | IPC_NOWAIT |
The main() function is straightforward: it iterates over the command-line arguments (starting from index 2), parses each into a sembuf array using parseOps(), then calls semop() with that array. Before and after each call it prints a timestamped message with the PID.
int main(int argc, char *argv[]) {
struct sembuf sops[MAX_SEMOPS];
int ind, nsops;
if (argc < 2)
usageError(argv[0]);
/* argv[1] = semid; argv[2..] = operation groups */
for (ind = 2; argv[ind] != NULL; ind++) {
nsops = parseOps(argv[ind], sops);
/* Print message BEFORE the call (may block here) */
printf("%5ld, %s: about to semop() [%s]\n",
(long)getpid(), currTime("%T"), argv[ind]);
/* This call may block if any operation can't be performed yet */
if (semop(getInt(argv[1], 0, "semid"), sops, nsops) == -1)
errExit("semop (PID=%ld)", (long)getpid());
/* If we reach here, all operations completed */
printf("%5ld, %s: semop() completed [%s]\n",
(long)getpid(), currTime("%T"), argv[ind]);
}
exit(EXIT_SUCCESS);
}
The key observation: if semop() blocks, the second printf is NOT reached until another process changes the semaphore values. This is exactly what the shell session demo shows.
This is the live demonstration from TLPI. A semaphore set of 2 semaphores is created, initialized to values 1 and 0 respectively. Then three background processes are launched that all try to perform operations — and all block. Let’s walk through every step.
$ ./svsem_create -p 2
32769 <-- Semaphore set ID returned by semget()
$ ./svsem_setall 32769 1 0
Semaphore values changed (PID=3658)
Semaphore 0 = 1 (one resource available), Semaphore 1 = 0 (no resource yet available).
$ ./svsem_op 32769 0-1,1-1 &
3659, 16:02:05: about to semop() [0-1,1-1]
[1] 3659
What happens: Process 3659 tries to subtract 1 from semaphore 0 (currently 1) AND subtract 1 from semaphore 1 (currently 0). Semaphore 1 has value 0, so the subtraction would make it negative — this is impossible. The entire operation blocks. No “completed” message is printed.
SEMNCNT of semaphore 1 increments to 1 (one process waiting for sem1 to increase).
$ ./svsem_op 32769 1-1 &
3660, 16:02:22: about to semop() [1-1]
[2] 3660
What happens: Process 3660 also tries to subtract 1 from semaphore 1 (still 0). It also blocks. SEMNCNT of semaphore 1 is now 2 (processes 3659 and 3660 are both waiting).
$ ./svsem_op 32769 0=0 &
3661, 16:02:27: about to semop() [0=0]
[3] 3661
What happens: Process 3661 waits for semaphore 0 to equal 0. Semaphore 0 is currently 1, so the zero-test blocks. SEMZCNT of semaphore 0 increments to 1.
$ ./svsem_mon 32769
Semaphore changed: Sun Jul 25 16:01:53 2010
Last semop(): Thu Jan 1 01:00:00 1970
Sem # Value SEMPID SEMNCNT SEMZCNT
0 1 0 1 1
1 0 0 2 0
This table tells you the full story:
| Semaphore | Value | SEMNCNT | SEMZCNT | Explanation |
|---|---|---|---|---|
| 0 | 1 | 1 | 1 | Process 3659 is waiting to decrement (NCNT=1). Process 3661 is waiting for it to reach 0 (ZCNT=1). |
| 1 | 0 | 2 | 0 | Processes 3659 and 3660 are both waiting to decrement (NCNT=2). No one is waiting for zero (ZCNT=0). |
You don’t need TLPI’s toolchain to recreate this demo. Here is a self-contained program that creates the semaphore set, forks multiple children to perform operations (some of which will block), and monitors the SEMNCNT values.
/* sem_block_demo.c
Demonstrates semop() blocking and SEMNCNT tracking.
Compile: gcc sem_block_demo.c -o sem_block_demo
Run: ./sem_block_demo
*/
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
static void print_time(void) {
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
char buf[9];
strftime(buf, sizeof(buf), "%H:%M:%S", tm_info);
printf("[%s] ", buf);
}
static void semop_op(int semid, int num, int op, int flags,
const char *label) {
struct sembuf sop;
sop.sem_num = (unsigned short)num;
sop.sem_op = (short)op;
sop.sem_flg = (short)flags;
print_time();
printf("PID %d: about to semop(%s)\n", getpid(), label);
fflush(stdout);
if (semop(semid, &sop, 1) == -1) {
perror("semop");
exit(EXIT_FAILURE);
}
print_time();
printf("PID %d: semop(%s) COMPLETED\n", getpid(), label);
fflush(stdout);
}
static void print_sem_status(int semid) {
printf("\n--- Semaphore Status ---\n");
printf("%-6s %-6s %-8s %-8s\n",
"Sem#", "Value", "SEMNCNT", "SEMZCNT");
int s;
for (s = 0; s < 2; s++) {
int val = semctl(semid, s, GETVAL);
int ncnt = semctl(semid, s, GETNCNT);
int zcnt = semctl(semid, s, GETZCNT);
printf("%-6d %-6d %-8d %-8d\n", s, val, ncnt, zcnt);
}
printf("------------------------\n\n");
}
int main(void) {
/* Create set of 2 semaphores */
int semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);
if (semid == -1) { perror("semget"); exit(1); }
/* Initialize: sem0=1, sem1=0 */
union semun arg;
unsigned short vals[2] = { 1, 0 };
arg.array = vals;
if (semctl(semid, 0, SETALL, arg) == -1) {
perror("semctl SETALL"); exit(1);
}
printf("Created semaphore set %d: sem0=1, sem1=0\n\n", semid);
/* Fork child 1: try to decrement BOTH sem0 and sem1 (will block) */
pid_t c1 = fork();
if (c1 == 0) {
sleep(1); /* Let parent see initial state */
/* Must do two separate semop() calls since we want
"0-1 AND 1-1" as an atomic pair — simulate with array */
struct sembuf ops[2];
ops[0].sem_num = 0; ops[0].sem_op = -1; ops[0].sem_flg = 0;
ops[1].sem_num = 1; ops[1].sem_op = -1; ops[1].sem_flg = 0;
print_time();
printf("PID %d: about to semop(0-1,1-1)\n", getpid());
fflush(stdout);
if (semop(semid, ops, 2) == -1) { perror("semop"); exit(1); }
print_time();
printf("PID %d: semop(0-1,1-1) COMPLETED\n", getpid());
exit(0);
}
/* Fork child 2: try to decrement sem1 only (will block) */
pid_t c2 = fork();
if (c2 == 0) {
sleep(1);
semop_op(semid, 1, -1, 0, "1-1");
exit(0);
}
/* Fork child 3: wait for sem0 to equal 0 (will block) */
pid_t c3 = fork();
if (c3 == 0) {
sleep(1);
semop_op(semid, 0, 0, 0, "0=0");
exit(0);
}
/* Parent: let children start and block, then show status */
sleep(2);
printf("\n=== All 3 operations are now blocking ===\n");
print_sem_status(semid);
/* Now unblock by incrementing sem1 by 2 */
printf("Parent: adding 2 to sem1 to unblock waiting processes...\n\n");
sleep(1);
struct sembuf unlock;
unlock.sem_num = 1; unlock.sem_op = +2; unlock.sem_flg = 0;
if (semop(semid, &unlock, 1) == -1) { perror("semop unlock"); exit(1); }
/* Also decrement sem0 to 0 to unblock child 3 */
sleep(1);
struct sembuf dec0;
dec0.sem_num = 0; dec0.sem_op = -1; dec0.sem_flg = 0;
semop(semid, &dec0, 1);
/* Wait for all children */
waitpid(c1, NULL, 0);
waitpid(c2, NULL, 0);
waitpid(c3, NULL, 0);
printf("\nAll children finished.\n");
print_sem_status(semid);
semctl(semid, 0, IPC_RMID);
printf("Semaphore set removed.\n");
return 0;
}
The program demonstrates several important C programming patterns worth understanding.
a) strtol() for robust integer parsing:
char *endptr;
long value = strtol(str, &endptr, 10);
/* endptr points to the first character that was NOT part of the number.
This lets you know where the number ended and what comes after. */
b) strchr() to find a character in a string:
/* Check if sign char is one of +, -, = */
if (strchr("+-=", *sign) == NULL) {
/* Not a valid operator */
}
c) isdigit() for validating input characters:
#include <ctype.h>
if (!isdigit((unsigned char)*remaining)) {
/* Input is not a digit – reject it */
}
/* Note: cast to unsigned char is important for portability */
d) Bitwise OR to combine flags:
sops[numOps].sem_flg = 0;
if (*flags == 'n') sops[numOps].sem_flg |= IPC_NOWAIT;
if (*flags == 'u') sops[numOps].sem_flg |= SEM_UNDO;
e) getpid() + timestamp in log messages for multi-process debugging:
#include <unistd.h>
printf("%5ld, %s: about to semop() [%s]\n",
(long)getpid(), currTime("%T"), argv[ind]);
/* PID + time is essential when multiple processes share a terminal */
Because semop() is atomic. The operation requests BOTH sem0 and sem1 to be decremented. Sem1 is 0, so decrementing it would produce a negative value, which is not allowed. Since the complete operation (both decrements together) cannot be satisfied, the entire call blocks — even though sem0 individually could have been decremented.
It means 2 processes are currently blocked in semop() waiting for semaphore 1’s value to increase enough to satisfy their decrement request. In the demo, PIDs 3659 and 3660 are both waiting. When another process increments sem1 sufficiently, one or both will unblock.
The POSIX standard does not specify the order. In Linux, the kernel typically wakes processes in order of priority or FIFO among equal-priority processes, but this is implementation-defined. You should never rely on a specific unblocking order when multiple processes are waiting on the same semaphore.
SEMPID stores the PID of the last process that performed a successful semop() on that semaphore. Since no semop() has completed (all three are blocked), the PID remains 0 (the initial value). The timestamp for “Last semop()” also shows epoch time (1970) for the same reason.
Use strtol() with an end-pointer to read numbers and detect where they end. Use strchr() to check for operator characters (+, -, =). Use isdigit() to validate digit characters. Use pointer arithmetic to walk through the string, tracking the current parse position. Always cast char arguments to unsigned char when passing to isdigit() and similar functions to avoid undefined behavior on platforms where char is signed.
None of the background processes would block. Instead, each would immediately get errno = EAGAIN and print an error or handle the non-blocking failure. The SEMNCNT and SEMZCNT counters would remain 0 since no process actually enters the blocked-wait state.
Next: SEM_UNDO Flag – Automatic Semaphore Cleanup on Process Exit
