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_orderguarantees 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 usingis_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
Ifstd::atomic<T>::is_always_lock_freeistrueandTis 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()andstd::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