Complete Writer / Reader Example POSIX Shared Memory with Semaphore Synchronization

 

Complete Writer / Reader Example
POSIX Shared Memory with Semaphore Synchronization
Exercise
54-1 (TLPI)
2 programs
writer + reader
sync
semaphore

What We Are Building

This is the complete, production-style example for POSIX shared memory IPC. It is equivalent to the System V shared memory transfer example from Chapter 48 of TLPI (Listing 48-2 and 48-3), but rewritten using POSIX shared memory. This also directly addresses Exercise 54-1.

The writer reads from standard input (or a file) in chunks and writes each chunk into shared memory. After each chunk, it signals the reader using a POSIX semaphore. The reader waits for the signal, reads the chunk, and signals the writer to continue. This implements a classic producer-consumer pipeline over shared memory.

Design: Shared Memory Layout and Synchronization

Shared Memory Region Layout:
struct shmbuf (at offset 0)
sem_t sem_writer
semaphore
(writer waits here)
sem_t sem_reader
semaphore
(reader waits here)
size_t cnt
bytes in
this chunk
char buf[BUF_SIZE]
data payload

The semaphores are embedded directly inside the shared memory region, initialized with PTHREAD_PROCESS_SHARED (or sem_init with pshared=1). This avoids needing a separate named semaphore.

Transfer Protocol:
Writer ←→ Reader
Creates SHM, initializes semaphores Opens existing SHM
Reads chunk from stdin into buf sem_wait(sem_reader) — blocks
Sets cnt = bytes read
sem_post(sem_reader) — wakes reader Wakes up, reads buf[0..cnt]
sem_wait(sem_writer) — blocks sem_post(sem_writer) — wakes writer
Repeats or exits if cnt == 0 (EOF) Exits if cnt == 0 (EOF signal)

Shared Header: shm_xfr.h

Both programs include this header to agree on the shared memory layout:

/* shm_xfr.h — shared definitions for writer and reader */

#ifndef SHM_XFR_H
#define SHM_XFR_H

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

#define SHM_NAME   "/ep_xfr"        /* name of shared memory object */
#define BUF_SIZE   1024              /* data payload per chunk */

/*
 * The shared memory region contains this structure at offset 0.
 * Both semaphores live inside shared memory — no named semaphores needed.
 */
struct shmbuf {
    sem_t  sem_writer;          /* writer waits on this */
    sem_t  sem_reader;          /* reader waits on this */
    size_t cnt;                 /* number of bytes in buf (0 = EOF) */
    char   buf[BUF_SIZE];       /* data payload */
};

#endif /* SHM_XFR_H */

Key design point: Embedding the semaphores inside shared memory means we need only one object, and both programs access the semaphores through the same mapped pointer. The semaphores must be initialized with sem_init(..., 1, ...) where the second argument 1 means “process-shared”.

Writer Program: posix_shm_xfr_writer.c
/*
 * posix_shm_xfr_writer.c
 *
 * Reads from stdin in chunks, transfers each chunk to reader
 * via POSIX shared memory + semaphores.
 *
 * Compile: gcc posix_shm_xfr_writer.c -o writer -lrt -lpthread
 * Usage:   ./writer < input_file.txt
 */

#include "shm_xfr.h"

