How to Use POSIX Shared Memory in C

Shared memory is an efficient Inter-Process Communication (IPC) mechanism that enables separate processes to access the same physical memory segment. By mapping a common region of physical memory into the distinct virtual address spaces of multiple processes, data can be exchanged directly without the overhead of copying information through the operating system kernel. In C, POSIX APIs provide a standardized, file-descriptor-based interface—utilizing functions such as shm_open, ftruncate, and mmap—to create, size, and map these shared segments across process boundaries.

Understanding Shared Memory Architecture

Operating systems enforce memory isolation by default. Every process operates within its own private virtual address space, translated into physical RAM through page tables managed by the Memory Management Unit (MMU) and the OS kernel. Under normal circumstances, one process cannot read or write to the virtual address space of another.

Shared memory bypasses this restriction by mapping the exact same physical pages of RAM into the page tables of two or more independent processes. While each process may assign a different virtual address to this region, the underlying physical storage is identical. As a result, writes executed by one process are instantly visible to all other processes mapping that segment, eliminating the data copying inherent to pipes, message queues, and sockets.

Key POSIX Shared Memory APIs

POSIX shared memory objects are referenced by names (typically starting with a forward slash, such as /my_shm) and handled via standard file descriptor paradigms. The core functions include:

  • shm_open(const char *name, int oflag, mode_t mode): Creates or opens a POSIX shared memory object. It returns an integer file descriptor, similar to open(). Objects exist in a virtual file system (often mounted at /dev/shm on Linux).
  • ftruncate(int fd, off_t length): Sets the size of the shared memory object in bytes. Newly created shared memory objects initially have a size of zero.
  • mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset): Maps the shared memory file descriptor directly into the calling process's virtual address space, returning a pointer to the mapped memory.
  • munmap(void *addr, size_t length): Unmaps the designated address range from the process's address space.
  • shm_unlink(const char *name): Removes the shared memory object name from the system. The physical memory is released once all processes close and unmap the region.

Mapping Shared Memory Across Processes

Establishing a shared memory region requires a coordinated workflow between a producing process and a consuming process.

Step 1: Writing Process (Creating and Mapping)

The creating process generates the shared object, assigns its storage capacity, and maps it into its address space to write data.

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

#define SHM_NAME "/example_shm"
#define SHM_SIZE 4096

int main(void) {
    // 1. Create the shared memory object
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        return 1;
    }

    // 2. Configure the size
    if (ftruncate(fd, SHM_SIZE) == -1) {
        perror("ftruncate failed");
        close(fd);
        return 1;
    }

    // 3. Map the object into virtual memory
    char *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return 1;
    }

    // 4. Write data to the shared memory region
    const char *message = "Hello from the producing process!";
    strncpy(ptr, message, SHM_SIZE);

    printf("Data written to shared memory: %s\n", ptr);

    // Keep memory accessible until unmapped
    munmap(ptr, SHM_SIZE);
    close(fd);

    return 0;
}

Step 2: Reading Process (Opening and Mapping)

The receiving process opens the existing object by its name and maps it into its own address space to read the contents.

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

#define SHM_NAME "/example_shm"
#define SHM_SIZE 4096

int main(void) {
    // 1. Open the existing shared memory object
    int fd = shm_open(SHM_NAME, O_RDONLY, 0666);
    if (fd == -1) {
        perror("shm_open failed");
        return 1;
    }

    // 2. Map the object into this process's address space
    char *ptr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return 1;
    }

    // 3. Read the data
    printf("Data read from shared memory: %s\n", ptr);

    // 4. Clean up resources
    munmap(ptr, SHM_SIZE);
    close(fd);

    // 5. Remove the shared object name from the system
    shm_unlink(SHM_NAME);

    return 0;
}

Important Considerations

  • Synchronization: POSIX shared memory does not supply intrinsic synchronization. If multiple processes read and write simultaneously, race conditions and memory corruption will occur. Processes typically coordinate access using POSIX named semaphores (sem_open) or process-shared mutexes (pthread_mutexattr_setpshared).
  • Flags in mmap: The MAP_SHARED flag is critical. Using MAP_PRIVATE instead creates a copy-on-write mapping, meaning changes remain local to that process and are not propagated to other processes sharing the file descriptor.
  • Compilation: When compiling C programs using POSIX shared memory routines, link against the real-time library by passing the -lrt flag to the compiler (e.g., gcc producer.c -o producer -lrt).