What is memory mapping, and how does it relate to main memory in operating systems

What is memory mapping, and how does it relate to main memory in operating systems

If you’ve ever wondered how a program that’s only a few megabytes in size manages to work with files that are gigabytes large, or how an operating system loads a 500 MB application in a fraction of a second, the answer usually comes down to one core technique: memory mapping. It’s one of those operating system concepts that quietly powers almost everything — from how .so and .dll files get loaded, to how databases like SQLite and PostgreSQL achieve blazing performance, to how your web browser renders images without reading them byte by byte.

This article breaks memory mapping down from the ground up: what it is, why it exists, how it interacts with main memory (RAM), and how it’s implemented across Linux, Windows, Android, iOS, and UNIX systems.

What Is Main Memory, First?

Before diving into memory mapping, it helps to get main memory itself straight. Main memory — RAM — is the fast, volatile storage where a running program’s instructions and data live while the CPU is actively working with them. It sits above the CPU’s cache hierarchy (L1/L2/L3) and below secondary storage (SSD/HDD) in the classic memory hierarchy:

CPU Registers  →  L1/L2/L3 Cache  →  Main Memory (RAM)  →  Secondary Storage (Disk/SSD)
   fastest, smallest                                          slowest, largest

Main memory is organized as a flat array of addressable bytes. Every running process, in a modern OS, doesn’t see physical RAM directly — it sees a virtual address space, a private, contiguous-looking range of addresses that the OS and the CPU’s Memory Management Unit (MMU) translate into actual physical RAM locations using page tables.

This virtualization is what makes memory mapping possible.

What Is Memory Mapping?

Memory mapping is the technique of taking a resource — most commonly a file, but also a device or a shared memory segment — and projecting it directly into a process’s virtual address space. Once mapped, the process can access that resource simply by reading or writing to memory addresses, as if the file were a giant array in RAM, without explicit read() or write() system calls.

Think of it as building a bridge between secondary storage (or another resource) and a process’s address space, with the OS’s virtual memory system doing the translation work behind the scenes.

The Traditional I/O Model vs. Memory-Mapped I/O

Traditional file I/O:

  1. Process calls read().
  2. Kernel copies data from disk into a kernel buffer (page cache).
  3. Kernel copies data again from the kernel buffer into the user-space buffer.
  4. Process now has its own copy of the data.

That’s two copies and two context switches (user → kernel → user) for every chunk of data read.

Memory-mapped I/O:

  1. Process calls mmap() (or MapViewOfFile() on Windows).
  2. The OS updates the process’s page table to point at the file’s pages in the page cache — no data is copied yet.
  3. When the process touches a mapped address, a page fault occurs, and the kernel loads that specific page from disk into the page cache and maps it in.
  4. Subsequent accesses to that page are pure memory accesses — no system call overhead at all.

This lazy, on-demand loading (called demand paging) is why memory mapping is so efficient: you only pay the I/O cost for the parts of the file you actually touch.

How Memory Mapping Relates to Main Memory

The relationship is direct and mechanical:

  • Virtual memory bridges files and RAM. A memory-mapped region is a range of virtual addresses in the process’s address space. The physical backing for those addresses is a set of physical page frames in main memory, which the OS fills in on demand from the underlying file.
  • The page cache is the shared meeting point. On Linux, the same physical pages used for the page cache (which normally buffers file reads/writes) are the pages that get mapped into a process when it uses mmap(). This means multiple processes mapping the same file share the exact same physical RAM pages — no duplication.
  • Page tables do the translation. Each process has its own page table. When you access a mapped virtual address, the MMU walks the page table to find the physical frame. If the page isn’t resident (i.e., it hasn’t been loaded from disk yet, or it’s been evicted), a page fault traps into the kernel, which resolves it.
  • Main memory is finite, so mapping doesn’t mean “all in RAM.” Mapping a 10 GB file into a process’s 64-bit address space is trivial because address space is enormous (up to 128 TB or more usable on 64-bit systems) — but only the pages actually accessed get pulled into physical RAM, and the OS can evict them under memory pressure since they’re backed by the file itself (no need to write to swap first, unless the page is dirty and the underlying mapping is MAP_PRIVATE copy-on-write).

A Simple Diagram

