Anonymous Shared Memory

Anonymous shared memory is a region created with mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0) — a writable, shared mapping with no named backing file. It is the cleanest way for a parent and its forked children (or cloned threads) to share a block of memory: the parent creates the mapping before forking, and because mappings are inherited across fork, parent and child end up pointing at the same physical pages, so a write by one is instantly visible to the other (mmap(2)). Despite the word “anonymous,” the kernel does not leave such a mapping unbacked: when MAP_SHARED | MAP_ANONYMOUS is requested, the kernel quietly creates an internal, unnamed shmem/tmpfs file to hold the pages (see shmem_zero_setup() in mm/shmem.c), because a shared region needs a stable kernel object to anchor the page-table entries of multiple address spaces to. This is the IPC twin of MAP_PRIVATE | MAP_ANONYMOUS — the ordinary “give me zeroed memory” used by malloc — except that the shared variant survives the copy-on-write split that fork would otherwise impose.

Mental Model

The right way to think about anonymous shared memory is: it is tmpfs-backed shared memory with the filename erased. Three Linux memory concepts collapse into one here. (1) An anonymous mapping normally means “memory not backed by a file on disk” — what you get from malloc, a thread stack, or mmap(MAP_ANONYMOUS). (2) Shared memory means “the same physical pages appear in two or more page tables, so writes propagate.” (3) tmpfs/shmem is the in-RAM filesystem the kernel uses to hold page-cache-backed memory that has no real disk file. Anonymous shared memory sits exactly at the intersection: it is shared (so it needs a kernel object whose pages multiple mappers can attach to), but anonymous (so that object has no path you can open). The kernel reconciles these by creating a tmpfs inode with its link count cleared — an unlinked file that exists only as long as something maps it.

flowchart TB
  subgraph BEFORE["Parent calls mmap(MAP_SHARED|MAP_ANONYMOUS) BEFORE fork"]
    P0["Parent VMA<br/>vm_flags: VM_SHARED<br/>vm_file -> unnamed tmpfs inode"]
    P0 --> INODE["Internal shmem inode<br/>(nlink = 0, no path)<br/>holds the physical pages"]
  end
  subgraph AFTER["After fork()"]
    PP["Parent VMA<br/>vm_file -> same inode"]
    CC["Child VMA (inherited)<br/>vm_file -> same inode"]
    PP --> SHARED["Same physical pages<br/>(page cache of the tmpfs inode)"]
    CC --> SHARED
  end
  BEFORE -.->|"fork copies the VMA,<br/>not the pages"| AFTER

How a MAP_SHARED | MAP_ANONYMOUS region is set up and inherited. What it shows: the parent’s virtual memory area (VMA) points at an internal, pathless tmpfs inode whose page cache is the shared memory; fork duplicates the VMA descriptor (and its vm_file pointer) into the child, so both processes’ page tables resolve to the same pages. The insight to take: sharing is achieved not by copying pages but by both VMAs naming the same kernel file object — which is exactly why the mapping must be created before the fork, and why it stays shared instead of copy-on-write-splitting the way an anonymous private mapping would.

Mechanical Walk-through

Start from the syscall. A call to mmap(addr, length, prot, flags, fd, offset) with flags containing both MAP_SHARED and MAP_ANONYMOUS reaches the kernel’s do_mmap() path. MAP_ANONYMOUS means “the mapping is not backed by any file; its contents are initialized to zero,” and the fd argument is ignored (portable code passes -1) (mmap(2)). MAP_SHARED means “updates to the mapping are visible to other processes mapping the same region” (mmap(2)). The combination — supported since Linux 2.4 per the man page — is the interesting case.

In mm/mmap.c, after the VMA is allocated, the kernel branches on whether there is a file and whether the mapping is shared. For an anonymous mapping with VM_SHARED set, it does not take the plain-anonymous path; instead it calls shmem_zero_setup(vma) (mm/mmap.c v6.12, mmap_region):

    } else if (vm_flags & VM_SHARED) {
        error = shmem_zero_setup(vma);   /* shared anon -> back with a tmpfs file */
        ...
    } else {
        vma_set_anonymous(vma);          /* private anon -> truly file-less */
    }

