POSIX Shared Memory (shm_open, shm_unlink, mmap)

POSIX Shared Memory provides the highest-throughput inter-process communication (IPC) mechanism available on Unix-like operating systems. By mapping the exact same physical RAM pages directly into the virtual address spaces of two or more distinct processes, applications can read and write data at raw memory bus speeds (nanoseconds) without copying data through the kernel.

Unlike pipes, message queues, and sockets—which require context switches and copying data from user-space to kernel-space buffers—shared memory operates completely in user space once established.

System Interfaces & Lifecycle

Operating on POSIX shared memory requires combining <sys/mman.h>, <sys/stat.h>, and <fcntl.h>:

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

int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);
int ftruncate(int fd, off_t length);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int close(int fd);

The 4-Step Lifecycle Pattern

1. shm_open()       -> Creates or opens a named memory object; returns integer fd.
2. ftruncate()      -> SIZES the object (mandatory on creation; defaults to 0 bytes).
3. mmap()           -> Maps the object into the process virtual address space.
4. shm_unlink()     -> Marks the name for deletion when all processes close it.

Step-by-Step Implementation

Step 1: shm_open() (Creating the Object)

The name parameter must conform to POSIX naming rules:

  • Must begin with a leading forward slash (/).
  • Must not contain any further embedded slashes (e.g., "/app_shm_block", not "/app/shm").
  • Should not exceed NAME_MAX (typically 255 characters).
int fd = shm_open("/worker_buffer", O_CREAT | O_RDWR, 0660);
if (fd == -1) {
    perror("shm_open failed");
    exit(EXIT_FAILURE);
}

On Linux, shm_open() creates a file inside the memory-backed tmpfs filesystem mounted at /dev/shm/worker_buffer.

Step 2: ftruncate() (The Mandatory Sizing Step)

When a shared memory object is newly created via O_CREAT, its initial size is exactly 0 bytes.

If you call mmap() on a 0-byte shared memory object and attempt to dereference the returned pointer, the operating system kernel immediately terminates your process with a SIGBUS (Bus Error) signal because no physical pages or backing storage exist for that offset.

const size_t SHM_SIZE = 1024 * 1024; // 1 MB

if (ftruncate(fd, SHM_SIZE) == -1) {
    perror("ftruncate failed");
    close(fd);
    shm_unlink("/worker_buffer");
    exit(EXIT_FAILURE);
}

Step 3: mmap() (Mapping into Virtual Memory)

To share modifications with other processes, you must specify MAP_SHARED:

void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
    perror("mmap failed");
    close(fd);
    exit(EXIT_FAILURE);
}

// The file descriptor can be closed immediately after mmap();
// the memory mapping remains active until munmap() is called.
close(fd);

Step 4: shm_unlink() and Cleanup

Calling shm_unlink("/worker_buffer") removes the name from the system directory (/dev/shm). Similar to unlinking a standard file, existing memory mappings and open file descriptors remain valid until every process calls munmap() or exits.

Synchronizing Shared Memory Access

Because shared memory performs no synchronization, concurrent reads and writes produce race conditions and corrupted data unless synchronized by the application.

The two standard POSIX mechanisms for synchronizing shared memory are:

1. Process-Shared Mutex (PTHREAD_PROCESS_SHARED)

Place a pthread_mutex_t directly inside the shared memory structure and initialize it with PTHREAD_PROCESS_SHARED:

struct SharedPayload {
    pthread_mutex_t lock;
    size_t count;
    char buffer[4096];
};

struct SharedPayload *payload = (struct SharedPayload *)ptr;

// On the creator process:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&payload->lock, &attr);
pthread_mutexattr_destroy(&attr);

2. POSIX Named Semaphores

Use sem_open() to coordinate producer/consumer queues across processes.

POSIX Shared Memory vs. Linux memfd_create()

Linux 3.17 introduced memfd_create(), which creates completely anonymous memory-backed file descriptors:

Feature POSIX Shared Memory (shm_open) Linux memfd_create()
Portability Standard POSIX (Linux, macOS, BSD, Solaris). Linux-specific (kernel 3.17+).
Namespace Named globally in the filesystem (/dev/shm). Completely anonymous; no filesystem path.
Access Control Any process with file permissions can open it by name. Only accessible via inheritance (fork) or passing over Unix socket (SCM_RIGHTS).
Memory Sealing Not supported. Supports F_ADD_SEALS (prevents shrinking, writing, or truncation).

Frequently Asked Questions (FAQ)

Why does calling mmap() on a newly created shared memory object trigger SIGBUS?

Because a newly created shared memory object has an initial size of 0 bytes, so any read or write access attempts to address pages past the end of the file. To allocate physical backing pages and prevent SIGBUS, the creator process must call ftruncate(fd, size) to size the object before calling mmap() or dereferencing pointers.

What happens to existing memory mappings when shm_unlink() is called?

Existing memory mappings remain fully accessible and functional until the processes explicitly call munmap() or terminate. shm_unlink() only removes the named object from the global namespace (e.g. /dev/shm), preventing new processes from discovering or attaching to it; it does not prematurely free memory currently in use.

How do you link programs using POSIX shared memory in C?

On Linux, you must link against the Real-Time library by appending -lrt to your compiler command (gcc main.c -lrt -lpthread). While macOS and newer glibc versions have folded librt functions directly into standard libc, adding -lrt ensures cross-platform compilation across Debian, CentOS, and older Linux distributions.

Related Topics