Reading from POSIX Shared Memory pshm_read, fstat, PROT_READ mapping

 

Reading from POSIX Shared Memory
Chapter 54 — Part 3: pshm_read, fstat, PROT_READ mapping
Topic
Reading Data
Key Call
fstat + mmap
Pattern
Reader Process

Reading Data from a Shared Memory Object

A reader process opens an existing shared memory object, uses fstat() to discover its current size, maps it with PROT_READ, and reads from the mapped pointer. The critical pattern here is using fstat() to query the object’s size rather than hard-coding it — this keeps the reader decoupled from the writer, since the writer can resize the object to match the data it writes.

Key Terms
fstat() struct stat st_size PROT_READ MAP_SHARED O_RDONLY write(STDOUT_FILENO) sb.st_size

How the Reader Discovers the Data Size

The reader doesn’t know in advance how many bytes the writer placed. It uses fstat() on the fd to query the object’s current size (st_size):

shm_open
(O_RDONLY)
fstat(fd, &sb)
get sb.st_size
mmap
(size=sb.st_size)
write(stdout)
sb.st_size bytes

pshm_read.c — Complete Reader Program

This is the program from TLPI Listing 54-3. It opens an existing shared memory object, queries its size via fstat(), maps it, and prints the content:

/* pshm_read.c
 * Opens a POSIX shared memory object and prints its content to stdout.
 * Usage: ./pshm_read shm-name
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>   /* for struct stat, fstat() */
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;
    char *addr;
    struct stat sb;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s shm-name\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Open the existing shared memory object (read-only) */
    fd = shm_open(argv[1], O_RDONLY, 0);
    if (fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    /* Get the current size of the shared memory object */
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        exit(EXIT_FAILURE);
    }

    /* Map it read-only — size comes from fstat, not a hardcoded constant */
    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    /* Close fd — mapping stays valid */
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    /* Print the content — use write() not printf() (no null-terminator assumed) */
    write(STDOUT_FILENO, addr, sb.st_size);
    printf("\n");

    munmap(addr, sb.st_size);
    exit(EXIT_SUCCESS);
}

Shell session showing writer then reader:

# Create a zero-size shared memory object
./pshm_create -c /demo_shm 0

# Writer copies a string in
./pshm_write /demo_shm "hello from shared memory"

# Check size
ls -l /dev/shm/demo_shm

# Reader prints it
./pshm_read /demo_shm
hello from shared memory

Why use write() Instead of printf() or puts()?

printf() and puts() expect a null-terminated C string. But the writer copied only the string bytes (via memcpy()), without a null terminator. If the shared memory happens to contain a null byte beyond the data, printf() would work by luck. But the correct approach is:

  • Use write(STDOUT_FILENO, addr, sb.st_size) to write exactly st_size bytes, no null needed
  • Or ensure the writer always includes a null terminator by adding 1 to the length
/* Safe approach in writer — include null terminator */
len = strlen(argv[2]) + 1;   /* +1 for '\0' */
ftruncate(fd, len);
memcpy(addr, argv[2], len);

/* Then in reader, printf is safe */
printf("%s\n", addr);

fstat() on a Shared Memory fd

fstat() works on the file descriptor returned by shm_open() just as it does on regular file fds:

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

int main(void)
{
    struct stat sb;

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

    if (fstat(fd, &sb) == -1) { perror("fstat"); exit(1); }

    printf("Size     : %ld bytes\n", (long)sb.st_size);
    printf("Mode     : %o\n",        (unsigned)sb.st_mode);
    printf("UID      : %d\n",        (int)sb.st_uid);
    printf("Last mod : %ld\n",       (long)sb.st_mtime);

    close(fd);
    return 0;
}

Key field for shared memory readers is sb.st_size — the current size set by the last ftruncate() call.

Reading Structured Data from Shared Memory

When the shared memory contains a known struct layout (set by the writer), cast the pointer directly:

/* consumer.c — reads SensorData struct from shared memory */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

typedef struct {
    int   sensor_id;
    float temperature;
    float humidity;
    char  label[32];
} SensorData;

int main(void)
{
    int fd = shm_open("/sensor_shm", O_RDONLY, 0);
    if (fd == -1) { perror("shm_open"); exit(1); }

    struct stat sb;
    fstat(fd, &sb);

    /* Map read-only */
    SensorData *data = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    /* Cast pointer and read fields directly */
    printf("Sensor ID   : %d\n",   data->sensor_id);
    printf("Temperature : %.2f C\n", data->temperature);
    printf("Humidity    : %.2f %%\n", data->humidity);
    printf("Label       : %s\n",   data->label);

    munmap(data, sb.st_size);
    shm_unlink("/sensor_shm");  /* cleanup */
    return 0;
}

