How mmap Works: File-Backed Pages, MAP_PRIVATE, and Why Zero-Copy Matters
How mmap Works: File-Backed Pages, MAP_PRIVATE, and Why Zero-Copy Matters
A process that wants the contents of a 4 GB file does not have to issue a 4 GB read. It can ask the kernel to attach that file to a range of virtual addresses. After that, ordinary loads and stores become page faults, page-cache fills, and eventually bytes. That attachment is mmap.
The interesting part is not the function call by itself. It is the contract that follows: an address range is connected to a file or to anonymous memory, permissions control which CPU operations are legal, and the kernel supplies physical pages only as the process touches them. This article explains how mmap works as a mechanism, including file-backed versus anonymous mappings, MAP_SHARED versus MAP_PRIVATE, the page cache, dirty pages, msync, and munmap.
It is not a second tour of page tables, the syscall ABI, or write-ahead logging. For background, see how virtual memory works, how system calls work, and how write-ahead logging works.
1. The contract: an address range with rules
A typical call looks like this:
void *p = mmap(NULL, length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, offset);
On success, mmap returns the starting virtual address of a mapping. On failure it returns MAP_FAILED and sets errno. The returned pointer is not a buffer containing the whole file. It is the beginning of an address range whose pages can be populated later.
length specifies the mapped range in bytes. The file offset must be aligned to the system page size on Linux; the mapping address supplied through addr, when relevant, also has page-alignment constraints. Applications commonly pass NULL for addr and let the kernel choose a suitable aligned location. The length itself need not be a multiple of the page size, but the kernel operates in pages and the final page has page-granular behavior.
The protection flags describe permitted access: PROT_READ, PROT_WRITE, and PROT_EXEC, possibly combined. A mapping without PROT_READ cannot be read as ordinary data, even if its underlying file is readable. The kernel and hardware enforce these permissions through the memory-management machinery.
MAP_SHAREDmakes stores visible through other mappings of the same region and makes changes eligible to propagate to the underlying file.MAP_PRIVATEprovides a private, copy-on-write view. Reads can use file-backed pages, but a write creates a process-private page instead of changing the file.MAP_ANONYMOUScreates memory with no file backing. With the usual Linux form,fdis ignored andoffsetis zero.MAP_FIXEDrequests a specific address rather than a hint. It is dangerous because an overlapping mapping may be replaced; it should be used only when the address layout is deliberately controlled.
munmap removes a range from the process address space. mprotect changes permissions for a mapped range, subject to its alignment and platform rules. msync asks the kernel to synchronize file-backed changes according to flags such as MS_SYNC or MS_ASYNC. These operations are separate: changing a byte, making it visible to another mapping, and ensuring it reaches stable storage are different concerns.
2. File-backed pages and the page cache
Suppose a process maps a 4 GB file but immediately reads only the first 100 bytes. The kernel does not normally read all 4 GB at mapping time. The initial mmap establishes metadata describing the relationship between virtual pages and the file. The first access to an absent page triggers a page fault.
The fault handler checks the mapping, validates the access, and locates or obtains the corresponding file page. Linux commonly uses the page cache for this purpose. The kernel may read a page, or a group of pages through readahead, from storage into memory. It then makes that physical page available through the process's mapping and resumes the faulting instruction. From user code's perspective, an ordinary load eventually returns the requested byte.
The same cached file page can be associated with mappings in multiple processes. If two processes map the same file region with MAP_SHARED, they can see each other's stores through that shared page-cache state, subject to normal memory-ordering and synchronization requirements. This is not a promise that unsynchronized concurrent updates are safe: two writers can still race, overwrite fields, or observe partially coordinated application state.
For a writable shared mapping, a store usually marks the relevant page dirty. The kernel can later write the dirty page back to the file. msync gives an application a way to request synchronization for a range, but the exact durability guarantee also depends on the filesystem and storage stack. If an application needs a transactional record format, it still needs a protocol; a shared mapping does not replace checksums, locks, ordering, or a journal.
The file size matters at the edges. Access beyond the part of the file that can be backed by valid pages may generate SIGBUS, rather than a friendly end-of-file result. In particular, if another process truncates a file underneath an existing mapping, a later access to pages beyond the new end can deliver SIGBUS. A mapping is not an automatic promise that the file will remain at least that large forever.
3. MAP_PRIVATE and copy-on-write
MAP_PRIVATE is often misunderstood as “read-only.” It is not. A private mapping may have PROT_WRITE, but writes are isolated from the file and from other private mappings.
Initially, a private mapping can point at the same clean file-backed physical pages used for ordinary reads. On the first store to one of those pages, the processor faults because the shared page is not writable in this mapping. The kernel allocates a new physical page, copies the old contents into it, updates this process's mapping, and retries the store. From then on, this process reads its private copy. The original file and other readers still use the original contents.
This is copy-on-write, or COW. The copying happens at page granularity, not per byte. Writing one byte can therefore consume an entire page of private memory. Conversely, untouched pages remain efficiently shared.
COW is central to process creation and program loading. After fork, parent and child initially share physical pages while each receives a logically separate address space. A write causes only the affected page to split. Executable images and shared libraries also rely on mappings with carefully selected permissions and private writable data regions. A loader can map file contents, share clean code pages across processes, and keep relocations or mutable state private.
4. Anonymous mappings and the role of brk
An anonymous mapping has no file whose bytes must be read or written. It is a way to reserve and populate virtual memory for stacks, heaps, arenas, scratch buffers, and shared memory designs. With MAP_ANONYMOUS, newly used pages are initialized as zero rather than loaded from a file.
Traditional process heaps can grow with brk and sbrk, but allocators also use mmap for large allocations, independent arenas, guard regions, and memory that should be returned to the kernel without moving the main heap break. The choice depends on allocator policy and workload. Large anonymous ranges benefit from demand paging: reserving address space does not mean every physical page is immediately allocated.
Anonymous mappings can also be shared deliberately, for example by mapping shared anonymous memory before creating related processes. The important distinction is backing: file-backed mappings have file offsets and page-cache relationships, while anonymous mappings have no persistent file representation.
5. Worked example: shared bytes versus private bytes
Consider a file whose first byte is initially A. One process opens it read-write and maps one page:
int fd = open("data.bin", O_RDWR);
size_t n = 4096;
unsigned char *p = mmap(NULL, n,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'B';
if (msync(p, n, MS_SYNC) == -1) {
/* handle the synchronization failure */
}
munmap(p, n);
close(fd);
The store to p[0] changes the shared file-backed page in memory and marks it dirty. msync requests that the range be synchronized; with MS_SYNC, the call waits for the requested synchronization to complete or report an error. A second process that maps the same file region with MAP_SHARED can observe B, assuming it maps after the store or uses suitable synchronization.
Now change only the mapping flag:
unsigned char *q = mmap(NULL, n,
PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
q[0] = 'C';
msync(q, n, MS_SYNC);
The second process and the file still observe B, not C. The first store to q[0]
caused a private copy of its page. Calling msync on this private modification does not turn it into a file update. MAP_PRIVATE controls visibility and file propagation; it is not an alternate durability mode.
6. Cost model: faults, TLBs, and zero-copy trade-offs
Every mapped access is not a system call. Once a page is present and the translation is cached, a load or store can execute like other memory access. There is still translation overhead: the CPU's translation lookaside buffer, or TLB, caches virtual-to-physical translations. Walking a very large mapping can cause TLB misses, cache misses, and memory-bandwidth pressure.
The first access can be a minor fault when the data is already in memory or can be installed without waiting for storage. It can be a major fault when the kernel must wait for disk or another slow backing store. Readahead may fetch neighboring pages, but random access to a cold file can produce many expensive faults.
Why can a tight scan of a large file be faster with mmap than with read into a user buffer? The kernel can populate page-cache pages and expose those bytes directly in the process's mapped address space, avoiding an explicit second copy from a kernel read buffer into an application buffer. That is the practical “zero-copy” benefit here: fewer data copies along the path, not a magical absence of page faults or hardware movement.
It is not always faster. A sequential read can have excellent readahead and predictable I/O behavior. A mapped workload with sparse random access may fault repeatedly. A huge mapping can pressure address space, page tables, TLBs, and the page cache. Memory limits and overcommit policy can make anonymous or private COW pages fail later than the original reservation. A file truncated under a mapping can cause SIGBUS. Measure the access pattern rather than treating mmap as a universal replacement for read.
7. Databases and language runtimes
LMDB is a well-known example of a database design built around mapping database files and reading records through mapped addresses. Other engines use mappings selectively for indexes, immutable segments, or caches. Mapping can make a large read-only structure easy to address and can let the kernel reclaim clean pages under pressure. The database still needs a consistency model, versioning, locking, and a recovery strategy.
Language runtimes and native allocators commonly use anonymous mmap for large objects, arenas, stacks, and guard pages. A runtime can reserve a region, commit pages on demand, and release whole ranges with munmap. This is separate from mapping a persistent file, even though the same primitive supplies both.
MAP_SHARED with concurrent writers requires an application-level concurrency protocol. Use appropriate process-shared synchronization, atomics where their rules apply, record layout discipline, and crash-consistency techniques. A mapping does not make compound updates atomic, and msync does not make a group of writes a transaction. For a broader systems context, compare this with how Linux containers use namespaces: both rely on kernel isolation and resource mechanisms, but neither removes the need for an explicit correctness model.
8. Misconceptions to remove
- “mmap is always faster than read.” No. It can reduce copying and make random structures convenient, but cold random faults, TLB pressure, and memory contention can make it worse.
- “munmap immediately frees all RAM.” It removes the virtual mapping. Clean file pages may remain useful in the page cache, and other mappings may still reference them. Anonymous or dirty private pages have their own reclaim behavior.
- “MAP_PRIVATE writes are durable.” They are private modifications. They do not update the underlying file, even if
msyncis called. - “The mapping grows when the file grows.” The mapped length is the length requested. A later file extension does not automatically enlarge that address range; remapping or a new mapping is needed.
- “A mapped file is safe to access after truncation.” No. Access to invalidated portions can raise
SIGBUS, so file lifetime and size changes must be coordinated. - “Shared means synchronized.” Shared mappings share visibility, not a complete locking or transaction protocol. Concurrent writers still need coordination.
9. Takeaways
mmapattaches a page-granular virtual address range to a file or anonymous backing store; it does not eagerly copy the whole file.- First touches can fault pages in from the page cache, making ordinary loads and stores the interface to file data.
MAP_SHAREDallows shared visibility and file-backed writeback;MAP_PRIVATEuses copy-on-write and keeps stores out of the file.- Anonymous mappings supply demand-paged memory for allocators, stacks, arenas, and large objects without persistent file backing.
msync,munmap, andmprotectaddress synchronization, lifetime, and permissions respectively; none substitutes for a data-consistency protocol.- Zero-copy benefits come from avoiding an explicit extra copy into a user buffer, but page faults, TLB misses, I/O latency, and memory pressure remain real costs.
- Always account for page-aligned offsets, mapping length, concurrent modification, and
SIGBUSif the mapped file can be truncated.