Anonymous Mappings Linux Memory Mappings

 

Anonymous Mappings
Chapter 49.7 — Linux Memory Mappings — TLPI Deep Dive
2
Creation Methods
6+
Code Examples
10
Interview Q&A

Series Navigation: ← Prev: 49.6 mmap Flags  |  You are here: 49.7 Anonymous Mappings

What is an Anonymous Mapping?

A regular mmap() call maps a file into memory. An anonymous mapping is one with no file behind it — just raw zero-initialized memory pages handed directly by the kernel.

Anonymous mappings are the foundation of dynamic memory allocation on Linux. Every time you call malloc() for a large chunk (>128 KB), glibc quietly uses an anonymous mapping under the hood. They are also the standard way for related processes (parent/child) to share memory without creating a named file.

Key Terms to Know

anonymous mapping MAP_ANONYMOUS MAP_ANON /dev/zero MAP_PRIVATE anonymous MAP_SHARED anonymous malloc internals MMAP_THRESHOLD fork() + mmap IPC shared memory _BSD_SOURCE _SVID_SOURCE

File Mapping vs Anonymous Mapping

To understand anonymous mappings, contrast them with file mappings:

File Mapping
Process Virtual Memory
Page Cache (RAM)
File on Disk

Pages backed by a real file. Writes propagate to disk (MAP_SHARED) or stay private (MAP_PRIVATE CoW).

Anonymous Mapping
Process Virtual Memory
Physical RAM (zeroed)
No file on disk

Pages backed only by RAM (and swap if needed). No file involved. Pages start zeroed.

Two Ways to Create an Anonymous Mapping

Linux offers two equivalent techniques. They produce identical results. They exist for historical reasons: MAP_ANONYMOUS comes from BSD, /dev/zero comes from System V.

Method 1: MAP_ANONYMOUS Flag (Modern, Preferred)

Pass MAP_ANONYMOUS in the flags and set fd = -1. The offset argument is ignored (no file to seek into). This is the standard approach in modern code.

Feature test macro required: You must define either _BSD_SOURCE or _SVID_SOURCE before including <sys/mman.h> to get the MAP_ANONYMOUS definition. On newer glibc, _DEFAULT_SOURCE also works. MAP_ANON is a synonym for MAP_ANONYMOUS.
#define _BSD_SOURCE   /* Needed to expose MAP_ANONYMOUS */
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
    size_t length = 4096;  /* One page */

    /* Create a private anonymous mapping */
    void *addr = mmap(
        NULL,                          /* Let kernel choose address */
        length,
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS,
        -1,                            /* No file — fd must be -1 */
        0                              /* Offset ignored */
    );

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

    /* Memory is already zeroed by the kernel */
    printf("First byte: %d\n", *(unsigned char *)addr);  /* Prints: 0 */

    /* Write to it */
    strcpy((char *)addr, "Hello from anonymous mmap!");
    printf("Data: %s\n", (char *)addr);

    /* Clean up */
    if (munmap(addr, length) == -1)
        perror("munmap");

    return 0;
}

Method 2: /dev/zero (Classic System V Style)

Open /dev/zero and pass its file descriptor to mmap(). /dev/zero is a special virtual device — reading from it always returns zero bytes; writes to it are discarded.

Note: After mmap() returns, you can close the file descriptor. The mapping stays alive — the fd is only needed during the mmap call.
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int fd;
    void *addr;
    size_t length = 4096;

    /* Open /dev/zero */
    fd = open("/dev/zero", O_RDWR);
    if (fd == -1) {
        perror("open /dev/zero");
        exit(EXIT_FAILURE);
    }

    /* Map /dev/zero — same effect as MAP_ANONYMOUS */
    addr = mmap(NULL, length, PROT_READ | PROT_WRITE,
                MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    /* Close fd — mapping is unaffected */
    if (close(fd) == -1)
        perror("close");

    /* Memory is zeroed */
    printf("First byte: %d\n", *(unsigned char *)addr);  /* 0 */
    strcpy((char *)addr, "Hello from /dev/zero mmap!");
    printf("Data: %s\n", (char *)addr);

    munmap(addr, length);
    return 0;
}

Private vs Shared Anonymous Mappings

