JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

mmap and Memory-Mapped Files

Every mechanism this rung built — page tables, demand paging, page faults, copy-on-write, per-page protection — comes together in one elegant system call. mmap() lets you reach a file by simply dereferencing a pointer, no read() loop in sight, and the kernel pulls in pages only as you touch them.

A file you can point at

Up to now, when an earlier rung taught you to touch a file, the verbs were always the same: open() to get a descriptor, then a read() loop to haul bytes into a buffer you allocated, then close(). The data lives on disk, you copy it into your own memory, you work on the copy, and if you change it you copy it back with write(). That copying is the whole shape of ordinary file I/O. The file and your memory are two separate worlds, and read() and write() are the ferries between them. mmap() proposes something startling: what if the file were memory? What if you could reach byte 5000 of a file by writing `p[5000]`, the way you reach element 5000 of an array — no read(), no buffer, no ferry?

That is exactly what a memory mapping is. You ask the kernel to take a file (or a fresh blank region) and weave it into your process's address space, so that a contiguous range of virtual addresses now stands for the contents of the file. mmap() hands you back a pointer to the first byte of that range. From then on the file is reachable through plain pointer arithmetic and dereferencing: `*p` is the file's first byte, `p[100]` is its byte at offset 100, and assigning `p[100] = 'X'` changes the file. The read() loop has not become faster — it has vanished. The mapping is the most natural payoff of everything this rung built, because a virtual address that resolves to file data instead of anonymous memory is still just a virtual address, translated the same way through the same page tables.

The call, argument by argument

mmap() looks busy at first — six arguments — but each one answers a plain question, and most of the time you set the same defaults. The signature is `void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)`. addr is a hint for where in your address space to place the mapping; pass NULL and let the kernel choose, which is what you almost always want. length is how many bytes to map. prot is the protection you want on those pages — PROT_READ, PROT_WRITE, PROT_EXEC, or PROT_NONE — OR'd together exactly like the open flags you saw in the file rung. fd and offset name the file and the byte within it where the mapping begins; offset must be a multiple of the page size. And flags decides the single most important thing about the mapping, which the next section is entirely about.

Two honest details before code. First, the protection you choose is the same per-page protection the MMU enforces on every access — ask for a read-only mapping with PROT_READ alone, then store into it, and you earn the protection-violation page fault that surfaces to your program as a segmentation fault, exactly the SIGSEGV you met when a null pointer or wild pointer wandered into a forbidden page. The protection is not advisory; the hardware checks it on the address translation of every single access. Second, mmap() reports failure not with NULL but with the special value MAP_FAILED (which is (void *)-1), so the check is `if (p == MAP_FAILED)`, not a comparison against NULL — a small trap worth remembering.

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

int fd = open("data.bin", O_RDONLY);
if (fd == -1) { perror("open"); return 1; }

struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); return 1; }
size_t len = st.st_size;                 /* map the whole file */

/* read-only, shared with the file, starting at offset 0 */
char *p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) { perror("mmap"); return 1; }

close(fd);                  /* the mapping stays valid after close */

char first = p[0];          /* touching p[0] triggers a page fault */
char mid   = p[len / 2];    /* and so does this, on a different page */

munmap(p, len);             /* tear the mapping down when done */
A whole file mapped read-only. fstat() gives the length; the fd may be closed once the mapping exists; munmap() releases it. The first touch of any page is what actually pulls its bytes off disk.

Shared, private, and copy-on-write

The flags argument carries the decision that defines what kind of mapping you have, and it almost always reduces to one of two words. MAP_SHARED means your writes go through to the file and become visible to every other process that has the same file mapped — the mapped pages and the file are the same data, so storing `p[10] = 'Z'` eventually lands that byte on disk and in everyone else's view. This is how mmap() becomes a tool for shared memory: two processes that both MAP_SHARED the same file are looking at one set of physical pages, and a write by one is instantly a read by the other, with no copying and no system call per byte. MAP_PRIVATE, by contrast, gives you a private view: you can read the file's contents, but any write you make is yours alone and never touches the file or anyone else.

