Chapter 79: IPC: System V Semaphores for Shared Memory Synchronization
Chapter Objectives
Upon completing this chapter, you will be able to:
- Understand the fundamental principles of semaphores and their role in preventing race conditions.
- Implement System V semaphores to manage concurrent access to shared memory segments.
- Utilize the core System V semaphore system calls:
semget()
,semop()
, andsemctl()
. - Design and build multi-process applications on a Raspberry Pi 5 that safely share data using IPC.
- Debug common synchronization issues related to semaphore implementation.
- Properly manage the lifecycle of IPC resources to prevent system leaks.
Introduction
In the world of embedded Linux, performance and efficiency are paramount. Many designs leverage multiple processes working in concert to handle complex tasks, from managing sensor data to controlling actuators. As we’ve seen in previous chapters, shared memory is one of the fastest Inter-Process Communication (IPC) mechanisms available, allowing different processes to access the same region of physical memory directly. However, this power comes with significant risk. When multiple processes attempt to read from and write to a shared resource simultaneously, the result is often chaos—a situation known as a race condition. Data can become corrupted, system states can become inconsistent, and the entire application can fail in unpredictable ways.
This chapter tackles this fundamental problem of concurrency control head-on. We will explore semaphores, one of the oldest and most effective synchronization primitives in the UNIX world. Specifically, we will focus on the robust and widely supported System V semaphore facility. You will learn how to use these powerful tools to act as “traffic cops” for your shared memory segments, ensuring that only one process can enter a “critical section” of code at a time. By mastering semaphores, you will gain the ability to build complex, multi-process embedded applications that are not only fast but also reliable and correct. We will move from theory to practice, implementing a complete, synchronized shared memory application on the Raspberry Pi 5, giving you a tangible and practical understanding of how to enforce order in a concurrent environment.
Technical Background
The Philosophical Core of Semaphores
To truly appreciate the role of semaphores, one must first understand the problem they solve: mutual exclusion. Imagine a narrow bridge that can only support the weight of one vehicle at a time. If two cars try to cross from opposite ends simultaneously, they will meet in the middle, causing a deadlock. A traffic light at each end of the bridge solves this problem. It acts as a signaling mechanism, ensuring that only one direction has a green light (permission to cross) while the other has a red light (a command to wait).
A semaphore is the software equivalent of this traffic light. It is, at its heart, a special integer counter managed by the operating system kernel. The term itself, coined by computer science pioneer Edsger W. Dijkstra in the 1960s, combines the Greek words sema (signal) and phoros (bearer). A semaphore bears a signal that processes can use to coordinate their actions.
The two fundamental operations on a semaphore, originally named P and V by Dijkstra, are the keys to its function. The P operation (from the Dutch proberen, “to test”) attempts to decrement the semaphore’s value. If the value is greater than zero, the decrement succeeds, and the process continues its execution. If the value is zero, the process is blocked—put to sleep by the kernel—until another process performs a V operation. The V operation (from verhogen, “to increase”) increments the semaphore’s value. If there are any processes blocked waiting on that semaphore, the kernel wakes one of them up, allowing it to complete its P operation and proceed.
Crucially, the kernel guarantees that these P and V operations are atomic. This means that the check of the semaphore’s value and the subsequent increment or decrement are performed as a single, indivisible instruction from the perspective of all other processes. There is no possibility for one process to interrupt another in the middle of a semaphore operation, which is what prevents race conditions at the synchronization level itself.
graph TD subgraph "Semaphore Value: 1 (Available)" A[Process A wants to enter<br>Critical Section] --> A1{"P(sem): Is sem > 0?"} A1 -- Yes --> A2[Decrement sem to 0<br><b>Enter Critical Section</b>]:::process end subgraph "Semaphore Value: 0 (Locked)" B[Process B wants to enter<br>Critical Section] --> B1{"P(sem): Is sem > 0?"} B1 -- No --> B2((Process B is Blocked by Kernel)):::check end subgraph "Process A Exits" A2 --> A3["<b>Exit Critical Section</b><br>V(sem): Increment sem to 1"]:::process A3 --> K((Kernel Wakes Up Process B)):::system end subgraph "Process B Proceeds" K --> B3["Process B's P(sem) now succeeds<br>Decrement sem to 0<br><b>Enter Critical Section</b>"]:::process end A --> B B2 -.-> K classDef process fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff classDef decision fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff classDef check fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff classDef endo fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff classDef system fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
System V Semaphores: The Kernel’s Gatekeeper
While the concept of a semaphore is simple, the Linux implementation, particularly the System V variant, adds layers of functionality and structure. System V IPC objects (including shared memory, message queues, and semaphores) are not identified by file paths but by a key. This key, of type key_t
, is a long integer that acts as a system-wide name for the IPC resource. Processes that want to use the same IPC object must agree on a common key. A common convention is to use the ftok()
(“file to key”) function, which generates a key from a file path and a project ID, ensuring that unrelated applications do not accidentally collide.
Instead of creating a single semaphore, the System V API works with semaphore sets. A single semget()
call can create an array of semaphores, all identified by a single semaphore identifier. This is useful for complex synchronization scenarios where different semaphores in a set might protect different sub-resources or represent different states (e.g., one semaphore for “empty slots” and another for “full slots” in a producer-consumer buffer).
The three primary system calls for managing System V semaphores are:
semget(key_t key, int nsems, int semflg)
: This is the “get semaphore” call. It either creates a new semaphore set or gets the identifier of an existing one.key
: The public name for the semaphore set. UsingIPC_PRIVATE
creates a set that can only be used by related processes (e.g., parent and child).nsems
: The number of semaphores in the set. This is only used when creating a new set.- semflg: A bitmask of flags. IPC_CREAT tells the system to create the set if it doesn’t exist. Combining it with IPC_EXCL ensures that you exclusively create it, failing if it already exists. Permissions (e.g., 0666) are also specified here, similar to file permissions.On success, semget() returns a non-negative integer, the semaphore identifier (semid), which is the private handle used for all subsequent operations.
semop(int semid, struct sembuf *sops, size_t nsops)
: This is the workhorse function that performs operations on the semaphores in a set. It is incredibly powerful because it can perform a list of operations on multiple semaphores within the set atomically.semid
: The identifier returned bysemget()
.sops
: A pointer to an array ofsembuf
structures. Each structure defines one operation.nsops
: The number ofsembuf
structures in the array.
sembuf
structure is the core of the operation, containing three fields:sem_num
: The index of the semaphore within the set (e.g., 0 for the first semaphore).sem_op
: The operation to perform. A negative value corresponds to a P operation (acquiring the resource), a positive value to a V operation (releasing it), and a value of zero means to wait until the semaphore’s value is zero.sem_flg
: Flags, most commonlySEM_UNDO
. This is a crucial feature for robust applications. It tells the kernel to automatically undo the operation when the process terminates, whether gracefully or by crashing. This prevents a process from acquiring a semaphore and dying, leaving the resource permanently locked.
semctl(int semid, int semnum, int cmd, ...)
: This is the “control” function, a multi-tool for managing semaphores.semid
andsemnum
: Identify the semaphore set and the specific semaphore within it.cmd
: The command to perform. Important commands include:SETVAL
: To initialize the value of a specific semaphore. Semaphores are not initialized to a useful value upon creation; this step is mandatory.GETVAL
: To get the current value of a semaphore.IPC_RMID
: To remove the semaphore set from the system. This is a critical cleanup step.IPC_STAT
: To get status information about the semaphore set.
Binary vs. Counting Semaphores
The logic we’ve discussed so far naturally leads to two primary patterns of semaphore use. The most common is the binary semaphore, also known as a mutex (short for mutual exclusion). Its value is only ever 0 or 1. It is initialized to 1 (“available”). The first process to perform a P operation acquires the lock (value becomes 0), and any subsequent process is blocked until the first process performs a V operation to release the lock (value returns to 1). This is the digital equivalent of a key to a single room; only one process can hold the key at a time.
A counting semaphore, on the other hand, can be initialized to any non-negative value N
. This is used to control access to a pool of N
identical resources. For example, if an embedded device has four available processing slots for a specific type of task, a counting semaphore could be initialized to 4. Each time a process wants to use a slot, it performs a P operation. The first four processes will succeed. The fifth will block until one of the other processes finishes and performs a V operation, freeing up a slot.
In our primary use case—protecting a single shared memory segment—a binary semaphore is the appropriate tool. We will use a single semaphore, initialize its value to 1, and use semop
to perform a P operation (sem_op = -1
) before accessing the shared memory and a V operation (sem_op = 1
) immediately after. This simple, powerful pattern is the foundation of safe concurrent programming with shared memory.
Practical Examples
This section provides a hands-on demonstration of using System V semaphores to protect a shared memory segment on a Raspberry Pi 5. We will create two programs: a “writer” that periodically writes the system uptime to a shared buffer, and a “reader” that reads and displays this information. A binary semaphore will ensure that the reader never accesses the buffer while the writer is in the middle of updating it.

