Memory Mappings nonymous Mappings (Private & Shared)

 

Chapter 49: Memory Mappings
Part 4 of 5 โ€” Anonymous Mappings (Private & Shared)
๐Ÿ”ฒ Topic
Anonymous Mappings
๐ŸŽฏ Level
Intermediate
๐Ÿ“š Source
TLPI Ch49
๐Ÿ’ผ Target
TI / ST / Qualcomm

What is an Anonymous Mapping?

An anonymous mapping has no backing file. Instead, the pages are initialized to zero by the kernel. You can think of it as mapping a virtual file whose content is always zero โ€” no file descriptor is required.

Anonymous mappings are created by passing MAP_ANONYMOUS (also spelled MAP_ANON on some systems) in the flags, setting fd = -1, and offset = 0.

Like file mappings, anonymous mappings can be either private (used for memory allocation) or shared (used for IPC between parent/child processes).

Key Terms in This Part:

MAP_ANONYMOUS MAP_ANON Private Anonymous Shared Anonymous malloc() internals Zero-page fork() IPC Demand paging Overcommit

Private Anonymous Mapping (MAP_PRIVATE | MAP_ANONYMOUS)

A private anonymous mapping creates a region of zeroed memory that belongs entirely to the calling process. Each call to mmap() with these flags yields a distinct, independent region โ€” different from all other anonymous mappings, even ones of the same size.

Private Anonymous Mapping
mmap() call 1
Distinct region A
zeroed RAM
private to process
โ‰ 
mmap() call 2
Distinct region B
zeroed RAM
private to process
No sharing between them. After fork(), child gets COW copies โ€” changes invisible between parent and child.

Primary use case: malloc() for large allocations.

glibc’s malloc() uses mmap() with MAP_PRIVATE|MAP_ANONYMOUS for allocations above a threshold (typically 128 KB by default, configurable via mallopt(M_MMAP_THRESHOLD, ...)). For smaller allocations, it uses the brk()/sbrk() heap. The advantage of using mmap() for large blocks: free() can immediately return the virtual address space to the OS via munmap(), unlike the heap which grows but rarely shrinks.

/*
 * private_anon.c โ€” Private anonymous mapping for memory allocation
 * Demonstrates what malloc() does internally for large blocks
 * Compile: gcc -o private_anon private_anon.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

#define ALLOC_SIZE  (512 * 1024)   /* 512 KB */

int main(void)
{
    char *buf;
    long  page_size;
    int   i;

    page_size = sysconf(_SC_PAGESIZE);
    printf("Page size: %ld\n", page_size);

    /* Allocate 512KB using private anonymous mapping (like malloc does) */
    buf = mmap(NULL,
               ALLOC_SIZE,
               PROT_READ | PROT_WRITE,
               MAP_PRIVATE | MAP_ANONYMOUS,
               -1,   /* fd must be -1 for anonymous */
               0);   /* offset must be 0 for anonymous */

    if (buf == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    printf("Allocated %d bytes at %p\n", ALLOC_SIZE, (void*)buf);

    /* Pages are demand-paged: physical RAM allocated only when touched */
    /* All bytes are initially zero */
    printf("First byte (should be 0): %d\n", buf[0]);
    printf("Last  byte (should be 0): %d\n", buf[ALLOC_SIZE - 1]);

    /* Write to the region โ€” pages are now actually allocated in RAM */
    memset(buf, 0xAB, ALLOC_SIZE);
    printf("Filled %d bytes with 0xAB\n", ALLOC_SIZE);

    /* Use the memory */
    for (i = 0; i < 10; i++) {
        printf("buf[%d] = 0x%02X\n", i * 4096, (unsigned char)buf[i * 4096]);
    }

    /* Release back to OS immediately (unlike heap) */
    munmap(buf, ALLOC_SIZE);
    printf("Memory returned to OS via munmap()\n");

    return 0;
}

Demand Paging โ€” Pages Are Lazy

When you call mmap(), the kernel does not immediately allocate physical RAM for all the requested pages. It only sets up the virtual address space entry. Physical frames are allocated on demand โ€” only when your code actually reads or writes a particular page for the first time.

Demand Paging Flow:
1
mmap() โ†’ kernel sets up VMA (virtual memory area) entry only. No physical RAM yet.
2
Process accesses page for the first time โ†’ page fault raised by hardware MMU.
3
Kernel handles fault: allocates physical frame, fills with zeros (anon) or loads from file (file mapping), updates page table.
4
Process resumes. Subsequent accesses to same page: no fault, direct RAM access.

This is why mmap(NULL, 1 GB, ...) succeeds immediately on a machine with 512 MB RAM โ€” the 1 GB is just virtual address space reservation. Physical pages are only consumed as you touch them.

/* MAP_POPULATE: force all pages into RAM at mmap() time
 * Avoids demand-paging faults later โ€” useful for real-time code */
buf = mmap(NULL, size,
           PROT_READ | PROT_WRITE,
           MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,
           -1, 0);

Shared Anonymous Mapping (MAP_SHARED | MAP_ANONYMOUS)

A shared anonymous mapping creates a region that has no backing file, but is shared between a parent process and children created via fork(). After fork(), parent and child share the same physical pages โ€” changes by one are visible to the other. There is no copy-on-write.

This is essentially the same as System V shared memory (shmget/shmat) but simpler to use โ€” no IPC key, no shmctl cleanup required.

Shared Anonymous After fork()
Parent Process
map[0] = 99;
Sees child’s changes
โ†’ Shared โ†’
Physical RAM Pages
(no COW)
โ† Shared โ†
Child Process
sees map[0] == 99
map[1] = 42;
/*
 * shared_anon_ipc.c โ€” IPC between parent and child via shared anonymous mapping
 * Compile: gcc -o shared_anon_ipc shared_anon_ipc.c
 *
 * Parent and child share an integer counter in the mapping.
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>

#define REGION_SIZE  4096

int main(void)
{
    int   *shared;
    pid_t  pid;

    /*
     * Create shared anonymous mapping BEFORE fork()
     * so both parent and child inherit it and share pages.
     */
    shared = mmap(NULL,
                  REGION_SIZE,
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANONYMOUS,
                  -1,   /* fd = -1 for anonymous */
                  0);   /* offset = 0 for anonymous */

    if (shared == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    shared[0] = 0;   /* Initialize counter */
    shared[1] = 0;

    pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* === CHILD === */
        printf("Child PID %d starting\n", getpid());

        /* Write to shared region โ€” parent will see this */
        shared[0] = 100;
        shared[1] = 200;

        printf("Child: wrote shared[0]=%d, shared[1]=%d\n",
               shared[0], shared[1]);
        munmap(shared, REGION_SIZE);
        exit(0);

    } else {
        /* === PARENT === */
        /* Wait for child to finish */
        waitpid(pid, NULL, 0);

        /* Read values written by child */
        printf("Parent: shared[0]=%d (expect 100)\n", shared[0]);
        printf("Parent: shared[1]=%d (expect 200)\n", shared[1]);

        munmap(shared, REGION_SIZE);
    }

    return 0;
}

