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
To understand anonymous mappings, contrast them with file mappings:
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.
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.
_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;
}
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.
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
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.
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;
}
#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;
}
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.
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.0x7f001000
0x7f001000
#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 */
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;
}
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;
}
Understanding when malloc uses mmap vs the heap (sbrk) is important for performance tuning:
|MAP_ANONYMOUS)
free() → munmap()
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;
}
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 |
- You need process-local scratch memory
- Implementing a custom allocator
- Large temporary buffers
- Stack-like memory arenas
- Replacing large malloc calls
- 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
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.
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.
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.
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.
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.
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.
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.
/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.
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.
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.
