Frontend Labs
⋯

57. Memory-mapped files: mmap for real files, msync, and shared mappings

You've now seen mmap twice: Section 39 used it anonymously to grab raw pages from the OS for a custom allocator; Part IX §49.2 used it on /dev/mem to reach hardware registers. Its most common real-world use is neither of those — mapping an actual file into memory, so reading it is just reading memory, with no explicit read() loop at all.

57.1 Reading a file via mmap instead of Section 12.2's fread loop

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int mmap_read_file(const char *path, const char **out_data, size_t *out_len) {
    *out_data = NULL;
    *out_len = 0;

    int fd = open(path, O_RDONLY);
    if (fd < 0) { perror("open"); return 0; }

    struct stat st;
    if (fstat(fd, &st) != 0) { perror("fstat"); close(fd); return 0; }
    if (st.st_size == 0) { close(fd); return 1; }   // empty file: success, no mapping

    void *map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);   // safe to close once mapped — the mapping itself keeps the pages available
    if (map == MAP_FAILED) { perror("mmap"); return 0; }

    *out_data = map;
    *out_len = (size_t)st.st_size;
    return 1;     // *out_data is NOT malloc'd — release it with munmap(), never free()
}

int main(void) {
    size_t len = 0;
    const char *data = NULL;
    if (!mmap_read_file("/etc/hosts", &data, &len)) return 1;
    if (!data) { puts("empty file"); return 0; }

    printf("mapped %zu bytes, first line: ", len);
    for (size_t i = 0; i < len && data[i] != '\n'; i++) putchar(data[i]);
    putchar('\n');

    munmap((void *)data, len);
    return 0;
}

There's no read() call at all — the kernel demand-pages the file's contents in as you touch them, and the OS's page cache backs the memory directly, so re-reading the same region a second time is often cheap. For a multi-gigabyte file you only need to scan once, this avoids both Section 12.2's explicit read loop and the extra copy into a user-space buffer that a read()-into-your-own-buffer approach performs.

57.2 Writing through a mapping, and why msync exists

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(void) {
    const char *path = "/tmp/mmap_demo.txt";
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) { perror("open"); return 1; }

    size_t len = 64;
    if (ftruncate(fd, (off_t)len) != 0) {   // must size the file before mapping it writable —
        perror("ftruncate"); close(fd); return 1;   // mmap doesn't grow the file for you
    }

    char *map = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (map == MAP_FAILED) { perror("mmap"); return 1; }

    strcpy(map, "written straight through the mapping, no write() call");
    msync(map, len, MS_SYNC);   // force this write to disk now, instead of waiting for
                                  // the kernel's own writeback timing to get around to it
    munmap(map, len);

    FILE *f = fopen(path, "r");   // prove it landed on disk, via an ordinary read this time
    char buf[64] = {0};
    fread(buf, 1, sizeof buf - 1, f);
    fclose(f);
    printf("read back: %s\n", buf);
    return 0;
}

MAP_SHARED (instead of MAP_PRIVATE) means writes go back to the underlying file and are visible to any other process that maps the same file MAP_SHARED — this is a legitimate, fast IPC mechanism between unrelated processes with no pipe or socket involved at all, each one just reading/writing the same mapped region.

57.3 The trap: accessing past what you mapped is SIGBUS, not a normal error

char *map = mmap(NULL, 100, PROT_READ, MAP_PRIVATE, fd, 0);
char last_byte = map[99];   // fine, if the file is at least 100 bytes
char oops      = map[500];  // if the file is shorter than 500 bytes: SIGBUS, not a clean error —
                              // there's no bounds check here any more than there was in §29.1

Unlike a read() past EOF (which just returns 0 bytes, cleanly), an out-of-bounds access on a mapped region is a hardware page fault the kernel turns into SIGBUS — Section 55's signal-handling material is exactly what you'd reach for to catch this defensively in, say, a library that maps files of a size it doesn't fully control.

Real-world example. This is how SQLite's optional mmap I/O mode works (Section 33), how git reads multi-gigabyte pack files without loading them wholesale into a buffer, and — closing a loop back to Section 40 — precisely the mechanism the dynamic linker (ld.so) uses to map a shared library's code and data segments into your process at program startup, before main() ever runs.


That's the full arc — from the toolchain in Part I to reading real allocator, kernel-module, and mmap internals here in Part X. What follows is reference material, not more reading: the C17→C23 feature diff, the UB cheat sheet, compiler flags, a glossary, and further reading — check back against these while you're actually writing C, rather than reading them front to back.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36