Shared Anonymous Mapping with a Process-Shared Mutex

For safe concurrent access, embed a pthread_mutex_t with PTHREAD_PROCESS_SHARED attribute inside the shared anonymous region:

/*
 * shared_mutex.c โ€” Shared anonymous mapping + process-shared mutex
 * Compile: gcc -o shared_mutex shared_mutex.c -lpthread
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/wait.h>

/* Layout of the shared region */
typedef struct {
    pthread_mutex_t lock;   /* Must be process-shared */
    int             counter;
} SharedData;

int main(void)
{
    SharedData           *sd;
    pthread_mutexattr_t   attr;
    pid_t                 pid;
    int                   i;

    /* Allocate shared region */
    sd = mmap(NULL, sizeof(SharedData),
              PROT_READ | PROT_WRITE,
              MAP_SHARED | MAP_ANONYMOUS,
              -1, 0);
    if (sd == MAP_FAILED) { perror("mmap"); exit(1); }

    /* Initialize mutex with PTHREAD_PROCESS_SHARED */
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&sd->lock, &attr);
    pthread_mutexattr_destroy(&attr);

    sd->counter = 0;

    pid = fork();
    if (pid == 0) {
        /* Child: increment counter 1000 times */
        for (i = 0; i < 1000; i++) {
            pthread_mutex_lock(&sd->lock);
            sd->counter++;
            pthread_mutex_unlock(&sd->lock);
        }
        printf("Child done\n");
        munmap(sd, sizeof(SharedData));
        exit(0);
    } else {
        /* Parent: increment counter 1000 times */
        for (i = 0; i < 1000; i++) {
            pthread_mutex_lock(&sd->lock);
            sd->counter++;
            pthread_mutex_unlock(&sd->lock);
        }
        waitpid(pid, NULL, 0);
        printf("Final counter: %d (expect 2000)\n", sd->counter);

        pthread_mutex_destroy(&sd->lock);
        munmap(sd, sizeof(SharedData));
    }
    return 0;
}

malloc() Internals โ€” How glibc Uses mmap()

glibc’s malloc() uses two strategies depending on allocation size:

