Virtual Memorymmapmemory-mapped filepage cachemap_sharedmap_private

Memory Mapping

mmap makes a file appear as a range of the address space — reads become page faults served from the page cache, writes dirty shared pages the kernel flushes later — which is why databases and high-performance systems love it and why its unpredictable stalls and lack of write control make some of them avoid it.

ConceptualUnix-styleLinuxWindows
▶ InteractiveInterview question
Progress

The problem

Reading a 10 GB file through read() copies every byte from the page cache into your buffer and costs a syscall per chunk. The page cache already holds the bytes in RAM, in pages, with addresses. Why not just point your address space at them?

A file that looks like an array

mmap(addr, len, PROT_READ, MAP_SHARED, fd, offset) creates a region in the address space whose pages are backed by the file: virtual page N of the region is byte range offset + N × 4096 of the file. No data is read at that moment; the kernel records the region (a VMA with the file and offset, The Virtual Address Space) and returns a pointer. The first access to each page faults (Page Faults); the handler finds the page in the page cache — reading it from disk if it is not there — and installs a PTE pointing at that page-cache frame directly. There is no copy: the process’s virtual page *is* the page cache page.

From then on the file is memory. p[1_000_000] is a load; memcpy over it is a sequential read with readahead; a struct overlaid on it is a parser with no parsing. munmap removes the region; msync forces dirty pages to disk; madvise tells the kernel the access pattern (MADV_SEQUENTIAL, MADV_RANDOM, MADV_WILLNEED to prefetch, MADV_DONTNEED to drop).

A load from a mapped file, first touch
  1. p[i] — a load from the mapped regionTLB miss, PTE not present
  2. Page fault → kernelthe VMA says: file F, offset i
  3. Page cache lookuphit → frame already holds the file page (minor fault)
  4. Miss → block layer → deviceread the page and readahead neighbours (major fault); process sleeps
  5. Map the page-cache frame into the processPTE = that frame; no copy, shared with every other mapper and with read()
  6. Resume the loadsubsequent accesses to this page: TLB hit, no kernel

The page cache does the work

Everything that makes mmap efficient is the page cache’s doing. Two processes mapping the same file map the same frames, so a shared library’s code, a database’s data files or a dataset used by ten workers exist in RAM once. read() on the same file copies out of the same frames, so mapped and unmapped access are coherent. Reclaim (Memory Pressure, Swap and the OOM Killer) treats mapped file pages like any file pages: clean ones can be dropped and refetched, which means a mapped 10 GB file on an 8 GB machine works — the kernel keeps the hot part resident and the process never manages a cache.

Writes distinguish the two mapping modes. With MAP_SHARED a store dirties the page-cache page; the kernel writes it back on its own schedule (writeback threads, dirty_expire_centisecs, or when dirty pages exceed dirty_ratio), and msync(MS_SYNC) forces it now — the mapped-file equivalent of fsync. Every other mapper sees the store immediately, because it is the same frame. With MAP_PRIVATE the first store to a page triggers copy-on-write (Copy-on-Write): the process gets a private anonymous copy, the file is never modified, and the copy consumes anonymous memory that only swap can evict. Executables and libraries are mapped private: code shared, data copied on first write.

  • MAP_SHARED: pages are the page cache; writes reach the file and all other mappers; msync = fsync.
  • MAP_PRIVATE: reads share the cache; the first write to a page copies it; the file never changes.
  • MAP_ANONYMOUS: no file at all — how allocators and thread stacks get memory (What Happens When I Allocate Memory?).
  • Coherent with read()/write() on Linux and most Unixes because both go through the same page cache.

Why databases and high-performance systems use it

The appeal is concrete. Zero read syscalls: after the first fault, accessing a page is a load, not a trap — a hash index over a mapped file does lookups at memory speed. Zero copies: read() moves bytes page cache → user buffer; mmap has no user buffer. Free caching: the kernel’s reclaim is the buffer manager, sized automatically to whatever RAM is not otherwise used, shared across processes. Simplicity: the storage engine is pointer arithmetic over one array. LMDB (behind OpenLDAP and many embedded uses) is built entirely on a read-only shared mapping with copy-on-write B-tree pages; SQLite offers it as an option; Kafka and many log-structured stores rely on the page cache and sendfile in the same spirit; MongoDB’s original MMAPv1 engine and early LevelDB used it for reads.

The same properties apply beyond databases: language runtimes map executables and shared objects (that *is* exec and dlopen), JITs map code caches, Go and Rust allocators obtain arenas by anonymous mmap, and the Zero-Copy: Serving a File Without Touching It and Memory Mapping, the Page Cache and Network I/O lessons show mapped buffers handed to the NIC without a copy.

Pitfalls

Conceptual

The costs are exactly the mirror of the benefits. Faults at unpredictable times: a "memory access" can become a 10 ms disk read, inside a critical section, while holding a lock, in a thread that thought it was doing arithmetic — and there is no error path, no timeout, no way to cancel; the thread is in D state. read() at least tells you where the I/O happens. No control over writeback: with MAP_SHARED the kernel may write a dirty page at any moment and in any order, so a database that needs "the log page before the data page" cannot rely on mapped writes and must msync in the right order or use write + fsync. Transactional safety therefore usually means the engine uses mmap for *reads* and explicit I/O for writes — the 2022 paper "Are You Sure You Want to Use MMAP in Your DBMS?" catalogues why several engines moved away from it, and PostgreSQL never used it for data.

Further sharp edges: SIGBUS if the file is truncated under the mapping; I/O errors surface as signals rather than return codes; a 32-bit process cannot map more than 2–3 GB at once and must window through a large file; every mmap/munmap is a VMA change and a TLB shootdown across all threads (The TLB); and the page-cache "buffer manager" has no idea which of your pages matter — the backup job that flushes it takes your working set with it. Huge mapped regions also carry page-table costs (Page Tables) that explicit buffers do not.

Windows exposes the same mechanism as two calls: CreateFileMapping makes a section object from a file (or the page file for anonymous shared memory), and MapViewOfFile maps a window of it into the address space; FlushViewOfFile is msync, and UnmapViewOfFile is munmap. The semantics — page cache backing, copy-on-write private views, coherence with ReadFile — match Unix closely, which is why cross-platform engines can offer mmap on both.

Key points

  • mmap maps file pages into the address space; the first touch of each page faults it in from the page cache (or disk), and thereafter it is memory.
  • Mapped pages are the page cache frames themselves: shared between processes, coherent with read()/write(), reclaimable under pressure.
  • MAP_SHARED writes reach the file when the kernel decides or when you msync; MAP_PRIVATE writes copy-on-write and never touch the file.
  • Benefits: no read syscalls after the first fault, no copies, a kernel-managed cache, code that is pointer arithmetic.
  • Costs: stalls at unpredictable places with no error path, no ordering control over writeback, SIGBUS on truncation, 32-bit address limits, TLB shootdowns on map/unmap.
  • Windows: CreateFileMapping + MapViewOfFile, same model.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why is mmap faster than read() for random access?

read() costs a trap and a copy per call. After the first fault a mapped page costs a load. For a hash index doing millions of small random reads, that is the difference between ~1 µs and ~1 ns per access.

Why does the mapped file survive the process using more memory than exists?

Because the pages are page cache, and the page cache is reclaimable. The kernel drops cold clean pages and refetches them on demand; the process never sees anything but slower accesses.

Why do serious databases avoid mmap for writes?

Durability needs ordering — the log before the data, both before the commit — and mmap writes back whenever it likes. Explicit write plus fsync gives the engine the order; mmap gives it a race.

Why can a memory access take 10 ms?

Because with mmap it may be a disk read in disguise. The transparency that makes it convenient is what makes its latency invisible until it is not.

Memory mapping

mmap: the file becomes part of your address space
1 MB file (16 pages shown, 4 kB each). Touching a mapped page pulls it through the page cache and wires the same frame into the process — one copy in RAM, no matter who maps it.
Page cache (kernel, shared by everyone)
Process A · virtual pages (MAP_SHARED)
Process B · also maps the file (MAP_SHARED)
How A accesses the file
Physical pages used
2
Copies of the data
one (page cache)
Click a virtual page to touch it.
Windows has the same idea under a different name: CreateFileMapping + MapViewOfFile, backed by the same unified cache manager.
Unix-style

How it fails

What the failure looks like from inside real software.

  • A request handler holding a mutex touches a cold mapped page: 10 ms in a major fault while every other thread queues on the lock; the profile shows time in filemap_fault.
  • A crash after "writing" to a MAP_SHARED region: the data was dirty in the page cache and never made it to disk; the missing msync was the bug.
  • SIGBUS during a read of a mapped log file that a rotation job truncated.
  • A 32-bit build of a tool fails to map a 4 GB file: mmap: Cannot allocate memory with 60 GB free — the address space, not RAM, is exhausted.
  • A multi-threaded service that maps and unmaps per request: system time dominated by TLB shootdowns; reusing a mapping fixed it.
  • A backup or cat of a large unrelated file evicts the mapped index of a database with no buffer pool of its own; queries become I/O-bound until it refaults.