Process Virtual Address Space          Physical Main Memory (RAM)
+------------------------+
| Code (.text)           |
| Data / Heap            |
| Stack                  |
| mmap'd region  ---------------->  [ Page Frame ] <-- backed by file on disk
|   (file-backed)        |          [ Page Frame ] <-- loaded on demand (page fault)
+------------------------+          [ Page Frame ] <-- shared with other processes
                                          |
                                          v
                                    Secondary Storage (the actual file)

Types of Memory Mapping

  1. File-backed mapping — maps a file’s contents. Changes can be written back to disk (MAP_SHARED) or kept private to the process (MAP_PRIVATE, copy-on-write).
  2. Anonymous mapping — not backed by any file; used for allocating raw memory (this is how malloc() gets large allocations under the hood via mmap(MAP_ANONYMOUS)).
  3. Shared memory mapping — multiple unrelated processes map the same physical memory region for fast inter-process communication (IPC), avoiding the overhead of pipes or sockets.
  4. Device memory mapping — maps hardware device registers or memory-mapped I/O (MMIO) regions into a process’s or the kernel’s address space, common in device drivers.

Memory Mapping in Practice

Linux/UNIX: mmap()

#include <sys/mman.h>
#include <fcntl.h>

int fd = open("largefile.dat", O_RDONLY);
size_t length = file_size; // obtained via fstat
void *addr = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);

// Now access addr[0], addr[1], ... just like an array
char first_byte = ((char *)addr)[0];

munmap(addr, length);
close(fd);

This is the exact mechanism used by the dynamic linker (ld.so) to load shared libraries (.so files), by databases like SQLite (via mmap mode) and LMDB, and by tools like grep and text editors for fast large-file scanning.

Windows: CreateFileMapping() / MapViewOfFile()

Windows uses a two-step API:

HANDLE hFile = CreateFile(L"largefile.dat", GENERIC_READ, ...);
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID pView = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

// pView now points to the mapped file contents

Windows also uses this mechanism internally for loading .dll and .exe images — the PE loader memory-maps the executable image rather than reading it whole into a buffer.

Android

Android, being Linux-based, uses the same mmap() syscall under the hood. It’s central to how the Zygote process model works: Android pre-loads common classes and resources into a process (Zygote) and then fork()s new app processes from it. Thanks to copy-on-write mapping, all forked app processes share the same physical pages for unchanged memory, dramatically reducing per-app memory footprint and app startup time. APK resources and .dex/.oat files are also frequently memory-mapped via ART (Android Runtime) for fast class loading.

iOS / macOS (XNU / Darwin)

iOS and macOS use mmap() as well (Darwin is UNIX-based), plus a higher-level vm_map API used internally for shared framework caching. Apple’s dyld shared cache — a single file bundling nearly all system frameworks — is memory-mapped once and shared read-only across every running process on the device, which is a major reason iOS apps launch quickly despite iOS devices having comparatively limited RAM.

Real-World Use Cases

Use CaseHow Memory Mapping Helps
Dynamic library loading (.so, .dll)Code pages are mapped and shared across all processes using the library; only touched pages are loaded
Databases (SQLite, LMDB, RocksDB)Large data files are accessed like in-memory arrays; OS handles caching and eviction
Large file processing (grep, log analyzers)Avoids double-buffering; OS page cache does the heavy lifting
Inter-process communicationShared memory mappings let processes exchange data at memory speed
Executable loadingOS loads a program’s code/data segments lazily as pages are touched, speeding up process startup
Copy-on-write process creation (fork())Parent and child share mapped pages until either writes, minimizing memory duplication

Benefits of Memory Mapping

  • Performance: Eliminates redundant copying between kernel and user buffers.
  • Lazy loading: Only the parts of a file actually accessed consume physical RAM.
  • Shared memory efficiency: Multiple processes can share one physical copy of a file’s data.
  • Simplified programming model: Access file data using ordinary pointers/array indexing instead of manual buffer management.
  • Automatic caching: The OS page cache manages eviction using its usual page replacement policy, so “hot” data naturally stays resident.

