Synchronizing Shared Memory Access Semaphores, Mutexes & Process Coordination

 

Synchronizing Shared Memory Access
Chapter 54 — Part 5: Semaphores, Mutexes & Process Coordination
Topic
Synchronization
Tools
Semaphore, Mutex
Problem
Race Conditions

Why Shared Memory Needs Synchronization

Shared memory gives multiple processes direct access to the same physical RAM — there is no kernel buffering or ordering. If two processes read and write the shared region concurrently without coordination, the result is a data race: unpredictable, corrupted data.

In the TLPI shell session for Chapter 54, the “synchronization” was informal: the user ran programs one after the other. In real applications, you must use a synchronization primitive. The two most common choices are POSIX named semaphores and POSIX process-shared mutexes stored inside the shared memory itself.

Key Terms
sem_open() sem_wait() sem_post() pthread_mutex_t PTHREAD_PROCESS_SHARED pthread_mutexattr_setpshared() data race critical section producer-consumer

The Race Condition Problem

Consider two processes both incrementing a shared counter:

/* WITHOUT synchronization — race condition! */
/* Shared memory layout */
typedef struct { int counter; } SharedData;

/* Process A reads: counter = 5 */
/* Process B reads: counter = 5   ← reads before A writes */
/* Process A writes: counter = 6  */
/* Process B writes: counter = 6  ← overwrites A's update */
/* Final value: 6 — should be 7! */
Process A (time →) Shared counter Process B (time →)
read counter → 5 5
5 read counter → 5
write counter = 6 6
6 ← WRONG write counter = 6 (lost A’s update!)

Method 1: Protect with a POSIX Named Semaphore

The simplest approach: use a named semaphore initialized to 1 (binary semaphore / mutex). Any process that wants to access the shared memory must first acquire (decrement) the semaphore and release (increment) it when done.

/* shm_sem_writer.c — Writer using a named semaphore */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <unistd.h>

#define SHM_NAME "/shared_data"
#define SEM_NAME "/shared_sem"
#define SHM_SIZE  256

int main(void)
{
    /* Create semaphore: initial value = 1 (unlocked) */
    sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0600, 1);
    if (sem == SEM_FAILED) { perror("sem_open"); exit(1); }

    /* Open or create shared memory */
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }
    ftruncate(fd, SHM_SIZE);

    char *addr = mmap(NULL, SHM_SIZE,
                      PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    for (int i = 1; i <= 5; i++) {
        /* ---- Enter critical section ---- */
        sem_wait(sem);

        snprintf(addr, SHM_SIZE, "Message %d from writer (pid=%d)", i, getpid());
        printf("[Writer] Wrote: %s\n", addr);

        /* ---- Leave critical section ---- */
        sem_post(sem);

        sleep(1);
    }

    munmap(addr, SHM_SIZE);
    sem_close(sem);
    return 0;
}
/* shm_sem_reader.c — Reader using the same semaphore */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>

#define SHM_NAME "/shared_data"
#define SEM_NAME "/shared_sem"

int main(void)
{
    sem_t *sem = sem_open(SEM_NAME, 0);   /* open existing semaphore */
    if (sem == SEM_FAILED) { perror("sem_open"); exit(1); }

    int fd = shm_open(SHM_NAME, O_RDONLY, 0);
    if (fd == -1) { perror("shm_open"); exit(1); }

    struct stat sb;
    fstat(fd, &sb);
    char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    for (int i = 0; i < 5; i++) {
        sem_wait(sem);                   /* acquire */
        printf("[Reader] Read: %s\n", addr);
        sem_post(sem);                   /* release */
        sleep(1);
    }

    munmap(addr, sb.st_size);
    sem_close(sem);
    return 0;
}
gcc -o writer shm_sem_writer.c -lrt -lpthread
gcc -o reader shm_sem_reader.c -lrt -lpthread
./writer &
./reader

Method 2: Mutex Stored Inside the Shared Memory

A more elegant approach: embed a pthread_mutex_t directly in the shared memory struct. This requires setting PTHREAD_PROCESS_SHARED so the mutex works across processes (not just threads).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>

#define SHM_NAME "/mutex_demo"

/* Struct that lives in shared memory — includes the mutex */
typedef struct {
    pthread_mutex_t mutex;
    int             counter;
    char            message[128];
} SharedRegion;

