Why Map a File Instead of Reading It Through read()
The classic way to read a file is open(), then a read() loop copying data into a buffer. Some databases, linkers and other programs choose mmap(): they map a file into their address space and access it through memory. This is one I/O model, not a strategy shared by every database.
The difference isn’t cosmetic. read() performs a syscall and copies data from the page cache into your buffer — two copies of the same byte in RAM. mmap eliminates that copy: your pointer points directly at page cache pages, and the kernel loads them lazily (demand paging) only on actual access. For a 50 GB file from which you read random fragments, this is a fundamental difference.
This article dissects mmap: the mechanics of demand paging, the difference between MAP_SHARED and MAP_PRIVATE, the relationship with the page cache, and — equally important — when mmap is a trap: small files, random writes, the risk of SIGBUS on truncation.
What mmap Is — Mapping Instead of Copying
mmap (memory map) is a syscall that creates a new mapping in the process’s virtual address space. The mapping can point to a file (file-backed) or to anonymous memory (anonymous — exactly what the glibc allocator uses for large allocations). Once a file is mapped, memory access is translated by the MMU into access to the corresponding parts of the file.
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = open("/data/large.bin", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); close(fd); return 1; }
if (st.st_size <= 0) { close(fd); return 0; }
/* Map the entire file into the address space — no copying */
unsigned char *data = mmap(
NULL, // kernel chooses the address
st.st_size, // mapping size
PROT_READ, // read only
MAP_PRIVATE, // private copy-on-write
fd,
0 // offset in the file
);
if (data == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
close(fd); // fd can be closed — the mapping remains valid
/* Read the file like an ordinary array — kernel loads pages lazily */
printf("First byte: 0x%02x\n", data[0]);
if (st.st_size > 1024L * 1024 * 1024) printf("Byte at 1 GB: 0x%02x\n", data[1024L * 1024 * 1024]);
// A missing page-table mapping may fault; the data may already be cached.
munmap(data, st.st_size); // release the mapping
return 0;
}After mmap() you can close the file descriptor: the mapping holds its own reference. Later access may cause a page fault handled by the kernel. A fault is not necessarily a program error, as explained in the anatomy of a segfault.
Demand Paging — Lazy Loading
With an ordinary mapping and no flags requesting early population, mmap() establishes the address range and permissions. Pages and page-table entries may be populated on access. A fault can use existing cached data or require I/O; readahead may fetch more than the page being touched.
# See demand paging in action — minor vs major page faults
$ /usr/bin/time -v ./mmap_program 2>&1 | grep -i fault
Major (requiring I/O) page faults: 1247 # pages read from disk
Minor (reclaiming a frame) page faults: 89 # pages already in page cache
# Compare read/mmap syscall counts; strace does not count page faults
$ strace -c -e trace=read,mmap ./read_version 2>&1 | tail -5
$ strace -c -e trace=mmap ./mmap_version 2>&1 | tail -5Random access does not require reading the whole file. mmap() allows access to selected ranges; pread() reads at a specified offset without a separate lseek(). Actual I/O depends on the cache, readahead and access pattern.
MAP_SHARED vs MAP_PRIVATE — The Fundamental Difference
The mapping mode flag determines what happens on write and whether changes are visible to other processes. This distinction is the source of both power and subtle bugs.
| Aspect | MAP_SHARED | MAP_PRIVATE |
|---|---|---|
| Write reaches the file | Yes (via page cache) | No (copy-on-write) |
| Visibility to other processes | Yes — shared pages | No — private copy after write |
| Mechanism on write | Page marked dirty, writeback | CoW — page copy for the writer |
| Use case | IPC, shared memory, writing to a file | Loading code (.text), read-only data |
MAP_SHARED makes changes visible to processes mapping the same region. Access still needs synchronization, such as a process-shared mutex; visibility also does not guarantee durable storage. The futex article explains the waiting mechanism.
MAP_PRIVATE uses copy-on-write — the same mechanism that powers the fork() system call: as long as you only read, you share pages with the page cache. The first write to a page creates a private copy, visible only to your process. This is how each program’s .text segment is loaded — all instances of /bin/bash share the same code pages, as long as none of them modifies them.
Shared Memory Between Processes
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
/* Shared anonymous mapping */
#include <sys/mman.h>
#include <string.h>
int main(void) {
/* Anonymous MAP_SHARED — shared with child processes after fork() */
char *shared = mmap(
NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, // no file, pure memory
-1, 0
);
if (shared == MAP_FAILED) { perror("mmap"); return 1; }
pid_t pid = fork();
if (pid == -1) { perror("fork"); munmap(shared, 4096); return 1; }
if (pid == 0) {
/* The parent reads this message after waiting for the child. */
strcpy(shared, "message from child");
return 0;
}
wait(NULL);
printf("Parent reads: %s\n", shared); // sees the child's write
munmap(shared, 4096);
return 0;
}The Relationship with the Page Cache
Here mmap reveals its true nature. When you map a file-backed file, the mapping’s pages are page cache pages — the same cache Linux uses to buffer ordinary buffered file I/O. This is why mmap eliminates the copy: read() copies from the page cache into your buffer, while mmap maps the page cache directly into your address space.
If file data is already cached, attaching it to the mapping can avoid a device read. This mechanism is shared by read() and mmap(), as explained in Linux page cache. Private pages modified through CoW differ from the original file pages.
# Check how much of the file is in page cache (before and after mmap)
$ vmtouch /data/large.bin
Files: 1
Directories: 0
Resident Pages: 0/12800 0% # nothing cached
$ ./mmap_program /data/large.bin # maps and reads fragments
$ vmtouch /data/large.bin
Resident Pages: 3200/12800 25% # only touched pages in cache
# Control cache behavior via madvise in code:
# MADV_SEQUENTIAL — aggressive readahead (linear reading)
# MADV_RANDOM — disable readahead (random access)
# MADV_DONTNEED — discard mapping pages; does not guarantee page-cache eviction
# MADV_WILLNEED — prefetch (access announcement)madvise() is the key to mmap performance: you inform the kernel about the access pattern, and it adjusts readahead. For linear reading MADV_SEQUENTIAL enables aggressive prefetch; for random access MADV_RANDOM disables wasteful readahead.
When mmap Is a Trap
mmap isn’t universally better than read(). There are scenarios where it’s slower, riskier, or simply inappropriate.
| Scenario | Problem |
|---|---|
| Small files (< a few KB) | mmap/munmap + page fault overhead > cost of a simple read() |
| Sequential read once | read() with a buffer is often faster — simpler readahead |
| Random writes to a file | Dirty pages + unpredictable writeback, hard to control |
| File may be truncated | SIGBUS on access to a page past the new file end |
| Network filesystems (NFS) | mmap consistency semantics on NFS are problematic |
| Need to handle I/O errors | read() returns an error; mmap signals via SIGBUS/SIGSEGV |
The most dangerous trap is SIGBUS. If you map a file and then another process (or you) truncates it via truncate(), access to a page past the new file end generates SIGBUS — a signal that by default kills the process. Unlike read(), which would politely return an error or a shorter read, mmap has no way to “softly” report this problem at access time.
A global sigjmp_buf is not a general solution for safe mapped-file access, especially in a multithreaded program. Establish who can truncate the file and coordinate its lifetime. If you need errors returned by an ordinary call, consider pread().
mmap vs read() — Decision Matrix
| Scenario | Choice |
|---|---|
| Random access to a huge file (>100 MB) | Compare mmap and pread using the same data ranges |
| File read repeatedly by many processes | mmap — shared page cache |
| Shared memory IPC between processes | mmap (MAP_SHARED) with an agreed synchronization mechanism |
| Sequential read of the whole file once | read() — simpler, good readahead |
| Small configuration files | read() — mmap overhead doesn’t pay off |
| File of variable size / truncation risk | read() — no SIGBUS risk |
| Streaming, pipe, socket | read() — mmap requires a seekable fd |
| Critical I/O error handling | read() — errors as a return value |
| Very high I/O throughput | io_uring — measure batching and queue depth; there is no universal IOPS threshold |
Diagnostics — Tools
| Tool | Use |
|---|---|
cat /proc/<pid>/maps | All process mappings (file-backed and anonymous) |
cat /proc/<pid>/smaps | Per-mapping details: RSS, dirty, swap, shared/private |
pmap -x <pid> | Readable mapping summary with sizes |
vmtouch | How much of a file is in page cache; manual load/release |
/usr/bin/time -v | Program’s major vs minor page faults |
strace -e mmap,munmap,madvise | Actual mapping calls and hints |
perf stat -e page-faults | Page fault profile under load |
Conclusion: mmap as a Precise Tool
mmap isn’t a “faster read()” — it’s a different model of file access that eliminates copying by mapping the page cache directly into the process’s address space. For random access to huge files, sharing memory between processes, and files read repeatedly, it can be a suitable tool. For small files, pure streaming, and scenarios requiring hard I/O error handling — read() remains the right choice.
Choose based on the access pattern and error handling. An absence of read() calls says nothing by itself about device traffic or time spent handling page faults.
Next time you see a database mapping a multi-gigabyte file instead of reading it through read() — you now know it isn’t an accident, but a deliberate engineering decision grounded in demand paging and the shared page cache.
The repository includes a Python mmap-versus-read experiment. Its mm.read() calls and slices create bytes objects, so it does not measure access without copying. The random methods generate different offsets, and file creation warms the cache. Treat its results as observations about this script, not a verdict on the APIs.
Related articles
- malloc internals — Why free() Doesn’t Return Memory to the System — mmap for large anonymous allocations and allocator mechanics
- Anatomy of a Segfault — From MMU Through Kernel to gdb Core Dump — page faults and mappings in /proc/pid/maps
- epoll vs io_uring — When the Event Loop Isn’t Enough — alternative high-throughput I/O models
- What fork() in Linux Really Does Under the Hood — copy-on-write shared by MAP_PRIVATE and fork()
Technical references: mmap(2), madvise(2).


