Data Transfer via Shared Memory Writer/Reader with Semaphore Sync |

 

Data Transfer via Shared Memory
Chapter 48 Part 4 – Writer/Reader with Semaphore Sync | EmbeddedPathashala
Part 4
Practical Example
Writer
stdin → shared memory
Reader
shared memory → stdout

This section builds a complete, working two-process application that transfers data through shared memory — a writer that reads from stdin and copies data into the shared segment, and a reader that copies data from the segment to stdout. Together they behave like a pipe, but use shared memory internally.

The critical challenge is synchronization: the writer must not overwrite the segment before the reader has consumed the previous block, and the reader must not read until the writer has placed fresh data. This is solved using a pair of System V binary semaphores.

Key Concepts in This Example

Binary Semaphore reserveSem() releaseSem() WRITE_SEM READ_SEM struct shmseg EOF signalling Alternating access semget() semctl() IPC_RMID cleanup

The Synchronization Problem

Shared memory has no built-in signalling. If the writer fills the segment and the reader isn’t ready yet, the writer would immediately overwrite the data before the reader could consume it. We need the two processes to take turns:

Writer Process
reserveSem(WRITE_SEM)
Wait for writer’s turn
copy stdin block
→ shared memory
releaseSem(READ_SEM)
Signal reader to go

Shared
Memory

Reader Process
reserveSem(READ_SEM)
Wait for reader’s turn
copy shared memory
→ stdout
releaseSem(WRITE_SEM)
Signal writer to go

Two semaphores control access: WRITE_SEM (semaphore 0) controls the writer’s turn, and READ_SEM (semaphore 1) controls the reader’s turn. At startup, WRITE_SEM is available (value=1) and READ_SEM is in-use (value=0), so the writer always goes first.

Phase WRITE_SEM READ_SEM Who runs
Initial state 1 (available) 0 (in-use) Writer goes first
Writer reserves WRITE_SEM 0 0 Writer copies data
Writer releases READ_SEM 0 1 Reader unblocked
Reader reserves READ_SEM 0 0 Reader copies data
Reader releases WRITE_SEM 1 0 Writer unblocked — cycle repeats

Shared Header File (svshm_xfr.h)

Both writer and reader include a common header that defines the shared memory layout, the IPC keys, and the buffer size. This ensures both programs agree on the data structure.

/* svshm_xfr.h — shared by writer and reader */
#ifndef SVSHM_XFR_H
#define SVSHM_XFR_H

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sem.h>
#include <sys/shm.h>

/* IPC keys — both processes must use the same values */
#define SHM_KEY    0x1234     /* key for shared memory segment */
#define SEM_KEY    0x5678     /* key for semaphore set */

/* Permissions for IPC objects: owner+group read/write */
#define OBJ_PERMS  (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)

/* Semaphore indices within the semaphore set */
#define WRITE_SEM  0          /* semaphore 0: writer's turn */
#define READ_SEM   1          /* semaphore 1: reader's turn */

/* Transfer buffer size — can override with: cc -DBUF_SIZE=2048 */
#ifndef BUF_SIZE
#define BUF_SIZE   1024
#endif

/*
 * Structure imposed on the shared memory segment.
 * cnt = number of valid bytes currently in buf.
 * cnt == 0 signals EOF (writer has finished).
 */
struct shmseg {
    int  cnt;           /* bytes used in buf; 0 = EOF signal */
    char buf[BUF_SIZE]; /* the actual data block */
};

#endif /* SVSHM_XFR_H */

struct shmseg in shared memory
int cnt ← 4 bytes (offset 0)

0 = EOF, >0 = bytes valid in buf
char buf[1024] ← 1024 bytes

data block; first cnt bytes are valid
Total: 1028 bytes (padded to 4096 by kernel)

Binary Semaphore Helper Functions

The writer and reader use four helper functions for binary semaphore operations. These wrap semop() to either decrement (reserve/lock) or increment (release/unlock) a semaphore by 1.

