A writable MAP_SHARED mapping lets a process modify file-backed state with ordinary memory stores. The bytes are addressed through virtual memory rather than passed to write(), but the mapping still participates in filesystem state: modifications can become visible through other shared mappings and file I/O, and dirty pages can later be written back to storage.
That interface compresses several mechanisms into one address range. CPU stores, page faults, page-cache residency, filesystem writeback, and storage persistence can all participate in the lifetime of the same bytes. Treating a successful store as equivalent to durable file output collapses boundaries that the operating system keeps distinct.
The mapping establishes a relationship, not a private copy
For a regular file, mmap() associates a virtual address interval with a file region. With MAP_SHARED, updates to the mapping are carried through to the underlying file according to the platform’s mapping and writeback semantics. This differs from MAP_PRIVATE, where modifications are private to the mapping and do not become file updates through that mapping.
The file offset supplied to mmap() must satisfy the platform’s alignment requirements. Accesses within the mapped range are then translated through virtual-memory mappings. A page may not be resident when first touched; the kernel can fault it in and establish the relevant page-table entry before the instruction completes.
The important boundary is that the application receives an address, not ownership of detached bytes. A store such as:
mapped[4096] = 0x7f;can dirty file-backed memory. No write() call is required for that particular modification. The absence of a write syscall at the modification site does not make the operation independent of the filesystem.
Visibility and persistence are separate properties
A modified shared mapping can be observable before the changed data is durably stored. On Linux, shared file mappings normally interact with the page cache, so mapped access and buffered file I/O can refer to the same cached file data. That coherence is an operating-system property, not a general rule for every mapping facility on every platform.
Suppose one process changes a byte through a writable shared mapping while another process reads the same file region. The second process can observe the modified cached state even though storage has not yet completed writeback. Visibility to a concurrent observer therefore does not prove crash persistence.
The reverse distinction matters as well. Persistence operations have scope and preconditions. msync() operates on mapped pages in an address range, while fsync() operates through a file descriptor and has filesystem-defined effects on file data and metadata. Neither API turns arbitrary surrounding application state into one atomic transaction.
This creates at least three useful states to distinguish:
store issued
|
mapped state modified
|
change observable through coherent file state
|
dirty data written back
|
required storage persistence boundary completedThe exact transitions depend on the operating system, filesystem, hardware, flags, and error outcomes. They are not one indivisible event.
msync controls a mapping range
On Linux, msync() requests synchronization for pages covering a specified mapped address range. MS_SYNC requests synchronous completion of the update operation; MS_ASYNC requests asynchronous scheduling. The address must satisfy the required page alignment, and the range must refer to mapped memory as required by the API.
That range-oriented contract is significant. A process may map a large file but synchronize only a subset:
if (msync(mapped + page_offset, page_len, MS_SYNC) == -1) {
/* handle synchronization failure */
}This code is valid only when mapped + page_offset meets the platform’s alignment rule and the range is appropriate for the mapping. The call concerns the mapped range, not every byte of every file reachable by the process.
Linux has implementation behavior that can make some msync() uses appear unnecessary for ordinary shared mappings because dirty mapped pages are tracked for writeback and the page cache is coherent with file I/O. That observation must not be promoted into a portable language or POSIX guarantee. Code requiring a defined synchronization boundary should express that boundary with the documented API and handle errors.
File size remains an external constraint
A mapping has a virtual length, but the backing file has its own size. Mapping pages does not generally extend a regular file merely because the requested virtual range is larger. Access to pages corresponding wholly beyond the end of the file can raise a hardware-backed fault delivered to the process, commonly SIGBUS on Linux for this case.
This makes file truncation a concurrency hazard. A process can hold a mapping that was valid when created, while another process truncates the file. The virtual mapping can remain present even though part of its backing file region no longer exists. A later access to the affected pages can fail.
The race is not repaired by checking file size before every access. A separate actor can truncate after the check. Applications that permit concurrent resizing need a synchronization or ownership contract covering both file size and mapped access.
Growing a file also requires an explicit file-size operation such as ftruncate() before relying on newly backed regions. The mapping and the file’s logical length are related, but they are not the same object.
Dirty mapped pages are resource state
Memory mapping can make file modification look like ordinary in-process mutation, which can hide the resource lifetime involved. Dirty pages consume kernel-managed state and are subject to writeback policy. The process may continue executing while writeback happens later, and writeback can encounter errors that were not present at the original CPU store.
This temporal separation changes error handling. A plain assignment has no return value for a later storage failure. Software that requires a persistence boundary needs an operation capable of reporting synchronization errors and must decide what those errors mean for the higher-level state machine.
Unmapping with munmap() removes the process’s virtual mapping. It should not be treated as a substitute for an explicit durability operation when the application contract requires synchronized persistence. Likewise, process exit ending the address-space lifetime is not a useful durability protocol.
The mapping lifetime, dirty-page lifetime, and storage lifetime overlap, but none is a complete proxy for the others.
Shared mappings do not create application-level transactions
Two fields updated through the same mapping are still separate memory operations unless a stronger mechanism groups them. Another observer can potentially see a mixed logical state if the application’s synchronization contract permits concurrent access between the stores.
For example:
record->payload = new_payload;
record->generation = new_generation;The source-level sequence does not by itself provide an atomic record update across threads, processes, crashes, or storage. Compiler rules, CPU memory ordering, synchronization primitives, cache coherence, filesystem writeback, and crash consistency are separate layers.
A mutex shared between participating processes can coordinate live access when correctly initialized and used for interprocess synchronization. It does not automatically make a multi-field update crash-atomic. A crash-consistent format may instead use techniques such as versioned records, copy-on-write structures, append records, checksums, or explicit commit markers, each with its own ordering and persistence requirements.
The mapped representation therefore needs the same care as any other shared mutable format. Convenient addressing does not add transactional semantics.
Concurrent access needs a memory synchronization contract
When multiple threads or processes access the same mapped bytes concurrently, file mapping answers where the bytes come from; it does not settle data-race semantics for the programming language.
In C and C++, concurrent non-atomic accesses can be constrained by the language memory model when threads in one program are involved. Interprocess synchronization adds platform-specific requirements because language abstractions do not automatically cover every process-shared primitive. Atomic types, process-shared mutexes, futex-based protocols, or other documented mechanisms may be appropriate depending on the representation and participants.
A file lock is also a different mechanism. Advisory locks coordinate cooperating processes through a locking convention, but they do not generally transform arbitrary mapped loads and stores into language-level atomic operations. The lock protocol must be observed by every participant that relies on it.
The interface boundary is therefore two-dimensional: the mapping establishes file-backed memory, while a separate synchronization design establishes safe concurrent mutation.
Persistence protocols must name their boundary
A robust file-backed mapping design states what event counts as publication and what event counts as persistence. Those events may differ.
A process can publish a new in-memory version to peers after updating mapped state and releasing a synchronization primitive. It can then perform a persistence operation before acknowledging a durable commit to an external caller. Another design may keep updates private elsewhere, copy a completed image into a shared mapping, synchronize the relevant range, and only then advance a durable generation marker.
Each protocol has different failure windows. The critical property is that the software does not infer durability from visibility or infer atomicity from addressability.
MAP_SHARED is powerful precisely because it connects memory access to file-backed state with little ceremony at each load or store. That same connection removes the syscall boundary that often reminds code that I/O is occurring. Correct designs restore the missing boundaries explicitly: mapped-range validity, concurrency ownership, publication ordering, synchronization errors, and the storage event required by the application’s persistence contract.