int main(void)
{
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }
    ftruncate(fd, sizeof(SharedRegion));

    SharedRegion *shm = mmap(NULL, sizeof(SharedRegion),
                             PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shm == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    /* Initialize the mutex for PROCESS-SHARED use */
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&shm->mutex, &attr);
    pthread_mutexattr_destroy(&attr);

    shm->counter = 0;

    pid_t pid = fork();
    if (pid == 0) {
        /* Child process — increment counter 1000 times */
        for (int i = 0; i < 1000; i++) {
            pthread_mutex_lock(&shm->mutex);
            shm->counter++;
            pthread_mutex_unlock(&shm->mutex);
        }
        printf("[Child]  Done. counter = %d\n", shm->counter);
        munmap(shm, sizeof(SharedRegion));
        exit(0);
    } else {
        /* Parent process — increment counter 1000 times */
        for (int i = 0; i < 1000; i++) {
            pthread_mutex_lock(&shm->mutex);
            shm->counter++;
            pthread_mutex_unlock(&shm->mutex);
        }
        wait(NULL);
        printf("[Parent] Final counter = %d (expected 2000)\n", shm->counter);

        /* Cleanup */
        pthread_mutex_destroy(&shm->mutex);
        munmap(shm, sizeof(SharedRegion));
        shm_unlink(SHM_NAME);
    }

    return 0;
}
gcc -o mutex_demo mutex_demo.c -lrt -lpthread
./mutex_demo
[Child]  Done. counter = 1352          # varies per run
[Parent] Final counter = 2000          # always 2000 with mutex

Classic Producer-Consumer with Shared Memory + Semaphores

Two semaphores coordinate a single-slot shared buffer: one tracks “empty slots” and one tracks “full slots”:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/wait.h>

#define SHM_NAME  "/pc_shm"
#define SEM_EMPTY "/pc_empty"   /* 1 = slot is empty, 0 = slot is full */
#define SEM_FULL  "/pc_full"    /* 1 = data ready to read, 0 = no data */

typedef struct {
    int value;
} SharedSlot;

