POSIX Shared Memory in Linux
Zero-copy IPC between processes, and the semaphore discipline that keeps it safe
In the previous lecture we covered “copy the message” IPC — FIFOs, Unix sockets, and POSIX message queues, where the kernel moves your data twice: once into a holding area, once back out to the receiver. POSIX shared memory takes the opposite approach: map the same physical memory into two or more processes’ address spaces, and let them read and write it directly, with zero copying after setup.
This is lecture 9 in our free Linux kernel development course, part of a broader free embedded systems course we publish at EmbeddedPathashala covering Linux internals from bootloader to application layer at no cost.
What You Will Learn
- Why shared memory trades copying for synchronization
- The shm_open / ftruncate / mmap workflow
- Naming rules for POSIX shared memory segments
- Using a named semaphore to synchronize access
- A working writer/reader shared memory demo
- Cleanup and common pitfalls
Prerequisites
This lecture builds directly on the previous one on message-based IPC. You should understand file descriptors and be comfortable compiling and linking C programs against POSIX real-time extensions (-lrt) and POSIX threads (-lpthread).
Why Share Memory At All
Copy-based IPC is simple precisely because the kernel does all the hard work of keeping data consistent — a message is either fully delivered or not delivered at all, and the kernel serializes access for you. Shared memory removes that safety net. Once two processes can both write to the same bytes at the same time, you are responsible for making sure they don’t step on each other, and that a reader never sees a half-written update.
The payoff is speed: for large or frequent transfers — video frames, audio buffers, large sensor batches — avoiding the double-copy of FIFO/socket/queue IPC can matter a great deal on a resource-constrained embedded board.
Shared Memory Mapped Into Two Address Spaces
The Three-Step Workflow
Setting up POSIX shared memory always follows the same three calls:
| Step | Function | Purpose |
|---|---|---|
| 1 | shm_open(3) | Create or open a named shared memory object, returns a file descriptor |
| 2 | ftruncate(2) | Set the size of the object — it starts at zero bytes when newly created |
| 3 | mmap(2) | Map the object into the calling process’s address space |
Segment names follow the same rule as message queue names: they must begin with exactly one leading / and contain no further slashes, for example /ep-shm-demo. Under the hood, these objects typically live in tmpfs, visible at /dev/shm on most distributions:
$ ls -la /dev/shm
-rw-r--r-- 1 ravi ravi 4096 Aug 27 10:15 ep-shm-demo
Synchronizing Access
Shared memory tells you nothing about when the other side has finished writing. You need an explicit signal. A POSIX named semaphore (sem_open(3)) is the standard tool: the writer posts the semaphore after finishing a write, and the reader waits on it before reading. For a simple one-writer, one-reader demo, a single semaphore initialized to zero is enough to hand off each update.
Hands-On: A Shared Memory Writer/Reader Demo
This demo has two original programs, both prefixed ep_: ep_shm_writer creates a shared segment holding a small sensor-style struct, writes a few updates, and signals a semaphore after each; ep_shm_reader waits on that semaphore and prints each update as it arrives.
/* ep_shm_common.h */
#ifndef EP_SHM_COMMON_H
#define EP_SHM_COMMON_H
#define EP_SHM_NAME "/ep-shm-demo"
#define EP_SEM_NAME "/ep-shm-demo-sem"
typedef struct {
int sample_id;
float temperature_c;
} ep_reading_t;
#endif
/* ep_shm_writer.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>
#include "ep_shm_common.h"
int main(void)
{
int fd = shm_open(EP_SHM_NAME, O_CREAT | O_RDWR, 0644);
if (fd == -1) { perror("shm_open"); return 1; }
if (ftruncate(fd, sizeof(ep_reading_t)) == -1) {
perror("ftruncate");
return 1;
}
ep_reading_t *shared = mmap(NULL, sizeof(ep_reading_t),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared == MAP_FAILED) { perror("mmap"); return 1; }
sem_t *sem = sem_open(EP_SEM_NAME, O_CREAT, 0644, 0);
if (sem == SEM_FAILED) { perror("sem_open"); return 1; }
for (int i = 1; i sample_id = i;
shared->temperature_c = 25.0f + i;
printf("ep_shm_writer: wrote sample %d (%.1fC)\n",
shared->sample_id, shared->temperature_c);
sem_post(sem);
sleep(1);
}
munmap(shared, sizeof(ep_reading_t));
close(fd);
sem_close(sem);
return 0;
}
/* ep_shm_reader.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>
#include "ep_shm_common.h"
int main(void)
{
int fd = shm_open(EP_SHM_NAME, O_RDONLY, 0644);
if (fd == -1) { perror("shm_open"); return 1; }
ep_reading_t *shared = mmap(NULL, sizeof(ep_reading_t),
PROT_READ, MAP_SHARED, fd, 0);
if (shared == MAP_FAILED) { perror("mmap"); return 1; }
sem_t *sem = sem_open(EP_SEM_NAME, 0);
if (sem == SEM_FAILED) { perror("sem_open"); return 1; }
for (int i = 0; i sample_id, shared->temperature_c);
}
munmap(shared, sizeof(ep_reading_t));
close(fd);
sem_close(sem);
shm_unlink(EP_SHM_NAME);
sem_unlink(EP_SEM_NAME);
return 0;
}
Build and Run
$ gcc -o ep_shm_writer ep_shm_writer.c -lrt -lpthread
$ gcc -o ep_shm_reader ep_shm_reader.c -lrt -lpthread
# start the reader first in one terminal
$ ./ep_shm_reader
# then the writer in another terminal
$ ./ep_shm_writer
ep_shm_writer: wrote sample 1 (26.0C)
ep_shm_writer: wrote sample 2 (27.0C)
ep_shm_writer: wrote sample 3 (28.0C)
Reader terminal output:
ep_shm_reader: sample 1 = 26.0C
ep_shm_reader: sample 2 = 27.0C
ep_shm_reader: sample 3 = 28.0C
The reader blocks on sem_wait until the writer calls sem_post, so the two processes stay correctly synchronized without either one busy-polling or reading a half-updated struct.
Real-World Use Cases
| Scenario | Why shared memory fits |
|---|---|
| Camera pipeline handing frames to an encoder process | Frame buffers are large and frequent — copying twice per frame wastes CPU and bandwidth |
| Audio ring buffer between capture and processing daemons | Continuous low-latency data with a producer/consumer pattern |
| Shared configuration/state table read by several services | Many readers, infrequent writer, no per-read copy needed |
Common Mistakes and Troubleshooting
Forgetting ftruncate — a newly created shared memory object starts at zero bytes. Calling mmap before sizing it results in a failed mapping or a SIGBUS on first access.
SIGBUS on access past the mapped size — if one process maps a smaller size than another expects (for example after changing the struct without recompiling both programs), accessing the extra bytes raises SIGBUS, not a clean error return. Always keep the shared struct definition in one header both programs include.
No synchronization at all — mapping shared memory does not serialize access. Skipping the semaphore (or another lock) means the reader can observe a torn write, especially for structs larger than the platform’s atomic write size.
Leaked segments after testing — like message queues, /dev/shm entries and named semaphores outlive a crashed process. Run ls /dev/shm and clean up manually with rm or shm_unlink-based tooling if a test run didn’t exit cleanly.
Best Practices
- Always pair every shared memory region with an explicit synchronization primitive — never assume “it usually works” ordering.
- Keep the shared struct’s layout in a single shared header, and add a version field if the layout might change across builds.
- Round the segment size up to a multiple of the system page size (
sysconf(_SC_PAGESIZE)) to avoid surprises from the underlying page-granular mapping. - Unlink both the shared memory object and any semaphore in your shutdown path, not just on the success path — use signal handlers if the process might be killed.
- For anything beyond a single writer/single reader pair, consider a proper ring buffer with atomic head/tail indices rather than a single semaphore.
Performance Considerations
Shared memory’s whole value proposition is avoiding copies, so measure whether that copy avoidance actually matters for your workload before adding the synchronization complexity. For small, infrequent messages, the two-copy cost of a message queue or socket is usually cheaper in engineering time than a hand-rolled shared memory protocol, even if it’s technically slower in raw throughput. Reach for shared memory when profiling shows the copy overhead is real — large buffers, high frequency, or tight latency budgets.
Security Considerations
A shared memory segment’s permissions are set at shm_open time via the mode argument, exactly like a regular file. Any process with sufficient permission to open the segment can read or write it — there’s no built-in access control beyond that, so avoid world-writable segments on any system where untrusted local processes might run, and consider a dedicated group for processes that legitimately need access.
Summary and Key Takeaways
Key Takeaways
- Shared memory removes copying by mapping the same physical pages into multiple processes, but shifts the synchronization burden onto you.
- The workflow is always
shm_open→ftruncate→mmap. - A named semaphore is the simplest correct way to signal “data is ready” between writer and reader.
- Segments and semaphores persist past process exit — always unlink them in cleanup code.
Conclusion
Between this lecture and the previous one, you now have the full picture of Linux IPC’s two fundamental strategies: copy-based mechanisms that trade a small performance cost for kernel-managed safety, and shared memory that trades that safety for raw speed. Most real embedded systems end up using both — a message queue or socket for control and signalling, and shared memory for the bulk data path. Understanding when each is the right tool is one of the most transferable skills in systems programming, on embedded Linux or anywhere else.
Frequently Asked Questions
Why does shared memory need a semaphore if mmap already shares the data?
mmap only shares the memory pages — it says nothing about timing. Without a synchronization primitive, a reader could read the struct while the writer is halfway through updating it, seeing inconsistent data. A semaphore (or other lock) makes the handoff safe.
What happens if I skip ftruncate before mmap?
A freshly created shared memory object is zero bytes in size. Mapping it before resizing typically fails or produces a mapping you can’t safely access — always call ftruncate to set the size first.
Where does POSIX shared memory actually live on disk?
It doesn’t live on disk at all — POSIX shared memory objects are typically backed by tmpfs and visible under /dev/shm, which is RAM-backed, not persistent storage.
Do I need root privileges to use shm_open?
No. Any user can create a shared memory segment; access is governed by the mode bits passed to shm_open, just like a regular file’s permissions.
What causes a SIGBUS when using shared memory?
SIGBUS typically happens when a process accesses memory beyond the actual size of the underlying shared memory object — for example if one program’s struct definition doesn’t match the size the segment was truncated to.
Can more than two processes share the same segment?
Yes. Any number of processes can shm_open and mmap the same named segment. Coordinating more than two participants safely usually calls for more than a single binary semaphore, though — consider a proper reader/writer lock or ring buffer design.
How do I clean up a shared memory segment left over from a crashed test?
List /dev/shm and remove the stale entry directly, or write a small cleanup call to shm_unlink with the same name your program used. The same applies to named semaphores via sem_unlink.
Continue Your Free Linux Kernel Journey
This lecture is part of EmbeddedPathashala’s free Linux device drivers and kernel development course — no signup fees, just structured, practical embedded Linux content.
Next Lecture Browse Full Course
2 Comments