MAP_PRIVATE Anonymous Mapping

A private anonymous mapping gives a process a private block of zeroed memory. No other process can see it. It is the building block of dynamic memory allocation.

glibc malloc() internals: When you call malloc() for a chunk larger than MMAP_THRESHOLD (128 KB by default), glibc calls mmap(MAP_PRIVATE | MAP_ANONYMOUS) internally. When free() is called on that chunk, glibc calls munmap() — the memory is returned to the OS immediately, which reduces fragmentation. The threshold is tunable via mallopt(M_MMAP_THRESHOLD, new_value).
/* How glibc malloc works internally for large allocations */
#include <sys/mman.h>

/* This is conceptually what malloc does for chunks > 128 KB */
void *large_alloc(size_t size)
{
    /* Round up to page boundary */
    size_t pages = (size + 4095) & ~4095UL;

    void *ptr = mmap(NULL, pages,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);
    if (ptr == MAP_FAILED)
        return NULL;
    return ptr;
}

void large_free(void *ptr, size_t size)
{
    size_t pages = (size + 4095) & ~4095UL;
    munmap(ptr, pages);   /* Memory returned to OS immediately */
}

int main(void)
{
    /* malloc internally uses mmap for this large allocation */
    void *buf = malloc(200 * 1024);  /* 200 KB — above MMAP_THRESHOLD */
    /* ... use buf ... */
    free(buf);  /* glibc calls munmap, not just marks as free */
    return 0;
}
Practical use — stack-like memory pool:
#define _BSD_SOURCE
#include <sys/mman.h>
#include <stdint.h>

/* Simple bump allocator on a MAP_PRIVATE anonymous region */
typedef struct {
    uint8_t *base;
    size_t   capacity;
    size_t   used;
} Arena;

Arena arena_create(size_t size)
{
    Arena a;
    a.base = mmap(NULL, size, PROT_READ|PROT_WRITE,
                  MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    a.capacity = (a.base != MAP_FAILED) ? size : 0;
    a.used = 0;
    return a;
}

void *arena_alloc(Arena *a, size_t n)
{
    n = (n + 7) & ~7UL;  /* 8-byte align */
    if (a->used + n > a->capacity) return NULL;
    void *ptr = a->base + a->used;
    a->used += n;
    return ptr;
}

void arena_destroy(Arena *a)
{
    munmap(a->base, a->capacity);
    a->base = NULL;
}
MAP_SHARED Anonymous Mapping — IPC Without a File

A shared anonymous mapping creates a region of memory that can be shared between related processes (parent and child after fork()). Neither process needs a file on disk — this is simpler than System V shared memory or POSIX shared memory when processes are already related.

How it works with fork(): Create the shared mapping before calling fork(). The child inherits the mapping. Both parent and child now share the same physical pages. Writes by either process are visible to the other.

Shared Anonymous Mapping After fork()

Parent Process
Virtual addr
0x7f001000

Shared Physical Pages
int shared_val = 1
zeroed at start

Child Process
Virtual addr
0x7f001000
Both virtual addresses map to the same physical memory. Writes by child are seen by parent.
#define _BSD_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.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(EXIT_FAILURE);
    }

    *shared = 1;  /* Parent sets initial value */

    switch (fork()) {
    case -1:
        perror("fork");
        exit(EXIT_FAILURE);

    case 0:  /* Child process */
        printf("Child sees:  %d\n", *shared);  /* Prints 1 */
        (*shared)++;                             /* Increment to 2 */
        printf("Child wrote: %d\n", *shared);
        if (munmap(shared, sizeof(int)) == -1)
            perror("munmap child");
        exit(EXIT_SUCCESS);

    default:  /* Parent process */
        wait(NULL);  /* Wait for child */
        printf("Parent sees: %d\n", *shared);  /* Prints 2 — child's write visible */
        munmap(shared, sizeof(int));
        break;
    }
    return 0;
}
/* Expected output:
   Child sees:  1
   Child wrote: 2
   Parent sees: 2      ← Child's modification is visible to parent */

Same Example Using /dev/zero

For portability on older systems that do not have MAP_ANONYMOUS, use /dev/zero. The behavior is identical.

