Shared Memory

Shared memory is the fastest IPC mechanism. Two processes map the same physical memory into their address spaces. There is no copying through the kernel -- a write by one process is instantly visible to the other. The cost: you must synchronize access yourself.

POSIX Shared Memory in C

Three steps: create/open, set the size, map it.

/* shm_writer.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096

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

    /* Set its size */
    if (ftruncate(fd, SHM_SIZE) == -1) {
        perror("ftruncate");
        return 1;
    }

    /* Map it into our address space */
    void *ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    close(fd);  /* fd no longer needed after mmap */

    /* Write data */
    const char *msg = "Hello from shared memory!";
    memcpy(ptr, msg, strlen(msg) + 1);
    printf("Writer: wrote '%s'\n", msg);

    /* Keep running so reader can access */
    printf("Writer: press Enter to clean up...\n");
    getchar();

    munmap(ptr, SHM_SIZE);
    shm_unlink(SHM_NAME);
    return 0;
}
/* shm_reader.c */
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096

int main(void) {
    int fd = shm_open(SHM_NAME, O_RDONLY, 0);
    if (fd == -1) {
        perror("shm_open");
        return 1;
    }

    void *ptr = mmap(NULL, SHM_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    close(fd);

    printf("Reader: got '%s'\n", (char *)ptr);

    munmap(ptr, SHM_SIZE);
    return 0;
}

Compile both with -lrt (for shm_open):

gcc -o shm_writer shm_writer.c -lrt
gcc -o shm_reader shm_reader.c -lrt

Run the writer first, then the reader in another terminal.

The memory layout:

Process A (writer)               Process B (reader)
+------------------+            +------------------+
| Virtual Memory   |            | Virtual Memory   |
|                  |            |                  |
|  mmap region  ------+    +------  mmap region   |
|                  |   |    |   |                  |
+------------------+   |    |   +------------------+
                       v    v
                  +------------+
                  | Physical   |
                  | Memory     |
                  | (shared)   |
                  +------------+

The shm_open / mmap API

FunctionPurpose
shm_open(name, flags, mode)Create or open a shared memory object (lives under /dev/shm/)
ftruncate(fd, size)Set the size of the shared memory object
mmap(addr, len, prot, flags, fd, offset)Map the object into the process address space
munmap(addr, len)Unmap the region
shm_unlink(name)Remove the shared memory object

Caution: shm_unlink removes the name from the filesystem, but the memory stays mapped until all processes call munmap or exit. If you forget shm_unlink, the shared memory persists across reboots (it lives in /dev/shm/). Check with ls /dev/shm/.

Try It: Run shm_writer, then look at /dev/shm/my_shm with ls -la /dev/shm/. You will see a file. Run shm_reader, then press Enter in the writer to clean up. Verify the file is gone.

Sharing Structured Data

You can share any fixed-size structure. Use offsetof and fixed-width types for portability.

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

typedef struct {
    int32_t counter;
    char    message[64];
} SharedData;

int main(void) {
    const char *name = "/struct_shm";
    int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, sizeof(SharedData));

    SharedData *data = mmap(NULL, sizeof(SharedData),
                            PROT_READ | PROT_WRITE,
                            MAP_SHARED, fd, 0);
    close(fd);

    data->counter = 0;
    strcpy(data->message, "initialized");

    pid_t pid = fork();
    if (pid == 0) {
        /* Child increments counter */
        for (int i = 0; i < 100000; i++)
            data->counter++;    /* WARNING: no synchronization! */
        strcpy(data->message, "child was here");
        _exit(0);
    }

    /* Parent also increments */
    for (int i = 0; i < 100000; i++)
        data->counter++;        /* WARNING: race condition! */

    wait(NULL);
    printf("Counter: %d (expected 200000)\n", data->counter);
    printf("Message: %s\n", data->message);

    munmap(data, sizeof(SharedData));
    shm_unlink(name);
    return 0;
}

The counter will be wrong due to the race condition. We need synchronization.

Process-Shared Mutex

