Writing to POSIX Shared Memory pshm_write, ftruncate, memcpy

 

Writing to POSIX Shared Memory
Chapter 54 — Part 2: pshm_write, ftruncate, memcpy
Topic
Writing Data
Key Call
ftruncate + memcpy
Pattern
Writer Process

Writing Data into a Shared Memory Object

Once a shared memory object exists (created by another process or the same one), a writer process opens it, optionally resizes it to fit the data, maps it, and copies data into the mapped region using memcpy() or direct pointer assignment. This is the fastest way to hand data to another process — no kernel involvement per write.

The key insight: after mmap() you have a regular C pointer. Writing to that pointer writes directly into the shared pages. Any other process that has mapped the same object will immediately see the change (subject to CPU cache coherency, which the kernel and hardware manage for you on the same machine).

Key Terms
ftruncate() mmap() memcpy() MAP_SHARED PROT_READ|PROT_WRITE close(fd) strlen()

Writer Process Flow
shm_open(name, O_RDWR, 0)
Open existing object
ftruncate(fd, len)
Resize to match data
mmap(…, fd, 0)
Map into address space
memcpy(addr, data, len)
Copy data into shared region
close(fd)
fd no longer needed; mapping stays

pshm_write.c — Complete Writer Program

This is the program from TLPI Listing 54-2. It opens an existing shared memory object and copies a string into it, resizing the object first:

/* pshm_write.c
 * Opens an existing POSIX shared memory object and writes a string into it.
 * Usage: ./pshm_write shm-name string
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;
    size_t len;
    char *addr;

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

    /* Open the EXISTING shared memory object for read+write */
    fd = shm_open(argv[1], O_RDWR, 0);
    if (fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    /* Resize the object to exactly hold the string */
    len = strlen(argv[2]);
    if (ftruncate(fd, len) == -1) {
        perror("ftruncate");
        exit(EXIT_FAILURE);
    }
    printf("Resized to %ld bytes\n", (long)len);

    /* Map the object into the process address space */
    addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    /* Close the fd — we no longer need it; the mapping stays active */
    if (close(fd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }

    /* Copy the string into the shared region */
    printf("Copying %ld bytes\n", (long)len);
    memcpy(addr, argv[2], len);

    /* The mapping is cleaned up on exit, but let's be explicit */
    munmap(addr, len);

    exit(EXIT_SUCCESS);
}

Compile and run:

# First create the object (from Part 1 program):
./pshm_create -c /demo_shm 0

# Write a string into it:
./pshm_write /demo_shm "hello world"

# Check the size changed:
ls -l /dev/shm/demo_shm

Why Close fd After mmap()?

A common question: once you have called mmap(), the file descriptor is no longer needed to access the shared memory. The mapping is maintained by the kernel independently of the fd.

Closing the fd early is good practice:

  • Frees up a file descriptor slot
  • Prevents accidental use of the fd later in the code
  • The mapping remains valid until munmap() or process exit
/* After mmap(), fd can be closed safely */
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(1); }

close(fd);  /* fd no longer needed — mapping stays */

/* Now use addr directly */
strcpy(addr, "safe to use addr even after close(fd)");

ftruncate() Behavior on Shared Memory

ftruncate() can both grow and shrink a shared memory object:

  • Growing: New bytes are zero-filled
  • Shrinking: Data beyond the new size is discarded. Any existing mappings that extend beyond the new size will cause SIGBUS if accessed
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    int fd = shm_open("/resize_demo", O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }

    /* Start at 1024 bytes */
    ftruncate(fd, 1024);

    char *addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }

    /* Write to byte 1000 — fine */
    addr[1000] = 'X';
    printf("addr[1000] = %c\n", addr[1000]);

    /* Grow to 2048 — new bytes are zero-filled */
    ftruncate(fd, 2048);
    printf("addr[1500] (new, zeroed) = %d\n", (int)addr[1500]);

    munmap(addr, 1024);
    close(fd);
    shm_unlink("/resize_demo");
    return 0;
}

Writing Structured Data into Shared Memory

You are not limited to strings. You can write any C struct into shared memory:

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

/* Shared data structure */
typedef struct {
    int  sensor_id;
    float temperature;
    float humidity;
    char  label[32];
} SensorData;

