What are POSIX Shared Memory and Semaphores-Free Linux Device Drivers Course In Hyderabad

PREV_LEC | NEXT_LEC

POSIX Shared Memory and Semaphores
Lecture 6 of the free embedded Linux development course — safely sharing data between processes

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.

POSIX shared memory shm_open mmap named semaphores sem_wait sem_post free linux device drivers course free embedded systems course

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.

Two Processes Mapping One Shared Segment
Process A (writer) Process B (reader) +——————-+ +——————-+ | private stack/heap | | private stack/heap | +——————-+ +——————-+ | mmap() ———> /dev/shm/ep-msg <——— mmap() | +——————-+ +——————-+ \ / \__________ same physical pages ___/ (tmpfs, backed by RAM)

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:

  1. Call shm_open() with a name starting with a slash, e.g. "/ep-msg", to get a file descriptor.
  2. Call ftruncate() on that descriptor to size the segment — a freshly created object is zero bytes long.
  3. 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.

CallPurpose
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

Forgetting sem_unlink() / shm_unlink(): named objects persist in /dev/shm even after every process exits. If you re-run a test repeatedly without cleaning up, you’ll silently attach to a stale segment from a previous run. Call shm_unlink("/ep-msg") and sem_unlink("/ep-msg-sem") once you’re truly done with them.
Comparing mmap()’s return against NULL: always check against MAP_FAILED — see the note above.
Sizing mismatch: if one process creates the segment with a struct of one size and another process (built from stale object files) maps it expecting a different struct layout, you’ll get memory corruption with no compiler warning. Keep the struct definition in one shared header.

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

PREV_LEC | NEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *