mmap — When Memory-Mapped I/O Beats read() and write()

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 -5

Practical 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.

AspectMAP_SHAREDMAP_PRIVATE
Write reaches the fileYes (via page cache)No (copy-on-write)
Visibility to other processesYes — shared pagesNo — private copy after write
Mechanism on writePage marked dirty, writebackCoW — page copy for the writer
Use caseIPC, shared memory, writing to a fileLoading 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.

ScenarioProblem
Small files (< a few KB)mmap/munmap + page fault overhead > cost of a simple read()
Sequential read onceread() with a buffer is often faster — simpler readahead
Random writes to a fileDirty pages + unpredictable writeback, hard to control
File may be truncatedSIGBUS 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 errorsread() 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

ScenarioChoice
Random access to a huge file (>100 MB)mmap — demand paging, zero wasted I/O
File read repeatedly by many processesmmap — shared page cache
Shared memory IPC between processesmmap (MAP_SHARED) — fastest IPC
Sequential read of the whole file onceread() — simpler, good readahead
Small configuration filesread() — mmap overhead doesn’t pay off
File of variable size / truncation riskread() — no SIGBUS risk
Streaming, pipe, socketread() — mmap requires a seekable fd
Critical I/O error handlingread() — errors as a return value
Very high I/O throughput (>200k IOPS)io_uring — completion-based, bypasses both

Diagnostics — Tools

ToolUse
cat /proc/<pid>/mapsAll process mappings (file-backed and anonymous)
cat /proc/<pid>/smapsPer-mapping details: RSS, dirty, swap, shared/private
pmap -x <pid>Readable mapping summary with sizes
vmtouchHow much of a file is in page cache; manual load/release
/usr/bin/time -vProgram’s major vs minor page faults
strace -e mmap,munmap,madviseActual mapping calls and hints
perf stat -e page-faultsPage 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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top