PROT_READ vs PROT_READ|PROT_WRITE

The mapping protection flags determine what the process can do with the mapped region:

Flag Read Write Use Case
PROT_READ ✗ (SIGSEGV) Reader / Consumer process
PROT_READ | PROT_WRITE Writer / Producer or read-modify-write
PROT_NONE Guard pages, reserved regions
/* Reader maps with PROT_READ only */
char *addr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);

/* This write would cause SIGSEGV: */
/* addr[0] = 'X';  */

Complete Roundtrip: Writer + Reader in One Program

This demo creates a shared object, forks, writer process fills it, reader process reads it:

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

#define SHM_NAME "/roundtrip_shm"
#define MESSAGE  "Hello from the writer process!"

int main(void)
{
    /* Create the shared memory object upfront */
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }

    size_t len = strlen(MESSAGE) + 1;
    ftruncate(fd, len);
    close(fd);

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* ---- CHILD: Writer ---- */
        int wfd = shm_open(SHM_NAME, O_RDWR, 0);
        char *addr = mmap(NULL, len, PROT_READ | PROT_WRITE,
                          MAP_SHARED, wfd, 0);
        close(wfd);

        memcpy(addr, MESSAGE, len);
        printf("[Writer pid=%d] Wrote: %s\n", getpid(), addr);

        munmap(addr, len);
        exit(0);

    } else {
        /* ---- PARENT: Reader — wait for child to write ---- */
        wait(NULL);  /* wait for writer to finish */

        int rfd = shm_open(SHM_NAME, O_RDONLY, 0);
        struct stat sb;
        fstat(rfd, &sb);

        char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, rfd, 0);
        close(rfd);

        printf("[Reader pid=%d] Read:  %s\n", getpid(), addr);
        munmap(addr, sb.st_size);

        shm_unlink(SHM_NAME);  /* cleanup */
    }

    return 0;
}
gcc -o roundtrip roundtrip.c -lrt
./roundtrip
[Writer pid=12346] Wrote: Hello from the writer process!
[Reader pid=12345] Read:  Hello from the writer process!

Interview Questions & Answers

Q1. Why does pshm_read use fstat() to get the object size instead of a hardcoded constant?

The reader has no prior knowledge of how many bytes the writer placed in the object. Using fstat() to query sb.st_size keeps the reader generic and correct regardless of the data size, since the writer resizes the object with ftruncate() to match the data length before writing.

Q2. Why use write(STDOUT_FILENO, addr, sb.st_size) instead of printf(“%s”, addr)?

printf() requires a null-terminated string. The writer may have copied data without including a null terminator (using memcpy() with the exact string length). write() outputs exactly sb.st_size bytes without needing a null terminator, making it correct regardless of whether the data ends with one.

Q3. Can a reader map the shared memory with PROT_READ | PROT_WRITE even if it only wants to read?

Technically yes, but it is bad practice. Using PROT_READ alone enforces read-only access at the hardware MMU level, so any accidental write causes a SIGSEGV immediately rather than silently corrupting shared data. It also signals intent clearly in the code.

Q4. What happens if the reader calls mmap() before the writer has called ftruncate()?

If the object still has size zero (never been truncated), mmap() with size zero fails with EINVAL. If the reader queries size via fstat() and gets zero, it should either wait/retry, or the shared design should ensure the writer creates and sizes the object before the reader starts.

Q5. Does munmap() on the reader side affect the writer’s mapping?

No. Each process has its own independent mapping. munmap() only removes the mapping from the calling process’s address space. The writer’s mapping and the physical shared pages are unaffected.

Q6. A reader calls fstat() and gets st_size = 1024. It maps 1024 bytes and reads. Meanwhile the writer calls ftruncate(fd, 512) to shrink the object. What happens?

The reader’s mapping still covers 1024 bytes in virtual address space. Accessing bytes 512-1023 will cause a SIGBUS because those physical pages no longer back that virtual range. This is why concurrent resize and access operations must be coordinated with synchronization.

Continue Learning

Next: Removing shared memory objects with shm_unlink()

Part 4: shm_unlink() & Cleanup → ← Part 2: Writing

Leave a Reply

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