That single if/else is the whole story: shared anonymous memory gets a backing file (shmem_zero_setup), private anonymous memory does not (vma_set_anonymous). The asymmetry exists because a private anonymous page can live entirely inside one mm_struct’s page tables and be reclaimed via the anon-rmap/swap machinery, whereas a shared page must be reachable from several address spaces independently of any one of them, which requires a shared kernel object — an inode with a page cache.

Now look at shmem_zero_setup() itself (mm/shmem.c v6.12):

int shmem_zero_setup(struct vm_area_struct *vma)
{
    struct file *file;
    loff_t size = vma->vm_end - vma->vm_start;
 
    file = shmem_kernel_file_setup("dev/zero", size, vma->vm_flags);
    if (IS_ERR(file))
        return PTR_ERR(file);
 
    if (vma->vm_file)
        fput(vma->vm_file);
    vma->vm_file = file;
    vma->vm_ops = &shmem_anon_vm_ops;
 
    return 0;
}

Line by line: it computes the region size from the VMA’s start and end addresses; it calls shmem_kernel_file_setup("dev/zero", size, ...) to create a fresh file on the kernel-internal shm_mnt tmpfs mount (the name "dev/zero" is purely cosmetic — it is what shows up in /proc/<pid>/maps, not a path you can open); it stores that file in vma->vm_file so the VMA now has a backing object; and it installs shmem_anon_vm_ops as the VMA’s fault handlers. From this point the mapping behaves like a MAP_SHARED mapping over a tmpfs file — except the file has no name.

The “unnamed” part is concrete. Following the call chain into __shmem_file_setup() (mm/shmem.c v6.12):

    inode = shmem_get_inode(&nop_mnt_idmap, mnt->mnt_sb, NULL,
                            S_IFREG | S_IRWXUGO, 0, flags);
    ...
    inode->i_size = size;
    clear_nlink(inode);   /* It is unlinked */

clear_nlink(inode) sets the inode’s hard-link count to zero — the inode is born unlinked. A normal file with nlink == 0 would be deleted; this one survives only because the open struct file (and, transitively, the VMAs that hold it) keep a reference. This is the kernel mechanism that makes the memory “anonymous”: there is a real inode and a real page cache, but nothing in any directory points to it, so it has no name and no path. (shmem_kernel_file_setup additionally passes S_PRIVATE, which bypasses Linux Security Module (LSM) checks on the inode, since the file is reachable only through its mapping.)

Why fork keeps it shared. When a process calls fork, the child gets a copy of the parent’s address space layout — each VMA is duplicated, including its vm_flags and its vm_file pointer. The man page states it plainly: “Memory mapped by mmap() is preserved across fork(2), with the same attributes” (mmap(2)). For a VM_SHARED VMA, “with the same attributes” means the child’s VMA also carries VM_SHARED and also points at the same backing inode. The pages themselves are not copied; both processes’ page-table entries are populated (lazily, on fault) from the same tmpfs page cache. So a store by the child to address X lands in the same physical page the parent reads at its own address for X.

Contrast this sharply with MAP_PRIVATE | MAP_ANONYMOUS, the everyday malloc/brk substitute. A private mapping is “a private copy-on-write mapping. Updates to the mapping are not visible to other processes” (mmap(2)). After fork, parent and child initially share the physical pages read-only, but the first write by either side triggers a copy-on-write (CoW) fault: the kernel allocates a private copy, points the writer’s page-table entry at it, and the two processes diverge. That CoW split is exactly what MAP_SHARED suppresses — the shared VMA’s pages are writable-shared, never copied. The mental shortcut: MAP_PRIVATE anonymous memory is “private after fork”; MAP_SHARED anonymous memory is “shared after fork.” That one-word difference is the entire IPC mechanism. See MAP_SHARED vs MAP_PRIVATE for the full CoW story and Anonymous vs File-Backed Memory for the anonymous/file-backed axis.

Lifetime. Because the backing inode is unlinked from birth, the region’s lifetime is governed purely by references. Each VMA mapping the region holds a reference to the struct file, which holds a reference to the inode. When a process munmaps the region (or exits, which unmaps everything), its VMA drops its reference. When the last mapper unmaps, the file’s refcount hits zero, the inode’s refcount follows, and — with nlink already zero — the inode and all its pages are freed. There is no ipcrm, no shm_unlink, no leak: the memory disappears the instant nobody maps it. This is a major ergonomic advantage over System V shared memory (which persists in a global namespace until explicitly removed — see System V Shared Memory) and even over POSIX /dev/shm objects (which persist until shm_unlink).

