What shared memory does and does not give you
On POSIX systems you typically create the region with shm_open, size it with ftruncate and map it with mmap and MAP_SHARED. A parent can also map an anonymous shared region before fork. Either way, both processes now see the same physical pages, and a write by one is visible to the other.
That is all. There is no mutual exclusion, no ordering between one process's writes and the other's reads, and no notice when the other side disappears. Two processes updating a counter or a record in the region race exactly like two threads would. The difference is that the usual thread tools do not cross the process boundary by default.
Two layout rules follow from the mapping. Each process may map the region at a different virtual address, so the region must never hold raw pointers into itself. Store offsets from the base instead. And anything the region depends on, such as the lock, has to live inside the region, because process synchronisation only works on memory both sides can see.
Process synchronisation with a shared mutex
A pthread mutex placed in the shared region works across processes only if it was initialised with the process-shared attribute. The default is PTHREAD_PROCESS_PRIVATE, and using a private mutex from two processes is undefined behaviour. It may pass a quick test and then leave a waiter that is never woken.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <pthread.h>
struct shared {
pthread_mutex_t lock;
long committed_seq;
long records[1024];
};
int shared_init(struct shared *s) {
pthread_mutexattr_t a;
int rc = pthread_mutexattr_init(&a);
if (rc != 0) return rc;
pthread_mutexattr_setpshared(&a, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust(&a, PTHREAD_MUTEX_ROBUST);
rc = pthread_mutex_init(&s->lock, &a);
pthread_mutexattr_destroy(&a);
s->committed_seq = 0;
return rc;
}
static void repair(struct shared *s) {
(void)s;
}
int shared_lock(struct shared *s) {
int rc = pthread_mutex_lock(&s->lock);
if (rc == EOWNERDEAD) {
repair(s);
rc = pthread_mutex_consistent(&s->lock);
}
rc;
}
Initialisation is its own race. If both processes try to initialise the mutex, or one locks it while the other is still initialising, behaviour is undefined. The usual answers are to initialise before fork, or to let only the process that created the object with O_CREAT | O_EXCL initialise it. The other process then waits for a ready flag that the creator sets last, with release semantics. A POSIX unnamed semaphore placed in the region with sem_init(&sem, 1, 1) is the main alternative, and a named semaphore from sem_open avoids the placement question altogether.
The mutex also handles memory ordering for you. Writes made before an unlock are visible to the process that next acquires the lock. Code that skips the lock and relies on volatile gets none of that guarantee.
What happens when the holder dies
With a plain process-shared mutex, nothing happens, and that is the failure. The lock word still records an owner that no longer exists. The next pthread_mutex_lock blocks forever, and so does every process after it. No error, no timeout, just a hung system that looks idle.
A robust mutex, standardised in POSIX.1-2008, changes this. When the owner terminates while holding it, the next successful locker receives EOWNERDEAD instead of 0. On Linux this is backed by the kernel, which walks a per-thread list of held robust locks when a thread exits. The caller does own the lock at that point. If it calls pthread_mutex_consistent and later unlocks, the mutex returns to normal. If it unlocks without that call, the mutex becomes permanently unusable and every later lock attempt returns ENOTRECOVERABLE.
Semaphores cannot do this, because a semaphore has no owner. If a process dies after sem_wait and before sem_post, the count is simply lost, and nothing can tell a dead holder from a slow one. System V semaphores offer SEM_UNDO, which asks the kernel to reverse that process's adjustments when it exits. That restores the count but, like the robust mutex, says nothing about the data.
Recovering the lock is the easy half
Most candidates who know robust mutexes stop at EOWNERDEAD, as if getting the lock back were the problem. It is not. The owner died inside the critical section, which is the one place where the shared data is allowed to break its invariants. It may have written half a record, bumped a length without filling the slot, or unlinked a node without relinking it. EOWNERDEAD tells you that happened, not what or where.
So the strong answer designs the writes for recovery before anything goes wrong. Write the new record into a free slot first, then publish it with one aligned store of committed_seq. That store must be a release store (C11 atomic_store_explicit with memory_order_release, or a barrier before a plain store); otherwise the compiler or CPU may make it visible before the record it vouches for. A recoverer can then discard everything past the last committed sequence number, which is what repair does in the sketch above. Alternatives are a dirty flag with a generation counter, or a journal of intended changes. If the structure genuinely cannot be repaired, the honest recovery is to mark the region invalid and rebuild it from a source of truth, and saying so is better than calling pthread_mutex_consistent on data you know is corrupt.
Keep the critical section short for the same reason. Every instruction inside it is one more point where a death leaves work half done.
Shared memory gives you the bytes, and a process-shared robust mutex gives you the lock back. The data the dead process was halfway through writing is a separate problem, and only your write layout can solve it.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).