Virtual Memory · Linux userspace / kernel ABI

mmap with MAP_SHARED and MAP_PRIVATE

Uses a real two-process example to compare mappings that share the same file page with copy-on-write private mappings and their dirty/writeback paths.

Series
18 / 38
Build
cc -std=c17 -Wall -Wextra -O2 mmap_file.c -o mmap_file
Run
./mmap_file shared.dat
Kernel
Linux 6.18.37 LTS

Why are changes made through MAP_PRIVATE not written to the original file?

A write fault in a MAP_SHARED mapping attaches the page-cache page as writable and marks it dirty, making the change visible to other shared mappers and file I/O. A MAP_PRIVATE mapping may initially read the file page, but a write creates an anonymous private copy.

msync requests dirty-page writeback within the mapping type and filesystem policy. CPU-cache coherence, visibility to another process, and storage durability are guarantees at different layers.

Structure diagram

Figure 1. Shared writes and private COW diverging from the same file page

Process A · MAP_SHARED

  • VA 0x7000
  • write 'S'
  • PTE → page cache

File page cache

  • inode index 0
  • dirty folio
  • msync/writeback

Process B · MAP_PRIVATE

  • read → page cache
  • write fault
  • PTE → anonymous page

Private page B

  • content 'P'
  • Private_Dirty
  • not reflected in file

A MAP_SHARED write dirties a page-cache page, while a MAP_PRIVATE write creates a process-private anonymous page.

Call path

Figure 2. From userspace code to observable results
open file inode/page cache
mmap shared or private VMA
write fault page-cache/COW branch
msync request writeback
reader check visibility

The mapping flag determines which backing object owns a write after a page fault. Look not at whether virtual addresses match, but at whether the page cache or an anonymous page becomes dirty.

Figure 3. Major points along the kernel-internal path
mmap_region attach vm_file
filemap_fault page cache lookup
do_wp_page shared/private branch
set_page_dirty shared write
writeback filesystem I/O

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
mm/mmap.c mmap_region() Attach a file and sharing flags to the VMA
mm/filemap.c filemap_fault(), filemap_map_pages() Attach a file page-cache page to the PTE
mm/memory.c do_wp_page(), wp_page_copy() Create a COW page on a private-mapping write

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 mmap_file.c -o mmap_file
01#define _POSIX_C_SOURCE 200809L
02#include <fcntl.h>
03#include <stdio.h>
04#include <string.h>
05#include <sys/mman.h>
06#include <unistd.h>
07
08int main(int argc, char **argv)
09{
10    if (argc != 2)
11        return 2;
12    long page = sysconf(_SC_PAGESIZE);
13    int fd = open(argv[1], O_RDWR | O_CREAT | O_CLOEXEC, 0644);
14    if (fd < 0 || ftruncate(fd, page) < 0)
15        return 1;
16
17    char *shared = mmap(NULL, (size_t)page, PROT_READ | PROT_WRITE,
18                        MAP_SHARED, fd, 0);
19    char *private = mmap(NULL, (size_t)page, PROT_READ | PROT_WRITE,
20                         MAP_PRIVATE, fd, 0);
21    if (shared == MAP_FAILED || private == MAP_FAILED)
22        return 1;
23
24    memcpy(shared, "shared", 7);
25    if (msync(shared, (size_t)page, MS_SYNC) < 0)
26        return 1;
27    memcpy(private, "private", 8);
28    printf("shared='%s' private='%s'\n", shared, private);
29    munmap(private, (size_t)page);
30    munmap(shared, (size_t)page);
31    close(fd);
32    return 0;
33}

Code notes

Source line 14ftruncate(fd, page)

Ensures the file is long enough before mapping it. Accessing a page beyond EOF can generate SIGBUS.

Source line 18MAP_SHARED, fd, 0

Attaches the first page's page-cache backing as a shared mapping. A write is visible to another mapping of the same inode page.

Source line 20MAP_PRIVATE, fd, 0

Reads may use the file page, but a write fault branches to a private anonymous copy.

Source line 25msync(shared

Requests completion of writeback for the shared dirty range. This does not automatically guarantee directory-entry durability or protection against storage-device power loss.

Source line 27memcpy(private

Only the private mapping changes; the string 'private' is not written to the file or the shared mapping.

Detailed behavior

01

Match file size to mapping size

Although mmap can create a page-granular range, it does not create valid data beyond file EOF. Bytes after EOF in the final partial page may read as zero, but accessing the next page is subject to SIGBUS.

If another process truncates the file, a process with an existing mapping can also receive SIGBUS on a later access.

02

Separate visibility from synchronization

On a cache-coherent system, another process can see a CPU store to a MAP_SHARED page, but data-structure consistency is not guaranteed. A protocol using atomics, mutexes, sequence counters, or similar mechanisms is required.

msync is not a thread memory-ordering primitive.

03

mmap I/O still pays page-fault and writeback costs

Reducing read/write syscall count does not remove first-access faults, dirty throttling, reclaim, or filesystem writeback. Measure latency through faults and storage interactions, not syscall count alone.

It may benefit random access and zero-copy parsing, but introduces costs for handling truncate, SIGBUS, and address-space pressure.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
file-backed VMAAttached to the inode address_space by mmap and retained until munmapvm_pgoff, shared/private flag
page cache folioCreated by file read/fault and passes through dirty/writeback/reclaimindex, dirty, writeback
private COW pageCreated on a MAP_PRIVATE write fault and belongs only to the process mmanonymous, dirty, RSS

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
SIGBUSThe file is truncated while mapped, or access goes beyond EOFInspect si_addr and the current file size
Private changes are absent from the fileNormal COW behavior of MAP_PRIVATEsmaps Anonymous/Private_Dirty
msync returns EINVALThe address/range is not page-aligned, or the flags are invalidCheck mapping boundaries and page size

Verify it yourself

  1. After the program runs, hexdump shared.dat and verify that only the shared change was recorded.
  2. Have another process map the file with MAP_SHARED and distinguish visibility before and after msync from persistence to storage.
  3. Have another process call ftruncate(0) while the mapping exists, then inspect the SIGBUS handler and the limits of safe recovery.
Run./mmap_file shared.dat
Tracestrace -e trace=openat,ftruncate,mmap,msync,munmap,close ./mmap_file shared.dat

Primary sources