A regular pthread_mutex_t only works within a single process. For cross-process synchronization, use PTHREAD_PROCESS_SHARED.

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

typedef struct {
    pthread_mutex_t lock;
    int counter;
} SharedData;

int main(void) {
    const char *name = "/mutex_shm";
    int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, sizeof(SharedData));

    SharedData *data = mmap(NULL, sizeof(SharedData),
                            PROT_READ | PROT_WRITE,
                            MAP_SHARED, fd, 0);
    close(fd);

    /* Initialize process-shared mutex */
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&data->lock, &attr);
    pthread_mutexattr_destroy(&attr);

    data->counter = 0;

    pid_t pid = fork();
    if (pid == 0) {
        for (int i = 0; i < 100000; i++) {
            pthread_mutex_lock(&data->lock);
            data->counter++;
            pthread_mutex_unlock(&data->lock);
        }
        _exit(0);
    }

    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&data->lock);
        data->counter++;
        pthread_mutex_unlock(&data->lock);
    }

    wait(NULL);
    printf("Counter: %d (expected 200000)\n", data->counter);

    pthread_mutex_destroy(&data->lock);
    munmap(data, sizeof(SharedData));
    shm_unlink(name);
    return 0;
}

Compile with:

gcc -o shm_mutex shm_mutex.c -lrt -pthread

Now the counter is always 200000. The key line is pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED).

Caution: The mutex must be stored in the shared memory region itself, not on the stack or heap of either process. Both processes must access the same pthread_mutex_t object.

Rust: Shared Memory with memmap2

Rust does not have a standard-library shared memory API. The memmap2 crate provides a safe wrapper around mmap.

Add to Cargo.toml:

[dependencies]
memmap2 = "0.9"
nix = { version = "0.29", features = ["mman", "fs"] }
// shm_writer.rs
use nix::fcntl::OFlag;
use nix::sys::mman::{shm_open, shm_unlink};
use nix::sys::stat::Mode;
use nix::unistd::ftruncate;
use memmap2::MmapMut;
use std::fs::File;
use std::os::fd::FromRawFd;
use std::io::Write;

const SHM_NAME: &str = "/rust_shm";
const SHM_SIZE: usize = 4096;

fn main() {
    // Create shared memory
    let fd = shm_open(
        SHM_NAME,
        OFlag::O_CREAT | OFlag::O_RDWR,
        Mode::S_IRUSR | Mode::S_IWUSR,
    )
    .expect("shm_open failed");

    ftruncate(&fd, SHM_SIZE as i64).expect("ftruncate failed");

    let file = unsafe { File::from_raw_fd(fd.as_raw_fd()) };
    let mut mmap = unsafe {
        MmapMut::map_mut(&file).expect("mmap failed")
    };

    let msg = b"Hello from Rust shared memory!";
    mmap[..msg.len()].copy_from_slice(msg);

    println!("Writer: wrote message. Press Enter to clean up.");
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf).unwrap();

    drop(mmap);
    shm_unlink(SHM_NAME).ok();
}
// shm_reader.rs
use nix::fcntl::OFlag;
use nix::sys::mman::shm_open;
use nix::sys::stat::Mode;
use memmap2::Mmap;
use std::fs::File;
use std::os::fd::FromRawFd;

const SHM_NAME: &str = "/rust_shm";
const SHM_SIZE: usize = 4096;

fn main() {
    let fd = shm_open(SHM_NAME, OFlag::O_RDONLY, Mode::empty())
        .expect("shm_open failed -- is the writer running?");

    let file = unsafe { File::from_raw_fd(fd.as_raw_fd()) };
    let mmap = unsafe {
        Mmap::map(&file).expect("mmap failed")
    };

    // Find the null terminator or use a fixed length
    let end = mmap.iter().position(|&b| b == 0).unwrap_or(SHM_SIZE);
    let msg = std::str::from_utf8(&mmap[..end]).unwrap();
    println!("Reader: got '{}'", msg);
}