Challenges and Pitfalls

  • Address space fragmentation: On 32-bit systems, large mappings could exhaust the (comparatively tiny) 4 GB address space quickly — a major historical motivation for the move to 64-bit.
  • Page fault overhead: The first access to each page still costs a page fault and disk I/O; badly-patterned access (e.g., random access on spinning disks) can thrash performance.
  • Synchronization complexity: With MAP_SHARED mappings, concurrent writers must coordinate; there’s no built-in locking.
  • Error handling is unusual: Errors that would normally surface as a read() return code (e.g., disk I/O failure) instead surface as a SIGBUS signal (on UNIX-like systems) when you access an unavailable page — code that’s not expecting this can crash unexpectedly.
  • Dirty page writeback timing: Writes to a MAP_SHARED mapping aren’t necessarily flushed to disk immediately; you often need msync() to force persistence, which trips up developers expecting synchronous durability.

Troubleshooting Tips

  • “Out of memory” with mmap on 32-bit systems: Usually address space exhaustion, not physical RAM exhaustion. Check with cat /proc/self/maps on Linux to see fragmentation.
  • Unexpected SIGBUS crashes: Almost always means the underlying file shrank or an I/O error occurred after mapping — always check file size stability for mapped files that might be truncated by another process.
  • Changes not appearing on disk: Remember to call msync() (Linux/UNIX) or FlushViewOfFile() (Windows) if you need durability guarantees before the OS decides to write back dirty pages on its own schedule.
  • High memory usage reported by tools like top: Memory-mapped files often inflate “virtual memory” (VSZ) numbers dramatically since they represent address space reservation, not actual RAM use — check “resident set size” (RSS) instead for real physical memory consumption.

Best Practices

  1. Use MAP_PRIVATE for read-only or copy-on-write access patterns (e.g., loading executables) to avoid accidental shared writes.
  2. Use madvise() (Linux) to hint access patterns — MADV_SEQUENTIAL, MADV_RANDOM, or MADV_WILLNEED — so the kernel can optimize read-ahead.
  3. Always call msync() before relying on data being persisted for MAP_SHARED writable mappings.
  4. Unmap regions (munmap()) as soon as they’re no longer needed to free address space and allow the kernel to reclaim pages.
  5. Prefer memory mapping for large, mostly-read files; for small files accessed a handful of times, plain read()/write() can actually be faster due to page fault setup overhead.

Summary

Memory mapping is the mechanism that lets a process treat a file (or another resource) as if it were part of its own memory, by projecting it into the process’s virtual address space and letting the OS’s paging system fill in physical RAM pages on demand. It sits at the intersection of virtual memory and the file system, and it’s the quiet engine behind shared libraries, fast databases, efficient process creation, and large-file processing across Linux, Windows, Android, and iOS alike. Understanding it is essential to understanding how modern operating systems make the most of limited physical RAM while giving every process the illusion of a huge, private address space.

FAQs

Q: Is memory mapping the same as virtual memory? No. Virtual memory is the broader system that gives every process its own address space and handles translation to physical RAM. Memory mapping is a specific technique built on top of virtual memory that projects a file or resource into that address space.

Q: Does memory mapping load the entire file into RAM immediately? No. Pages are loaded on demand, only when accessed (demand paging), unless you explicitly request prefetching (e.g., MADV_WILLNEED).

Q: Can two processes share the same memory-mapped file? Yes — this is one of the most common uses of MAP_SHARED mappings, and it’s a fast form of inter-process communication since both processes see the same physical pages.

Q: What happens if I map a file larger than my available RAM? That’s fine, because only the touched portions get loaded into physical pages. Pages can also be evicted and reloaded as needed, similar to normal virtual memory management.

Q: Is memory-mapped I/O always faster than regular file I/O? Usually for large files or repeated access, yes — but for small, one-time reads, the overhead of setting up the mapping and handling page faults can make regular read() calls faster.

References

  • The Linux mmap(2) man page — man7.org/linux/man-pages
  • Microsoft Docs: File Mapping — learn.microsoft.com
  • “Operating System Concepts” by Silberschatz, Galvin, and Gagne (memory-mapped files chapter)
  • Android Open Source Project (AOSP) documentation on Zygote and ART
  • Apple Developer Documentation: dyld shared cache
Total
1
Shares

Leave a Reply

Previous Post
Explain the concept of big-endian and little-endian in the ARM architecture

Explain the concept of big-endian and little-endian in the ARM architecture

Next Post
Explain how file mapping contributes to efficient memory utilization

Explain how file mapping contributes to efficient memory utilization

Related Posts