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(), and semctl().
  • 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:

  1. 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. Using IPC_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.
  2. 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 by semget().
    • sops: A pointer to an array of sembuf structures. Each structure defines one operation.
    • nsops: The number of sembuf structures in the array.
    The 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 commonly SEM_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.
  3. semctl(int semid, int semnum, int cmd, ...): This is the “control” function, a multi-tool for managing semaphores.
    • semid and semnum: 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.

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.

Plaintext
/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.

C
// 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.

C
// 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.

C
// 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

  1. Create the Makefile: This Makefile will compile both programs.
Makefile
# 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.

Bash
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.

Bash
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.

Bash
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.

Bash
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.

Bash
# 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) and semctl(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.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Forgetting to Initialize Semaphore The first process that tries to lock the semaphore hangs indefinitely. The application appears frozen. Always pair a semget() with IPC_CREAT with a call to semctl(semid, 0, SETVAL, ...) to set a known initial value (usually 1 for a binary semaphore).
Incorrect semop Arguments Unpredictable behavior, such as deadlocks or processes failing to lock. Using sem_op = 0 by mistake can cause a process to wait for the semaphore to become zero, which may never happen. Double-check the sembuf struct. Use sem_op = -1 to lock (P operation) and sem_op = 1 to unlock (V operation). Encapsulate this logic in helper functions like lock() and unlock().
Not Using SEM_UNDO A process acquires a lock and then crashes. The semaphore remains locked, blocking all other processes from accessing the resource until the system is rebooted. Always include the SEM_UNDO flag in the sem_flg field of your sembuf struct. This tells the kernel to automatically release the semaphore if the process terminates unexpectedly.
Leaving Orphaned IPC Resources ipcs command shows a growing list of semaphores and shared memory segments. Eventually, calls to semget() or shmget() may fail. During development, use ipcrm to clean up. In production, design a graceful shutdown where a designated process removes resources using semctl(..., IPC_RMID, ...).
Incorrect Key Generation Reader/client process fails to start, with shmget or semget returning an error like “No such file or directory”. Ensure all processes use the exact same path and project ID for ftok(). Define these constants in a shared header file (common.h) to guarantee consistency.

Exercises

These exercises are designed to reinforce the concepts of semaphore-based synchronization and build upon the example code.

  1. Modify for Verbose Locking:
    • Objective: Gain a clearer visual understanding of the blocking mechanism.
    • Task: Modify the reader.c program. Before calling lock_semaphore(), print a message like “Reader: Attempting to lock…”. In the writer.c program, increase the sleep() 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.
  2. Implement a Cleanup Program:
    • Objective: Practice programmatic removal of IPC resources.
    • Task: Create a new program called cleanup.c. This program should get the semid and shmid for the resources created by the writer and then use semctl() and shmctl() with the IPC_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. Use ipcs again to confirm that the shared memory segment and semaphore set have been successfully removed.
  3. 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:
      1. empty: A counting semaphore initialized to 5, representing the number of empty slots in the buffer.
      2. 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.
  4. 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 with IPC_RMID to avoid leaking kernel resources. The SEM_UNDO flag is a critical tool for ensuring locks are released even if a process crashes.

Further Reading

  1. The Linux man-pages: The authoritative source for system call details.
    • man 2 semget
    • man 2 semop
    • man 2 semctl
    • man 7 svipc
  2. 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.
  3. 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.
  4. “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.
  5. 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/
  6. “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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top