/* binary_sems.h — helper declarations */
#ifndef BINARY_SEMS_H
#define BINARY_SEMS_H

#include <sys/sem.h>

/* Initialize semaphore semNum in set semId to "available" (value=1) */
int initSemAvailable(int semId, int semNum);

/* Initialize semaphore semNum in set semId to "in use" (value=0) */
int initSemInUse(int semId, int semNum);

/* Reserve (P / decrement / wait) — blocks if value is 0 */
int reserveSem(int semId, int semNum);

/* Release (V / increment / signal) — wakes a waiting process */
int releaseSem(int semId, int semNum);

#endif
/* binary_sems.c — implementation */
#include "binary_sems.h"
#include <errno.h>

/* union semun must be defined on Linux (not in any header) */
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

int initSemAvailable(int semId, int semNum) {
    union semun arg;
    arg.val = 1;  /* available = 1 */
    return semctl(semId, semNum, SETVAL, arg);
}

int initSemInUse(int semId, int semNum) {
    union semun arg;
    arg.val = 0;  /* in-use = 0 */
    return semctl(semId, semNum, SETVAL, arg);
}

/*
 * reserveSem: decrement by 1.
 * Blocks if value is 0 (someone else holds it).
 * Retries on EINTR (interrupted by signal).
 */
int reserveSem(int semId, int semNum) {
    struct sembuf sops;
    sops.sem_num = semNum;
    sops.sem_op  = -1;  /* decrement */
    sops.sem_flg = 0;
    while (semop(semId, &sops, 1) == -1) {
        if (errno != EINTR)
            return -1;
        /* EINTR: signal interrupted us, retry */
    }
    return 0;
}

/*
 * releaseSem: increment by 1.
 * Wakes up any process blocked in reserveSem.
 */
int releaseSem(int semId, int semNum) {
    struct sembuf sops;
    sops.sem_num = semNum;
    sops.sem_op  = +1;  /* increment */
    sops.sem_flg = 0;
    return semop(semId, &sops, 1);
}

The Writer Program

The writer creates the semaphore set and shared memory segment, then loops reading blocks from stdin into the shared segment. After each write it signals the reader. When stdin reaches EOF, it sends a zero-length block as the termination signal.

/* svshm_xfr_writer.c
 * Reads from stdin, transfers blocks to shared memory.
 * Must be started BEFORE the reader.
 *
 * Compile: gcc -o writer svshm_xfr_writer.c binary_sems.c
 * Usage:   ./writer < inputfile
 */
#include "svshm_xfr.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* semun must be defined manually on Linux */
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