int main(void)
{
    int fd = shm_open("/sensor_shm", O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }

    ftruncate(fd, sizeof(SensorData));

    SensorData *data = mmap(NULL, sizeof(SensorData),
                            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) { perror("mmap"); exit(1); }

    close(fd);

    /* Write structured data directly */
    data->sensor_id   = 42;
    data->temperature = 36.6f;
    data->humidity    = 65.0f;
    strncpy(data->label, "Room-A", sizeof(data->label));

    printf("Written sensor_id=%d, temp=%.1f, hum=%.1f, label=%s\n",
           data->sensor_id, data->temperature,
           data->humidity, data->label);

    munmap(data, sizeof(SensorData));
    /* Don't unlink yet — reader will open it */
    return 0;
}

Important: Multiple Writers Need Synchronization

If two processes write to the same shared memory region simultaneously without any coordination, the result is a data race — the data will be corrupted. Shared memory provides no built-in locking.

You must use a synchronization primitive such as:

  • POSIX semaphores (sem_wait / sem_post)
  • A mutex stored inside the shared memory itself (using pthread_mutexattr_setpshared)
  • File locking (fcntl / flock) on the fd
/* Pattern: use a semaphore to protect shared memory writes */
#include <semaphore.h>
#include <fcntl.h>
#include <sys/mman.h>

/* Writer side */
sem_t *sem = sem_open("/my_sem", O_CREAT, 0600, 1); /* initial value=1 */
char  *shm = /* ... mmap ... */;

sem_wait(sem);         /* lock */
memcpy(shm, data, len);/* write */
sem_post(sem);         /* unlock */

Synchronization is covered in depth in Part 4 of this series.

Complete Demo: Producer (Writer) Side

This example shows a producer that writes timestamped messages into shared memory in a loop:

/* producer.c — writes messages to shared memory */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>

#define SHM_NAME "/prod_cons"
#define BUF_SIZE 256

typedef struct {
    int  seq;
    char message[BUF_SIZE];
} SharedMsg;

int main(void)
{
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0600);
    if (fd == -1) { perror("shm_open"); exit(1); }

    ftruncate(fd, sizeof(SharedMsg));

    SharedMsg *msg = mmap(NULL, sizeof(SharedMsg),
                          PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (msg == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    for (int i = 1; i <= 5; i++) {
        msg->seq = i;
        snprintf(msg->message, BUF_SIZE,
                 "Message #%d from producer (pid=%d)", i, getpid());
        printf("Wrote: %s\n", msg->message);
        sleep(1);
    }

    munmap(msg, sizeof(SharedMsg));
    printf("Producer done. Run consumer now.\n");
    return 0;
}
# Compile:
gcc -o producer producer.c -lrt
./producer &
# Then run the consumer (from Part 3)

Interview Questions & Answers

Q1. In pshm_write, the program calls ftruncate() before mmap(). Why?

The shared memory object must have a non-zero size before it can be meaningfully mapped. The program resizes it to exactly the length of the string it wants to copy, then maps that exact size. This avoids wasting memory and ensures the reader (which uses fstat() to query the size) gets the correct byte count.

Q2. After calling mmap(), can we close() the file descriptor? Will the mapping survive?

Yes. Once mmap() has been called, the kernel maintains the mapping independently of the file descriptor. Closing the fd does not affect the mapping. The mapping remains valid until munmap() is called explicitly, or the process exits.

Q3. What is MAP_SHARED in mmap() and why is it required for IPC?

MAP_SHARED means writes to the mapped region are reflected back to the underlying object (the shared memory file) and are visible to other processes that have mapped the same object. The alternative MAP_PRIVATE uses copy-on-write semantics — changes are private to the process and never written back, making IPC impossible.

Q4. What happens if you call ftruncate() to shrink a shared memory object while another process has it mapped at the old (larger) size?

The other process’s mapping still covers the old range in its virtual address space. If it tries to access memory beyond the new (smaller) size, it receives a SIGBUS signal, which by default terminates the process. This is why coordinating size changes with all processes sharing the object is important.

Q5. Can you use write() instead of memcpy() to put data into a shared memory mapped region?

No. Once the object is mapped, the pointer addr is a regular memory pointer. write() is a system call that operates on file descriptors, not on memory pointers. You use memcpy(), strcpy(), struct assignment, or any normal C memory operation. (You could still call write(fd, ...) using the fd directly, but that bypasses the mapping entirely.)

Q6. What protection flags would you use for a read-only consumer mapping?

PROT_READ only. Attempting to write to a PROT_READ mapping causes a SIGSEGV. A read-only mapping is also good practice for consumers to prevent accidental modification of shared data.

Continue Learning

Next: Reading data from a shared memory object using fstat and mmap

Part 3: Reading from Shared Memory → ← Part 1: Overview

Leave a Reply

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