Small/Medium Allocations
Uses the brk() heap โ€” a single contiguous region grown by brk()/sbrk(). Fast for many small allocs. Heap can only grow, rarely shrinks.
Typical threshold: < 128 KB
Large Allocations
Uses mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0). Each large alloc gets its own mapping. On free(), munmap() is called โ€” memory immediately returned to OS.
Default threshold: >= 128 KB (MMAP_THRESHOLD)
/*
 * malloc_mmap_demo.c โ€” Observe malloc() using mmap for large allocations
 * Compile: gcc -o malloc_mmap malloc_mmap_demo.c
 *
 * Use strace to see mmap syscalls:
 *   strace -e trace=mmap,munmap ./malloc_mmap
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *small;
    char *large;

    /* Small: typically uses brk heap, no mmap() */
    small = malloc(1024);
    printf("Small alloc (1KB) at %p\n", (void*)small);
    free(small);

    /* Large: glibc calls mmap() under the hood */
    large = malloc(256 * 1024);   /* 256 KB */
    printf("Large alloc (256KB) at %p\n", (void*)large);
    /* free() calls munmap() internally โ€” memory returned to OS immediately */
    free(large);

    return 0;
}
/* Run with: strace -e trace=brk,mmap,munmap ./malloc_mmap
 * You will see mmap(NULL, 262144, ..., MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
 * for the large allocation and munmap() on free(). */
/* Tuning malloc's mmap threshold with mallopt() */
#include <malloc.h>

/* Force mmap for any allocation >= 64KB */
mallopt(M_MMAP_THRESHOLD, 64 * 1024);

/* Limit number of concurrent mmap'd regions */
mallopt(M_MMAP_MAX, 64);

Private vs Shared Anonymous โ€” Quick Comparison
Attribute Private Anonymous Shared Anonymous
Flags MAP_PRIVATE|MAP_ANONYMOUS MAP_SHARED|MAP_ANONYMOUS
fd / offset -1 / 0 -1 / 0
Initial content All zeros All zeros
Copy-on-write after fork() Yes โ€” changes isolated per process No โ€” changes shared
IPC use case No (private) Yes (related processes via fork)
Primary use Dynamic memory allocation (malloc) Shared memory IPC between parent/child
Backed by file No No
Visible in /proc/PID/maps Yes, shows [anon] or blank path Yes, shows [anon] or blank path

๐ŸŽฏ Interview Questions โ€” Anonymous Mappings
Q1. What is an anonymous mapping and how is it different from a file mapping?
An anonymous mapping has no backing file โ€” pages are zero-initialized and backed only by swap space (or RAM). A file mapping has a corresponding file on disk; pages are loaded from or flushed to that file. For anonymous mappings, fd is -1 and offset is 0. Anonymous mappings are used for dynamic memory allocation and IPC, while file mappings are used for file I/O and loading program code.
Q2. When does glibc’s malloc() use mmap() instead of brk()?
By default, glibc uses mmap(MAP_PRIVATE|MAP_ANONYMOUS) for allocations at or above the MMAP_THRESHOLD, which defaults to 128 KB. The advantage is that free() can immediately return the memory to the OS via munmap(), reducing memory footprint. The threshold is tunable via mallopt(M_MMAP_THRESHOLD, size).
Q3. What is demand paging and why is it important for mmap()?
Demand paging means physical RAM is not allocated at mmap() time โ€” only virtual address space is reserved. A physical frame is allocated only when the process first accesses (reads or writes) each virtual page. This makes large mappings cheap to create, allows overcommitting memory, and is fundamental to how shared libraries and program loading work efficiently. The first access triggers a page fault; the kernel handles it by allocating a frame and loading data (or zeroing for anon).
Q4. What is the difference between shared anonymous and shared file mapping for IPC?
Shared anonymous: Only works between related processes (parent/child via fork()) because the mapping must be created before fork(). There is no file involved, so no disk I/O and no cleanup needed. Shared file mapping: Works between any processes, related or not โ€” they just need to agree on the filename. The file acts as the rendezvous point. After unmapping, the file persists (useful as a record, but also needs manual cleanup).
Q5. Why must fd=-1 and offset=0 for anonymous mappings?
Anonymous mappings have no backing file, so there is no file descriptor to refer to โ€” passing -1 for fd signals this. Similarly, offset is meaningless without a file, so it must be 0. These values are mandated by POSIX; passing any other value with MAP_ANONYMOUS results in EINVAL on most systems.
Q6. How does the kernel implement the zero-initialization guarantee for anonymous mappings?
The kernel maintains a special read-only zero page (a physical frame filled with zeros). When a new anonymous page is first read (before any write), the kernel maps all processes’ virtual pages to the same zero page โ€” this is the “zero-page optimization.” When a process writes to such a page, copy-on-write kicks in: a new zeroed frame is allocated privately, the write proceeds. This saves RAM when many anonymous pages are allocated but never written to.
Q7. A child process modifies a shared anonymous mapping. Does the parent see the change?
Yes, if the mapping was created with MAP_SHARED|MAP_ANONYMOUS before the fork(). There is no copy-on-write for shared mappings โ€” parent and child share the same physical pages, so a write by either is immediately visible to the other. If the mapping was MAP_PRIVATE|MAP_ANONYMOUS, copy-on-write applies and the parent would NOT see the child’s changes.

Chapter 49 Series

Part 4 of 5 โ€” Anonymous Mappings

โ† Part 3: File Mappings Next: Advanced Topics โ†’ ๐Ÿ  Home

Leave a Reply

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