Code and Configuration

A complete parent/child example. The parent creates the shared region, forks, and the two processes communicate through it.

#include <sys/mman.h>
#include <unistd.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
 
struct shared {
    atomic_int flag;       /* simple hand-off signal */
    char msg[64];
};
 
int main(void) {
    /* 1. Create the shared region BEFORE fork. fd is -1 for MAP_ANONYMOUS. */
    struct shared *s = mmap(NULL, sizeof(*s),
                            PROT_READ | PROT_WRITE,
                            MAP_SHARED | MAP_ANONYMOUS,   /* the key combo */
                            -1, 0);
    if (s == MAP_FAILED) { perror("mmap"); return 1; }
 
    /* Memory is zero-initialized by MAP_ANONYMOUS, so flag == 0, msg == "". */
    atomic_store(&s->flag, 0);
 
    pid_t pid = fork();
    if (pid == 0) {
        /* 2. Child: write into the SAME physical pages the parent sees. */
        strcpy(s->msg, "hello from child");
        atomic_store(&s->flag, 1);          /* publish */
        _exit(0);
    }
 
    /* 3. Parent: spin until the child publishes (toy sync; use a futex
          in real code -- see the cross-links). */
    while (atomic_load(&s->flag) == 0)
        ;                                    /* busy-wait, illustrative only */
    printf("parent read: %s\n", s->msg);     /* prints child's message */
 
    munmap(s, sizeof(*s));                    /* drop this mapper's reference */
    return 0;
}

Commentary on the load-bearing lines. The mmap call passes -1 as the file descriptor (required by some implementations and recommended by the man page when MAP_ANONYMOUS is set) and 0 as the offset (ignored for anonymous maps). The region is zeroed for free — MAP_ANONYMOUS guarantees “contents are initialized to zero,” so s->flag starts at 0 and s->msg is an empty string without any explicit memset. The fork happens after the mmap; reverse the order and each process would get its own private region, communicating nothing. The child’s strcpy and atomic_store write to the shared pages; the parent’s atomic_load reads them back. The atomics matter: the C11 atomic_int ensures the flag write is not torn and is observed in order relative to the msg write — without that, the parent might see flag == 1 before msg is visible. The busy-wait is deliberately a toy; production code replaces it with a futex or process-shared semaphore living in the region.

You can see the backing file. After mapping, inspect /proc/self/maps: a MAP_SHARED | MAP_ANONYMOUS region appears with the pathname /dev/zero (deleted) — the "dev/zero" name shmem_zero_setup gave the inode, plus the (deleted) suffix the kernel appends to any file-backed mapping whose file has been unlinked (proc_pid_maps(5)). That (deleted) is the visible fingerprint of the clear_nlink we saw above. Since Linux 6.2 you can also name such a region with prctl(PR_SET_VMA_ANON_NAME, ...), after which it shows as [anon_shmem:yourname] in /proc/<pid>/maps — a debugging aid added precisely because anonymous shared regions were otherwise indistinguishable in process maps (proc_pid_maps(5), LWN 913935).

Anonymous-Shared vs /dev/shm — When to Use Which

There are two ways to get shared, RAM-backed memory in Linux, and they differ on exactly one axis: whether the backing object has a name.

Anonymous shared (MAP_SHARED | MAP_ANONYMOUS) has no name. Only processes that inherit the mapping across fork/clone can reach it — there is no path, descriptor, or key by which an unrelated process could find it. This is its strength (zero namespace pollution, automatic cleanup, nothing to leak, nothing for an attacker to open) and its limitation (related processes only). Use it whenever the sharers have a common ancestor: a parent farming work to forked workers, a process and its threads sharing a control block, a library that wants scratch memory shared with children it spawns.

Named POSIX shared memory (shm_open + mmap(MAP_SHARED) over /dev/shm/<name>) has a name. The object lives in the tmpfs mounted at /dev/shm, “a shared memory object will exist until the system is shut down, or until all processes have unmapped the object and it has been deleted with shm_unlink(3)” (shm_overview(7)). Because it has a path, any process that knows the name (and has permission) can shm_open and map it — no shared ancestry required. The cost is that you now own a named object with kernel persistence: it survives every process exit and must be explicitly shm_unlinked, or it leaks until reboot. Use it when the sharers are unrelated — separately launched daemons, a server and clients started independently, anything where there is no common ancestor to inherit a mapping from. See POSIX Shared Memory and shm_open and Shared Memory via mmap.