#include <sys/mman.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int fd, *shared;

    /* Open /dev/zero */
    fd = open("/dev/zero", O_RDWR);
    if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }

    /* Create shared mapping via /dev/zero */
    shared = mmap(NULL, sizeof(int),
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED,   /* Note: no MAP_ANONYMOUS needed */
                  fd, 0);
    if (shared == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }

    close(fd);  /* fd not needed after mmap */

    *shared = 42;

    switch (fork()) {
    case 0:
        printf("Child reads: %d\n", *shared);   /* 42 */
        *shared = 100;
        munmap(shared, sizeof(int));
        exit(EXIT_SUCCESS);
    default:
        wait(NULL);
        printf("Parent reads: %d\n", *shared);  /* 100 */
        munmap(shared, sizeof(int));
    }
    return 0;
}

Sharing a Struct Between Parent and Child

You can share any data structure, not just a single integer. Map enough space for your struct and both parent and child access it directly.

#define _BSD_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int   counter;
    char  message[64];
    float result;
} SharedData;

int main(void)
{
    SharedData *data = mmap(NULL, sizeof(SharedData),
                             PROT_READ | PROT_WRITE,
                             MAP_SHARED | MAP_ANONYMOUS,
                             -1, 0);
    if (data == MAP_FAILED) { perror("mmap"); exit(1); }

    /* Parent initializes */
    data->counter = 0;
    strcpy(data->message, "hello");
    data->result  = 0.0f;

    pid_t pid = fork();
    if (pid == 0) {
        /* Child: do some "work" and store results */
        data->counter = 99;
        strcpy(data->message, "done by child");
        data->result = 3.14f;
        munmap(data, sizeof(SharedData));
        exit(EXIT_SUCCESS);
    }

    wait(NULL);

    /* Parent reads child's results */
    printf("counter : %d\n",   data->counter);   /* 99 */
    printf("message : %s\n",   data->message);   /* done by child */
    printf("result  : %.2f\n", data->result);    /* 3.14 */

    munmap(data, sizeof(SharedData));
    return 0;
}

glibc malloc() and MMAP_THRESHOLD

Understanding when malloc uses mmap vs the heap (sbrk) is important for performance tuning:

malloc() Strategy Decision
malloc(size)
Is size > MMAP_THRESHOLD (128 KB)?
YES
mmap(MAP_PRIVATE
|MAP_ANONYMOUS)
free() → munmap()
NO
Heap (brk/sbrk)
Free list reuse
free() → back to pool
#include <malloc.h>
#include <stdio.h>

/* Adjust the mmap threshold */
int main(void)
{
    /* Default: allocations > 128 KB use mmap */
    /* Change threshold to 512 KB */
    mallopt(M_MMAP_THRESHOLD, 512 * 1024);

    /* Now only allocations > 512 KB will use mmap */
    /* Smaller allocations go through the heap */

    /* Check current stats */
    struct mallinfo2 mi = mallinfo2();
    printf("mmap allocations: %zu\n", mi.hblks);   /* # of mmap regions */
    printf("mmap bytes      : %zu\n", mi.hblkhd);  /* bytes via mmap */

    return 0;
}
Why mmap for large malloc? When free() is called on an mmap-backed block, glibc immediately calls munmap() and returns those pages to the OS. This avoids heap fragmentation where a large freed block sits unused in the middle of the heap and cannot be reclaimed.

MAP_ANONYMOUS vs /dev/zero — Side-by-Side

Aspect MAP_ANONYMOUS /dev/zero
Origin BSD System V
File descriptor fd = -1 (no fd needed) Must open /dev/zero first
Portability Most Linux/BSD systems Most UNIX systems
POSIX standard Not in SUSv3 Not in SUSv3
Initialization Zero-filled Zero-filled
Feature macro _BSD_SOURCE or _SVID_SOURCE None needed
Modern preference ✓ Preferred Used for portability

When to Use Private vs Shared Anonymous Mappings
Use MAP_PRIVATE | MAP_ANONYMOUS when:
  • You need process-local scratch memory
  • Implementing a custom allocator
  • Large temporary buffers
  • Stack-like memory arenas
  • Replacing large malloc calls