int main(void)
{
    int           fd;
    struct shmbuf *shmp;
    ssize_t       bytes_read;

    /* ---- Step 1: Create shared memory object ---- */
    fd = shm_open(SHM_NAME,
                  O_CREAT | O_RDWR,
                  S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    /* ---- Step 2: Set size ---- */
    if (ftruncate(fd, sizeof(struct shmbuf)) == -1) {
        perror("ftruncate");
        exit(EXIT_FAILURE);
    }

    /* ---- Step 3: Map ---- */
    shmp = mmap(NULL,
                sizeof(struct shmbuf),
                PROT_READ | PROT_WRITE,
                MAP_SHARED,
                fd, 0);
    if (shmp == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    close(fd);   /* fd not needed after mmap */

    /*
     * Initialize semaphores INSIDE shared memory.
     * Second arg (pshared=1): semaphore is shared between processes.
     * Third arg: initial value.
     *   sem_writer starts at 1 — writer can write immediately.
     *   sem_reader starts at 0 — reader blocks until writer posts.
     */
    if (sem_init(&shmp->sem_writer, 1, 1) == -1) {
        perror("sem_init sem_writer");
        exit(EXIT_FAILURE);
    }
    if (sem_init(&shmp->sem_reader, 1, 0) == -1) {
        perror("sem_init sem_reader");
        exit(EXIT_FAILURE);
    }

    printf("Writer: shared memory ready. Reading from stdin...\n");

    /* ---- Transfer loop ---- */
    for (;;) {

        /* Wait until writer is allowed to write (sem_writer > 0) */
        if (sem_wait(&shmp->sem_writer) == -1) {
            perror("sem_wait sem_writer");
            break;
        }

        /* Read a chunk from stdin */
        bytes_read = read(STDIN_FILENO,
                          shmp->buf,
                          BUF_SIZE);
        if (bytes_read == -1) {
            perror("read");
            break;
        }

        shmp->cnt = bytes_read;   /* tell reader how many bytes */

        /* Signal reader: data is ready */
        if (sem_post(&shmp->sem_reader) == -1) {
            perror("sem_post sem_reader");
            break;
        }

        /* EOF: cnt == 0 signals reader to exit */
        if (bytes_read == 0) {
            printf("Writer: EOF, transfer complete.\n");
            break;
        }
    }

    /* ---- Cleanup ---- */
    /* Let reader finish before destroying semaphores */
    sem_wait(&shmp->sem_writer);   /* wait for reader's final ack */

    sem_destroy(&shmp->sem_writer);
    sem_destroy(&shmp->sem_reader);
    munmap(shmp, sizeof(struct shmbuf));
    shm_unlink(SHM_NAME);

    return 0;
}

Reader Program: posix_shm_xfr_reader.c
/*
 * posix_shm_xfr_reader.c
 *
 * Reads chunks from shared memory written by the writer.
 *
 * Compile: gcc posix_shm_xfr_reader.c -o reader -lrt -lpthread
 * Usage:   ./reader > output_file.txt
 *
 * Run WRITER first, then READER in another terminal.
 */

#include "shm_xfr.h"

int main(void)
{
    int           fd;
    struct shmbuf *shmp;
    ssize_t       bytes_written;
    size_t        total = 0;

    /* ---- Open existing shared memory ---- */
    /* Writer must have created it first */
    fd = shm_open(SHM_NAME, O_RDWR, 0);
    if (fd == -1) {
        perror("shm_open (run writer first)");
        exit(EXIT_FAILURE);
    }

    /* ---- Map ---- */
    shmp = mmap(NULL,
                sizeof(struct shmbuf),
                PROT_READ | PROT_WRITE,
                MAP_SHARED,
                fd, 0);
    if (shmp == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    close(fd);

    printf("Reader: connected to shared memory.\n");

    /* ---- Receive loop ---- */
    for (;;) {

        /* Wait until writer posts data */
        if (sem_wait(&shmp->sem_reader) == -1) {
            perror("sem_wait sem_reader");
            break;
        }

        /* cnt == 0 means EOF from writer */
        if (shmp->cnt == 0) {
            printf("Reader: received EOF.\n");
            /* Ack the writer so it can clean up */
            sem_post(&shmp->sem_writer);
            break;
        }

        /* Write received bytes to stdout */
        bytes_written = write(STDOUT_FILENO,
                              shmp->buf,
                              shmp->cnt);
        if (bytes_written == -1) {
            perror("write");
            break;
        }

        total += shmp->cnt;

        /* Signal writer: buffer is free */
        if (sem_post(&shmp->sem_writer) == -1) {
            perror("sem_post sem_writer");
            break;
        }
    }

    fprintf(stderr, "Reader: total bytes received = %zu\n", total);
    munmap(shmp, sizeof(struct shmbuf));
    return 0;
}

How to Build and Run
# ---- Build ----
gcc posix_shm_xfr_writer.c -o writer -lrt -lpthread
gcc posix_shm_xfr_reader.c -o reader -lrt -lpthread

# ---- Run: Terminal 1 — start writer with input file ----
./writer < /etc/passwd

# ---- Run: Terminal 2 — start reader, save output ----
./reader > /tmp/output.txt

# ---- Verify: output matches input ----
diff /etc/passwd /tmp/output.txt
# (no output = files are identical)

Order matters: Start the writer first (it creates and initializes the shared memory), then start the reader. If you reverse the order, the reader’s shm_open() will fail with ENOENT.

Common Mistakes and How to Avoid Them
Mistake Symptom Fix
Forgot ftruncate() mmap fails or SIGBUS on access Always call ftruncate(fd, size) after creating
MAP_PRIVATE instead of MAP_SHARED Writes not seen by other process Use MAP_SHARED for IPC
Stale object from previous run shm_open with O_EXCL returns EEXIST rm /dev/shm/ep_xfr before re-running
sem_init with pshared=0 Semaphore only works within same process Use sem_init(&sem, 1, value) (pshared=1)
Storing absolute pointers in shared region Segfault in other process Store offsets from start of region
Forgot -lrt -lpthread Linker errors for shm_open, sem_init Add -lrt -lpthread to compile command

Alternative: Process-Shared Mutex Inside Shared Memory

You can also use a POSIX mutex (with PTHREAD_PROCESS_SHARED) embedded inside the shared memory region. This is useful for mutual exclusion (one writer at a time) rather than the signaling protocol above.

#include <pthread.h>

struct shared_data {
    pthread_mutex_t lock;   /* must initialize with PROCESS_SHARED attr */
    int             counter;
    char            data[256];
};

/* Initializing the mutex (done by creator process): */
struct shared_data *shmp = /* ... mmap result ... */;

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&shmp->lock, &attr);
pthread_mutexattr_destroy(&attr);

/* Using the mutex from any process: */
pthread_mutex_lock(&shmp->lock);
shmp->counter++;
pthread_mutex_unlock(&shmp->lock);

/* Cleanup (done by one process when all are done): */
pthread_mutex_destroy(&shmp->lock);

The PTHREAD_PROCESS_SHARED attribute is what allows a mutex (or condition variable) stored in shared memory to be used by threads in different processes. Without it, the mutex only works between threads of the same process.

Interview Questions — Complete Example and Synchronization

Q1. Why is synchronization required when using shared memory for IPC?

Shared memory provides no built-in locking. Without synchronization, a reader could read while the writer is in the middle of writing — seeing partial or corrupted data. A semaphore, mutex, or other synchronization primitive must coordinate access so that a reader only reads after the writer has finished writing, and vice versa.

Q2. What is the difference between sem_init() with pshared=0 and pshared=1?

pshared=0 creates a thread-local semaphore — only threads within the same process can use it. pshared=1 creates a process-shared semaphore — it can be shared between different processes via shared memory. For IPC between separate processes, you must use pshared=1 and place the semaphore in a shared memory region.

Q3. In the writer/reader example, what does a cnt value of 0 mean?

It is the EOF sentinel. When the writer reads zero bytes from stdin (end of file), it stores cnt = 0 in the shared buffer and posts the reader’s semaphore one final time. The reader checks for cnt == 0 and exits the loop. This is a clean out-of-band signal that requires no separate “done” flag.

Q4. Why do we close the fd after mmap() in both programs?

After mmap(), the fd is no longer needed. The mapping maintains its own reference to the underlying shared memory object. Closing the fd frees the file descriptor number in the process’s fd table — a limited resource. The mapping and the data are unaffected.

Q5. How would you handle the writer crashing without calling shm_unlink()?

Three approaches: (1) Install a signal handler for SIGTERM/SIGINT/SIGHUP that calls shm_unlink() before exit. (2) Use the “unlink early” pattern — call shm_unlink() right after mmap() so the name disappears even if the process crashes. (3) Use a wrapper or supervisor that runs rm /dev/shm/ep_xfr on startup to clean up stale objects. Note: SIGKILL cannot be caught, so option 2 is the only crash-safe approach.

Q6. Can you use named semaphores (sem_open) instead of unnamed ones (sem_init) for this design?

Yes. Named semaphores (sem_open("/my_sem", O_CREAT, 0600, value)) live in the kernel independently and do not need to be placed in shared memory. However, they require separate creation, opening, and sem_unlink() for cleanup — more objects to manage. Embedding unnamed semaphores directly in shared memory (with pshared=1) is cleaner: one object for everything.

Q7. What happens if the reader crashes while holding the semaphore?

The writer will block indefinitely on sem_wait(), waiting for a post that never comes. POSIX semaphores do not have automatic release on crash (unlike some mutex implementations with PTHREAD_MUTEX_ROBUST). Robust designs use timeouts (sem_timedwait()) or a watchdog process that monitors the peer and destroys/recreates the shared memory if it detects a crash.

Chapter 54 Complete!

You have covered all topics in POSIX Shared Memory (Chapter 54)

← Comparison Chapter Index

Leave a Reply

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