int main(void)
{
    /* Create semaphores */
    sem_t *empty = sem_open(SEM_EMPTY, O_CREAT | O_EXCL, 0600, 1); /* 1=empty */
    sem_t *full  = sem_open(SEM_FULL,  O_CREAT | O_EXCL, 0600, 0); /* 0=no data */

    if (empty == SEM_FAILED || full == SEM_FAILED) {
        /* Clean previous leftovers if needed */
        sem_unlink(SEM_EMPTY); sem_unlink(SEM_FULL);
        empty = sem_open(SEM_EMPTY, O_CREAT, 0600, 1);
        full  = sem_open(SEM_FULL,  O_CREAT, 0600, 0);
    }

    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }
    ftruncate(fd, sizeof(SharedSlot));

    SharedSlot *slot = mmap(NULL, sizeof(SharedSlot),
                            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (slot == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    pid_t pid = fork();
    if (pid == 0) {
        /* ---- CONSUMER ---- */
        for (int i = 0; i < 5; i++) {
            sem_wait(full);             /* wait until data is ready */
            printf("[Consumer] got value = %d\n", slot->value);
            sem_post(empty);            /* signal slot is now empty */
        }
        munmap(slot, sizeof(SharedSlot));
        exit(0);
    } else {
        /* ---- PRODUCER ---- */
        for (int i = 1; i <= 5; i++) {
            sem_wait(empty);            /* wait until slot is empty */
            slot->value = i * 10;
            printf("[Producer] put value = %d\n", slot->value);
            sem_post(full);             /* signal data is ready */
            usleep(200000);             /* 200ms */
        }
        wait(NULL);

        /* Cleanup */
        munmap(slot, sizeof(SharedSlot));
        shm_unlink(SHM_NAME);
        sem_close(empty);  sem_unlink(SEM_EMPTY);
        sem_close(full);   sem_unlink(SEM_FULL);
    }

    return 0;
}

Read-Write Lock in Shared Memory

For scenarios with many readers and one writer, use a process-shared read-write lock:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <pthread.h>
#include <unistd.h>

#define SHM_NAME "/rwlock_demo"

typedef struct {
    pthread_rwlock_t rwlock;
    int              data[16];
} SharedRWData;

int main(void)
{
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }
    ftruncate(fd, sizeof(SharedRWData));

    SharedRWData *shm = mmap(NULL, sizeof(SharedRWData),
                             PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shm == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    /* Initialize process-shared rwlock */
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_rwlock_init(&shm->rwlock, &attr);
    pthread_rwlockattr_destroy(&attr);

    /* Writer: exclusive lock */
    pthread_rwlock_wrlock(&shm->rwlock);
    for (int i = 0; i < 16; i++) shm->data[i] = i * 2;
    printf("Writer: filled data array\n");
    pthread_rwlock_unlock(&shm->rwlock);

    /* Reader: shared lock (multiple readers can hold simultaneously) */
    pthread_rwlock_rdlock(&shm->rwlock);
    printf("Reader: data[0]=%d data[7]=%d data[15]=%d\n",
           shm->data[0], shm->data[7], shm->data[15]);
    pthread_rwlock_unlock(&shm->rwlock);

    /* Cleanup */
    pthread_rwlock_destroy(&shm->rwlock);
    munmap(shm, sizeof(SharedRWData));
    shm_unlink(SHM_NAME);
    return 0;
}

Synchronization Methods Comparison
Method API Pros Cons
Named semaphore sem_open/wait/post Simple, no struct sharing needed Extra named object to manage
Process-shared mutex in SHM pthread_mutex_t + PSHARED Self-contained in shared memory Must init before any process uses it
Process-shared rwlock in SHM pthread_rwlock_t + PSHARED Allows concurrent readers More complex, writer starvation risk
Unnamed semaphore in SHM sem_init(sem, 1, val) No separate named object needed Must use pshared=1

Unnamed Semaphore Embedded in Shared Memory

You can embed a sem_t directly in the shared struct using sem_init() with pshared=1:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/wait.h>

#define SHM_NAME "/unnamed_sem_demo"

typedef struct {
    sem_t sem;    /* pshared semaphore embedded in shared memory */
    int   value;
} SharedBlock;

int main(void)
{
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }
    ftruncate(fd, sizeof(SharedBlock));

    SharedBlock *blk = mmap(NULL, sizeof(SharedBlock),
                            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (blk == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    /*
     * sem_init(sem, pshared, value)
     * pshared = 1: semaphore shared between PROCESSES (not just threads)
     * value   = 1: initially unlocked
     */
    if (sem_init(&blk->sem, 1, 1) == -1) { perror("sem_init"); exit(1); }
    blk->value = 0;

    pid_t pid = fork();
    if (pid == 0) {
        /* Child: increment value 5 times */
        for (int i = 0; i < 5; i++) {
            sem_wait(&blk->sem);
            blk->value++;
            printf("[Child]  value now %d\n", blk->value);
            sem_post(&blk->sem);
            usleep(100000);
        }
        munmap(blk, sizeof(SharedBlock));
        exit(0);
    } else {
        /* Parent: increment value 5 times */
        for (int i = 0; i < 5; i++) {
            sem_wait(&blk->sem);
            blk->value++;
            printf("[Parent] value now %d\n", blk->value);
            sem_post(&blk->sem);
            usleep(100000);
        }
        wait(NULL);
        printf("Final value = %d (expected 10)\n", blk->value);
        sem_destroy(&blk->sem);
        munmap(blk, sizeof(SharedBlock));
        shm_unlink(SHM_NAME);
    }
    return 0;
}

Interview Questions & Answers

Q1. Why does shared memory need explicit synchronization, while pipes and message queues do not?

Pipes and message queues go through the kernel, which serializes all reads and writes and provides atomic semantics per message. Shared memory is direct RAM access — the kernel does not serialize or order operations. Two processes writing simultaneously produce a data race. The programmer must explicitly lock access using a semaphore, mutex, or other primitive.

Q2. What is PTHREAD_PROCESS_SHARED and why is it needed?

By default, POSIX mutexes and condition variables are only usable within a single process (among its threads). Setting PTHREAD_PROCESS_SHARED via pthread_mutexattr_setpshared() makes the mutex usable from multiple processes, provided it is stored in memory that all those processes can access — i.e., shared memory.

Q3. What is the difference between sem_open() and sem_init() for process synchronization?

sem_open() creates a named semaphore that exists as an object in the filesystem. Any process can open it by name. sem_init() creates an unnamed semaphore; with pshared=1 it must be stored in shared memory for cross-process use. Named semaphores are simpler to share between unrelated processes; unnamed semaphores embedded in shared memory avoid the need to manage a separate named object.

Q4. In the producer-consumer example, what would happen if both the empty and full semaphores started at 1?

The consumer would be able to read immediately even though no data has been produced yet, reading uninitialized or stale data. The correct initialization is empty=1 (one free slot) and full=0 (no data ready). The producer waits on empty before writing; the consumer waits on full before reading.

Q5. Can you use a regular (non-pshared) mutex stored in shared memory to synchronize processes?

No. A mutex without PTHREAD_PROCESS_SHARED uses internal state that is only valid within the creating process’s address space (e.g., it might reference a process-local futex). Using it from another process is undefined behavior and will likely deadlock or crash. Always set PTHREAD_PROCESS_SHARED for cross-process mutexes.

Q6. What is the advantage of embedding the mutex/semaphore inside the shared memory struct?

It keeps the lock and the data it protects together as a single self-contained object. Any process that maps the shared memory automatically has access to the synchronization object without needing to open a separate named semaphore. It also simplifies cleanup: when the shared memory is removed, the lock goes with it.

Q7. What happens to a process-shared mutex if the process holding the lock crashes?

By default, the mutex becomes permanently locked (deadlocked) — other processes waiting on it will block forever. To handle this, use a robust mutex by setting PTHREAD_MUTEX_ROBUST via pthread_mutexattr_setrobust(). When the owner crashes, the next locker gets EOWNERDEAD and can call pthread_mutex_consistent() to recover.

Series Complete!

You have covered all topics in Chapter 54: POSIX Shared Memory

Leave a Reply

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