MAP_PRIVATE, MAP_SHARED & msync() Copy-on-Write, and file synchronization

 

MAP_PRIVATE, MAP_SHARED & msync()
Understanding visibility of changes, Copy-on-Write, and file synchronization

The Core Question: Who Sees Your Changes?

When you write to a memory-mapped region, two questions arise: (1) Do other processes see your change? (2) Does the change get written back to the file on disk? The answer depends entirely on whether you used MAP_SHARED or MAP_PRIVATE.

MAP_SHARED vs MAP_PRIVATE – Side by Side
Property MAP_SHARED MAP_PRIVATE
Changes visible to other processes? Yes — all processes mapping the same file see each other’s writes No — changes are process-private
Changes written back to file? Yes — kernel eventually writes dirty pages to the file No — the file is never modified
Copy-on-Write on write? No — writes go directly to the shared page Yes — kernel creates a private copy of the page on first write
Typical use case IPC shared memory, memory-mapped I/O, database files Loading .so libraries, reading config files, loading ELF text segments

Copy-on-Write (CoW) in MAP_PRIVATE

Before a write, the MAP_PRIVATE page is shared with the file’s page cache. On the first write, the kernel:

Process A
Virtual Page
Shared Physical Page
(from file page cache)
Before first write: both the process’s virtual page and the file page cache point to the same physical page
Process A
Virtual Page
NEW Private Physical Page
(modified copy)
File Page Cache Original Physical Page
(unchanged)
After first write: kernel allocates a new physical page for the process; file page cache still has the original

Example 1: MAP_SHARED – Two Processes Sharing a File

Writer process maps the file and writes data:

/* writer.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

#define FILE_PATH  "shared.dat"
#define MAP_SIZE   64

int main(void) {
    int fd;
    char *addr;

    fd = open(FILE_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); exit(1); }
    if (ftruncate(fd, MAP_SIZE) == -1) { perror("ftruncate"); exit(1); }

    addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    strncpy(addr, "Hello from writer!", MAP_SIZE);
    printf("Writer wrote: %s\n", addr);

    /* Give reader a moment */
    sleep(2);

    printf("Writer sees now: %s\n", addr);  /* may see reader's reply */
    munmap(addr, MAP_SIZE);
    return 0;
}

Reader process maps the same file and reads:

/* reader.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

#define FILE_PATH  "shared.dat"
#define MAP_SIZE   64

int main(void) {
    int fd;
    char *addr;

    sleep(1); /* Wait for writer */

    fd = open(FILE_PATH, O_RDWR);
    if (fd == -1) { perror("open"); exit(1); }

    addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    printf("Reader sees: %s\n", addr);
    strncpy(addr, "Reader replied!", MAP_SIZE);

    munmap(addr, MAP_SIZE);
    return 0;
}

Example 2: MAP_PRIVATE – File Remains Unchanged After Write
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

