Imagine a process acquires a semaphore (decrements it to 0), then crashes before releasing it. Other processes waiting on that semaphore are now stuck forever. There is no mechanism to automatically “undo” the change.
The SEM_UNDO flag tells the kernel: “if this process dies, automatically reverse any semaphore changes I made.” This is the System V equivalent of an automatic lock release on process exit.
When you perform a semaphore operation with SEM_UNDO, the kernel does not simply record each individual operation. Instead it maintains a running total called semadj โ one per semaphore per process.
Think of semadj as “the net adjustment this process has made to this semaphore using SEM_UNDO”.
| Operation (with SEM_UNDO) | sem_op | semadj change | semadj total |
|---|---|---|---|
| Acquire lock (subtract 1) | -1 | +1 | 1 |
| Acquire again (subtract 1) | -1 | +1 | 2 |
| Release once (add 1) | +1 | -1 | 1 |
| Process exits โ kernel applies semadj = 1 reversal | โ | -1 (reverse) | 0 |
The semadj value is the negation of the sum of all SEM_UNDO operations. On exit, the kernel subtracts the semadj total from the semaphore’s current value โ which effectively reverses the net effect of all SEM_UNDO operations this process made.
You simply set the SEM_UNDO bit in sem_flg when calling semop(). No other changes are needed.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
/*
* Acquire semaphore with SEM_UNDO.
* If this process dies unexpectedly, the kernel will add back +1.
*/
void sem_lock_with_undo(int semid, int sem_num) {
struct sembuf sop;
sop.sem_num = sem_num;
sop.sem_op = -1;
sop.sem_flg = SEM_UNDO; /* โ this is the only difference */
if (semop(semid, &sop, 1) == -1) {
perror("semop lock");
exit(EXIT_FAILURE);
}
}
void sem_unlock_with_undo(int semid, int sem_num) {
struct sembuf sop;
sop.sem_num = sem_num;
sop.sem_op = +1;
sop.sem_flg = SEM_UNDO; /* undo the lock operation */
if (semop(semid, &sop, 1) == -1) {
perror("semop unlock");
exit(EXIT_FAILURE);
}
}
int main(void)
{
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
semctl(semid, 0, SETVAL, 1); /* 1 = unlocked */
printf("PID %d: acquiring semaphore with SEM_UNDO...\n", getpid());
sem_lock_with_undo(semid, 0);
printf("PID %d: semaphore acquired, doing critical work...\n", getpid());
/* Simulate crash: call abort() or kill(getpid(), SIGKILL) */
/* In normal flow, you'd call sem_unlock_with_undo() */
/* But if we crash, the kernel will undo the -1 automatically */
sleep(2);
printf("PID %d: releasing semaphore with SEM_UNDO...\n", getpid());
sem_unlock_with_undo(semid, 0);
printf("Semaphore value = %d\n", semctl(semid, 0, GETVAL));
semctl(semid, 0, IPC_RMID);
return 0;
}
This example forks a child that acquires a semaphore with SEM_UNDO then deliberately crashes (via abort()). A monitoring process shows the semaphore value is automatically restored by the kernel.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/sem.h>
#include <sys/wait.h>
int main(void)
{
/* Create semaphore, start at 1 (available) */
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
semctl(semid, 0, SETVAL, 1);
printf("Initial semaphore value: %d\n", semctl(semid, 0, GETVAL));
pid_t child = fork();
if (child == 0) {
/* CHILD acquires with SEM_UNDO */
struct sembuf acquire = {0, -1, SEM_UNDO};
if (semop(semid, &acquire, 1) == -1) {
perror("semop child");
exit(1);
}
printf("Child PID=%d: acquired semaphore (value now %d)\n",
getpid(), semctl(semid, 0, GETVAL));
printf("Child PID=%d: simulating crash with abort()...\n", getpid());
abort(); /* crash! kernel will apply SEM_UNDO */
}
/* PARENT waits for child to crash */
int status;
wait(&status);
if (WIFSIGNALED(status))
printf("\nChild died from signal %d\n", WTERMSIG(status));
/* Kernel should have restored semaphore to 1 */
printf("Semaphore value AFTER child crash: %d\n", semctl(semid, 0, GETVAL));
printf("(Expected: 1 because SEM_UNDO restored it)\n");
semctl(semid, 0, IPC_RMID);
return 0;
}
Expected output:
Initial semaphore value: 1
Child PID=2345: acquired semaphore (value now 0)
Child PID=2345: simulating crash with abort()...
Child died from signal 6
Semaphore value AFTER child crash: 1
(Expected: 1 because SEM_UNDO restored it)
Without SEM_UNDO, the semaphore value would remain at 0 after the crash, and any other process waiting to acquire it would be stuck forever.
- Process A acquires sem (val 1โ0)
- Process B waits (blocked)
- Process A crashes
- Semaphore stays at 0
- Process B blocks forever
- System needs manual intervention
- Process A acquires sem with SEM_UNDO (val 1โ0)
- Process B waits (blocked)
- Process A crashes
- Kernel applies semadj: val 0โ1
- Process B wakes up
- System recovers automatically
When you explicitly set a semaphore value using semctl() with SETVAL or SETALL, the kernel clears all semadj values for that semaphore in all processes.
This makes sense: if you forcibly set a semaphore to a specific value, the historical record of “what adjustments were made” is no longer meaningful. The semadj running total would produce nonsensical results if kept.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
int main(void)
{
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
semctl(semid, 0, SETVAL, 1);
/* Acquire with SEM_UNDO -- builds up semadj = 1 */
struct sembuf acquire = {0, -1, SEM_UNDO};
semop(semid, &acquire, 1);
printf("After acquire: semval = %d\n", semctl(semid, 0, GETVAL));
/*
* Forcibly set semaphore to 5.
* This CLEARS the semadj value for ALL processes.
* When this process exits, it will NOT try to add back 1.
*/
semctl(semid, 0, SETVAL, 5);
printf("After SETVAL(5): semval = %d\n", semctl(semid, 0, GETVAL));
printf("Note: semadj has been cleared by SETVAL\n");
/*
* When this process exits now, the semaphore stays at 5.
* The -1 from SEM_UNDO has been forgotten by SETVAL.
*/
semctl(semid, 0, IPC_RMID);
return 0;
}
| Event | semadj Behaviour | Reason |
|---|---|---|
| fork() | Child does NOT inherit semadj | Child should not undo its parent’s operations |
| exec() | semadj values ARE preserved | Allows exec’d program to auto-adjust on exit (useful for cleanup) |
| clone(CLONE_SYSVSEM) | Threads share semadj | Required for POSIX thread conformance (used by NPTL pthread_create) |
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/wait.h>
int main(void)
{
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
semctl(semid, 0, SETVAL, 2);
/* Parent acquires with SEM_UNDO: semadj = 1, semval = 1 */
struct sembuf op = {0, -1, SEM_UNDO};
semop(semid, &op, 1);
printf("Parent: semval after acquire = %d\n", semctl(semid, 0, GETVAL));
pid_t child = fork();
if (child == 0) {
/*
* Child does NOT inherit parent's semadj.
* Even if child exits normally, it will NOT undo the parent's -1.
* Child only undoes operations it itself performs with SEM_UNDO.
*/
printf("Child: semval = %d (sees parent's change but not semadj)\n",
semctl(semid, 0, GETVAL));
printf("Child: exiting WITHOUT any SEM_UNDO operations\n");
exit(0);
}
wait(NULL);
printf("After child exit: semval = %d (child did not undo parent's op)\n",
semctl(semid, 0, GETVAL));
semctl(semid, 0, IPC_RMID);
return 0;
}
SEM_UNDO only undoes the semaphore value change. It does NOT restore the actual shared resource to a consistent state. For example, if a process modified a shared memory region, crashed, and left data corrupted โ SEM_UNDO will release the semaphore but the shared data is still corrupted.
If the semaphore value has been decreased below the amount that needs to be undone (by other processes), the kernel cannot fully reverse the adjustment. Linux resolves this by decreasing the semaphore as far as possible (to 0) and exiting. Some other UNIX systems skip the undo entirely.
| Step | Action | Semaphore Value |
|---|---|---|
| 1 | Initial state | 0 |
| 2 | Process A: +2 with SEM_UNDO (semadj = -2) |
2 |
| 3 | Process B: -1 (no SEM_UNDO) |
1 |
| 4 | Process A dies โ kernel tries to undo: semval - 2 = -1 |
IMPOSSIBLE (would go negative) |
| 5 | Linux resolves: sets value to 0 (decrements as far as possible) | 0 |
If an undo operation would raise the semaphore above the maximum allowed value (SEMVMX = 32767), Linux still performs the adjustment, potentially pushing the value above the limit. This is considered anomalous behavior.
This reproduces the classic example from TLPI: operate on two semaphores โ one with SEM_UNDO and one without โ then exit. Only the SEM_UNDO operation is reversed.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/wait.h>
int main(void)
{
/* Create set with 2 semaphores */
int semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0600);
/* Both start at 0 */
unsigned short init[2] = {0, 0};
union semun { int v; struct semid_ds *b; unsigned short *a; } arg;
arg.array = init;
semctl(semid, 0, SETALL, arg);
printf("Initial: sem[0]=%d sem[1]=%d\n\n",
semctl(semid, 0, GETVAL),
semctl(semid, 1, GETVAL));
pid_t child = fork();
if (child == 0) {
struct sembuf ops[2];
/* sem[0]: +1 WITH SEM_UNDO โ will be reversed on child exit */
ops[0].sem_num = 0;
ops[0].sem_op = +1;
ops[0].sem_flg = SEM_UNDO;
/* sem[1]: +1 WITHOUT SEM_UNDO โ will NOT be reversed on child exit */
ops[1].sem_num = 1;
ops[1].sem_op = +1;
ops[1].sem_flg = 0;
semop(semid, ops, 2);
printf("Child (PID=%d): after semop:\n", getpid());
printf(" sem[0] = %d (SEM_UNDO applied)\n", semctl(semid, 0, GETVAL));
printf(" sem[1] = %d (no SEM_UNDO)\n\n", semctl(semid, 1, GETVAL));
printf("Child: exiting now...\n");
exit(0); /* kernel will undo sem[0] only */
}
wait(NULL);
printf("After child exit:\n");
printf(" sem[0] = %d (expected 0 โ SEM_UNDO reversed +1)\n",
semctl(semid, 0, GETVAL));
printf(" sem[1] = %d (expected 1 โ no SEM_UNDO, change kept)\n",
semctl(semid, 1, GETVAL));
semctl(semid, 0, IPC_RMID);
return 0;
}
Expected output:
Initial: sem[0]=0 sem[1]=0
Child (PID=3456): after semop:
sem[0] = 1 (SEM_UNDO applied)
sem[1] = 1 (no SEM_UNDO)
Child: exiting now...
After child exit:
sem[0] = 0 (expected 0 โ SEM_UNDO reversed +1)
sem[1] = 1 (expected 1 โ no SEM_UNDO, change kept)
Q1: What problem does SEM_UNDO solve?
If a process holds a semaphore (keeps it at 0) and dies unexpectedly, other processes blocked on that semaphore would wait forever. SEM_UNDO tells the kernel to automatically reverse the process’s semaphore adjustments on exit, allowing blocked processes to proceed.
Q2: What is semadj and how does the kernel use it?
semadj is a per-process, per-semaphore running total of all SEM_UNDO operations performed by that process. When the process terminates, the kernel subtracts the semadj total from the current semaphore value โ effectively reversing the net effect of all SEM_UNDO operations.
Q3: Does a child process inherit the parent’s semadj values after fork()?
No. A child created by fork() starts with semadj values of 0. This prevents a child from accidentally undoing its parent’s semaphore operations when the child exits.
Q4: Are semadj values preserved across exec()?
Yes. Unlike fork(), exec() preserves semadj values. This allows a process to adjust a semaphore with SEM_UNDO, then exec() a new program that doesn’t touch the semaphore at all โ when that exec’d program eventually exits, the semaphore adjustment is automatically applied.
Q5: What happens to semadj values when you call semctl(SETVAL) on a semaphore?
SETVAL (and SETALL) clears the semadj values to 0 in all processes. Since SETVAL forces an absolute value, the historical adjustment record would be meaningless.
Q6: What does Linux do when it cannot fully undo a semaphore adjustment on process exit?
Linux decrements the semaphore as far as possible (to 0) rather than blocking forever or skipping the undo. This is Linux’s resolution of an undefined behavior case โ other UNIX implementations may skip the undo entirely.
Q7: Why is SEM_UNDO described as “less useful than it first appears”?
Because it only restores the semaphore value โ it does not restore the actual shared resource to a consistent state. If a process modifies shared memory and crashes, SEM_UNDO releases the semaphore but leaves the shared memory corrupted. True recovery requires application-level crash handling.
Next: Binary Semaphores Protocol โ Implementing a Clean Mutex API