Now look closely at what MAP_PRIVATE has to promise and you will recognise an old friend from guide 4. The kernel must let many processes share one read-only copy of the file's pages to save memory, yet guarantee that the instant you write to a page, your change stays private. That is precisely copy-on-write. Until you write, your private mapping points at the very same physical pages the file's read-only data occupies, shared and marked read-only in your page table. Your first store to such a page traps as a write-protection fault; the kernel quietly makes a private copy of just that one page, repoints your page-table entry at the copy, marks it writable, and lets your store complete. You write to the copy; the original file page is untouched. fork() reuses this exact trick for a whole address space — mmap() reuses it for one file.

Why it is fast: demand paging does the work

The first time you walk through a mapped file, watch what the machinery actually does, because it is guide 3 in action. mmap() returned instantly and read nothing; your page table now has entries for the whole mapped range, but they are marked not-present. The moment you dereference a pointer into a page you have not touched, the MMU finds the not-present entry and raises a page fault. The kernel catches it, sees the address falls in a file mapping, reads exactly that one page of the file off disk into a free physical frame, fixes the page-table entry to point at that frame and mark it present, and resumes your instruction. Your `*p` completes as if nothing happened. This is demand paging in its purest form: pages of the file are loaded only when, and only if, you actually touch them.

This is why mapping a 2 GiB file and reading one struct out of the middle of it is nearly free. A read()-based program would have to seek and copy at least a block; the mmap() version touches one page, takes one page fault, and is done — the other 2 GiB of file pages are never brought in because you never pointed at them. And once a page is in, the translation for it can live in the TLB, so the second access to the same page costs nothing extra at all — no fault, no kernel, just a hardware-fast translation. The cost model flips: with read() you pay a system call and a copy up front for everything you might use; with mmap() you pay a single page fault, lazily, only for the pages you truly touch.

Anonymous mappings, durability, and the sharp edges

Not every mapping is backed by a file. Pass MAP_ANONYMOUS in the flags (and fd = -1) and mmap() gives you a region backed by nothing but zero-filled pages — a fresh, blank block of memory. This is, quietly, where a lot of your memory already comes from: a large malloc() often does not extend the heap with sbrk() at all but instead asks mmap() for an anonymous region, and the C library hands you a slice of it. An anonymous MAP_SHARED mapping made before a fork() is also a clean way to share memory between a parent and child, since the shared pages survive into the child where a MAP_PRIVATE copy-on-write region would split. The same machine handles both file-backed and anonymous memory; the only difference is what a fresh page is filled from — a disk block, or zeros.

Now the honest hazards, because mmap() trades the awkwardness of read() loops for a different set of foot-guns. First, durability is not automatic. When you store into a MAP_SHARED page, you have changed a page in memory; the kernel writes it back to disk on its own schedule, which may be much later, and a crash in between can lose those writes. If you need the change on disk now — a database commit, a journal entry — you call msync(p, len, MS_SYNC) to force the dirty pages out, the mmap() analogue of the fsync() you would use after write(). Second, the mapping has a fixed size frozen at mmap() time; you cannot grow a file by storing past its end through the mapping, and touching mapped pages that lie beyond the file's actual length earns a SIGBUS, a cousin of SIGSEGV that specifically means "this mapped address has no backing."

  1. open() the file and check for -1, then fstat() it to learn its exact length — a mapping must not run past the file's real size.
  2. Call mmap() with NULL addr, the length, the prot bits you need, and MAP_SHARED or MAP_PRIVATE; check the result against MAP_FAILED, not NULL.
  3. You may close() the file descriptor now — the mapping keeps the file alive on its own, independent of the fd.
  4. Use the region as plain memory: read with *p / p[i], and for a writable shared mapping, write with p[i] = ...; first touch of each page faults its data in.
  5. If durability matters, msync() the dirty pages before relying on them; then munmap(p, len) to release the mapping when finished.

Step back and see how completely mmap() is a consequence of this rung rather than a new idea. The pointer it returns is a virtual address; the file-to-pages correspondence lives in the page table; the bytes arrive by demand paging on a page fault; a private mapping stays private through copy-on-write; and the read/write/exec rules are the MMU's per-page protection checked on every translation. mmap() did not add machinery — it gave you a handle on the machinery the kernel was running for your memory all along. That is a fitting close to virtual memory: the abstraction was never just hiding the hardware from you; with one system call, it hands you the keys.