~/atomic_operations_across_multiple_processes_via_shared_memory.log
Jun 24, 2026

Atomic Operations Across Multiple Processes via Shared Memory

The Problem

Two processes need to communicate without copying data and without blocking locks. Standard C++ abstractions like std::mutex and std::condition_variable are bound to a single process's address space. However, concurrent access to shared data across process boundaries must still be safe and synchronized.


How Shared Memory Works

Using shm_open() + mmap() maps the same physical page frames into multiple virtual address spaces. While virtual addresses differ between processes, the underlying physical memory is identical.

Process A (RW)              Physical Memory              Process B (RO)
──────────────              ───────────────              ──────────────
Virtual: 0x7f000000 ──────► Page Frame: 0x1234000 ◄────── Virtual: 0x6a000000
        (mmap r/w)            (same bytes)               (mmap r/o)

The CPU's Memory Management Unit (MMU) translates each process's virtual address to the exact same physical frame. All CPU coresβ€”regardless of which process they are executingβ€”operate on the same physical cache lines, maintaining cache coherence via hardware protocol (e.g., MESI).


Why Lock-Free Atomics Work Across Processes

A lock-free std::atomic<T> stores its state strictly inside the memory location of T itselfβ€”it contains no hidden pointers, no process-local state, and no mutexes.

Because atomicity guarantees are provided by CPU hardware operating on physical cache lines, a lock-free std::atomic<uint32_t> in shared memory behaves identically whether accessed by multiple threads in one process or multiple processes entirely:

  • Reads and writes are indivisible.
  • std::memory_order guarantees visibility and instruction ordering across CPU cores/processes.

The Three Fundamental Requirements

1. Lock-Free Guarantee

The atomic type must be guaranteed lock-free by hardware. If an atomic is not lock-free, the C++ runtime uses an internal fallback lock (often allocated on the process's private heap), making it completely inaccessible to other processes.

[!WARNING] Verify at Compile Time
Always check lock-free status at compile-time using is_always_lock_free:

static_assert(std::atomic<uint32_t>::is_always_lock_free, "uint32_t must be lock-free");
static_assert(std::atomic<uint64_t>::is_always_lock_free, "uint64_t must be lock-free");
  • Supported: Native integer types up to 64-bits on x86-64 and ARM64.
  • Not Supported: Structs or types larger than the native register/double-word width (e.g., 128-bit on some platforms) are typically not lock-free.

2. Natural Alignment

The atomic variable must be naturally aligned in the shared memory struct layout. Unaligned or byte-packed access can span multiple cache lines, causing the hardware to lose atomicity guarantees.

struct alignas(8) ShmLayout {
    std::atomic<uint32_t> head;   // 4-byte aligned β€” Correct
    std::atomic<uint32_t> tail;   // 4-byte aligned β€” Correct
};

static_assert(sizeof(std::atomic<uint32_t>) == 4);
static_assert(alignof(std::atomic<uint32_t>) == 4);

[!TIP] Alignment Best Practice
Avoid #pragma pack(1) or packing directives on shared memory structures containing atomic variables.


3. Same Physical Pages

Both processes must map the exact same file descriptor obtained via shm_open(). Mapping different handles or mapping mismatched offsets results in different physical pages, causing writes in Process A to be invisible to Process B.

// Process A (Owner / Read-Write)
int fd = shm_open("/my_channel", O_CREAT | O_RDWR, 0600);
ftruncate(fd, sizeof(ShmLayout));

auto* shm = static_cast<ShmLayout*>(
    mmap(nullptr, sizeof(ShmLayout), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
);

// Process B (Consumer / Read-Only)
int fd = shm_open("/my_channel", O_RDONLY, 0);

auto* shm = static_cast<ShmLayout*>(
    mmap(nullptr, sizeof(ShmLayout), PROT_READ, MAP_SHARED, fd, 0)
);
// shm->head and shm->tail refer to the exact same physical memory as in Process A

Memory Ordering Across Processes

The C++ memory ordering semantics (std::memory_order) apply across process boundaries when operating on shared physical memory:

Memory Order Cross-Process Meaning
memory_order_relaxed Indivisible read/write with no ordering guarantees relative to surrounding memory access.
memory_order_release (store) All stores preceding this operation in Process A become visible to Process B upon an acquire load.
memory_order_acquire (load) Prevents subsequent loads in Process B from being reordered before this atomic read.
memory_order_seq_cst Establishes a globally consistent total order across all processes and threads.

Practical Producer-Consumer Pattern

// Process A (Producer)
data_buffer[idx] = payload;                                       // 1. Write payload
shm->head.store(idx + 1, std::memory_order_release);             // 2. Publish (Release store)

// Process B (Consumer)
uint32_t h = shm->head.load(std::memory_order_acquire);          // 1. Acquire load
if (h > tail) {
    use(data_buffer[tail]);  // 2. Guaranteed to see payload written by Process A
}

What Works vs. What Fails in Shared Memory

Feature Shared Memory Compatible? Reason / Recommendation
std::atomic<T> (Lock-Free) YES Stores value directly in SHM memory address; hardware handles synchronization.
Raw Pointers (T*) NO Virtual addresses differ per process. Use offsets (uintptr_t relative to SHM base) or boost::interprocess::offset_ptr.
std::mutex NO Standard library implementation contains process-local state.
pthread_mutex_t YES (With Caveat) Native POSIX mutexes work if initialized with PTHREAD_PROCESS_SHARED.
std::condition_variable NO Contains process-specific internal OS primitive state.
Modern Blocking (std::atomic::wait) YES (C++20) Under Linux (futex), std::atomic::wait() / .notify_one() works across processes for lock-free atomics in SHM.

Summary Rule

[!SUCCESS] Practical Rule
If std::atomic<T>::is_always_lock_free is true and T is naturally aligned in shared memory, concurrent reads and writes from multiple processes carry the exact same atomicity and memory ordering guarantees as multi-threaded access within a single process.

Blocking Signaling Strategies

  • C++20 and newer: Use std::atomic::wait() and std::atomic::notify_one() directly on your shared atomic variable.
  • C++11 to C++17: Use POSIX named semaphores (sem_open, sem_wait, sem_post) or process-shared POSIX mutexes (pthread_mutexattr_setpshared).

References

Published Jun 24, 2026

← Back to articles