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.