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
| Function | Purpose |
|---|---|
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_unlinkremoves the name from the filesystem, but the memory stays mapped until all processes callmunmapor exit. If you forgetshm_unlink, the shared memory persists across reboots (it lives in/dev/shm/). Check withls /dev/shm/.
Try It: Run
shm_writer, then look at/dev/shm/my_shmwithls -la /dev/shm/. You will see a file. Runshm_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_tobject.
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:
mmapis inherently unsafe in Rust because another process can modify the mapped memory at any time, violating Rust's aliasing rules. Theunsafeblocks 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.
mmapin a device driver maps kernel buffers into user space (e.g., framebuffer devices, DMA buffers). Theremap_pfn_rangefunction in the kernel is the driver-side equivalent ofmmap.
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.cto map an existing file (like/etc/hostname) as read-only and print its contents without usingread(). Hint: usePROT_READandMAP_PRIVATE.
Knowledge Check
- What is the difference between
MAP_SHAREDandMAP_PRIVATE? - Why must a process-shared mutex be stored in the shared memory region itself?
- What does
msyncdo, and when would you need it?
Common Pitfalls
- Forgetting
ftruncate-- the shared memory object starts at size 0. Accessing unmapped memory causesSIGBUS. - Using
MAP_PRIVATEwhen you want sharing --MAP_PRIVATEcreates 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 --
mmapreserves 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.