AlgoMaster Logo

mmap and Memory-Mapped Files

31 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A service needs to search a 2 GiB index file. One approach repeatedly calls read() to copy file bytes into user-space buffers. Another approach asks the operating system to make the file appear inside the process's virtual address space.

After that mapping is established, the service can access file contents with ordinary loads:

The pointer is a virtual address. When the process first touches a nonresident part of the file, a page fault lets the kernel associate the virtual page with the corresponding file contents. Later accesses use normal memory instructions.

The Unix interface that creates this relationship is mmap(), short for memory map.

mmap() creates a virtual address range whose contents come from a file or an anonymous memory object.

Memory mapping unifies virtual memory and I/O. It does not load an entire file immediately, bypass the operating system's caches, or guarantee that a store is already durable on storage.

The mmap() Interface

On Linux and POSIX-style systems, the function has this form:

Its arguments describe six decisions:

A common read-only mapping is:

On success, mapping is the start of a page-aligned virtual region chosen by the kernel.

On failure:

The failure value is not NULL. Address zero is normally unavailable to applications, but the API specifically defines MAP_FAILED as the error result.

The initial mmap() call creates address-space metadata and page-table state needed to represent the range. It usually does not read every file page or allocate one physical frame for every mapped page.

Mappings Between Two Ranges

A file-backed mapping connects:

Suppose:

The relationship is:

The file does not need to appear at the same virtual address in another process. A second process can map the same file bytes at a different address:

Virtual addresses belong to each process. The file offset identifies the common backing content.

The mapping length is fixed when the mapping is created. Growing the file later does not automatically extend the process's virtual range. A larger virtual mapping requires a new or adjusted mapping operation.

On-Demand Residency of File Pages

Creating a file mapping establishes where file contents belong in the virtual address space. Physical residency is handled separately.

Suppose the process first reads an address in mapped file page 20:

If the file page is already resident in the operating system's file cache, the fault can be resolved without storage I/O and is normally minor.

If the contents must be read from storage, the fault is major and the thread waits for I/O.

The exact file-cache implementation belongs to the operating system, but one relationship is important here: ordinary buffered file I/O and memory-mapped I/O commonly interact with the same cached file contents. mmap() does not normally create a completely separate private file cache.

Mapping a 2 GiB file can therefore increase virtual size by 2 GiB while resident memory grows only for pages actually brought into memory.

Protection Flags

The protection argument describes what the process may do through the mapping.

Common flags are:

They can be combined:

The file descriptor's open mode and file permissions must support the requested mapping. A shared writable file mapping normally requires a descriptor opened for writing.

Protection is enforced at page granularity by the MMU. A mapping created with PROT_READ cannot be made writable by casting its pointer:

PROT_WRITE grants hardware permission to store through the mapping. Whether those stores are private or affect shared file contents depends on the mapping flags.

MAP_PRIVATE: Private Changes

MAP_PRIVATE creates a private mapping.

The process initially observes file contents directly, with the file page reaching its private virtual mapping.

If the process writes, the operating system preserves private-memory semantics using copy-on-write:

Example:

The store changes what this process sees through its private mapping. It is not a request to write 'X' to file offset zero.

Other processes mapping the file do not receive the private modification.

MAP_PRIVATE should not be treated as a guaranteed immutable snapshot of the file. External changes to the backing file and the behavior of still-unmodified private pages are subject to operating-system and API semantics. If an application requires a stable snapshot, it needs an explicit snapshot or consistency protocol.

MAP_SHARED: Shared Changes

MAP_SHARED creates a mapping whose modifications are associated with the shared backing object.

A store updates the shared cached file page:

  1. Process A performs a store.
  2. It lands in the shared file-backed physical page.
  3. Process B's mapping can observe the new bytes.
  4. The kernel eventually writes the dirty contents to the file.

Step 3 happens without any system call by either process, and step 4 happens on the kernel's schedule rather than at the moment of the store.

Two processes do not need the same virtual address:

MAP_SHARED provides shared bytes, not synchronization. If two processes update the same data concurrently without a lock, atomic protocol, or other coordination, they can create races and corrupt higher-level structure.

Visibility and durability are also different. Another process can observe a shared modification in memory before the kernel has written the dirty page to persistent storage.

MAP_PRIVATE and MAP_SHARED Compared

PropertyMAP_PRIVATEMAP_SHARED
Initial bytes come from fileYesYes
Writes visible through same process mappingYesYes
Writes visible to other shared mappersNo private changesYes
Writes update backing fileNoEventually, subject to synchronization
Typical write mechanismCopy-on-write private pageDirty shared file page
Interprocess synchronization requiredNot for private changesYes for shared mutable data