int main(void) {
    int fd;
    struct stat sb;
    char *addr;

    /* Create a file with known content */
    fd = open("private_test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    write(fd, "ORIGINAL CONTENT", 16);
    fstat(fd, &sb);

    addr = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE,
                MAP_PRIVATE,   /* private: changes do NOT go to file */
                fd, 0);
    if (addr == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    printf("Before write: %.*s\n", (int)sb.st_size, addr);

    /* Modify the mapping */
    memcpy(addr, "MODIFIED CONTENT", 16);
    printf("After write (in-memory): %.*s\n", (int)sb.st_size, addr);

    munmap(addr, sb.st_size);

    /* Now re-read the file — it should still have "ORIGINAL CONTENT" */
    fd = open("private_test.txt", O_RDONLY);
    char buf[17] = {0};
    read(fd, buf, 16);
    close(fd);
    printf("File on disk (unchanged): %s\n", buf);

    return 0;
}
/* Output:
   Before write: ORIGINAL CONTENT
   After write (in-memory): MODIFIED CONTENT
   File on disk (unchanged): ORIGINAL CONTENT
*/

msync() – Forcing Writes to the File

For MAP_SHARED mappings, the kernel will eventually write dirty pages back to the file, but there is no guarantee of when. If you need the file on disk to reflect your changes at a specific moment, use msync().

#include <sys/mman.h>

int msync(void *addr,    /* Start of region to sync */
          size_t length, /* Length of region         */
          int flags);    /* MS_SYNC or MS_ASYNC or MS_INVALIDATE */

/* Returns 0 on success, -1 on error */
Flag Behavior
MS_SYNC Write dirty pages to file and wait until I/O completes (blocking)
MS_ASYNC Schedule the write but don’t wait (non-blocking)
MS_INVALIDATE Invalidate cached pages so next access re-reads from file (for MAP_SHARED across processes)

Example 3: Using msync() for Reliable File Persistence
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

typedef struct {
    int  record_id;
    char name[32];
    int  value;
} Record;

int main(void) {
    int fd;
    Record *rec;
    size_t size = sizeof(Record);

    fd = open("database.dat", O_RDWR | O_CREAT | O_TRUNC, 0644);
    ftruncate(fd, size);

    rec = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (rec == MAP_FAILED) { perror("mmap"); exit(1); }
    close(fd);

    /* Write record */
    rec->record_id = 42;
    strncpy(rec->name, "temperature", 31);
    rec->value = 37;

    /* msync ensures data is on disk before we proceed */
    if (msync(rec, size, MS_SYNC) == -1) {
        perror("msync");
        exit(1);
    }

    printf("Record flushed to disk: id=%d name=%s value=%d\n",
           rec->record_id, rec->name, rec->value);

    munmap(rec, size);
    return 0;
}

Memory Mappings After fork()

When a process calls fork(), the child inherits all the parent’s mappings. The behavior depends on the mapping type:

Mapping Type Behavior After fork()
MAP_PRIVATE (file or anonymous) Parent and child share pages copy-on-write. Writes by either create a private copy — the other process does not see it.
MAP_SHARED (file) Parent and child both write to the same file pages. Writes by one are visible to the other immediately.
MAP_SHARED | MAP_ANONYMOUS This is the canonical way to create shared memory between parent and child: create the mapping before fork(), then both can communicate through it.

Example 4: Parent-Child IPC via Shared Anonymous Mapping
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    int *shared;

    /* Create shared anonymous mapping BEFORE fork() */
    shared = mmap(NULL, sizeof(int),
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANONYMOUS,
                  -1, 0);
    if (shared == MAP_FAILED) { perror("mmap"); exit(1); }

    *shared = 0;

    pid_t pid = fork();

    if (pid == 0) {
        /* Child increments the shared counter */
        for (int i = 0; i < 1000; i++) {
            (*shared)++;   /* NOTE: not atomic — demo only */
        }
        exit(0);
    } else {
        /* Parent also increments */
        for (int i = 0; i < 1000; i++) {
            (*shared)++;
        }
        wait(NULL);
        printf("Final counter (approx 2000): %d\n", *shared);
        munmap(shared, sizeof(int));
    }

    return 0;
}

Interview Questions & Answers
Q1. A process maps a file with MAP_PRIVATE and writes to it. Will the file change?

A: No. With MAP_PRIVATE, the kernel uses copy-on-write. On the first write, a private copy of the page is created for the process. All subsequent writes go to this private page. The file on disk and the file’s page cache remain unchanged.

Q2. If two processes map the same file with MAP_SHARED and one writes, does the other see the change immediately?

A: Yes, because both processes’ virtual pages point to the same physical page in the kernel’s page cache. Writing to the shared page is immediately visible through the other process’s mapping. There is no buffering between them.

Q3. Why do we need msync() if the kernel will flush dirty pages anyway?

A: The kernel writes dirty pages to disk lazily (for performance), but it gives no timing guarantees. If the system crashes before the flush, data is lost. msync(MS_SYNC) blocks until the data is safely on disk, making it suitable for transactional or journaling applications. It is analogous to fsync() but for mapped regions.

Q4. How do you set up IPC shared memory between a parent and child process using mmap()?

A: Create a MAP_SHARED | MAP_ANONYMOUS mapping before calling fork(). Both parent and child inherit the mapping and see each other’s writes because it is a shared mapping. Unlike System V shared memory, no key or ID is needed — the mapping is inherited directly across fork().

Q5. When loading a shared library (.so), which mapping type does the dynamic linker use and why?

A: The text (code) segment is loaded with MAP_PRIVATE. Since the .so code is read-only for most processes, they all initially share the same physical pages from the page cache (efficient). If a process somehow modifies a code page (unusual), copy-on-write gives it a private copy without affecting other processes. The data segment of a .so may use MAP_PRIVATE too, so each process gets its own initialized data.

Leave a Reply

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