Every process on Linux lives in its own private virtual address space. That isolation is a safety feature — one process can’t accidentally scribble over another’s memory — but it also means that if two processes genuinely need to exchange data quickly, they can’t just pass a pointer. POSIX shared memory solves this by letting two or more unrelated processes map the same physical memory into their own address spaces. Because that memory is now shared, we also need a way to stop both processes from writing to it at the same instant — that’s where POSIX semaphores come in. This lecture in our free linux kernel development course walks through both, with a small original demo you can build and run yourself.
What You Will Learn
- Why processes need shared memory in the first place
- The shm_open / ftruncate / mmap sequence, step by step
- Where /dev/shm actually lives and how it’s backed
- Why a race condition appears the moment two processes share memory
- Creating and using a named POSIX semaphore to fix it
- Building and running a complete two-process demo on the latest stable kernel
Prerequisites
- Comfortable with fork(), file descriptors, and basic C on Linux
- A glibc-based development machine (any current distro — Ubuntu, Fedora, Debian all work)
- gcc and the -pthread / -lrt link flags (covered below)
Why Not Just Use a Pipe or a File?
Pipes and regular files both work for passing data between processes, but they go through the kernel’s read/write path on every access — data gets copied from user space into a kernel buffer and back out again. For something that changes constantly, like a sensor reading updated hundreds of times a second, or a video frame buffer being written by one process and displayed by another, that copying overhead adds up fast.
Shared memory removes the copy entirely. Once two processes have mapped the same segment, writing to it in one process is instantly visible to a read in the other — there’s no read()/write() call at all, just ordinary pointer access. The trade-off is that the kernel is no longer serializing access for you the way it does with a pipe, so the processes have to coordinate access themselves. That’s the semaphore’s job.
Creating and Mapping a Shared Segment
On modern Linux, POSIX shared memory objects are exposed through a tmpfs filesystem, conventionally mounted at /dev/shm. Under the hood, shm_open() is just a thin wrapper that creates or opens a file in that filesystem — that’s genuinely all a “shared memory object” is on Linux. The sequence to set one up is:
- Call
shm_open()with a name starting with a slash, e.g."/ep-msg", to get a file descriptor. - Call
ftruncate()on that descriptor to size the segment — a freshly created object is zero bytes long. - Call
mmap()on the descriptor to get a pointer your process can actually read and write.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <semaphore.h>
#include <errno.h>
#define EP_SHM_NAME "/ep-msg"
#define EP_SEM_NAME "/ep-msg-sem"
#define EP_SHM_SIZE 256
struct ep_shared_msg {
unsigned int counter;
char text[240];
};
static struct ep_shared_msg *ep_map_shared_segment(int *out_fd, sem_t **out_sem)
{
int created = 1;
int fd = shm_open(EP_SHM_NAME, O_CREAT | O_EXCL | O_RDWR, 0666);
if (fd == -1 && errno == EEXIST) {
created = 0;
fd = shm_open(EP_SHM_NAME, O_RDWR, 0666);
}
if (fd == -1) {
perror("shm_open");
exit(EXIT_FAILURE);
}
if (created && ftruncate(fd, EP_SHM_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
struct ep_shared_msg *msg = mmap(NULL, EP_SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (msg == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
sem_t *sem = sem_open(EP_SEM_NAME, O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}
*out_fd = fd;
*out_sem = sem;
return msg;
}
Two small but important differences from an older-style implementation worth calling out: mmap() failure is checked against MAP_FAILED, not NULL — on Linux, NULL is technically a valid (if unusual) mapping address, and comparing against it is a latent bug. Second, ftruncate() is only called by whichever process actually created the segment; calling it again on an already-sized segment from a second process is harmless here but unnecessary, and in general re-truncating a live segment zero-fills it and would blow away real data.
The Race Condition — and the Fix
If process A writes msg->counter and msg->text as two separate field assignments, and process B happens to read the struct in between those two assignments, B sees a half-updated, inconsistent message. This isn’t a hypothetical — with two independent processes running on a multi-core machine, it happens routinely. The fix is a POSIX named semaphore: a counting lock that lives outside either process (also backed by a tmpfs-style object) so unrelated processes can share it by name, exactly like the shared memory segment itself.
| Call | Purpose |
|---|---|
sem_open(name, O_CREAT, mode, initial_value) | Create or attach to a named semaphore; initial value 1 makes it behave as a mutex |
sem_wait(sem) | Block until the semaphore’s value is > 0, then decrement it (enter the critical section) |
sem_post(sem) | Increment the semaphore’s value, waking one blocked waiter if any (leave the critical section) |
sem_close(sem) / sem_unlink(name) | Detach from the semaphore / remove its name once no process needs it anymore |
static void ep_write_message(struct ep_shared_msg *msg, sem_t *sem, const char *text)
{
sem_wait(sem);
msg->counter++;
snprintf(msg->text, sizeof(msg->text), "%s", text);
sem_post(sem);
}
static void ep_read_message(struct ep_shared_msg *msg, sem_t *sem)
{
sem_wait(sem);
printf("counter=%u text=\"%s\"\n", msg->counter, msg->text);
sem_post(sem);
}
Both the read and the write take the same semaphore before touching the struct, so the two field updates in ep_write_message() are now atomic from the reader’s point of view — ep_read_message() can never observe a half-written message.
Building and Running the Demo
Compile the writer and a matching reader (same struct, same names, just calling ep_read_message() in a loop instead) and link with -lrt -pthread — -lrt pulls in the shared-memory and semaphore symbols on older glibc; on current glibc (2.34+) these symbols live directly in libc, but keeping the flag is harmless and keeps the build portable to older systems.
$ gcc -O2 -Wall -o ep_shm_writer ep_shm_writer.c -lrt -pthread
$ gcc -O2 -Wall -o ep_shm_reader ep_shm_reader.c -lrt -pthread
# terminal 1
$ ./ep_shm_writer "hello from writer"
counter=1 text="hello from writer"
# terminal 2, run afterwards
$ ./ep_shm_reader
counter=1 text="hello from writer"
# confirm the backing object exists as an ordinary tmpfs file
$ ls -la /dev/shm/
-rw-r--r-- 1 ravi ravi 256 Aug 26 10:02 ep-msg
That last command is worth running yourself — it makes concrete that a POSIX shared memory object really is just a file in a RAM-backed filesystem, sized exactly as your ftruncate() call requested.
Common Mistakes and Troubleshooting
shm_unlink("/ep-msg") and sem_unlink("/ep-msg-sem") once you’re truly done with them.
MAP_FAILED — see the note above.
Best Practices
- Keep the critical section (time spent between sem_wait/sem_post) as short as possible
- Prefer O_EXCL when one specific process should own creation, falling back to attach-only otherwise
- Always unlink named objects during orderly shutdown
- Never assume a shared struct’s exact byte layout across compilers without control over alignment
Summary
POSIX shared memory gives unrelated processes a way to exchange data without a copy through the kernel, by mapping the same tmpfs-backed pages into each process’s address space with shm_open(), ftruncate(), and mmap(). Because the kernel no longer serializes access for you once memory is shared, a named POSIX semaphore — created with sem_open() and used via sem_wait()/sem_post() — is the standard way to protect the shared data from concurrent, torn writes.
FAQ
What’s the difference between POSIX shared memory and System V shared memory?
They solve the same problem but with different APIs — POSIX shared memory (shm_open/mmap) is the modern, file-descriptor-based interface and integrates naturally with mmap and poll-style tooling; System V shared memory (shmget/shmat) is the older interface with its own key-based namespace. New code should use the POSIX API.
Do I need root privileges to use /dev/shm?
No. /dev/shm is world-writable by default (with sticky-bit-style ownership per object), so any user process can create and use POSIX shared memory objects.
What happens to shared memory if a process crashes while holding the semaphore?
The semaphore stays decremented forever — this is a classic “orphaned lock” problem. POSIX semaphores don’t have built-in crash recovery; if this matters for your application, look at robust futex-based mutexes in shared memory instead.
Is shared memory access “thread-safe” if only one process is using it?
A single process sharing memory across its own threads doesn’t need shm_open at all — regular heap or global memory is already shared between threads. shm_open is specifically for separate processes with separate address spaces.
How big can a POSIX shared memory segment be?
It’s limited by available RAM (and tmpfs mount size, which defaults to half of physical RAM on most distros) rather than any fixed API limit.
Can I resize a shared memory segment after other processes have already mapped it?
You can ftruncate() it larger, but existing mappings in other processes won’t automatically grow — each process needs to re-mmap to see the new size, and doing this safely under concurrent access needs careful coordination.
Continue the Free Linux Kernel Development Course
Next up: creating and managing POSIX threads — the other major way Linux processes share work.
Next Lecture Course Index
2 Comments