Anonymous Mappings
Intermediate
TLPI Ch49
TI / ST / Qualcomm
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).
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.
zeroed RAM
private to process
zeroed RAM
private to process
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;
}
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.
mmap() โ kernel sets up VMA (virtual memory area) entry only. No physical RAM yet.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);
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.
Sees child’s changes
(no COW)
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;
}
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;
}
glibc’s malloc() uses two strategies depending on allocation size:
brk() heap โ a single contiguous region grown by brk()/sbrk(). Fast for many small allocs. Heap can only grow, rarely shrinks.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./*
* 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);
| 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 |
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.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).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).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).-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.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.Part 4 of 5 โ Anonymous Mappings
โ Part 3: File Mappings Next: Advanced Topics โ ๐ Home