Rust Note: mmap is inherently unsafe in Rust because another process can modify the mapped memory at any time, violating Rust's aliasing rules. The unsafe blocks here acknowledge that you are opting into shared-memory semantics, where the compiler cannot enforce data-race freedom.

When to Use Shared Memory

+------------------+-----------+----------+----------+
| Factor           | Pipe      | Socket   | Shm      |
+------------------+-----------+----------+----------+
| Speed            | Medium    | Medium   | Fastest  |
| Kernel copies    | 2 (w+r)   | 2 (w+r)  | 0        |
| Sync needed      | Built-in  | Built-in | Manual   |
| Unrelated procs  | No (FIFO) | Yes      | Yes      |
| Structured data  | Serialize | Serialize| Direct   |
| Complexity       | Low       | Medium   | High     |
+------------------+-----------+----------+----------+

Use shared memory when:

  • You need the absolute lowest latency (high-frequency trading, real-time audio).
  • You are transferring large amounts of data between processes.
  • The data is a fixed-size structure that both processes understand.

Do not use it when:

  • You need communication between machines (use sockets).
  • The data is small and infrequent (use pipes or message queues).
  • You cannot afford the complexity of manual synchronization.

Anonymous Shared Memory with mmap

You do not always need shm_open. For parent-child sharing, use MAP_SHARED | MAP_ANONYMOUS:

/* anon_shm.c */
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    int *shared = mmap(NULL, sizeof(int),
                       PROT_READ | PROT_WRITE,
                       MAP_SHARED | MAP_ANONYMOUS,
                       -1, 0);
    if (shared == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    *shared = 0;

    pid_t pid = fork();
    if (pid == 0) {
        *shared = 42;
        _exit(0);
    }

    wait(NULL);
    printf("Child set shared value to %d\n", *shared);

    munmap(shared, sizeof(int));
    return 0;
}

No filesystem name needed. The mapping is inherited by fork() and shared between parent and child.

Driver Prep: Linux kernel drivers use shared memory extensively. mmap in a device driver maps kernel buffers into user space (e.g., framebuffer devices, DMA buffers). The remap_pfn_range function in the kernel is the driver-side equivalent of mmap.

Memory-Mapped Files

mmap can also map regular files, giving you shared, persistent storage:

/* mmap_file.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

int main(void) {
    const char *path = "/tmp/mmap_test.dat";
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
    ftruncate(fd, 4096);

    char *map = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, 0);
    close(fd);

    strcpy(map, "Persisted via mmap");
    msync(map, 4096, MS_SYNC);  /* flush to disk */
    printf("Wrote to file via mmap\n");

    munmap(map, 4096);

    /* Verify by reading the file normally */
    fd = open(path, O_RDONLY);
    char buf[64];
    read(fd, buf, sizeof(buf));
    close(fd);
    printf("Read back: %s\n", buf);

    unlink(path);
    return 0;
}

Try It: Modify mmap_file.c to map an existing file (like /etc/hostname) as read-only and print its contents without using read(). Hint: use PROT_READ and MAP_PRIVATE.

Knowledge Check

  1. What is the difference between MAP_SHARED and MAP_PRIVATE?
  2. Why must a process-shared mutex be stored in the shared memory region itself?
  3. What does msync do, and when would you need it?

Common Pitfalls

  • Forgetting ftruncate -- the shared memory object starts at size 0. Accessing unmapped memory causes SIGBUS.
  • Using MAP_PRIVATE when you want sharing -- MAP_PRIVATE creates a copy-on-write mapping. Changes are not visible to other processes.
  • Not calling shm_unlink -- the shared memory object persists in /dev/shm/ until you remove it.
  • Assuming memory ordering -- on architectures with weak memory ordering (ARM, RISC-V), you need memory barriers or atomics even with shared memory. x86 is relatively forgiving but do not rely on it.
  • Mapping too much memory -- mmap reserves virtual address space but physical memory is allocated on demand (page faults). Still, do not map terabytes casually.
  • Storing pointers in shared memory -- pointers are process-local. Store offsets instead.