Use MAP_SHARED | MAP_ANONYMOUS when:
  • Parent-child IPC after fork()
  • Passing results from worker child to parent
  • Shared counters or flags between related processes
  • Lighter alternative to System V / POSIX shared memory

Interview Questions & Answers

Q1. What is an anonymous mapping? How is it different from a file mapping?

An anonymous mapping is created with mmap() but has no corresponding file. Pages are backed only by physical RAM (and swap if needed) and are initialized to zero. A file mapping maps a region of a file into the process’s address space — reads and writes go through the page cache to the file. Anonymous mappings have no file backing and no persistent storage — when the mapping is unmapped, the data is gone.

Q2. What are the two methods to create an anonymous mapping on Linux?

The first method uses MAP_ANONYMOUS in the flags with fd = -1. The second method opens /dev/zero and passes its file descriptor. Both produce identical results: zero-initialized pages with no file backing. MAP_ANONYMOUS derives from BSD; /dev/zero derives from System V.

Q3. How does glibc malloc() use anonymous mappings internally?

For allocations larger than MMAP_THRESHOLD (128 KB by default), glibc’s malloc() calls mmap(MAP_PRIVATE | MAP_ANONYMOUS) to get memory directly from the OS. When free() is called on such a block, glibc calls munmap() to return the pages immediately. This avoids heap fragmentation because large freed blocks do not stay in the heap free list.

Q4. How do you share memory between parent and child processes using anonymous mappings?

Create a MAP_SHARED | MAP_ANONYMOUS mapping before calling fork(). The child inherits the mapping. Both parent and child then share the same physical pages. Writes by either process are immediately visible to the other. This works because fork() duplicates the parent’s virtual memory layout, including all existing mappings, and a MAP_SHARED mapping is shared rather than copied.

Q5. Why do anonymous mappings start as zero-filled?

For security. If pages were handed to a new process with their old contents from a previous process, sensitive data (passwords, keys, personal data) could be leaked. The kernel zeroes pages before handing them to a new mapping. The exception is MAP_UNINITIALIZED, which skips zeroing for performance on dedicated embedded systems where there is no multi-tenant risk.

Q6. After calling mmap() with /dev/zero, can you close the file descriptor?

Yes. The file descriptor is only needed during the mmap() call. Once the mapping is established, the kernel maintains a reference to the underlying object internally. You can — and should — close the fd immediately after mmap() returns to avoid leaking file descriptors.

Q7. What feature test macro is needed for MAP_ANONYMOUS?

You must define either _BSD_SOURCE or _SVID_SOURCE before including <sys/mman.h>. On glibc 2.20+, _DEFAULT_SOURCE also works. MAP_ANON is a synonym and may be available without the macro on some systems. For maximum portability, define _BSD_SOURCE explicitly.

Q8. What is /dev/zero and what happens when you read or write it?

/dev/zero is a special virtual device file on Linux/UNIX. Reading from it always returns zero bytes — as many as you want, infinitely. Writing to it always succeeds but discards all written data. It is commonly used with dd to create zero-filled files (dd if=/dev/zero of=file bs=1M count=10) and with mmap() to create zero-initialized anonymous mappings.

Q9. Can unrelated processes share an anonymous mapping?

No. An anonymous mapping exists only within the process that created it and any children created via fork(). Unrelated processes have completely separate address spaces and cannot access the same anonymous mapping. For unrelated processes to share memory, use POSIX shared memory (shm_open()) or System V shared memory (shmget()), which have named identifiers that both processes can open independently.

Q10. What is the difference between MAP_PRIVATE anonymous and MAP_SHARED anonymous after fork()?

After fork(), a MAP_PRIVATE anonymous mapping is duplicated using Copy-on-Write. Parent and child start sharing physical pages, but as soon as either writes to a page, that page is copied so both get their own private copy. Changes are not visible to the other process.

A MAP_SHARED anonymous mapping is truly shared. Writes by either process go directly to the shared pages and are immediately visible to the other. There is no copying — both processes always see the same data.

Series Complete for Chapter 49.6 & 49.7

You have covered additional mmap flags and anonymous mappings in depth.

← Back: mmap Flags EmbeddedPathashala Home

Leave a Reply

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