int main(void)
{
    int           semid, shmid, bytes, xfrs;
    struct shmseg *shmp;
    union semun    dummy;

    /*
     * Step 1: Create semaphore set with 2 semaphores.
     * Writer initializes them: WRITE_SEM=available, READ_SEM=in-use.
     * This ensures writer goes first.
     * Writer must start before reader so it owns the semaphores.
     */
    semid = semget(SEM_KEY, 2, IPC_CREAT | OBJ_PERMS);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    if (initSemAvailable(semid, WRITE_SEM) == -1) {
        perror("initSemAvailable WRITE_SEM");
        exit(EXIT_FAILURE);
    }
    if (initSemInUse(semid, READ_SEM) == -1) {
        perror("initSemInUse READ_SEM");
        exit(EXIT_FAILURE);
    }

    /*
     * Step 2: Create shared memory segment.
     * Size = sizeof(struct shmseg) = 4 + BUF_SIZE bytes.
     */
    shmid = shmget(SHM_KEY, sizeof(struct shmseg), IPC_CREAT | OBJ_PERMS);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    /* Step 3: Attach — kernel chooses address */
    shmp = (struct shmseg *)shmat(shmid, NULL, 0);
    if (shmp == (struct shmseg *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    /*
     * Step 4: Transfer loop.
     * Each iteration: reserve our semaphore, read a block,
     * release the reader's semaphore.
     */
    xfrs = 0;
    bytes = 0;
    for (;;) {

        /* Wait for our turn (WRITE_SEM must be 1) */
        if (reserveSem(semid, WRITE_SEM) == -1) {
            perror("reserveSem WRITE_SEM");
            exit(EXIT_FAILURE);
        }

        /* Read up to BUF_SIZE bytes from stdin into shared buf */
        shmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);
        if (shmp->cnt == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        /* Signal the reader that a block is ready */
        if (releaseSem(semid, READ_SEM) == -1) {
            perror("releaseSem READ_SEM");
            exit(EXIT_FAILURE);
        }

        /*
         * EOF check: done AFTER releasing READ_SEM so reader
         * can see the cnt=0 sentinel and exit its loop cleanly.
         */
        if (shmp->cnt == 0)
            break;

        xfrs++;
        bytes += shmp->cnt;
    }

    /*
     * Step 5: Wait for reader to finish its last access.
     * When reader is done, it releases WRITE_SEM one final time.
     * We reserve it here — this confirms reader has exited.
     */
    if (reserveSem(semid, WRITE_SEM) == -1) {
        perror("reserveSem final");
        exit(EXIT_FAILURE);
    }

    /* Step 6: Cleanup — delete semaphores and shared memory */
    if (semctl(semid, 0, IPC_RMID, dummy) == -1) {
        perror("semctl IPC_RMID");
        exit(EXIT_FAILURE);
    }
    if (shmdt(shmp) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }
    if (shmctl(shmid, IPC_RMID, 0) == -1) {
        perror("shmctl IPC_RMID");
        exit(EXIT_FAILURE);
    }

    fprintf(stderr, "Writer: sent %d bytes in %d transfers\n", bytes, xfrs);
    exit(EXIT_SUCCESS);
}

The Reader Program

The reader opens the already-created semaphore set and shared memory segment, then loops reading blocks and writing them to stdout. It terminates when it sees a zero-length block (EOF sentinel from writer).

/* svshm_xfr_reader.c
 * Reads blocks from shared memory, writes to stdout.
 * Must be started AFTER the writer has created the IPC objects.
 *
 * Compile: gcc -o reader svshm_xfr_reader.c binary_sems.c
 * Usage:   ./reader > outputfile
 */
#include "svshm_xfr.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int                semid, shmid;
    const struct shmseg *shmp;

    /*
     * Step 1: Get IDs of semaphore set and shared memory.
     * No IPC_CREAT — writer must have created these already.
     */
    semid = semget(SEM_KEY, 0, 0);
    if (semid == -1) {
        perror("semget — start writer first!");
        exit(EXIT_FAILURE);
    }

    shmid = shmget(SHM_KEY, 0, 0);
    if (shmid == -1) {
        perror("shmget — start writer first!");
        exit(EXIT_FAILURE);
    }

    /*
     * Step 2: Attach read-only.
     * We never write to shared memory — only the writer does.
     * SHM_RDONLY prevents accidental corruption.
     */
    shmp = (const struct shmseg *)shmat(shmid, NULL, SHM_RDONLY);
    if (shmp == (const struct shmseg *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    /*
     * Step 3: Transfer loop.
     * Each iteration: wait for READ_SEM, check for EOF,
     * write block to stdout, release WRITE_SEM.
     */
    for (;;) {

        /* Wait for writer to signal a block is ready */
        if (reserveSem(semid, READ_SEM) == -1) {
            perror("reserveSem READ_SEM");
            exit(EXIT_FAILURE);
        }

        /* cnt == 0 is the EOF sentinel from writer */
        if (shmp->cnt == 0)
            break;

        /* Write the block to stdout */
        if (write(STDOUT_FILENO, shmp->buf, shmp->cnt) != shmp->cnt) {
            fprintf(stderr, "partial/failed write\n");
            exit(EXIT_FAILURE);
        }

        /* Release writer's semaphore — writer can now fill next block */
        if (releaseSem(semid, WRITE_SEM) == -1) {
            perror("releaseSem WRITE_SEM");
            exit(EXIT_FAILURE);
        }
    }

    /*
     * Step 4: EOF seen. Detach shared memory.
     * Release WRITE_SEM one final time so writer knows
     * we are done and can safely delete the IPC objects.
     */
    if (shmdt(shmp) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    if (releaseSem(semid, WRITE_SEM) == -1) {
        perror("releaseSem final");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

Building and Running

# Compile all files
gcc -o writer svshm_xfr_writer.c binary_sems.c
gcc -o reader svshm_xfr_reader.c binary_sems.c

# Run: writer reads from a file, reader saves to another file
# Writer MUST start first (it creates the IPC objects)
./writer < /etc/passwd &    # run writer in background
./reader > /tmp/output.txt  # run reader in foreground

# Verify the transfer
diff /etc/passwd /tmp/output.txt
echo $?   # should print 0 (files are identical)

# Check for leftover IPC objects (should be clean after run)
ipcs -ms

You can also pipe between them like a regular Unix pipeline:

# Pipe a large file through the shared memory transfer
./writer < /usr/bin/ls &
./reader > /tmp/ls_copy
diff /usr/bin/ls /tmp/ls_copy && echo "Transfer OK"

# Vary buffer size at compile time
gcc -DBUF_SIZE=4096 -o writer svshm_xfr_writer.c binary_sems.c
gcc -DBUF_SIZE=4096 -o reader svshm_xfr_reader.c binary_sems.c

Step-by-Step Execution Flow

Step Writer Semaphores (W/R) Reader
1 semget: create 2 semaphores
shmget: create segment
Init W=1, R=0
W=1 / R=0 Not started yet
2 Waiting for reader to start… W=1 / R=0 semget: open semaphores
shmget: open segment
shmat: attach (read-only)
3 reserveSem(W) → W=0
read() block from stdin → buf
releaseSem(R) → R=1
W=0 / R=1 Blocked on reserveSem(R)
4 Blocked on reserveSem(W) W=0 / R=0 reserveSem(R) → R=0
write() buf to stdout
releaseSem(W) → W=1
5 Steps 3–4 repeat for each block
6 stdin hits EOF:
cnt=0, releaseSem(R)
W=0 / R=1 Sees cnt=0 → exits loop
shmdt, releaseSem(W)
7 reserveSem(W) succeeds
semctl(IPC_RMID), shmdt
shmctl(IPC_RMID)
Deleted Already exited

EOF Signalling Design

A common design challenge in producer-consumer shared memory is: how does the producer tell the consumer “I’m done”? This example uses an elegant approach:

❌ Problem
How does reader know when all data has been transferred? It can’t check stdin — that belongs to the writer process.
✓ Solution
Writer sets shmp->cnt = 0 when stdin returns 0 (EOF). This zero-length block is the sentinel. Reader sees cnt=0 and knows to stop.
/* Writer: EOF signalling pattern */
shmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);
/*
 * If stdin is at EOF, read() returns 0.
 * shmp->cnt becomes 0 — the sentinel value.
 * Writer releases READ_SEM so reader can see this 0.
 * THEN writer checks and exits the loop.
 * This ordering ensures reader always sees the sentinel.
 */
releaseSem(semid, READ_SEM);  /* must happen BEFORE checking cnt */
if (shmp->cnt == 0)
    break;                    /* now exit */

/* Reader: EOF detection */
reserveSem(semid, READ_SEM);
if (shmp->cnt == 0)           /* sentinel seen */
    break;                    /* exit without writing to stdout */
write(STDOUT_FILENO, shmp->buf, shmp->cnt);

Why Writer Waits for the Final Semaphore

After sending the EOF sentinel, the writer does one more reserveSem(WRITE_SEM) before deleting the IPC objects. This is a subtle but critical synchronization point:

/* After the loop exits (cnt=0 sentinel sent): */

/*
 * Wait for reader to signal us one final time.
 * This tells us the reader has seen the sentinel,
 * detached, and released WRITE_SEM.
 * Only now is it safe to delete the IPC objects.
 * Without this wait, we might delete the semaphore
 * while the reader is still trying to release it!
 */
reserveSem(semid, WRITE_SEM);  /* blocks until reader finishes */

/* Now safe to destroy everything */
semctl(semid, 0, IPC_RMID, dummy);
shmdt(shmp);
shmctl(shmid, IPC_RMID, 0);

If the writer deleted IPC objects immediately after sending the sentinel, the reader might try to call releaseSem(WRITE_SEM) on a semaphore that no longer exists, causing an error. The final semaphore wait is a rendezvous point — both processes confirm they are done before anything is destroyed.

Interview Questions

Q1. Why does this application need semaphores alongside shared memory?

Shared memory has no built-in synchronization. Without semaphores, the writer could overwrite the shared buffer before the reader finishes reading it, or the reader could read stale data before the writer fills the buffer. The semaphores enforce strict alternating access — only one process touches the shared memory at any moment.

Q2. Why must the writer start before the reader?

Because the writer creates the System V semaphore set and shared memory segment (using IPC_CREAT). The reader only opens them (no IPC_CREAT). If the reader starts first, semget() and shmget() will fail with ENOENT because the objects don’t exist yet.

Q3. How is EOF communicated from writer to reader?

By a sentinel value: the writer stores 0 in shmp->cnt when read() returns 0 (stdin EOF). The writer still releases READ_SEM so the reader unblocks and sees this zero count. The reader checks if (shmp->cnt == 0) before writing to stdout and exits the loop. Using the existing data field as an out-of-band signal is a common, clean pattern that avoids adding extra IPC objects.

Q4. Why does the writer release READ_SEM before checking if cnt==0?

Because the reader must be able to observe the sentinel value (cnt=0). If the writer checked cnt first and broke out of the loop before releasing READ_SEM, the reader would be permanently blocked waiting on READ_SEM, causing a deadlock. The EOF check must come after signalling the reader.

Q5. Why does the writer do a final reserveSem(WRITE_SEM) after the loop?

This is a rendezvous: the writer blocks until the reader has finished its last operation (detached and released WRITE_SEM). This guarantees the reader is completely done before the writer destroys the semaphore set and shared memory segment. Without this wait, there is a race where the writer deletes the semaphore while the reader is still calling releaseSem() on it, causing an EINVAL error.

Q6. What happens if the writer crashes mid-transfer?

The reader will block forever on reserveSem(READ_SEM) because no one will ever release it. Additionally, the shared memory segment and semaphore set will persist as leaked IPC objects (visible with ipcs). In production code, install a signal handler (SIGTERM/SIGINT/SIGPIPE) that calls semctl(IPC_RMID) and shmctl(IPC_RMID) before exiting, and set a SEM_UNDO flag on semaphore operations so the kernel auto-releases semaphores on crash.

Q7. How does the reader attach shared memory with SHM_RDONLY? What protection does this give?

shmat(shmid, NULL, SHM_RDONLY) maps the segment with read-only page protection. Any write attempt by the reader (a bug, for example) causes a hardware protection fault which the kernel delivers as SIGSEGV. This is the same protection as const qualifiers but enforced at the hardware level — even a malicious or buggy pointer dereference can’t corrupt the segment.

Q8. How would you modify this design to support multiple readers?

With multiple readers you’d need a more complex semaphore scheme. One approach: use a counter in the shared segment to track how many readers have consumed the current block. The writer waits until all N readers increment the counter before refilling. Alternatively, use a single writer with a ring buffer and per-reader read positions — each reader tracks its own offset independently, and the writer only overwrites when all readers have passed a slot.

Chapter 48 – System V Shared Memory Series

EmbeddedPathashala.com | Free Embedded & Linux Tutorials

Leave a Reply

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