Why Databases Don’t Read Files Through read()
The classic way to read a file is open(), then a read() loop copying data into a userspace buffer. It works, it’s predictable, and for most applications it’s sufficient. Yet databases, JVM runtimes, linkers, and every system operating on huge files do something different: they map the file directly into the address space via mmap, then read it like an ordinary array in memory.
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;
fstat(fd, &st); // we need the file size
/* Map the entire file into the address space — no copying */
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"); 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]);
printf("Byte at 1 GB: 0x%02x\n", data[1024L * 1024 * 1024]);
// Each access to an unread page → page fault → kernel loads from disk
munmap(data, st.st_size); // release the mapping
return 0;
}Key observation: after mmap you can close the file descriptor — the mapping holds its own reference. Accessing data[0] or data[1GB] looks like ordinary array indexing, but underneath it triggers the page fault machinery we described in the anatomy of a segfault — except here the fault is legal and the kernel handles it, loading the appropriate page from disk.
Demand Paging — Lazy Loading
This is the core of mmap’s advantage. mmap by itself reads nothing from disk — it only creates the mapping and fills the page table with entries marked “not present.” Only when the program actually touches a specific address does the MMU generate a page fault, and the kernel loads that single page (4 KB) from disk.
# 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 page fault counts for read() vs mmap on the same file
$ strace -c -e trace=read,mmap ./read_version 2>&1 | tail -5
$ strace -c -e trace=mmap ./mmap_version 2>&1 | tail -5Practical consequence: if you map a 50 GB file but read only 100 MB scattered randomly — the kernel loads only those ~100 MB from disk, page by page, on demand. read() would require either loading the whole thing or complex lseek() + read() logic for each fragment. This is why random access to huge files is the flagship mmap use case.
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 pages shared: multiple processes mapping the same file see the same physical RAM pages, and one process’s write is immediately visible to the others. This is the basis of shared memory IPC — the fastest form of inter-process communication, because it eliminates any copying.
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
/* Process A — writes to a 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 (fork() == 0) {
/* Child sees the parent's writes — the same physical pages */
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 all file operations. 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.
Practical consequence: if the file is already in the page cache (because someone read it recently), mmap + access is pure minor page faults — zero disk I/O, just mapping existing pages. This same property makes mmap excellent for files read repeatedly by different processes: everyone shares one copy in the page cache.
# 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 — release pages from cache (after processing)
# 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.
/* Handling SIGBUS when truncation is a risk — an advanced pattern */
#include <signal.h>
#include <setjmp.h>
static sigjmp_buf jump_buffer;
void sigbus_handler(int sig) {
siglongjmp(jump_buffer, 1); // jump out of the access that caused SIGBUS
}
int safe_access(char *mapped_data, size_t offset) {
signal(SIGBUS, sigbus_handler);
if (sigsetjmp(jump_buffer, 1) == 0) {
volatile char c = mapped_data[offset]; // may raise SIGBUS
return c;
} else {
/* We land here after SIGBUS — the file was truncated */
fprintf(stderr, "Access past file end (truncation)\n");
return -1;
}
}mmap vs read() — Decision Matrix
| Scenario | Choice |
|---|---|
| Random access to a huge file (>100 MB) | mmap — demand paging, zero wasted I/O |
| File read repeatedly by many processes | mmap — shared page cache |
| Shared memory IPC between processes | mmap (MAP_SHARED) — fastest IPC |
| 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 (>200k IOPS) | io_uring — completion-based, bypasses both |
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’s a fundamentally better tool. For small files, pure streaming, and scenarios requiring hard I/O error handling — read() remains the right choice.
The key understandings: demand paging loads pages lazily on access, not on mmap; MAP_SHARED shares pages and writes to the file, MAP_PRIVATE uses copy-on-write; the pages of a file-backed mapping are page cache pages; and SIGBUS on truncation is a real production risk. This knowledge completes the picture of memory management — from malloc through page faults to direct file access.
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.
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()