The flag expresses the mapping's modification semantics. It does not decide read, write, or execute permission; that is the role of PROT_*.

These combinations are therefore meaningful:

Loading simulation...

Anonymous Mappings

mmap() can create memory with no file providing its initial contents.

On Linux:

For an anonymous mapping:

The virtual pages normally receive physical frames on first access. The process sees zero-initialized memory and can write it like heap storage.

Allocators use anonymous mappings for large allocations and arenas. Thread stacks and runtime-managed heaps can also be built from anonymous virtual regions.

An anonymous mapping is still page-based virtual memory. It has permissions, can remain partly nonresident, and is removed with munmap() or process exit.

Anonymous MAP_SHARED mappings can be inherited across fork() and used for interprocess communication. They share memory without a regular file pathname, but cooperating processes still need synchronization.

Offset and Address Alignment

The file offset passed to mmap() must be aligned to the system's page-size requirements. On common Linux systems, it must be a multiple of the base page size.

Suppose:

Offset 10000 is not page-aligned.

Align downward:

The desired byte lies:

into the mapping.

To expose desired_length bytes beginning at file offset 10000:

The original mapping base and complete mapped length must be retained for munmap().

The returned address is page-aligned. A requested length does not generally need to be a page multiple, but the kernel manages the covered range in whole pages internally.

This can expose padding within the final mapped page. Code must still obey the actual file size and intended data length rather than treating every address up to the rounded page boundary as valid file data.

Mappings Beyond End of File

A writable mapping does not safely create file bytes beyond the file's current size merely because the virtual range exists.

For a new 4 KiB shared mapping, first make the file large enough:

If a process accesses a mapped page beyond the backing file's valid range, Linux commonly raises SIGBUS. A similar failure can occur when another process truncates a file underneath an existing mapping.

This creates a risk absent from an ordinary private heap pointer:

Applications that map mutable files need a protocol preventing unsafe truncation while readers or writers hold mappings.

Growing the file also does not grow an existing virtual mapping. The file size and mapping length are related but independently managed.

Mapping Lifetime and File Descriptors

After a successful mmap(), the process can close the file descriptor without destroying the mapping:

The kernel retains the backing-object reference needed by the mapping.

The mapping remains until:

  • The process calls munmap() for the range
  • A replacement mapping removes or overwrites it
  • The process exits
  • exec() replaces the process address space

Closing the descriptor affects future descriptor operations, not the already-created mapping.

The reverse is also true. Calling munmap() removes the virtual range but does not close the original descriptor if it remains open.

These are separate resources:

Unmapping with munmap()

Remove a mapping with:

The address must identify the mapping at a suitable page boundary. The range can cover an entire mapping or a page-aligned subrange, depending on system rules.

After successful unmapping:

munmap() invalidates the process's virtual access. It does not necessarily erase cached file contents from physical memory.

Partial unmapping can split one virtual region into two:

Frequent mapping and unmapping has costs: kernel metadata updates, page-table work, and TLB invalidation. Mapping every tiny request independently is rarely efficient.

Visibility vs. Durability

A store through MAP_SHARED first changes a memory-resident page:

Another process mapping the same page can observe the changed bytes before storage write-back completes.

The msync() interface asks the kernel to synchronize a mapped range:

Common modes include:

On Linux, the starting address passed to msync() must be page-aligned.

For a shared writable mapping:

msync() is not a mutex and does not make a multi-field update atomic. It addresses synchronization with backing storage, not coordination between concurrent writers.

Durability has more layers than copying dirty bytes from memory toward a file. Filesystem metadata, file size, storage-device caches, and the ordering of related updates can matter after a crash. A correct persistent format needs an explicit durability and recovery protocol rather than assuming every memory store is instantly durable.

mmap() Versus read()

Both interfaces can access the same file contents, but they present different programming models.

Buffered read()

The application controls explicit I/O boundaries and receives errors at system-call sites.

Memory-mapped access

The application avoids an explicit copy from the kernel's cached file page into a separate user buffer for each read. It can navigate file structures with pointer-like indexing.

This does not mean mmap() is always faster.

mmap() can be attractive for:

  • Random access to large files
  • Repeated access to the same pages
  • Sharing read-only file pages across processes
  • Data structures naturally addressed by offsets
  • Workloads that benefit from demand paging

read() can be attractive for:

  • Streaming data once in large sequential buffers
  • Explicit control over I/O and buffer lifetime
  • Straightforward handling of short reads and errors
  • Avoiding page faults in latency-sensitive code
  • Processing files larger than a constrained virtual address space

