The Classic Transfer Problem
This tutorial implements a complete file transfer using shared memory: one process (writer) reads data from stdin and places it into a shared memory buffer; another process (reader) takes the data out and writes it to stdout. Because both processes access the same memory, they must take turns โ this is enforced with a pair of System V semaphores acting as a “ping-pong” between writer and reader.
This is the same program demonstrated in TLPI Chapter 48 (svshm_xfr_writer.c and svshm_xfr_reader.c). It cleanly shows how shared memory and semaphores are used together in practice.
| WRITER Reads from stdin Fills shmp->buf Sets shmp->cnt |
โท | SHARED MEMORY struct shmseg { int cnt; char buf[BUF_SIZE]; } |
โท | READER Reads from shmp->buf Writes to stdout Checks shmp->cnt==0 |
| Controlled by two semaphores: WRITE_SEM (writer’s turn) and READ_SEM (reader’s turn) | ||||
Semaphore protocol (ping-pong):
- WRITE_SEM starts at 1, READ_SEM starts at 0
- Writer waits on WRITE_SEM โ writes data โ signals READ_SEM
- Reader waits on READ_SEM โ reads data โ signals WRITE_SEM
- Writer sets cnt=0 and signals READ_SEM to signal end-of-file
- Reader sees cnt==0, breaks loop, signals WRITE_SEM so writer can clean up
Both writer and reader include this header to agree on keys, sizes, and semaphore indices:
/* svshm_xfr.h โ shared definitions for 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>
/* Keys for shmget() and semget() โ both processes use the same keys */
#define SHM_KEY 0x1234ABCD /* Key for shared memory segment */
#define SEM_KEY 0x5678EF01 /* Key for semaphore set */
/* Semaphore indices within the set */
#define WRITE_SEM 0 /* Index 0: writer's turn semaphore */
#define READ_SEM 1 /* Index 1: reader's turn semaphore */
/* Buffer size for each transfer block */
#define BUF_SIZE 1024
/* The layout of our shared memory segment */
struct shmseg {
int cnt; /* Number of valid bytes in buf.
0 means EOF (writer is done). */
char buf[BUF_SIZE]; /* The data buffer */
};
/* Helper functions for semaphore operations */
/* Decrement (wait/lock) semaphore[semNum] */
static inline int reserveSem(int semid, int semNum)
{
struct sembuf sop;
sop.sem_num = semNum;
sop.sem_op = -1; /* Decrement by 1; block if value is 0 */
sop.sem_flg = 0;
return semop(semid, &sop, 1);
}
/* Increment (signal/unlock) semaphore[semNum] */
static inline int releaseSem(int semid, int semNum)
{
struct sembuf sop;
sop.sem_num = semNum;
sop.sem_op = +1; /* Increment by 1; unblock a waiting process */
sop.sem_flg = 0;
return semop(semid, &sop, 1);
}
#endif /* SVSHM_XFR_H */
The writer creates the shared memory and semaphores, then repeatedly reads from stdin and places data into the shared buffer, signaling the reader after each block:
/* svshm_xfr_writer.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include "svshm_xfr.h"
/* Union required for semctl() IPC_SET / SETVAL */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main(void)
{
int semid, shmid;
int bytes, xfrs;
struct shmseg *shmp;
union semun semarg;
/* ---- Step 1: Create semaphore set with 2 semaphores ---- */
semid = semget(SEM_KEY, 2, IPC_CREAT | IPC_EXCL | 0660);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
/* WRITE_SEM = 1 (writer goes first) */
semarg.val = 1;
if (semctl(semid, WRITE_SEM, SETVAL, semarg) == -1) {
perror("semctl SETVAL WRITE_SEM");
exit(EXIT_FAILURE);
}
/* READ_SEM = 0 (reader waits) */
semarg.val = 0;
if (semctl(semid, READ_SEM, SETVAL, semarg) == -1) {
perror("semctl SETVAL READ_SEM");
exit(EXIT_FAILURE);
}
/* ---- Step 2: Create shared memory segment ---- */
shmid = shmget(SHM_KEY, sizeof(struct shmseg),
IPC_CREAT | IPC_EXCL | 0660);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
/* ---- Step 3: Attach the segment ---- */
shmp = shmat(shmid, NULL, 0);
if (shmp == (void *) -1) {
perror("shmat");
exit(EXIT_FAILURE);
}
/* ---- Step 4: Transfer loop ---- */
for (xfrs = 0, bytes = 0; ; xfrs++) {
/* Wait for our turn to write (WRITE_SEM must be > 0) */
if (reserveSem(semid, WRITE_SEM) == -1) {
perror("reserveSem WRITE_SEM");
exit(EXIT_FAILURE);
}
/* Read a block from stdin into shared buffer */
shmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);
if (shmp->cnt == -1) {
perror("read stdin");
exit(EXIT_FAILURE);
}
bytes += shmp->cnt;
/* Signal the reader that data is ready (or cnt==0 means EOF) */
if (releaseSem(semid, READ_SEM) == -1) {
perror("releaseSem READ_SEM");
exit(EXIT_FAILURE);
}
/* If we read 0 bytes, EOF โ stop after signaling reader */
if (shmp->cnt == 0)
break;
}
/* ---- Step 5: Wait for reader to finish ---- */
/* Reader will give us one final WRITE_SEM after processing EOF */
if (reserveSem(semid, WRITE_SEM) == -1) {
perror("reserveSem final");
exit(EXIT_FAILURE);
}
fprintf(stderr, "Sent %d bytes (%d xfrs)\n", bytes, xfrs);
/* ---- Step 6: Cleanup ---- */
if (shmdt(shmp) == -1) { perror("shmdt"); exit(EXIT_FAILURE); }
if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl IPC_RMID shm"); }
if (semctl(semid, 0, IPC_RMID) == -1) { perror("semctl IPC_RMID sem"); }
exit(EXIT_SUCCESS);
}
The reader (from the PDF) opens the existing shared memory and semaphores, then reads blocks and writes them to stdout:
/* svshm_xfr_reader.c โ directly from TLPI Chapter 48 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include "svshm_xfr.h"
int main(void)
{
int semid, shmid;
int xfrs, bytes;
struct shmseg *shmp;
/* ---- Step 1: Get IDs for semaphore set and shared memory
(created by writer โ do NOT use IPC_CREAT here) ---- */
semid = semget(SEM_KEY, 0, 0); /* 0 = "open existing" */
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
shmid = shmget(SHM_KEY, 0, 0); /* size=0, no IPC_CREAT */
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
/* ---- Step 2: Attach read-only (reader never writes to buf) ---- */
shmp = shmat(shmid, NULL, SHM_RDONLY);
if (shmp == (void *) -1) {
perror("shmat");
exit(EXIT_FAILURE);
}
/* ---- Step 3: Transfer loop ---- */
for (xfrs = 0, bytes = 0; ; xfrs++) {
/* Wait for writer to put data in buffer (READ_SEM > 0) */
if (reserveSem(semid, READ_SEM) == -1) {
perror("reserveSem READ_SEM");
exit(EXIT_FAILURE);
}
/* Check for EOF: writer sets cnt=0 at end */
if (shmp->cnt == 0)
break;
bytes += shmp->cnt;
/* Write the block to stdout */
if (write(STDOUT_FILENO, shmp->buf, shmp->cnt) != shmp->cnt) {
fprintf(stderr, "partial/failed write\n");
exit(EXIT_FAILURE);
}
/* Signal writer that we are done with this buffer */
if (releaseSem(semid, WRITE_SEM) == -1) {
perror("releaseSem WRITE_SEM");
exit(EXIT_FAILURE);
}
}
/* ---- Step 4: Detach ---- */
if (shmdt(shmp) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
/* ---- Step 5: Give writer one final WRITE_SEM so it can clean up ---- */
if (releaseSem(semid, WRITE_SEM) == -1) {
perror("releaseSem final");
exit(EXIT_FAILURE);
}
fprintf(stderr, "Received %d bytes (%d xfrs)\n", bytes, xfrs);
exit(EXIT_SUCCESS);
}
# Compile both programs
gcc svshm_xfr_writer.c -o svshm_xfr_writer
gcc svshm_xfr_reader.c -o svshm_xfr_reader
# Test with /etc/services (a reasonable-sized file)
wc -c /etc/services
764360 /etc/services
# Run writer in background, feeding it stdin from a file
./svshm_xfr_writer < /etc/services &
# Run reader, capturing its output
./svshm_xfr_reader > out.txt
# Expected output (from both processes writing to stderr):
# Received 764360 bytes (747 xfrs)
# Sent 764360 bytes (747 xfrs)
# Verify the transfer was lossless
diff /etc/services out.txt
# No output = files are identical โ
Why 747 transfers? The file is 764,360 bytes. Each transfer block is BUF_SIZE = 1024 bytes. So we need ceil(764360 / 1024) = 747 blocks. The last block has fewer than 1024 bytes, and the final transfer has cnt=0 to signal EOF.
| Step | Action | WRITE_SEM | READ_SEM | Who is running? |
|---|---|---|---|---|
| Init | Writer initializes semaphores | 1 | 0 | Writer |
| 1 | Writer: reserveSem(WRITE_SEM) โ 1-1=0 | 0 | 0 | Writer fills buffer |
| 2 | Writer: releaseSem(READ_SEM) โ 0+1=1 | 0 | 1 | Reader unblocks |
| 3 | Reader: reserveSem(READ_SEM) โ 1-1=0 | 0 | 0 | Reader reads buffer |
| 4 | Reader: releaseSem(WRITE_SEM) โ 0+1=1 | 1 | 0 | Writer unblocks โ repeat |
| EOF | Writer sets cnt=0, releaseSem(READ_SEM) | 0 | 1 | Reader wakes, sees cnt==0, breaks |
| Done | Reader: releaseSem(WRITE_SEM) โ writer can clean up | 1 | 0 | Writer deletes segment + semaphores |
Q1. Why does the writer use two semaphores instead of one?
One semaphore alone would not create the correct alternating access. WRITE_SEM controls when the writer may write; READ_SEM controls when the reader may read. Together they implement a producer-consumer handshake: writer signals reader after filling, reader signals writer after draining. With one semaphore, you cannot distinguish “writer’s turn” from “reader’s turn.”
Q2. Why does the reader attach with SHM_RDONLY if it could also attach read-write?
Using SHM_RDONLY is defensive programming โ if a bug in the reader code accidentally writes to the buffer, the OS will immediately kill it with SIGSEGV instead of silently corrupting data. It makes the program’s intent explicit and catches bugs early.
Q3. What happens if the writer crashes mid-transfer?
The reader blocks forever in reserveSem(READ_SEM) waiting for the write semaphore to be released. This is a classic “broken semaphore” problem with System V semaphores. One mitigation is SEM_UNDO (set sem_flg = SEM_UNDO) โ if a process exits, its semaphore operations are automatically reversed, unblocking the other process.
Q4. Why does the reader give the writer “one more turn” (releaseSem WRITE_SEM) after seeing cnt=0?
Because the writer is blocked in its final reserveSem(WRITE_SEM) call after sending the EOF signal. The writer is waiting for confirmation that the reader has finished processing before it deletes the shared memory and semaphores. Without this final signal, the writer would block forever and never clean up.
Q5. Why does the writer call shmctl(IPC_RMID) and semctl(IPC_RMID) rather than the reader?
The writer is the creator of both the shared memory and the semaphore set. By convention, the creator is responsible for cleanup. The reader should not delete resources it did not create. Also, the writer is the last one to run (it waits for the reader’s final signal), so it is safe for the writer to delete everything at that point.
