Shared memory allows multiple executors to access the same underlying storage, reducing the need for large data blocks to round-trip through IPC buffers. However, it does not automatically define who can write, when data is readable, the data format, or how to recover after a crash. The truly difficult part of many shared memory protocols is synchronization and lifecycle management.
Choose the Creation Method for Shared Objects First
Method
How another process gains access
Lifecycle characteristics
MAP_SHARED anonymous mapping
Created before fork and inherited by child processes
Typically suitable for processes with a common ancestor
POSIX shm_open
Opens an object by a convention name
Name management is separate from open references and mappings
Ordinary file MAP_SHARED
Opens the same file object
Must also consider file size and persistence semantics
memfd_create
Inherits fd or passed via SCM_RIGHTS
No public path name required; seals can be configured
SysV shmget/shmat
Attached via IPC identifier
Requires explicit management of "marked for deletion" and attachment relationships
mmap with MAP_SHARED returns MAP_FAILED on failure, not NULL. Mappings backed by files usually require the object to be extended to a sufficient size first; accessing beyond the actual file range may generate SIGBUS; concurrent truncation by the other party is also a protocol risk that needs handling.
The name for POSIX shm_open, such as /myregion, does not require the caller to directly open a /dev/shm path. Linux often implements these objects on tmpfs, but the API semantics should not be bound to that filesystem path. O_CREAT|O_EXCL can be used to identify the unique creator; the initializer and subsequent openers must coordinate so that neither side repeatedly initializes the synchronization object. shm_open(3)
Same Storage Does Not Equal Same Virtual Address
Two processes may map the same object to different addresses. Directly saving ordinary process pointers in shared data structures usually cannot be dereferenced by the other process; offsets relative to the object start, indices, or other protocol handles should be used instead.
Structure layout also involves integer width, alignment, byte order, versioning, and atomic operation support. If both programs are upgraded independently, binary layout compatibility cannot be assumed just because they use the same header file name.
MAP_SHARED causes writes to affect the shared object, but it does not replace synchronization. Cache coherence does not equal language-level happens-before. Publishing data should use correct inter-process locks, semaphores, or atomic protocols explicitly designed for this purpose; it cannot be cobbled together with volatile and busy-waiting for general synchronization.
Offsets also need validation, not just changing pointers to integers
Assume the shared region protocol allows a total access length of total, with a record providing offset and length. The check should be written as offset <= total and length <= total - offset; in fixed-width languages, directly calculating offset + length and then comparing may cause an overflow first. Below, all fields are constrained to unsigned 64-bit, and the range is verified.
offset=total and length=0 can represent a valid empty interval, but you cannot dereference that position based on this. After the range passes, you must also verify layout version, alignment, and object type; if another process can change the length or object size while reading the header, the validation result may also become invalid immediately. Validation and reading need to be guaranteed to be of the same version by the protocol, or changes need to be limited by immutable objects.
A shared offset or another process’s pointer?
The colored range belongs to the shared object; each process supplies its own coordinate origin. Moving the receiver base changes its pointer but not the object range. offset=256,length=0 is an empty boundary range, not an accessible byte.
0≤o≤T,0≤n≤T−o,pA=BA+o,pB=BB+o
Preparing the visual
A shared offset or another process’s pointer?
One object offset can produce different addresses in two processes. Validate offset≤total and length≤total−offset first; an empty range is not dereferenceable.
Semaphores: Counting Resources and Publishing Events
sem_wait obtains a count token upon success, and waits if the count is zero; sem_post increments the token and gives waiters a chance to continue. Semaphores do not have a fixed owner like a regular mutex; producers and consumers can post and wait from different threads or processes.
In sem_init(&sem, pshared, value), a non-zero pshared indicates cross-process sharing, but the sem_t must already be located in a shared region accessible by all processes. It does not turn a stack-based sem into shared memory, nor can it be re-initialized while there are still users. sem_init(3)
Complete Example: Parent Process Publishes, Child Process Reads
Save as shared_demo.c, compile with cc -std=c11 -Wall -Wextra -pthread shared_demo.c -o shared_demo. The program checks the publish result using the child process exit status, without relying on stdio buffered output.
Initialization is completed before fork, with an initial count of zero; the parent process writes data first and then posts, and the child process reads after a successful wait. The parent process waits for the child process to finish before destroying the semaphore, avoiding situations where there are still users. Retry on the EINTR path for sem_wait; other errors cannot be treated as having already obtained the token. sem_wait(3)
This is a one-time handover example, not a complete crash recovery protocol. If the producer dies before posting, the consumer may still wait forever; circular buffers also need empty slots, readable slots, sequence numbers, and shutdown states. Production systems should define timeouts, peer liveness detection, and strategies for discarding shared regions.
One-time ready cannot directly become a circular publishing protocol
If the parent process continuously writes value=1, posts, then writes value=2, posts, while the child process hasn't had time to read, the two tokens do not save two historical values. The child process might read 2 both times. Semaphore counts remember how many times a token can be obtained, not the copied payload.
Single-slot repeated handover can use two semaphores, empty and full, representing writable slots and readable slots respectively. The producer waits on empty, writes data, and posts full; the consumer waits on full, reads data, and posts empty. If the reader passes the shared region pointer to another asynchronous task, it cannot return empty until the task has finished reading, otherwise the next overwrite will race with the old task's read.
Protocol State
Who can access payload
Can it be released or overwritten?
Empty token not yet obtained
No published content available for consumption
Producer must obtain the token first
Producer holds slot
Producer writes
Consumer cannot read early
Full published
Consumer reads after obtaining token
Producer cannot overwrite early
Consumer finishes and returns empty
Next round producer
Can start a new round
A queue with N slots also requires correct index management. Multiple producers obtaining two empty tokens simultaneously does not automatically allocate them to different slots; index advancement still requires mutexes or atomic protocols. Abnormal exits may also lose tokens, which needs to be designed together with recovery strategies later.
Do two tokens preserve two payload versions?
In the broken protocol, publish v1 then v2 and consume twice: both reads return v2. Token counts survive, payload history does not. The safe handoff prevents republishing until consumption returns empty. A real implementation must establish this ordering with synchronization.
Preparing the visual
Do two tokens preserve two payload versions?
Semaphore tokens do not copy payload. Repeated single-slot handoff needs writable and readable ownership to prevent premature overwrite.
sem_open creates or opens a named semaphore; sem_close releases the reference for this process, and sem_unlink deletes the name. Unlink should not be understood as immediately destroying objects still opened by other processes, similar to shared memory name management.
Name deletion and data accessibility are different levels. Safe cleanup is often the responsibility of the creator, but it must be clear how later discoverers find the object, when attachment is no longer allowed, and whether old version objects still have users. Writing resource deletion responsibilities into the protocol is more reliable than "cleaning up when the program exits."
memfd and Sealing: Limiting Changes to Objects
memfd_create returns an anonymous memory file fd, which can be ftruncate'd, mmap'd, and passed to another process via Unix socket. MFD_CLOEXEC controls exec inheritance; if seals need to be added later, MFD_ALLOW_SEALING should usually be set at creation time.
F_SEAL_SHRINK prevents shrinking, F_SEAL_GROW prevents growing; they do not prohibit rewriting existing bytes. F_SEAL_WRITE has constraints with writable shared mappings; the semantics of F_SEAL_FUTURE_WRITE are different, and existing writable mappings need special consideration. Seals must be selected based on the properties that need to be guaranteed; not all seals can be called "read-only". memfd_create(2)
For example, if a consumer needs to trust that the object size does not change after inspection, it can require the corresponding length seal; to trust that the content is immutable, write restrictions also need to be checked. memfd is a building block, implying that not all IPC in systemd or other programs is based on it.
Use the Same memfd to Check Size and Content Restrictions Separately
Save as seal-demo.c. After creation, write abc, first prohibit shrinking and growing; at this point, rewriting the first existing byte still succeeds; then add F_SEAL_WRITE, and rewriting again is rejected. This example does not establish a writable shared mmap, so it will not trigger the condition of "adding a write seal failing due to existing writable mappings."
Compile and run with cc -Wall -Wextra seal-demo.c -o seal-demo. After the reader obtains the fd, it can check which restrictions the object currently has with F_GET_SEALS; these restrictions apply to the underlying object, not just some fd number of the sender. File sealing
F_SEAL_SEAL controls whether seals can be added later, and does not replace F_SEAL_WRITE. F_SEAL_FUTURE_WRITE allows existing writable shared mappings to continue modifying, so it cannot be used alone to prove that content is already immutable. When publishing a read-only snapshot, you should complete writing, handle existing mappings, and then establish and check the required content and length restrictions.
Mutex and Crash Recovery Cannot Rely Solely on Semaphore Counts
If a process dies after taking a semaphore token, POSIX semaphores do not automatically return it. If critical data structures require identifying that the lock holder has died, consider mutexes located in the shared region, simultaneously set with PTHREAD_PROCESS_SHARED and robust attributes, and design an EOWNERDEAD data repair path. See futex for mechanisms.
Repair may require operation logs, version states, or discarding the entire shared region. The lock becoming re-acquirable is only the first step of recovery, and does not mean that the length, indices, and data updated mid-way are already consistent.
SysV shared memory still has clear use cases and should not be uniformly excluded just because the interface is older. IPC_PRIVATE indicates creating a new object, not name-based access protection; permissions, attachment, and deletion behaviors should be handled separately.
File Locks: First Distinguish Ownership Models
Traditional fcntl record locks can apply cooperative locks to file byte ranges. F_SETLK does not wait, F_SETLKW can wait; errors must be checked against errno, and not all failures can be treated as "another process holds the lock."
Traditional record locks are associated with processes; closing other fds for the same file in this process may also release the lock on the file held by that process; OFD locks are associated with the open file description, and references after dup and fork will affect the final release. The two should not be collectively referred to as the same mechanism of "releasing when the current fd is closed". fcntl locks
File locks are usually advisory, and participants must jointly comply. Deleting the lock file and rebuilding may result in a new inode, causing old and new processes to lock different objects; daemon single-instance design needs to ensure everyone uses the same stable object.
Performance and Persistence
Shared memory reduces transmission copying, but cache line contention, false sharing, NUMA, and notification still have costs, and it is not guaranteed to be faster than pipes for small messages. It is suitable to first measure data scale, batching, read/write ratios, and backpressure requirements.
When mapped writes are correctly read by other processes is different from when they are persisted to media. File-backed mappings also need to consider msync, filesystems, and failure models; semaphore posts do not automatically make data power-fail safe.