File Structure
First, let’s establish our project directory structure. It’s simple, containing our two C source files and a common header file for the IPC key and structure definition.
/home/pi/ipc_demo/
├── common.h
├── writer.c
├── reader.c
└── Makefile
Common Header File (common.h
)
This file is crucial for ensuring both programs use the same IPC key and data structure. Using ftok()
with a file that is guaranteed to exist (like our Makefile
) is a reliable way to generate a consistent key.
// common.h
#ifndef COMMON_H
#define COMMON_H
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
// Define a key for our IPC resources.
// We'll use ftok on a known file path.
#define IPC_KEY_PATH "/tmp" // A common, stable path
#define IPC_KEY_PROJ_ID 'A'
// The structure we will place in shared memory
#define SHARED_MEM_SIZE 1024
struct shared_data {
long long write_count;
char message[256];
};
// A union required for semctl calls
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
#endif // COMMON_H
sequenceDiagram actor Writer actor Reader participant Kernel Writer->>Kernel: ftok("/tmp", 'A') Kernel-->>Writer: key_t Writer->>Kernel: shmget(key, ..., IPC_CREAT) Kernel-->>Writer: shmid Writer->>Kernel: semget(key, ..., IPC_CREAT) Kernel-->>Writer: semid Writer->>Kernel: semctl(semid, 0, SETVAL, 1) Kernel-->>Writer: Success loop Every 3 seconds Writer->>Kernel: semop(semid, op=-1) // Lock Note right of Writer: Enters Critical Section Kernel-->>Writer: Access Granted Writer->>Kernel: Writes to Shared Memory Writer->>Kernel: semop(semid, op=1) // Unlock Note right of Writer: Exits Critical Section Kernel-->>Writer: Success end Reader->>Kernel: ftok("/tmp", 'A') Kernel-->>Reader: key_t (same key) Reader->>Kernel: shmget(key, ..., 0) Kernel-->>Reader: shmid (existing) Reader->>Kernel: semget(key, ..., 0) Kernel-->>Reader: semid (existing) loop Every 1 second Reader->>Kernel: semop(semid, op=-1) // Lock Note right of Reader: Enters Critical Section Kernel-->>Reader: Access Granted Reader->>Kernel: Reads from Shared Memory Reader->>Kernel: semop(semid, op=1) // Unlock Note right of Reader: Exits Critical Section Kernel-->>Reader: Success end
The Writer Program (writer.c
)
The writer is responsible for creating and initializing both the shared memory segment and the semaphore. It then enters a loop, locking the semaphore, writing data to the shared memory, and unlocking the semaphore.
Warning: This program must be run first, as it creates and initializes the IPC resources.
// writer.c
#include "common.h"
void lock_semaphore(int semid) {
struct sembuf sop;
sop.sem_num = 0; // Operate on the first semaphore in the set
sop.sem_op = -1; // Lock operation (P operation)
sop.sem_flg = SEM_UNDO; // Ensure kernel cleans up if we crash
if (semop(semid, &sop, 1) == -1) {
perror("semop lock");
exit(1);
}
printf("Writer: Locked semaphore.\n");
}
void unlock_semaphore(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = 1; // Unlock operation (V operation)
sop.sem_flg = SEM_UNDO;
if (semop(semid, &sop, 1) == -1) {
perror("semop unlock");
exit(1);
}
printf("Writer: Unlocked semaphore.\n");
}
int main() {
key_t key;
int shmid, semid;
struct shared_data *shm_ptr;
union semun sem_union;
// 1. Generate the same IPC key
key = ftok(IPC_KEY_PATH, IPC_KEY_PROJ_ID);
if (key == -1) {
perror("ftok");
exit(1);
}
// 2. Create and get the shared memory segment
shmid = shmget(key, SHARED_MEM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 3. Attach the shared memory segment
shm_ptr = (struct shared_data *)shmat(shmid, NULL, 0);
if (shm_ptr == (void *)-1) {
perror("shmat");
exit(1);
}
// 4. Create the semaphore set (1 semaphore)
semid = semget(key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(1);
}
// 5. Initialize the semaphore (this is a critical step!)
// We initialize it to 1, making it a binary semaphore (mutex)
sem_union.val = 1;
if (semctl(semid, 0, SETVAL, sem_union) == -1) {
perror("semctl SETVAL");
exit(1);
}
printf("Writer: Semaphore initialized to 1.\n");
// 6. Main loop: write to shared memory every 3 seconds
shm_ptr->write_count = 0;
while (1) {
lock_semaphore(semid); // Wait for lock
// --- Critical Section Start ---
shm_ptr->write_count++;
snprintf(shm_ptr->message, sizeof(shm_ptr->message),
"System uptime: %ld seconds. Write count: %lld",
time(NULL), shm_ptr->write_count);
printf("Writer: Wrote to shared memory: \"%s\"\n", shm_ptr->message);
// --- Critical Section End ---
unlock_semaphore(semid); // Release lock
sleep(3);
}
// This part is technically unreachable in this example, but shows good practice
shmdt(shm_ptr);
// The creator should ideally be the one to remove the IPC objects
// shmctl(shmid, IPC_RMID, NULL);
// semctl(semid, 0, IPC_RMID, sem_union);
return 0;
}
The Reader Program (reader.c
)
The reader’s job is simpler. It gets access to the existing shared memory and semaphore. It does not create them. In its loop, it locks the semaphore, reads the data, and unlocks it, ensuring it always gets a complete, consistent message.
// reader.c
#include "common.h"
void lock_semaphore(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = -1;
sop.sem_flg = SEM_UNDO;
if (semop(semid, &sop, 1) == -1) {
perror("semop lock");
exit(1);
}
// No printf here to keep output clean
}
void unlock_semaphore(int semid) {
struct sembuf sop;
sop.sem_num = 0;
sop.sem_op = 1;
sop.sem_flg = SEM_UNDO;
if (semop(semid, &sop, 1) == -1) {
perror("semop unlock");
exit(1);
}
}
int main() {
key_t key;
int shmid, semid;
struct shared_data *shm_ptr;
// 1. Generate the same IPC key
key = ftok(IPC_KEY_PATH, IPC_KEY_PROJ_ID);
if (key == -1) {
perror("ftok");
exit(1);
}
// 2. Get the existing shared memory segment. Note: no IPC_CREAT
shmid = shmget(key, SHARED_MEM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
fprintf(stderr, "Is the writer process running?\n");
exit(1);
}
// 3. Attach the shared memory segment
shm_ptr = (struct shared_data *)shmat(shmid, NULL, 0);
if (shm_ptr == (void *)-1) {
perror("shmat");
exit(1);
}
// 4. Get the existing semaphore set. Note: no IPC_CREAT
semid = semget(key, 1, 0666);
if (semid == -1) {
perror("semget");
fprintf(stderr, "Is the writer process running and initialized?\n");
exit(1);
}
printf("Reader: Attached to shared memory and semaphore.\n");
// 5. Main loop: read from shared memory every second
while (1) {
lock_semaphore(semid);
// --- Critical Section Start ---
printf("Reader: Read from shared memory: \"%s\"\n", shm_ptr->message);
// --- Critical Section End ---
unlock_semaphore(semid);
sleep(1);
}
// Detach from shared memory
shmdt(shm_ptr);
return 0;
}
Build and Execution Steps
- Create the Makefile: This
Makefile
will compile both programs.
# Makefile
CC=gcc
CFLAGS=-Wall -Wextra -g
TARGETS=writer reader
all: $(TARGETS)
writer: writer.c common.h
$(CC) $(CFLAGS) -o writer writer.c
reader: reader.c common.h
$(CC) $(CFLAGS) -o reader reader.c
clean:
rm -f $(TARGETS)
2. Compile the Code: Open a terminal on your Raspberry Pi 5 in the ipc_demo
directory and run make
.
pi@raspberrypi:~/ipc_demo $ make
gcc -Wall -Wextra -g -o writer writer.c
gcc -Wall -Wextra -g -o reader reader.c
3. Run the Programs: You will need two separate terminal windows.
- Terminal 1: Start the writer process. It will create the IPC resources and begin writing.
pi@raspberrypi:~/ipc_demo $ ./writer
Writer: Semaphore initialized to 1.
Writer: Locked semaphore.
Writer: Wrote to shared memory: "System uptime: 1678886400 seconds. Write count: 1"
Writer: Unlocked semaphore.
Writer: Locked semaphore.
Writer: Wrote to shared memory: "System uptime: 1678886403 seconds. Write count: 2"
Writer: Unlocked semaphore.
...
- Terminal 2: Start the reader process. It will attach to the existing resources and start reading.
pi@raspberrypi:~/ipc_demo $ ./reader
Reader: Attached to shared memory and semaphore.
Reader: Read from shared memory: "System uptime: 1678886400 seconds. Write count: 1"
Reader: Read from shared memory: "System uptime: 1678886400 seconds. Write count: 1"
Reader: Read from shared memory: "System uptime: 1678886400 seconds. Write count: 1"
Reader: Read from shared memory: "System uptime: 1678886403 seconds. Write count: 2"
...
- You will observe the reader printing the same message multiple times until the writer updates it. Each message read is guaranteed to be complete.
Cleaning Up IPC Resources
System V IPC objects persist in the kernel until they are explicitly removed or the system reboots. It is critical to clean them up after you are done.
1. List IPC Objects: Use the ipcs
command to see the active shared memory segments and semaphores.
pi@raspberrypi:~/ipc_demo $ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x4104038a 0 pi 666 1024 2
------ Semaphore Arrays --------
key semid owner perms nsems
0x4104038a 0 pi 666 1
------ Message Queues --------
key msqid owner perms used-bytes messages
2. Remove IPC Objects: Use the ipcrm
command with the shmid
and semid
from the ipcs
output.
# Stop the writer and reader processes first (Ctrl+C)
pi@raspberrypi:~/ipc_demo $ ipcrm shm 0 # Remove shared memory with ID 0
pi@raspberrypi:~/ipc_demo $ ipcrm sem 0 # Remove semaphore with ID 0
Tip: In a real application, you would design a graceful shutdown mechanism where the last process to detach from the resources is responsible for calling
shmctl(shmid, IPC_RMID, NULL)
andsemctl(semid, 0, IPC_RMID, sem_union)
to programmatically remove them.
Common Mistakes & Troubleshooting
Implementing synchronization logic is notoriously tricky. A small mistake can lead to deadlocks or subtle race conditions that are difficult to reproduce. Here are some common pitfalls when working with System V semaphores.
Exercises
These exercises are designed to reinforce the concepts of semaphore-based synchronization and build upon the example code.
- Modify for Verbose Locking:
- Objective: Gain a clearer visual understanding of the blocking mechanism.
- Task: Modify the
reader.c
program. Before callinglock_semaphore()
, print a message like “Reader: Attempting to lock…”. In thewriter.c
program, increase thesleep()
duration inside the critical section to 5 seconds (sleep(5);
). - Verification: Run both programs. You should see the reader print “Attempting to lock…” and then pause. It will only print its “Read from shared memory…” message after the writer has unlocked the semaphore, which now takes longer. This demonstrates the reader blocking on the
semop
call.
- Implement a Cleanup Program:
- Objective: Practice programmatic removal of IPC resources.
- Task: Create a new program called
cleanup.c
. This program should get thesemid
andshmid
for the resources created by the writer and then usesemctl()
andshmctl()
with theIPC_RMID
command to remove them from the system. It should not create any resources, only find and remove them. - Verification: Run the writer and reader. Then, stop them. Use
ipcs
to verify the resources exist. Run./cleanup
. Useipcs
again to confirm that the shared memory segment and semaphore set have been successfully removed.
- The Bounded Buffer (Producer-Consumer) Problem:
- Objective: Use multiple counting semaphores to solve a classic concurrency problem.
- Task: Modify the shared memory structure to be an array of 5 strings. Modify the writer (now a “producer”) and reader (now a “consumer”). Use a set of two semaphores:
empty
: A counting semaphore initialized to 5, representing the number of empty slots in the buffer.- full: A counting semaphore initialized to 0, representing the number of filled slots.The producer must perform a P operation on empty before writing and a V operation on full after writing. The consumer must do the opposite: a P operation on full and a V on empty. You will also need a third, binary semaphore to protect the indices that track the current write/read position in the circular buffer.
- Verification: The producer should block if it tries to write to a full buffer, and the consumer should block if it tries to read from an empty one. The system should run without losing or corrupting messages.
- Debugging a Deadlock:
- Objective: Learn to identify and fix a common synchronization error.
- Task: You are given two processes that are supposed to exchange roles, but they deadlock. Each process needs to acquire two different semaphores (semA and semB) to proceed. Process 1 locks semA then semB. Process 2 locks semB then semA.
- Code Snippet (to be implemented):
// Process 1 Logic lock(semA); sleep(1); // Simulate work lock(semB); // ... do work ... unlock(semB); unlock(semA); // Process 2 Logic lock(semB); sleep(1); // Simulate work lock(semA); // ... do work ... unlock(semA); unlock(semB);
- Your Job: Write the full C programs for Process 1 and Process 2 that implement this flawed logic. Run them and confirm they deadlock. Then, fix the code by enforcing a global lock ordering (e.g., all processes must always lock semA before semB).
- Verification: The fixed programs should run concurrently without deadlocking.
Summary
- Race Conditions: Uncontrolled concurrent access to a shared resource, like a shared memory segment, can lead to data corruption and unpredictable behavior.
- Semaphores: A kernel-managed synchronization primitive, essentially an integer counter, used to control access to resources and prevent race conditions.
- Atomic Operations: The core semaphore operations (P and V, implemented via
semop
) are guaranteed by the kernel to be indivisible, which is the key to their effectiveness. - System V IPC: A classic UNIX IPC suite that includes shared memory and semaphores. Resources are identified system-wide by a
key_t
and managed via a private integer ID. - Core Functions:
semget()
: Creates a new semaphore set or gets the ID of an existing one.semop()
: Atomically performs one or more operations (lock, unlock, wait-for-zero) on semaphores within a set.semctl()
: A control interface used for initializing (SETVAL
) and removing (IPC_RMID
) semaphores.
- Binary vs. Counting: A binary semaphore (mutex) has a value of 0 or 1 and protects a single resource. A counting semaphore can have larger values and protects a pool of identical resources.
- Resource Management: System V IPC objects are persistent. They must be explicitly removed using
ipcrm
or a programmatic call withIPC_RMID
to avoid leaking kernel resources. TheSEM_UNDO
flag is a critical tool for ensuring locks are released even if a process crashes.
Further Reading
- The Linux
man-pages
: The authoritative source for system call details.man 2 semget
man 2 semop
man 2 semctl
man 7 svipc
- Advanced Programming in the UNIX Environment, 3rd Edition by W. Richard Stevens and Stephen A. Rago. Chapter 14 provides an exhaustive and definitive treatment of Inter-Process Communication, including a deep dive into System V semaphores.
- The Linux Programming Interface by Michael Kerrisk. Chapters 46 and 47 offer a modern, detailed, and example-rich exploration of System V semaphores and shared memory.
- “The Little Book of Semaphores” by Allen B. Downey. A free and accessible book that focuses on the patterns of using semaphores to solve common synchronization problems.
- Raspberry Pi Documentation: While not specific to semaphores, the official documentation provides essential information on setting up the development environment on your Raspberry Pi. https://www.raspberrypi.com/documentation/
- “Operating Systems: Three Easy Pieces” by Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau. The concurrency section of this excellent, free online textbook provides a clear conceptual background on why synchronization is necessary.