Performance depends on access pattern, page-fault cost, copying cost, storage behavior, read-ahead, cache pressure, page-table overhead, and concurrency.

Measure the actual workload rather than choosing mmap() solely because it removes explicit read() calls.

mmap() and the Page Cache

Memory mapping is sometimes confused with direct I/O.

A typical file-backed mapping uses the operating system's cached file pages:

The process reads from a physical page that represents file content. If ordinary buffered I/O later accesses the same file range, the kernel can use the same coherent cached data rather than maintaining unrelated copies.

This has useful consequences:

  • Multiple processes can share one resident read-only file page.
  • A mapped read can benefit from data brought in by an earlier read().
  • A read() can observe changes made through a coherent shared mapping.
  • Dirty mapped pages compete for memory and write-back resources like other cached file data.

It also means a giant mapping is not free merely because pages load lazily. Touching the entire mapping can populate large amounts of the file in memory and displace other useful cached or anonymous pages.

A Runnable Shared-Mapping Example

This Linux program creates a one-page file, maps it with MAP_SHARED, writes text through the mapping, synchronizes it, and unmaps it.

Compile and run:

The file is one page long because ftruncate() established its size before mapping. The message occupies the beginning, and the remaining bytes are zeros.

The example deliberately keeps the descriptor open until cleanup, although it could close it after successful mmap() and retain the mapping.

Safe Use of the Address Argument

Passing NULL as the first argument lets the kernel choose a suitable virtual address:

This is the normal choice.

A non-null address without fixed-placement flags is generally a hint. The kernel can choose another suitable location.

MAP_FIXED requests exact placement and can replace existing mappings that overlap the requested range. Used incorrectly, it can destroy a stack, library, heap, or other live region and cause immediate or delayed corruption.

Exact placement is needed only for specialized low-level software. General application code should let the operating system select the address.

A pointer stored before mmap() cannot predict where the kernel will place the new region. Always use the returned pointer.

Failure Modes

mmap() introduces failures at both setup time and access time.

Setup-time failure

The call returns MAP_FAILED for conditions such as:

  • Invalid length, flags, permissions, or offset alignment
  • File descriptor incompatible with the requested mapping
  • Address-space limits
  • Resource exhaustion
  • Unsupported mapping combination

Inspect errno after failure.

Access-time failure

The call can succeed, yet a later access can fail:

  • Page contents cannot be obtained because of an I/O error
  • Another process truncated the backing file
  • The access reaches a page beyond the valid file range
  • The process violates mapping permissions
  • The virtual range was unmapped concurrently or prematurely

Such failures commonly arrive as signals such as SIGBUS or SIGSEGV, not as a return value from the source-level load or store.

This changes error handling. A read() has an explicit error-return point. A mapped load looks ordinary in source code even though it can trigger I/O and a signal.

Applications using memory-mapped persistent data need file-lifetime rules, truncation coordination, bounds checks, and an error strategy appropriate to this asynchronous-looking fault path.

Mapping Costs

mmap() avoids some copying and can exploit demand paging, but it adds other costs:

  • Kernel virtual-region creation and removal
  • Page-table memory for resident mappings
  • Page faults on first access
  • TLB entries and misses across large ranges
  • TLB invalidation during unmapping or protection changes
  • Storage I/O hidden behind memory accesses
  • Dirty-page write-back for shared writable mappings

Mapping a very small file for one quick sequential read can cost more than one read() call.

Mapping a huge sparse file consumes virtual address space cheaply, but touching pages across it can create high page-table, TLB, fault, and cache costs.

Frequent mmap() and munmap() calls from many threads can also create address-space lock contention and multicore translation shootdowns.

The API changes where costs occur:

Neither cost model is universally smaller.

Summary

mmap() creates a virtual address range backed by a file or anonymous memory. File-backed pages are normally supplied on demand through the operating system's cached file contents, allowing ordinary loads and stores to act as I/O operations.

MAP_PRIVATE gives the process private copy-on-write modifications, while MAP_SHARED associates writes with shared file-backed pages visible to other mappers. Protection flags independently control reading, writing, and execution. Offsets must be page-aligned, files must be sized before mapped writes beyond their current end, and mappings remain valid independently of the original file descriptor.

Memory visibility is not storage durability: shared stores dirty cached pages, and msync() requests synchronization with the backing file but does not replace concurrency or crash-consistency protocols. mmap() can avoid explicit buffer copies and support efficient random access, yet page faults, TLB pressure, mapping updates, write-back, and access-time signals mean it is not universally faster or simpler than read().

Quiz

mmap and Memory-Mapped Files Quiz

5 quizzes