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.
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:
Wait for writer’s turn
→ shared memory
Signal reader to go
Memory
Wait for reader’s turn
→ stdout
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 */
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:
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
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.
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.
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.
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.
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.
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.
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.
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.
EmbeddedPathashala.com | Free Embedded & Linux Tutorials
