Anonymous vs File-Backed Memory
Every page of usable memory in a running Linux process belongs to one of two great families, and which family a page belongs to decides almost everything the kernel does with it. File-backed pages cache the contents of a file (or block device) on disk; they live in the page cache, and when the kernel needs to reclaim one it can simply drop a clean copy (the file is the backing store) or write a dirty one back to that file. Anonymous pages back memory that has no file behind it — the heap, the stack,
MAP_ANONYMOUSregions — and “represent memory that is not backed by a filesystem” (kernel admin-guide, Concepts overview). Because there is no file to write them to, the kernel can only reclaim an anonymous page by copying it out to swap. This single distinction — is there a file behind this page? — propagates through the reverse-mapping machinery (rmap), the LRU lists, the reclaim path, and theswappinesstunable. This note is about that split and everything it drives.
Mental Model — Two Families, One Question
The defining question for any user page is where can the kernel put this page if it needs the physical frame back? For a clean file page the answer is “nowhere — the file already holds it, just drop the frame.” For a dirty file page it is “write it back to the file.” For an anonymous page there is no file, so the answer is “copy it to swap, or if there is no swap, you cannot reclaim it at all and must invoke the OOM killer.” Everything else — how the page is found from a physical frame, which LRU list it sits on, how swappiness biases reclaim — is downstream of that one answer.
flowchart TB subgraph PG["A user page frame (struct folio)"] Q{"Backed by a file?"} end Q -->|"yes"| FILE["FILE-BACKED<br/>(page cache)<br/>mapping = address_space of an inode"] Q -->|"no"| ANON["ANONYMOUS<br/>heap / stack / MAP_ANONYMOUS<br/>mapping = anon_vma (low bit set)"] FILE -->|"reverse map via"| IMMAP["inode->i_mmap<br/>interval tree of VMAs"] ANON -->|"reverse map via"| AVMA["anon_vma chains<br/>(folio_add_anon_rmap)"] FILE -->|"reclaim: clean"| DROP["drop the frame<br/>(re-read from file later)"] FILE -->|"reclaim: dirty"| WB["writeback to the file<br/>(bdi flusher)"] ANON -->|"reclaim"| SWAP["write to SWAP<br/>(swap slot + swap cache)"] FILE -. "ages on" .-> LF["LRU_*_FILE lists"] ANON -. "ages on" .-> LA["LRU_*_ANON lists"]
The fundamental split. What it shows: a page frame is classified by whether a file backs it; that classification picks the reverse-mapping structure used to find every PTE that maps it (interval tree on the inode for file pages, anon_vma chains for anonymous pages), the reclaim destination (drop/writeback for file, swap for anonymous), and the LRU list family it ages on. The insight to take: “anonymous vs file-backed” is not a label — it is a fork in the code that runs through rmap, reclaim, and the LRU. Get this fork right and the rest of mm/ reclaim falls into place.
What Each Family Is
File-backed pages and the page cache
When a process reads a file, the kernel does not hand the data straight to the application from disk every time. It reads the file’s blocks into the page cache — an in-RAM cache of file contents — so that “the data is put into the page cache to avoid expensive disk access on the subsequent reads” (Concepts overview). A file-backed page is identified by the fact that its struct folio/struct page mapping field points at the struct address_space of an inode (the per-file index of cached pages — see Address Space and the Page Cache XArray), and the folio carries an index giving its offset within the file.
File-backed pages arrive in memory two ways. A plain read()/write() populates the page cache through the filesystem’s read_folio/writeback paths; an mmap(MAP_PRIVATE|MAP_SHARED, fd) maps those same cache pages into a process’s address space so the file’s contents appear as ordinary memory (see The Page Cache). Either way, the file on disk is the authoritative copy. The cached page is just a faster mirror.
Anonymous pages
An anonymous mapping has no file behind it. The admin-guide is precise: anonymous mappings “are implicitly created for program’s stack and heap or by explicit calls to mmap(2)” with MAP_ANONYMOUS, and they “only define virtual memory areas that the program is allowed to access” until first touched (Concepts overview). The mmap(2) man page confirms MAP_ANONYMOUS “is not backed by any file; its contents are initialized to zero” (mmap(2)). Heap growth via brk/sbrk, thread stacks, and malloc’s large allocations (which glibc services with anonymous mmap) all produce anonymous memory.
Because there is no file, an anonymous page’s only durable backing store is swap. Until the page is actually selected for eviction it has no swap slot at all — it exists only in RAM. The instant reclaim decides to evict it, the kernel allocates a swap slot, records that slot in the PTE, and the page enters the swap cache, which is “a special aspect of the page cache” that anonymous pages use only “when slots are allocated in the backing storage for page-out” (Gorman, Page Frame Reclamation).
Reverse Mapping — Finding Every PTE That Maps a Page
Reclaim works from a physical frame: vmscan picks a struct folio off an LRU list and must unmap it from every process that has it mapped before the frame can be reused. But a struct page does not natively know which page-table entries point at it — the page tables map virtual→physical, and reclaim needs physical→virtual. That inverse is reverse mapping (rmap), and mm/rmap.c opens by stating its purpose exactly: “physical to virtual reverse mappings… the anon methods track anonymous pages, and the file methods track pages belonging to an inode” (mm/rmap.c, v6.12). The two families use two entirely different rmap structures — this is the most consequential place the split shows up.
File-backed reverse mapping uses the inode. A struct address_space (one per inode) carries an i_mmap field: an interval tree of every VMA that maps any part of that file. To find all PTEs mapping a file page at file offset N, reclaim walks i_mmap, finds the VMAs whose file-offset range covers N, and for each computes the virtual address and locates the PTE. This is naturally many-to-one: the same file page can be mapped by many processes (every process that mmaped the shared library), and the interval tree finds them all.
Anonymous reverse mapping uses the anon_vma. Anonymous pages have no inode, so the kernel builds a parallel structure: each anonymous VMA links to an anon_vma object, and a page’s mapping field points at that anon_vma (with the low bit set — PAGE_MAPPING_ANON — to distinguish it from a file address_space pointer). When a page is faulted in, folio_add_new_anon_rmap records the linkage (mm/memory.c, do_anonymous_page, v6.12). The clever part is fork: when a process forks, the child’s anonymous VMAs are chained to the parent’s anon_vma via anon_vma_chain objects, so a COW-shared anonymous page can still be traced to every process that shares it. Walking the anon_vma (and its chained children) yields every PTE.
The locking hierarchy in mm/rmap.c makes the parallel explicit: for file pages reclaim takes mapping->i_mmap_rwsem; for anonymous pages it takes anon_vma->rwsem (mm/rmap.c lock ordering, v6.12). folio_referenced() — the function that asks “has anyone touched this page recently?” during aging — dispatches to the anon or file walker based on which kind the folio is.
Uncertain
Verify: the exact field names and the precise mechanics of the
anon_vmachaining acrossfork(theanon_vma_chain/ “same_vma” / “rb” interval-tree details) as of 6.12/6.18. Reason: the header lock-ordering comment andfolio_add_new_anon_rmapwere read directly, but the full forked-anon_vmadata-structure shape was not traced line-by-line in this task. To resolve: readanon_vma_clone()/anon_vma_fork()andstruct anon_vma_chaininmm/rmap.candinclude/linux/rmap.hat thev6.12tag. uncertain
Shared vs Private Mappings — An Orthogonal Axis
“Anonymous vs file-backed” is one axis; shared vs private (MAP_SHARED vs MAP_PRIVATE) is a second, orthogonal one. The four combinations behave differently:
- Private file mapping (
MAP_PRIVATE,fd) — the common case for executables and libraries. Reads come from the page cache (file-backed). The first write triggers copy-on-write: the kernel allocates a fresh anonymous page, copies the file page into it, and points the PTE at the private copy. Writes are never propagated to the file. Crucially, a page in a private file mapping starts file-backed and becomes anonymous on first write — the per-page accounting moves from file to anon at that moment. This is why a process’s anonymous footprint includes dirtied copies of mapped-file data. - Shared file mapping (
MAP_SHARED,fd) —mmap-based file I/O. Writes go to the page cache page and are eventually written back to the file by the writeback machinery (see Dirty Pages and Writeback). Stays file-backed; multiple processes share the same physical page cache pages. - Private anonymous mapping (
MAP_ANONYMOUS|MAP_PRIVATE) — ordinary process memory: heap, stack,mallocarenas. Zero-filled on first access, COW-shared acrossfork, backed only by swap. - Shared anonymous mapping (
MAP_ANONYMOUS|MAP_SHARED) — anonymous memory shared between a process and itsforked children (and used byshmem/tmpfs internally). Backed by swap, but the pages live in ashmemaddress_space, so they are reclaimed through the swap path while being indexed like page-cache pages.
The mmap(2) man page is the primary reference for these flags (mmap(2)). The key takeaway: file-backed vs anonymous answers “where does it get reclaimed to,” while shared vs private answers “do writes propagate and is the page COW’d.”
How the Split Drives the LRU Lists
The reclaim subsystem keeps pages on LRU (least-recently-used) lists so it can evict the coldest pages first. Linux does not keep one global LRU; it keeps several, and the anonymous/file split is baked directly into the list enumeration. From include/linux/mmzone.h at the v6.12 tag:
enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE,
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
LRU_UNEVICTABLE,
NR_LRU_LISTS
};(include/linux/mmzone.h, v6.12). Line by line: there are five per-node LRU lists. Anonymous pages age on LRU_INACTIVE_ANON / LRU_ACTIVE_ANON; file pages age on LRU_INACTIVE_FILE / LRU_ACTIVE_FILE; and LRU_UNEVICTABLE is the special list for pages that cannot be reclaimed at all (see below). The helper is_file_lru() returns true exactly for the two file lists, and is_active_lru() for the two active lists — the kernel constantly branches on these.
Why split anon from file at the LRU level? Because the two families have radically different reclaim cost. Dropping a clean file page is nearly free; swapping out an anonymous page costs a write to disk plus the future read back. The kernel therefore wants to bias reclaim toward whichever family is cheaper and less likely to be needed again. The swappiness tunable (/proc/sys/vm/swappiness, and per-cgroup memory.swappiness) is exactly this knob: it weights how aggressively reclaim scans the anonymous lists versus the file lists. A low swappiness tells the kernel “prefer to drop file cache before swapping anonymous memory”; a high value evens them out (see Swappiness and Reclaim Balance for the precise scan-ratio computation). Because the lists are separate, reclaim can independently track how cold each family is and apply the bias.
The same split appears in the from-scratch Multi-Generational LRU (MGLRU) reclaim algorithm, which still classifies pages as anon or file (its generations are tracked per type), and in the per-node lruvec that holds all five lists.
The unevictable LRU
The fifth list, LRU_UNEVICTABLE, holds pages that reclaim must never touch — most importantly pages locked into RAM with mlock(). The kernel documentation explains it was added so that vmscan does not “spend a lot of time scanning the LRU lists looking for the small fraction” of unevictable pages it can never free (Unevictable LRU Infrastructure). Notably, “the unevictable list does not differentiate between file-backed and anonymous, swap-backed folios” — once a page is unevictable, which family it came from no longer matters, so the two families collapse into one list here (Unevictable LRU). See The Unevictable LRU and mlock.
How Each Family Is Reclaimed
When free memory falls below the low watermark, kswapd (or, under harder pressure, direct reclaim) walks the LRU lists. The handling diverges sharply by family:
- Clean file page — the simplest case. The page cache page is an exact copy of disk blocks that have not been modified, so reclaim simply unmaps it (via the file
rmapwalk) and frees the frame. A later access re-reads it from the file. This is why dropping page cache is “free” and whyfreeon Linux shows most RAM “used” by cache that is instantly reclaimable. - Dirty file page — the page has been written but not yet flushed. Reclaim cannot drop it (the changes would be lost), so it must be written back to the file first. The writeback is normally done ahead of time by the
bdiflusher threads (see Dirty Pages and Writeback); if reclaim hits a dirty page it generally skips it and relies on writeback to clean it, rather than blocking on I/O in the reclaim path. - Anonymous page — there is no file to write to. Reclaim must allocate a swap slot, add the page to the swap cache, write it to the swap device, change the PTE from “present” to a swap entry encoding the slot, and free the frame. The next access faults;
handle_pte_faultsees a non-present PTE and routes todo_swap_page, which reads the page back from swap (a major fault — see Minor and Major Faults). If there is no swap configured, anonymous pages are simply unreclaimable, and sustained anonymous pressure goes straight to the OOM killer.
The admin-guide summarizes the membership of the reclaimable set: “The most notable categories of the reclaimable pages are page cache and anonymous memory” (Concepts overview) — i.e. exactly our two families, reclaimed by the two different mechanisms.
Failure Modes and How to Diagnose
“My box has tons of RAM ‘used’ but nothing is leaking.” Most of it is almost certainly clean file-backed page cache. Check free -m: the buff/cache column is reclaimable file pages. This is healthy — the kernel uses idle RAM as cache and drops it instantly under pressure. Anonymous memory is the column that actually commits RAM you cannot get back without swap.
“It’s swapping even though there’s free file cache.” This is the classic swappiness interaction. If swappiness is high, reclaim scans anonymous lists aggressively and evicts anon pages to swap even while droppable file cache exists. Lowering /proc/sys/vm/swappiness biases reclaim toward dropping file cache first. The inverse — refusing to swap at all (swappiness=0-ish behavior) — can push the system into OOM sooner because anonymous memory becomes effectively unreclaimable.
“Per-process memory accounting confuses me.” In /proc/<pid>/status, RssAnon is anonymous resident memory and RssFile is file-backed resident memory; RssShmem is shared-anonymous/shmem. A private file mapping that has been written to shifts pages from RssFile to RssAnon at the moment of the COW write — surprising if you expected mapped-file memory to stay “file.”
Uncertain
Verify: the exact
/proc/<pid>/statusfield names (RssAnon,RssFile,RssShmem) and/proc/<pid>/smapsAnonymous:semantics against theproc(5)/proc_pid_status(5)man page for the 6.12 era. Reason: stated from memory, not re-fetched in this task. To resolve: fetchman7.orgproc_pid_status(5). uncertain
Alternatives and Boundary Cases
The clean anon/file dichotomy has well-known fuzzy edges worth knowing:
- tmpfs / shmem is file-backed in structure (pages live in an
address_space, indexed by offset) but swap-backed in reclaim (there is no real file to write to, so dirty pages go to swap). It is the canonical “in between” case and is whyshmemhas its own accounting bucket. MADV_FREE(viamadvise) marks anonymous pages as lazily freeable: the kernel may drop them without swapping if they are not re-dirtied before reclaim reaches them, turning expensive anon reclaim into cheap drop-on-clean reclaim. This blurs the “anon always costs a swap write” rule.MADV_DONTNEEDon anonymous memory discards the pages immediately; a later read returns zeroes (back to the zero page), so the region reverts to the unpopulated, demand-zero state.
Production Notes
The anon/file split is the lens through which production memory problems are usually diagnosed. Container platforms expose it directly: cgroup-v2’s memory.stat reports anon and file byte counts separately, and a container thrashing on its memory.max limit is read very differently depending on whether the pressure is anonymous (real working-set growth — likely needs more memory or has a leak) or file (reclaimable cache — usually benign). The memcg controller runs per-cgroup reclaim over per-cgroup LRU lists that preserve the same five-way split. Pressure Stall Information (PSI) memory pressure rising while file cache is large but anon is near the limit is the signature of a workload that has outgrown its memory and is one step from a cgroup OOM kill.
See Also
- Demand Paging — how pages of either family are first populated (this note’s sibling): demand-zero for anonymous, demand-fill for file.
- The Page Fault Handler — the code path (
handle_pte_fault→do_anonymous_page/do_fault/do_swap_page) that classifies and populates faults. - Copy-on-Write and fork — how a private file page becomes anonymous, and how
forkshares anonymous pages. - The Page Cache / Address Space and the Page Cache XArray — the structure that holds file-backed pages.
- The Swap Cache / Linux Swap Subsystem / Swappiness and Reclaim Balance — where anonymous pages go under reclaim, and the knob that biases the split.
- The LRU Lists / Multi-Generational LRU / The Unevictable LRU and mlock — the lists the split drives.
- Memory Reclaim Overview / Dirty Pages and Writeback — clean-drop vs writeback vs swap.
- Page Table Entry — the hardware PTE that encodes “present” (mapped) vs a swap entry (swapped out). Memory Management Unit — the hardware that consumes it.
- MOC: Linux Memory Management MOC (§3 The Page-Fault Handler and Demand Paging).