A useful way to remember it: both are tmpfs underneath; anonymous shared memory is /dev/shm with the filename deleted immediately and inheritance as the only access path. If you want unrelated processes to find the memory, keep the name (/dev/shm); if everyone who needs it will inherit it, drop the name (anonymous) and let cleanup happen automatically.

There is also a middle option, memfd_create: an anonymous tmpfs file referenced by a file descriptor (no path, but passable over a Unix socket via SCM_RIGHTS), optionally sealed against modification. That covers the case where the sharers are unrelated but you can hand them an fd rather than a name.

Failure Modes and Common Misunderstandings

Mapping after the fork. The single most common bug: calling mmap(MAP_SHARED|MAP_ANONYMOUS) after fork rather than before. Each process then gets a distinct anonymous region; nothing is shared, and the “communication” silently fails. The mapping must exist in the common ancestor at fork time so both inherit the same VMA. There is no way to retroactively share an anonymous region with an already-existing unrelated process — that is precisely what the named /dev/shm variant is for.

Assuming MAP_PRIVATE shares across fork. Developers who know fork shares pages CoW sometimes assume an anonymous private mapping lets parent and child communicate. It does not: the first write CoW-splits the page and the two diverge. Only MAP_SHARED keeps writes mutually visible. The flag is the whole difference (MAP_SHARED vs MAP_PRIVATE).

No synchronization is included. Shared memory carries zero synchronization. Two processes writing the same word race exactly as two threads would; a reader can observe a half-written structure. The region must be paired with a synchronization primitive — a cross-process futex or a PTHREAD_PROCESS_SHARED mutex/semaphore placed inside the shared region itself. A mutex in process-private memory cannot coordinate two processes. See Synchronizing Shared Memory with Futexes and Semaphores.

Memory accounting confusion. Anonymous shared pages count as Shmem in /proc/meminfo and as shared in free(1), not as anonymous memory, because they are tmpfs-backed. Engineers debugging memory usage are sometimes surprised that a “shared anonymous” allocation shows up under shared/tmpfs rather than anon — this is correct and follows directly from the shmem_zero_setup backing (famzah 2020). Like all tmpfs, the pages can be pushed to swap under memory pressure (they are not pinned unless you mlock them).

MAP_NORESERVE and overcommit. Without MAP_NORESERVE, the kernel may reserve swap for the region; with it, a write to a page for which no memory is available raises SIGBUS/SIGSEGV (mmap(2)). For large sparse shared regions, this is a real consideration.

Uncertain

Verify: that an anonymous-shared region’s pages are reclaimable to swap (not pinned) under memory pressure, the same as ordinary tmpfs. Reason: this follows logically from the tmpfs/shmem backing and is stated for tmpfs generally, but I did not trace the specific reclaim path for shmem_anon_vm_ops in mm/shmem.c at v6.12. To resolve: confirm shmem_writepage handles anon-shared folios identically to named-tmpfs folios. uncertain

Production Notes

Anonymous shared memory is the backbone of many fork-based servers and runtimes. A classic pattern is a master process that maps a shared control/statistics region, then forks a pool of workers that all update the same counters — used by web servers and connection multiplexers to aggregate per-worker stats without IPC round-trips. Because cleanup is automatic, a crashing worker cannot leak the region (contrast System V shared memory, where a crashed process famously leaves segments behind that ipcs/ipcrm must mop up — see System V Shared Memory and ipcs ipcrm and System V IPC Limits).

The naming feature (PR_SET_VMA_ANON_NAME, Linux 6.2) exists because production debugging of large processes with many anonymous-shared regions was painful: every region showed as /dev/zero (deleted), indistinguishable in /proc/<pid>/smaps. Naming them lets per-region memory be attributed to a subsystem in tooling — the motivating use case was Android, where the kernel feature originated, and it was generalized upstream (LWN 913935).

When the sharers are threads rather than processes, note that threads already share the entire address space (they share one mm_struct), so anonymous shared memory is unnecessary between threads — a plain malloc’d buffer is already shared. Anonymous shared memory earns its keep specifically at the process boundary created by fork, which is where threads’ shared address space ends.

See Also