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.
This note pins its code claims to Linux 6.12 LTS (released 2024-11-17), a maintained long-term-support release; mainline is now in the 7.x series and anything dated later is labelled. Command output labelled “on this machine” was captured while writing the note, on Linux 7.1.8-200.fc44.x86_64 with 128 GiB of RAM, vm.swappiness = 10, and an 8 GiB zram swap device — the kernel differs from the code pin, so read the outputs as illustrations of shape, not of 6.12 behaviour.
Scope — this note and its neighbours
The reclaim cluster in this vault is deep, and this note deliberately stops where its neighbours begin:
| Note | Owns |
|---|---|
| This note | The classification itself: what makes a folio anonymous or file-backed, the fault paths that create each, the two reverse-mapping structures, the COW seam where one becomes the other, the tmpfs middle case, and how to read the split out of /proc |
| Memory Reclaim Overview | The reclaim machinery: watermarks, direct vs background reclaim, MGLRU generations, shrinkers, and the /proc/vmstat diagnosis workflow |
| Swappiness and Reclaim Balance | The swappiness knob’s full semantics and tuning |
| Copy-on-Write and fork | The COW mechanism itself — do_wp_page, reuse-vs-copy, Dirty COW, the pin-aware fork path |
| The Swap Cache / Linux Swap Subsystem | Where an anonymous folio goes once reclaim picks it |
| The Page Cache / The Page Cache and address_space | The file-backed side’s own machinery: readahead, dirty thresholds, the a_ops vtable |
So: this note derives why the two families need different reclaim, and hands off the how of reclaim itself.
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.
The two lifecycles, side by side
The single most useful picture in this whole area is the two families’ lives drawn in parallel: where each is born, what backs it while it lives, what reclaim does to it, and what happens when it is touched again. Almost every practical question — “why is my RSS not going down?”, “why is the machine swapping with free cache?”, “why did dropping caches not help?” — is answered by locating yourself on one of these two tracks.
flowchart TB subgraph ANON["ANONYMOUS — no file behind it"] direction TB A1["**Birth**<br/>first touch of heap/stack/<br/>MAP_ANONYMOUS<br/>→ do_anonymous_page()"] A2["read-only first touch:<br/>PTE points at the shared<br/>**zero page** — no frame<br/>allocated at all"] A3["**Backed by**<br/>nothing. RAM only.<br/>folio->mapping = anon_vma<br/>with bit 0 set"] A4["**Reverse map**<br/>anon_vma interval tree,<br/>anon_vma_chain per VMA"] A5["**Reclaim**<br/>must allocate a swap slot,<br/>enter the swap cache,<br/>WRITE it out,<br/>rewrite the PTE as a swap entry"] A6["**No swap configured?**<br/>unreclaimable →<br/>pressure goes to the OOM killer"] A7["**Refault**<br/>PTE not present →<br/>do_swap_page() → read from swap<br/>= a MAJOR fault"] A1 --> A3 --> A4 --> A5 --> A7 A1 -.-> A2 A5 -.-> A6 end subgraph FILE["FILE-BACKED — an inode behind it"] direction TB F1["**Birth**<br/>read()/write() or a fault on a<br/>file mapping<br/>→ filemap_fault() / read_folio()"] F3["**Backed by**<br/>the file on disk.<br/>folio->mapping = address_space,<br/>folio->index = offset"] F4["**Reverse map**<br/>address_space->i_mmap,<br/>an rbtree interval tree of VMAs"] F5C["**Reclaim, clean**<br/>unmap and free the frame.<br/>No I/O. The file still has it."] F5D["**Reclaim, dirty**<br/>must be written back to the file<br/>first — normally already done<br/>by the bdi flusher"] F7["**Refault**<br/>re-read from the file<br/>= a MAJOR fault<br/>(or minor, if still cached)"] F1 --> F3 --> F4 --> F5C --> F7 F4 --> F5D --> F5C end ANON -.->|"a MAP_PRIVATE file page<br/>crosses over on first WRITE<br/>(copy-on-write)"| FILE FILE -.->|"and never crosses back"| ANON
The anonymous and file-backed lifecycles in parallel, v6.12. What it shows: the two tracks are structurally identical in shape — birth, backing, reverse map, reclaim, refault — and differ at exactly one place: the reclaim step. A clean file page’s reclaim is a free folio_put(); an anonymous page’s reclaim is a disk write plus a PTE rewrite plus a future disk read. Everything else on the diagram is downstream of that asymmetry. The insight to take: notice the dashed arrow at the bottom. There is exactly one legal crossing between the families, it goes file → anonymous, it happens on the first write to a MAP_PRIVATE file mapping, and it is one-way. That single arrow explains why a process’s anonymous footprint can grow when it never called malloc(), and it is the reason copy-on-write and this note are neighbours.
To make the asymmetry concrete: on this machine, /proc/vmstat records 202,484,088 file pages stolen by reclaim against 3,430,896 anonymous ones — a 59:1 ratio — with vm.swappiness = 10. The kernel has scanned 318 million file pages and 5.2 million anonymous ones. That is not an accident of the workload; it is the policy consequence of the diagram above, and the next several sections trace how the code gets from “is there a file behind this?” to that ratio.
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).
The one-line definition that actually decides it
All of the prose above is downstream of a single predicate in include/linux/mm.h:
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
return !vma->vm_ops;
}That is the whole thing. A VMA is anonymous if and only if it has no vm_operations_struct vtable. A file mapping gets its vm_ops from the filesystem’s ->mmap handler (which is how ->fault becomes filemap_fault); an anonymous mapping has nobody to provide one, so the pointer is NULL, and the absence of the vtable is the classification. It is worth pausing on how economical that is: the kernel does not store a “this is anonymous” flag on the VMA, because the absence of a way to fetch data from a backing object already means exactly that.
How the Kernel Actually Decides — The Fault Dispatch
The classification is not consulted once at mmap() time and recorded; it is re-evaluated on every fault that finds no page-table entry. Following handle_pte_fault() down is the fastest way to see the split as executable code rather than as taxonomy.
flowchart TB HF["handle_pte_fault(vmf)<br/>mm/memory.c, v6.12"] HF --> Q1{"is there a PTE at all?<br/>(vmf->pte == NULL)"} Q1 -->|"no — nothing mapped"| DPM["do_pte_missing(vmf)"] Q1 -->|"yes"| Q2{"pte_present(orig_pte)?"} Q2 -->|"no — a swap entry"| SWP["**do_swap_page()**<br/>read the folio back from swap<br/>ANONYMOUS only<br/>(or shmem, via the swap cache)"] Q2 -->|"yes, and a write<br/>to a read-only PTE"| WP["**do_wp_page()**<br/>break COW → see<br/>Copy-on-Write and fork"] DPM --> Q3{"vma_is_anonymous(vma)?<br/>i.e. vma->vm_ops == NULL"} Q3 -->|"**yes**"| DAP["**do_anonymous_page()**<br/>zero page for a read,<br/>a fresh anon folio for a write"] Q3 -->|"**no**"| DF["do_fault(vmf)"] DF --> Q4{"write fault?"} Q4 -->|"no"| DRF["**do_read_fault()**<br/>→ vma->vm_ops->fault<br/>= filemap_fault()<br/>maps the page-cache folio<br/>read-only. Stays FILE."] Q4 -->|"yes, and VM_SHARED"| DSF["**do_shared_fault()**<br/>maps the page-cache folio<br/>writable; dirties it;<br/>writeback goes to the file.<br/>Stays FILE."] Q4 -->|"yes, and MAP_PRIVATE"| DCF["**do_cow_fault()**<br/>allocate a NEW anon folio,<br/>copy the file page into it,<br/>point the PTE at the copy.<br/>**FILE → ANON**"]
The fault dispatch at v6.12, with the classification highlighted. What it shows: four leaves, three of which are decided by vma_is_anonymous() and the VM_SHARED flag. do_pte_missing() is a two-line function — if (vma_is_anonymous(vmf->vma)) return do_anonymous_page(vmf); else return do_fault(vmf); — and do_fault() then splits three ways on write-ness and sharedness. The insight to take: the family a page ends up in is decided at fault time, from the VMA’s flags, not from anything stored on the page. This is why the same file page can be file-backed in one process and anonymous in another (each do_cow_fault() produces a private copy), and why the RssFile → RssAnon migration measured later in this note happens exactly when the write fault runs, not when the mapping is created.
do_anonymous_page(), walked
The anonymous path is short enough to read in full, and two of its branches are the note’s whole thesis in code:
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct folio *folio;
int nr_pages = 1;
/* File mapping without ->vm_ops ? */
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;
if (pte_alloc(vma->vm_mm, vmf->pmd)) /* make sure a PTE table exists */
return VM_FAULT_OOM;
/* Use the zero-page for reads */
if (!(vmf->flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
...
goto setpte; /* no frame allocated at all */
}
/* Allocate our own private page. */
ret = vmf_anon_prepare(vmf); /* ensures vma->anon_vma exists */
if (ret)
return ret;
folio = alloc_anon_folio(vmf); /* may be a large (mTHP) folio */
...
nr_pages = folio_nr_pages(folio);
addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
__folio_mark_uptodate(folio); /* barrier: zeroes visible before the PTE */
entry = mk_pte(&folio->page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry), vma);
...
folio_ref_add(folio, nr_pages - 1);
add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages); /* <-- RssAnon += */
count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC);
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);/* <-- joins the anon_vma */
folio_add_lru_vma(folio, vma); /* <-- joins the ANON LRU */
setpte:
set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);
...
}Condensed from mm/memory.c at v6.12, with the four load-bearing lines marked. Commentary:
if (vma->vm_flags & VM_SHARED) return VM_FAULT_SIGBUS;— an anonymous VMA that is alsoMAP_SHAREDmust never reach here. Shared anonymous mappings are implemented byshmem, which does installvm_ops, so they take thedo_fault()path instead. Reaching this line means something is structurally wrong, henceSIGBUSrather than a fixup. This is the first hint that “shared anonymous” is not really anonymous in the kernel’s internal sense.- The zero-page branch. A read fault on untouched anonymous memory allocates no memory at all: the PTE is pointed at the system-wide zero page, marked
pte_mkspecial()so nobody mistakes it for a normal mapped folio. This is whymalloc(1 GiB)followed by reading it shows almost no RSS growth (see The Zero Page and Lazy Allocation). There is no equivalent trick on the file side — reading a file page requires actually fetching the data. alloc_anon_folio(vmf)— since the multi-size THP work, this may return an order-4 (64 KiB) or larger folio rather than a single page, and the fault then installsnr_pagesPTEs at once withset_ptes(). Large anonymous folios are exactly this (Corbet, LWN, July 2023); see Transparent Huge Pages for the policy and Folios and the Folio Conversion for the type.- The three accounting calls.
add_mm_counter(mm, MM_ANONPAGES, nr_pages)is literally whereRssAnonin/proc/<pid>/statusis incremented.folio_add_new_anon_rmap()setsfolio->mappingto the VMA’sanon_vmawithPAGE_MAPPING_ANONset — the moment the folio becomes anonymous in the reverse-mapping sense.folio_add_lru_vma()puts it on an anonymous LRU list. Family membership is established by these three lines, in this order, in this function.
The file-backed counterpart, filemap_fault(), is the mirror image: it looks the folio up in the inode’s page cache (filemap_get_folio()), reads it through a_ops->read_folio on a miss, and hands back vmf->page for the generic code to map. It is walked step by step in Folios and the Folio Conversion (which uses it to show where folio vocabulary hands back to page vocabulary) and its surrounding machinery lives in The Page Cache. The classification-relevant difference is what it does not do: it never sets folio->mapping to an anon_vma, because the folio’s mapping already points at the inode’s address_space and its index at the file offset — the folio was born knowing which file it belongs to.
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, and the file lock sits above the anon lock in the documented ordering — mapping->i_mmap_rwsem → anon_vma->rwsem → mm->page_table_lock or pte_lock (mm/rmap.c lock-ordering comment, 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.
How the discriminator is stored: one bit
The kernel does not spend a byte on a “this folio is anonymous” flag. It steals bit 0 of the mapping pointer, which is always clear on a real struct address_space * because the structure is aligned:
#define PAGE_MAPPING_ANON 0x1
#define PAGE_MAPPING_MOVABLE 0x2
#define PAGE_MAPPING_KSM (PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)
#define PAGE_MAPPING_FLAGS (PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)
static __always_inline bool folio_test_anon(const struct folio *folio)
{
return ((unsigned long)folio->mapping & PAGE_MAPPING_ANON) != 0;
}(include/linux/page-flags.h, v6.12)
The comment above these definitions is worth quoting in full because it names three traps at once. First: “On an anonymous folio mapped into a user virtual memory area, folio->mapping points to its anon_vma, not to a struct address_space; with the PAGE_MAPPING_ANON bit set to distinguish it.” Second, the KSM case: on a merged page in a VM_MERGEABLE area, PAGE_MAPPING_MOVABLE is set alongside PAGE_MAPPING_ANON, and mapping then points at a private KSM structure — so a naive folio_test_anon() is true but the pointer is not an anon_vma. Third, and the one that actually bites: “For slab pages, since slab reuses the bits in struct page to store its internal states, the folio->mapping does not exist as such… please make sure that folio_test_slab(folio) actually evaluates to false before calling the following functions (e.g., folio_test_anon).”
And a naming warning, again from the source: “Please note that, confusingly, folio_mapping refers to the inode address_space which maps the folio from disk; whereas folio_mapped refers to user virtual address space into which the folio is mapped.” folio_mapping() is file identity; folio_mapped() is is anyone’s page table pointing at this?. They are unrelated questions with almost the same name.
The two structures, drawn
flowchart TB subgraph FILERM["FILE-BACKED reverse map — keyed by the inode"] direction TB FO["struct folio<br/>mapping = &inode->i_data<br/>index = 137 (page offset in file)"] AS["struct address_space<br/>(one per inode)"] IM["i_mmap : rb_root_cached<br/>**interval tree** of every VMA<br/>that maps any part of this file"] V1["VMA in process A<br/>covers file pages 0-511"] V2["VMA in process B<br/>covers file pages 100-200"] V3["VMA in process C<br/>covers file pages 900-1000"] FO --> AS --> IM IM -->|"query: which VMAs<br/>cover page 137?"| V1 IM --> V2 IM -.->|"pruned — range<br/>does not overlap"| V3 V1 --> PTE1["vma_address() → virtual addr<br/>→ walk page tables → PTE"] V2 --> PTE2["vma_address() → PTE"] end subgraph ANONRM["ANONYMOUS reverse map — keyed by the anon_vma tree"] direction TB FA["struct folio<br/>mapping = anon_vma | 0x1<br/>index = offset from mmap start"] AV["struct anon_vma (88 bytes)<br/>root, rwsem, refcount,<br/>num_children, rb_root"] RB["rb_root : interval tree of<br/>**anon_vma_chain** nodes"] AVC1["anon_vma_chain<br/>vma → parent's VMA<br/>same_vma, rb, rb_subtree_last"] AVC2["anon_vma_chain<br/>vma → child's VMA<br/>(linked by anon_vma_fork)"] FA --> AV --> RB RB --> AVC1 --> PA["parent's PTE"] RB --> AVC2 --> PB["forked child's PTE<br/>— still COW-sharing this folio"] end
The two reverse-mapping structures, v6.12, with field names and sizes read from the running kernel’s BTF. What it shows: both are interval trees keyed by a page offset, but they hang off different owners — the inode for file pages, a per-mapping anon_vma for anonymous ones — and the anonymous side needs an extra indirection object, the anon_vma_chain, because a single VMA can belong to several anon_vmas at once. The insight to take: the interval-tree query is the same shape in both cases (“which VMAs cover folio offset N?”), which is why rmap_walk_anon() and rmap_walk_file() are near-identical functions. The difference is entirely in who owns the tree, and that difference exists only because anonymous memory has no inode to hang it off.
Why fork() forces the extra indirection
The anon_vma_chain is the part people find hardest, and it exists for one reason: after fork(), a single anonymous folio may be COW-shared by a parent and any number of descendants, and reclaim must still be able to find every PTE pointing at it. The code is explicit about the ordering constraint:
int anon_vma_fork(struct vm_area_struct *vma, struct vm_area_struct *pvma)
{
/* Don't bother if the parent process has no anon_vma here. */
if (!pvma->anon_vma)
return 0;
/* Drop inherited anon_vma, we'll reuse existing or allocate new. */
vma->anon_vma = NULL;
/*
* First, attach the new VMA to the parent VMA's anon_vmas,
* so rmap can find non-COWed pages in child processes.
*/
error = anon_vma_clone(vma, pvma);
...
/* Then add our own anon_vma. */
anon_vma = anon_vma_alloc();
...
/*
* The root anon_vma's rwsem is the lock actually used when we
* lock any of the anon_vmas in this anon_vma tree.
*/
anon_vma->root = pvma->anon_vma->root;
anon_vma->parent = pvma->anon_vma;
get_anon_vma(anon_vma->root);
/* Mark this anon_vma as the one where our new (COWed) pages go. */
vma->anon_vma = anon_vma;
anon_vma_lock_write(anon_vma);
anon_vma_chain_link(vma, avc, anon_vma);
anon_vma->parent->num_children++;
anon_vma_unlock_write(anon_vma);
return 0;
}Read it as three moves. anon_vma_clone() first: the child’s new VMA is linked into every anon_vma the parent’s VMA belonged to, via one anon_vma_chain node each. The comment says exactly why — “so rmap can find non-COWed pages in child processes.” Folios that existed before the fork still point at the parent’s anon_vma, so the child must be reachable from there. Then a fresh anon_vma for the child, which is “the one where our new (COWed) pages go” — folios the child creates after the fork belong to it alone, and reclaim need not walk the parent’s tree to find them. And a shared root lock: anon_vma->root = pvma->anon_vma->root, with a reference taken on the root, because the whole forest formed by repeated forking is locked through one rwsem at the root. That is also why an anon_vma can outlive the process it belonged to: the refcount on the root pins it.
The structures themselves, read from this machine’s BTF, are small: struct anon_vma is 88 bytes (root, rwsem, refcount, num_children, num_active_vmas, parent, rb_root) and struct anon_vma_chain is 64 bytes (vma, anon_vma, same_vma, rb, rb_subtree_last). The two list/tree memberships on the chain node are the crux: same_vma threads all the anon_vmas a single VMA belongs to, while rb places the node in one anon_vma’s interval tree. One object, two memberships, which is precisely the many-to-many relation a fork tree creates and a plain pointer could not express.
The cost of this design is a known scaling hazard: a process that forks deeply and repeatedly builds a wide anon_vma forest, and reclaiming a single old folio may walk many chains under one root lock. This is the anonymous side’s analogue of a file page mapped by a thousand processes, and it is why num_children/num_active_vmas exist — anon_vma_clone() uses them to decide whether an existing anon_vma can be reused rather than allocating a new one.
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.”
flowchart TB M["mmap() flags"] M --> PF["**MAP_PRIVATE + fd**<br/>executables, libraries,<br/>config files"] M --> SF["**MAP_SHARED + fd**<br/>mmap-based file I/O,<br/>shared databases"] M --> PA["**MAP_PRIVATE + MAP_ANONYMOUS**<br/>heap, stacks,<br/>malloc arenas"] M --> SA["**MAP_SHARED + MAP_ANONYMOUS**<br/>parent/child shared regions,<br/>memfd, SysV shm"] PF --> PFR["read fault: do_read_fault()<br/>→ **FILE-backed**, counted in RssFile"] PF --> PFW["write fault: do_cow_fault()<br/>→ private copy, **ANONYMOUS**,<br/>counted in RssAnon.<br/>Never written to the file."] SF --> SFA["do_shared_fault()<br/>→ stays **FILE-backed**;<br/>dirtying it schedules writeback<br/>to the real file"] PA --> PAA["do_anonymous_page()<br/>→ zero page on read,<br/>fresh **ANONYMOUS** folio on write;<br/>COW-shared across fork()"] SA --> SAA["implemented by **shmem/tmpfs**:<br/>has vm_ops, so it is FILE-backed<br/>in structure — but its 'file' has no<br/>disk, so reclaim sends it to SWAP.<br/>Counted in RssShmem, not RssAnon."]
The four mmap() combinations and which family each produces, v6.12. What it shows: three of the four quadrants are stable, and one — private file mapping — is a hybrid whose pages start file-backed and migrate to anonymous individually as they are written. The insight to take: the bottom-right quadrant is the one that breaks the taxonomy. “Shared anonymous” memory is not anonymous inside the kernel at all: it has vm_ops, so vma_is_anonymous() is false, it lives in a shmem address_space and is indexed like page cache — yet it has no disk file, so reclaim must swap it. It gets its own accounting bucket (RssShmem, Shmem in /proc/meminfo) precisely because it fits in neither column. See The Awkward Middle below and Anonymous Shared Memory.
The Crossing — Watching a File Page Become Anonymous
The MAP_PRIVATE file mapping is where the taxonomy is decided per page, at the moment of the first write, and it is the one part of this note that can be demonstrated in twenty lines of C rather than argued. The program below creates a 64 MiB file, maps it MAP_PRIVATE, reads every page, then writes to the first half, then touches a separate 32 MiB MAP_ANONYMOUS region for contrast — printing /proc/self/status at each step.
int fd = open(path, O_RDWR|O_CREAT|O_TRUNC, 0644);
for (int i = 0; i < 64; i++) write(fd, one_mib_of_data, MB);
fsync(fd);
char *p = mmap(NULL, 64*MB, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
for (unsigned long i = 0; i < 64*MB; i += 4096) acc += p[i]; /* read every page */
for (unsigned long i = 0; i < 32*MB; i += 4096) p[i] = 1; /* write half of it */
char *a = mmap(NULL, 32*MB, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
for (unsigned long i = 0; i < 32*MB; i += 4096) a[i] = 1; /* plain anonymous */Run against a file on btrfs (a real disk-backed filesystem), on this machine:
---- start ----
VmRSS: 2820 kB RssAnon: 1140 kB RssFile: 1672 kB RssShmem: 8 kB
---- after reading 64 MiB of the MAP_PRIVATE file mapping ----
VmRSS: 68356 kB RssAnon: 1140 kB RssFile: 67208 kB RssShmem: 8 kB
---- after writing the first 32 MiB (COW) ----
VmRSS: 68356 kB RssAnon: 33908 kB RssFile: 34440 kB RssShmem: 8 kB
---- after touching a 32 MiB MAP_ANONYMOUS region ----
VmRSS: 101124 kB RssAnon: 66676 kB RssFile: 34440 kB RssShmem: 8 kB
Read the arithmetic, because it is exact. Reading the mapping added 65,536 kB to RssFile and nothing to RssAnon — 64 MiB of page cache, mapped read-only. Writing half of it moved 32,768 kB from RssFile to RssAnon (67,208 → 34,440 and 1,140 → 33,908) while leaving VmRSS unchanged at 68,356 kB — no new memory was consumed, the same 32 MiB simply changed family. That is do_cow_fault() running 8,192 times. Then the plain anonymous mapping added its 32,768 kB to RssAnon and to VmRSS both, because that memory genuinely is new.
The same crossing is visible inside a single VMA in /proc/<pid>/smaps, which is the more surprising view:
7f9484200000-7f9488200000 rw-p 00000000 00:24 1992482 /var/tmp/folio-demo-blob.bin
Size: 65536 kB
Rss: 65536 kB
Private_Clean: 32768 kB <-- still the file's page-cache pages
Private_Dirty: 32768 kB <-- the COW copies
Anonymous: 32768 kB <-- ...which are ANONYMOUS, inside a file-backed VMA
Swap: 0 kB
The kernel’s own documentation states the rule that makes this legal: “Anonymous shows the amount of memory that does not belong to any file. Even a mapping associated with a file may contain anonymous pages: when MAP_PRIVATE and a page is modified, the file page is replaced by a private anonymous copy” (Documentation/filesystems/proc.rst, v6.12).
stateDiagram-v2 [*] --> NotPresent NotPresent : PTE empty<br/>nothing resident FileMapped : **FILE-BACKED**<br/>PTE points at the page-cache folio<br/>read-only, counted in RssFile<br/>Private_Clean in smaps AnonCopy : **ANONYMOUS**<br/>PTE points at a private copy<br/>writable, counted in RssAnon<br/>Private_Dirty + Anonymous in smaps Swapped : swap entry in the PTE<br/>counted in VmSwap Dropped : frame freed<br/>file still holds the data NotPresent --> FileMapped : read fault<br/>do_read_fault → filemap_fault NotPresent --> AnonCopy : write fault on an untouched page<br/>do_cow_fault (reads the file, then copies) FileMapped --> AnonCopy : **first write**<br/>do_wp_page / do_cow_fault<br/>ONE-WAY FileMapped --> Dropped : reclaim — free, no I/O Dropped --> FileMapped : refault, re-read from the file AnonCopy --> Swapped : reclaim — allocate a swap slot,<br/>write it out Swapped --> AnonCopy : refault, do_swap_page<br/>reads it back from swap AnonCopy --> AnonCopy : further writes are free<br/>(the page is already private) note right of AnonCopy There is no edge back to FileMapped. Once a page in a MAP_PRIVATE file mapping is written, it is anonymous for the life of the mapping. end note
The life of one page in a MAP_PRIVATE file mapping. What it shows: a single page-table slot can be in five states, and the transition from FileMapped to AnonCopy is the family crossing — irreversible, triggered by a write, and free of any change in total RSS. The insight to take: the reclaim consequences flip at that arrow. Before it, evicting the page costs nothing and re-reading it costs one file read; after it, evicting costs a swap write and re-reading costs a swap read, and if there is no swap it cannot be evicted at all. A process that writes a byte into every page of a large mapped read-only data file has silently converted a fully reclaimable working set into an unreclaimable one. See Copy-on-Write and fork for the do_wp_page mechanics and the reuse-versus-copy decision.
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)
flowchart TB LV["struct lruvec<br/>one per memory node,<br/>and one per node **per cgroup**"] LV --> AA["LRU_ACTIVE_ANON<br/>34,157,484 kB here"] LV --> IA["LRU_INACTIVE_ANON<br/>11,328,164 kB here"] LV --> AF["LRU_ACTIVE_FILE<br/>16,911,736 kB here"] LV --> IF["LRU_INACTIVE_FILE<br/>32,162,364 kB here"] LV --> UN["LRU_UNEVICTABLE<br/>995,652 kB here<br/>**family-agnostic**"] AA -->|"demoted when not<br/>referenced"| IA IA -->|"promoted on a<br/>second reference"| AA AF -->|"demoted"| IF IF -->|"promoted"| AF IA -->|"reclaim: **swap write**"| SW["swap device"] IF -->|"reclaim, clean: **free**"| FR["frame returned<br/>to the buddy allocator"] IF -->|"reclaim, dirty:<br/>writeback first"| WB["the file"] GSC["get_scan_count()<br/>decides how many pages<br/>to scan on each list"] -.->|"ap = swappiness x cost ratio"| IA GSC -.->|"fp = (200 - swappiness) x cost ratio"| IF UN -.->|"never scanned —<br/>mlock, ramfs, secretmem"| X["reclaim skips it entirely"]
The five per-node LRU lists at v6.12, annotated with this machine’s current occupancy from /proc/meminfo. What it shows: the anon/file split is duplicated across the active/inactive axis, giving four evictable lists plus one that is deliberately family-blind. get_scan_count() apportions scanning between the two inactive lists using the swappiness-weighted cost ratio. The insight to take: the reason this is five lists and not two is that the two families need independent aging. If anon and file shared one list, a burst of file reads would push the entire anonymous working set to the cold end and the machine would swap the moment anything was read from disk. Splitting them means a large sequential read can churn LRU_INACTIVE_FILE without disturbing the anonymous lists at all — which is exactly the behaviour the 59:1 steal ratio measured earlier reflects.
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. Because the lists are separate, reclaim can independently track how cold each family is and apply the bias.
Where the split becomes a number: get_scan_count()
It is worth seeing the one function where “anon vs file” turns into two integers, because it makes clear that swappiness is not a probability and not a threshold — it is a weight on a cost ratio. From mm/vmscan.c at v6.12:
/*
* The amount of pressure we put on each LRU is inversely
* proportional to the cost of reclaiming each list, as
* determined by the share of pages that are refaulting, times
* the relative IO cost of bringing back a swapped out
* anonymous page vs reloading a filesystem page (swappiness).
* ...
* With swappiness at 100, anon and file have equal IO cost.
*/
total_cost = sc->anon_cost + sc->file_cost;
anon_cost = total_cost + sc->anon_cost;
file_cost = total_cost + sc->file_cost;
total_cost = anon_cost + file_cost;
ap = swappiness * (total_cost + 1);
ap /= anon_cost + 1;
fp = (MAX_SWAPPINESS - swappiness) * (total_cost + 1);
fp /= file_cost + 1;
fraction[0] = ap; /* anon share of the scan */
fraction[1] = fp; /* file share of the scan */
denominator = ap + fp;Walked symbol by symbol: sc->anon_cost and sc->file_cost are running measures of how expensive each list has recently been to reclaim, derived from refault rates — how often pages the kernel evicted had to be fetched straight back. ap (“anon pressure”) is swappiness scaled by the inverse of anon cost; fp (“file pressure”) is MAX_SWAPPINESS − swappiness scaled by the inverse of file cost. The scan of each list is then apportioned fraction[i] / denominator. So swappiness sets the baseline ratio, and observed refault behaviour modulates it: a system whose file cache is thrashing will have a high file_cost, which pushes pressure onto the anonymous list even at a low swappiness.
Four special cases short-circuit the formula entirely, and they are the ones that explain confusing behaviour in the field:
Condition in get_scan_count() | Result | Why it matters |
|---|---|---|
!sc->may_swap or !can_reclaim_anon_pages() | SCAN_FILE — never scan anon | With no swap device, anonymous memory is simply not scanned. It is not “reclaimed slowly”, it is invisible to reclaim. |
cgroup_reclaim(sc) && !swappiness | SCAN_FILE | swappiness=0 means “never swap” inside a cgroup, but globally the kernel “will swap to prevent OOM even with no swappiness” — the source comment says so explicitly. This is why swappiness=0 behaves differently in a container. |
!sc->priority && swappiness | SCAN_EQUAL | Close to OOM, the kernel abandons balancing and scans both lists equally. |
sc->file_is_tiny | SCAN_ANON — force-scan anon | If almost no file pages remain, there is nothing to drop, so anon must be scanned regardless of swappiness. |
sc->cache_trim_mode | SCAN_FILE | If there is plenty of inactive file cache, do not touch the anonymous working set at all. |
Swappiness and Reclaim Balance covers the tuning surface, and Memory Reclaim Overview the full scan loop and the MGLRU alternative that replaces this arithmetic with generations. The point for this note is narrower: every branch above is a branch on the anon/file classification, and none of them would exist if the two families cost the same to reclaim.
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.
The anonymous round trip, as a state machine
The anonymous side is the more intricate of the two because the folio does not simply disappear — it acquires a swap slot, spends time in a cache that is both swap and page cache, and can be reclaimed a second time without a second write if nothing modified it in the meantime.
stateDiagram-v2 [*] --> Unmapped Unmapped : no PTE, no frame<br/>(a read here uses the zero page) Resident : **Resident, anonymous**<br/>PTE present, on an ANON LRU<br/>counted in AnonPages / RssAnon SwapCached_Dirty : **In the swap cache, dirty**<br/>slot allocated, PTE rewritten as a<br/>swap entry, write in flight<br/>counted in SwapCached SwappedOut : **On the swap device**<br/>frame freed, PTE holds a swap entry<br/>counted in VmSwap / SwapTotal-SwapFree SwapCachedClean : **Back in RAM, still in swap cache**<br/>PTE present again, but the swap slot<br/>is still valid and the copies match<br/>counted in SwapCached AND AnonPages LazyFree : **MADV_FREE'd**<br/>clean anonymous, droppable<br/>without any swap write<br/>counted in LazyFree in smaps Unmapped --> Resident : write fault<br/>do_anonymous_page()<br/>alloc_anon_folio() Resident --> SwapCached_Dirty : reclaim picks it<br/>add_to_swap() + try_to_unmap() SwapCached_Dirty --> SwappedOut : I/O completes,<br/>folio freed SwappedOut --> SwapCachedClean : do_swap_page()<br/>MAJOR fault, read from swap SwapCachedClean --> SwappedOut : reclaimed again —<br/>**no write needed**, the swap<br/>copy is still valid SwapCachedClean --> Resident : written to —<br/>swap slot released, back to dirty Resident --> LazyFree : madvise(MADV_FREE) LazyFree --> Unmapped : reclaim drops it<br/>(a later read returns zeroes) LazyFree --> Resident : re-dirtied before<br/>reclaim reaches it Resident --> Unmapped : madvise(MADV_DONTNEED)<br/>or munmap()
The full anonymous round trip, v6.12. What it shows: six states, and two of them exist only to avoid repeating work. SwapCachedClean is the state Gorman describes as swap being “a special aspect of the page cache” — a folio that is resident and still has a valid on-disk copy, so a second eviction is free. LazyFree is MADV_FREE’s state, where an anonymous folio behaves like a clean file page: droppable with no I/O. The insight to take: the expensive edge is Resident → SwapCached_Dirty → SwappedOut, and every optimisation in this area — the swap cache, MADV_FREE, zswap, MGLRU keeping anon in the youngest generations — is an attempt to avoid traversing it. Compare the file-backed side, which has no equivalent diagram: a clean file folio goes from resident to freed in one step with no state in between, because the disk copy was never invalidated.
The SwapCached state is directly observable. On this machine, /proc/meminfo shows SwapCached: 1752 kB against SwapTotal: 8388604 kB and SwapFree: 5178708 kB — so about 3.1 GiB of anonymous memory is currently out on a zram device, of which 1.7 MiB is simultaneously resident. The kernel documentation states the purpose plainly: it is “memory that once was swapped out, is swapped back in but still also is in the swapfile (if memory is needed it doesn’t need to be swapped out AGAIN because it is already in the swapfile. This saves I/O)” (proc.rst, v6.12). See The Swap Cache for the mechanism and Linux Swap Subsystem for the backing devices.
The Awkward Middle — tmpfs, shmem, and Shared Anonymous Memory
The taxonomy has exactly one genuinely awkward case, and it is important because it is everywhere: /tmp on a modern distribution, /dev/shm, SysV shared memory, memfd_create(), MAP_SHARED|MAP_ANONYMOUS, and the DRM/GEM buffers a GPU driver hands to userspace are all the same mechanism — shmem, an in-kernel filesystem whose files have no disk behind them.
Shmem is file-backed in structure and swap-backed in reclaim. Its folios live in a struct address_space belonging to a shmem inode, indexed by file offset in an XArray exactly like page cache; vma_is_anonymous() is false for a shmem VMA because shmem installs vm_ops. But there is no block device under the inode, so a dirty shmem folio cannot be written back to “the file” — reclaim must send it to swap, using the same swap-cache machinery as anonymous memory. The folio carries PG_swapbacked, which is the flag that actually decides reclaim’s route; folio_test_swapbacked() is true for both anonymous folios and shmem folios, and that — not folio_test_anon() — is the predicate reclaim consults.
flowchart TB F["a resident user folio"] F --> Q1{"folio_test_anon()<br/>= mapping pointer<br/>has bit 0 set"} Q1 -->|"true"| Q1a["mapping points at an anon_vma<br/>(or a KSM struct, if<br/>PAGE_MAPPING_MOVABLE is also set)"] Q1 -->|"false"| Q1b["mapping points at a<br/>struct address_space"] Q1a --> Q2{"folio_test_swapbacked()<br/>= PG_swapbacked"} Q1b --> Q2 Q2 -->|"true"| SWB["**reclaim route: SWAP**"] Q2 -->|"false"| FIL["**reclaim route: drop or writeback**"] SWB --> A["ANONYMOUS<br/>anon = true, swapbacked = true<br/>counted in AnonPages / RssAnon"] SWB --> S["SHMEM / tmpfs<br/>anon = **false**, swapbacked = **true**<br/>counted in Shmem / RssShmem<br/>and inside Cached"] FIL --> FI["FILE-BACKED<br/>anon = false, swapbacked = false<br/>counted in Cached / RssFile"] A -.->|"and on the ANON LRU"| L1["LRU_*_ANON"] S -.->|"also on the ANON LRU —<br/>it is swap-backed"| L1 FI -.-> L2["LRU_*_FILE"]
Two predicates, three outcomes. What it shows: the classification everyone learns (folio_test_anon()) is not the one reclaim uses. Reclaim branches on folio_test_swapbacked() — PG_swapbacked — and shmem folios are the case where the two predicates disagree: not anonymous, but swap-backed. The insight to take: shmem folios go on the anonymous LRU lists despite living in an address_space, because LRU membership follows reclaim cost, not structure. This is why a machine with a large tmpfs shows large Active(anon) figures with no process holding matching RssAnon, and why swappiness affects tmpfs pressure at all.
This is not a theoretical nuance. Running the same COW demonstration program twice, once against a file on btrfs and once against a file on tmpfs (/tmp is tmpfs on this Fedora system, confirmed with findmnt -no FSTYPE /tmp), gives different accounting for identical code:
| Step | btrfs (/var/tmp) | tmpfs (/tmp) |
|---|---|---|
| start | RssAnon 1,140 · RssFile 1,672 · RssShmem 8 | RssAnon 1,140 · RssFile 1,536 · RssShmem 8 |
after reading 64 MiB MAP_PRIVATE | RssFile 67,208 · RssShmem 8 | RssFile 1,536 · RssShmem 65,544 |
| after writing the first 32 MiB | RssAnon 33,908 · RssFile 34,440 | RssAnon 33,908 · RssShmem 32,776 |
after a 32 MiB MAP_ANONYMOUS | RssAnon 66,676 | RssAnon 66,676 |
The same program, the same syscalls, two filesystems, all values in kB, measured on this machine. What it shows: on btrfs the mapped file pages are counted in RssFile; on tmpfs the identical pages are counted in RssShmem. The COW write behaves identically in both — 32 MiB migrates to RssAnon — but it is drawn out of a different bucket. The insight to take: RssFile does not mean “resident file-backed memory” in the loose sense; it means “resident memory backed by a real file”. Anything backed by shmem is broken out separately, precisely because its reclaim behaviour is anonymous-like even though its structure is file-like. If you are summing RssFile across processes to estimate reclaimable memory, tmpfs-backed pages will be missing from your total and are not droppable anyway.
The kernel’s own field definition says so: RssShmem is “size of resident shmem memory (includes SysV shm, mapping of tmpfs and shared anonymous mappings)” (proc.rst, v6.12) — three quite different-looking APIs, one accounting bucket, because they are one implementation.
Two further consequences are worth knowing before they surprise you:
Cachedin/proc/meminfoincludes tmpfs. Its definition is “In-memory cache for files read from the disk (the pagecache) as well as tmpfs & shmem.” So a machine with 8 GiB in/dev/shmshows 8 GiB of “cache” that is not droppable.echo 3 > /proc/sys/vm/drop_cacheswill not free a byte of it. TheShmemline tells you how much ofCachedis in this category — on this machine,Cached: 58,242,768 kBof whichShmem: 8,983,868 kB, i.e. about 15% of the apparent cache is unreclaimable-without-swap.VmSwapin/proc/<pid>/statusunder-reports shmem. The definition is “amount of swap used by anonymous private data (shmem swap usage is not included)”. For a process whose memory is mostly in a shared segment,VmSwapcan read zero while gigabytes of its working set are on the swap device.smapsis more honest here: for shmem mappings itsSwap:field “includes also the size of the mapped (and not replaced by copy-on-write) part of the underlying shmem object out on swap”, thoughSwapPssstill does not.
See Anonymous Shared Memory and memfd_create and Anonymous Memory Files for the API surface, and The Memory Cgroup memcg for how shmem is charged (to the cgroup that first touches a page, not the one that created the file — a common source of surprise in containers).
Reading the Split Out of /proc — Field by Field
This is the section to come back to when you are staring at a machine. Every field below was checked against Documentation/filesystems/proc.rst at v6.12, which is the authoritative definition; the sample values are from this machine (Linux 7.1.8, 128 GiB RAM, 8 GiB zram swap, swappiness=10).
System-wide: /proc/meminfo
MemTotal: 131150240 kB Active(anon): 34157484 kB
MemFree: 25160788 kB Inactive(anon): 11328164 kB
MemAvailable: 74599032 kB Active(file): 16911736 kB
Buffers: 1280 kB Inactive(file): 32162364 kB
Cached: 58242768 kB Unevictable: 995652 kB
SwapCached: 1752 kB Mlocked: 995652 kB
AnonPages: 37310376 kB Shmem: 8983868 kB
Mapped: 4044424 kB SwapTotal: 8388604 kB
Dirty: 38032 kB SwapFree: 5178708 kB
| Field | Which family | Definition (v6.12 proc.rst) and how to read it |
|---|---|---|
AnonPages | anon | “Non-file backed pages mapped into userspace page tables.” The true anonymous resident total. Note mapped into page tables — anonymous memory that is only in the swap cache is not here. |
Cached | file + shmem | “In-memory cache for files read from the disk (the pagecache) as well as tmpfs & shmem. Doesn’t include SwapCached.” Not all droppable — subtract Shmem. |
Shmem | the middle | “Total memory used by shared memory (shmem) and tmpfs.” The unreclaimable-without-swap slice of Cached. |
Buffers | file | “Relatively temporary storage for raw disk blocks”, and the docs note it “shouldn’t get tremendously large (20MB or so)”. At 1,280 kB here, it is noise; treat Cached as the real number. |
Active(anon) / Inactive(anon) | anon | The two anonymous LRU lists, summed across nodes. |
Active(file) / Inactive(file) | file | The two file LRU lists. A large Inactive(file) is the reclaim headroom — this is what cache_trim_mode looks at. |
Unevictable / Mlocked | neither | The fifth list. “the unevictable list does not differentiate between file-backed and anonymous, swap-backed folios” (Unevictable LRU) — once a folio is unevictable, its family stops mattering. |
SwapCached | anon (+shmem) | Resident and still valid on the swap device; a second eviction is free. |
Mapped | file | “files which have been mmapped, such as libraries.” A subset of Cached: the part that is in someone’s page tables. Cached − Mapped is roughly read/write cache nobody has mapped. |
MemAvailable | derived | “Calculated from MemFree, SReclaimable, the size of the file LRU lists, and the low watermarks in each zone.” Note what is absent: anonymous memory contributes nothing, because it is not reclaimable without a swap write. This is the single number to watch instead of MemFree. |
AnonHugePages | anon | PMD-mapped anonymous THP only — not mTHP. See Transparent Huge Pages, which documents this trap in detail. |
ShmemHugePages / FileHugePages | shmem / file | The equivalents for the other two families. |
The quick triage arithmetic: AnonPages + Shmem is memory you can only reclaim by swapping (or not at all); Cached − Shmem is roughly the droppable file cache; and MemAvailable is the kernel’s own estimate of the first number’s complement. On this machine that is ~37.3 GiB anonymous + ~8.6 GiB shmem against ~47 GiB of droppable cache, and MemAvailable of 71 GiB agrees.
Per-process: /proc/<pid>/status
Three fields, and the documentation is unambiguous that they partition VmRSS:
VmRSS = RssAnon + RssFile + RssShmem
| Field | Definition (v6.12 proc.rst) |
|---|---|
RssAnon | “size of resident anonymous memory” |
RssFile | “size of resident file mappings” |
RssShmem | “size of resident shmem memory (includes SysV shm, mapping of tmpfs and shared anonymous mappings)” |
VmSwap | “amount of swap used by anonymous private data (shmem swap usage is not included)” |
VmLck / VmPin | locked and pinned memory — unevictable regardless of family |
The three-way split has existed since Linux 4.5 in this form, and it is the fastest per-process answer to “is this process’s footprint reclaimable?”: RssFile mostly is, RssAnon mostly is not, RssShmem is not without swap. A caveat the docs flag separately: the old statm file’s shared field is not a fourth number — it is “the same as RssFile+RssShmem in status”, which is why statm cannot distinguish a mapped library from a tmpfs page.
Per-mapping: /proc/<pid>/smaps and smaps_rollup
smaps is where the classification becomes visible within one VMA, which is what makes it the right tool for the COW question. The fields that carry family information:
| Field | What it tells you |
|---|---|
Anonymous: | Bytes in this mapping that “do not belong to any file” — non-zero inside a file-backed VMA means COW has happened. |
Private_Clean: | Private and unmodified — for a file mapping, still the page cache’s own folio; droppable for free. |
Private_Dirty: | Private and modified — for a MAP_PRIVATE file mapping these are the anonymous copies. |
Shared_Clean: / Shared_Dirty: | Mapped by more than one PTE. Note the documented subtlety: “even a page which is part of a MAP_SHARED mapping, but has only a single pte mapped… is accounted as private and not as shared.” |
Swap: / SwapPss: | How much of this mapping is out on swap. For shmem mappings Swap includes the underlying object’s swapped-out pages; SwapPss does not. |
LazyFree: | Bytes marked with MADV_FREE — anonymous but droppable without a swap write. The docs warn “the printed value might be lower than the real value due to optimizations”. |
KSM: | Bytes merged by KSM; note that KSM-placed zeropages are excluded. |
THPeligible: | 1 if the mapping can take a naturally-aligned THP of any enabled size. |
smaps_rollup gives the same totals summed over every mapping in one read, and adds the three fields that answer the family question directly at process scope — Pss_Anon, Pss_File, Pss_Shmem. Reading smaps_rollup is dramatically cheaper than parsing smaps for a process with thousands of mappings, and it is the right thing to poll. From the demo process above:
### smaps_rollup
Rss: 101120 kB
Pss_Anon: 66664 kB <-- the COW copies + the MAP_ANONYMOUS region
Pss_File: 32771 kB <-- what remains file-backed
Pss_Shmem: 8 kB
Private_Clean: 32768 kB
Private_Dirty: 66672 kB
PSS (“proportional set size”) divides each page by the number of processes sharing it, so Pss_File for a shared library counts only this process’s share. That is what makes the three Pss_* fields the right basis for “who is using the memory” accounting across a machine, and plain Rss* the right basis for “what would be freed if this process died”.
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, RssFile is memory backed by a real file, and RssShmem is shmem/tmpfs/SysV-shm/shared-anonymous — the three sum to VmRSS, per the kernel’s own definition. A private file mapping that has been written to shifts pages from RssFile to RssAnon at the moment of the COW write, with VmRSS unchanged — measured above. If you expected mapped-file memory to stay “file”, that is the surprise.
“drop_caches freed nothing.” Two independent reasons, both family-related. First, the memory may be shmem, which Cached counts but which has no file to drop to — check the Shmem line. Second, the file pages may be dirty; drop_caches only drops clean pages, so run sync first (and note that drop_caches is documented as a debugging aid, not a tuning measure — see Memory Reclaim Overview).
“RssFile for this process is huge but the machine is fine.” File-backed resident pages are mostly shared — every process that maps libc counts the same physical pages in its RssFile. Summing RssFile across processes double-counts massively. Use Pss_File from smaps_rollup instead, which divides each page by its sharer count.
“The process is swapping but VmSwap says 0.” Its memory is probably in a shmem segment; VmSwap explicitly excludes shmem swap usage. Read the mapping’s Swap: line in smaps instead, which does include the underlying shmem object’s swapped-out pages.
“Anonymous memory keeps growing and I never called malloc.” Look for MAP_PRIVATE file mappings being written to. A JIT that maps a code file privately and patches it, a program that maps a large data file privately and normalises it in place, or a fork()ed child that touches inherited pages will all convert file-backed pages to anonymous ones one page at a time. smaps will show Anonymous: climbing inside a VMA whose name is a file path — the signature is unmistakable once you know to look for it.
“A page I mlocked still shows in the anon/file LRU counters.” It should not; mlock moves folios to LRU_UNEVICTABLE, and the unevictable list deliberately does not distinguish the two families. If your accounting expects Active(anon) + Inactive(anon) + Active(file) + Inactive(file) to cover all user memory, Unevictable is the missing term. On this machine that is 995,652 kB, all of it Mlocked. See The Unevictable LRU and mlock.
Alternatives and Boundary Cases
The clean anon/file dichotomy has well-known fuzzy edges worth knowing:
-
tmpfs / shmem — file-backed in structure, swap-backed in reclaim. Covered in full above; it is the case that most often breaks other people’s accounting.
-
MADV_FREE— 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 anonymous reclaim into cheap drop-on-clean reclaim. This is an anonymous page borrowing the file family’s reclaim economics, and it is measurable directly. Touching 64 MiB of anonymous memory and then callingmadvise(MADV_FREE)on it, on this machine:---- after touching 64 MiB anonymous ---- Rss: 65536 kB Private_Dirty: 65536 kB Anonymous: 65536 kB LazyFree: 0 kB ---- after madvise(MADV_FREE) ---- Rss: 65536 kB Private_Dirty: 0 kB Anonymous: 65536 kB LazyFree: 65348 kB ---- after re-dirtying ONE page ---- Rss: 65536 kB Private_Dirty: 4 kB Anonymous: 65536 kB LazyFree: 65344 kBRead it carefully:
RssandAnonymousdo not move — the pages are still resident and still anonymous — butPrivate_Dirtycollapses to zero andLazyFreetakes its place. The pages have become clean anonymous, which is the one combination the two-family model does not otherwise produce, and reclaim may now free them with no swap write at all. A single store then flips exactly 4 kB back toPrivate_Dirty. This is whyMADV_FREEis the right call for afree()d allocator arena that might be reused: the memory is returned to the kernel’s discretion without the allocator having to decide, and re-using it costs nothing if reclaim has not got there first. Note the small discrepancy — 65,348 kB ofLazyFreeagainst 65,536 kB ofAnonymous, a 188 kB shortfall.Uncertain
LazyFreereports 188 kB (47 pages) less than the full 64 MiB aftermadvise(MADV_FREE)over the whole region. Reason: the documentation notes only that "the printed value might be lower than the real value due to optimizations used in the current implementation", without saying which optimization; the measurement is from Linux 7.1.8, not the 6.12 pin, and could involve large-folio handling inmadvise_free_pte_range(). To resolve: readmm/madvise.c'sMADV_FREEwalker and thesmapsLazyFreeaccounting infs/proc/task_mmu.cat the running kernel's tag, and repeat the measurement withCONFIG_TRANSPARENT_HUGEPAGEmTHP sizes disabled. uncertainVerify: why
-
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. On a file mapping it means something different — drop the private copies and revert to the file’s contents — which is the same asymmetry in another guise. -
mlocked memory leaves the two-family world entirely forLRU_UNEVICTABLE, where, in the documentation’s words, the list “does not differentiate between file-backed and anonymous, swap-backed folios.” -
KSM pages are anonymous by the
PAGE_MAPPING_ANONtest but theirmappingpoints at a KSM structure rather than ananon_vma, andPAGE_MAPPING_MOVABLEis set alongside. Code that readsfolio->mappingas ananon_vmawithout checking must not run on them. -
ZONE_DEVICE/ DAX pages are file-backed in theaddress_spacesense but are not evictable in the normal way at all, because the “cache” is the storage. Persistent-memory DAX mappings deliberately bypass the page cache. -
A system with no swap collapses the taxonomy in the most consequential way possible:
get_scan_count()takes theSCAN_FILEbranch and anonymous memory stops being scanned entirely. All reclaim pressure lands on file cache, and when that runs out the OOM killer runs — there is no gradual degradation. This is worth stating plainly because “we removed swap for performance” is a common decision whose actual effect is to make half of memory unreclaimable.
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.
Reading a cgroup’s split
memory.stat in a v2 cgroup is the per-container version of /proc/meminfo, and its first three lines are exactly the taxonomy of this note. From the cgroup this note was written in:
anon 1372831744 <- 1.28 GiB anonymous
file 4286623744 <- 3.99 GiB file-backed (INCLUDES shmem)
kernel 264577024
shmem 3114815488 <- 2.90 GiB of the 'file' figure is shmem
file_mapped 144842752 <- of the file total, what is in page tables
file_dirty 200704 <- needs writeback before it can be dropped
file_writeback 0
swapcached 0
inactive_anon 251240448
active_anon 4236140544
The trap is the second line: file includes shmem, so “3.99 GiB of reclaimable cache” is really 4.29 GB − 3.11 GB ≈ 1.17 GB of droppable file cache plus 2.9 GB of shmem that can only go to swap. In this cgroup, in other words, nearly three quarters of the apparent page cache is not droppable. A container that appears to have generous cache headroom and OOMs anyway is very often this: a large /dev/shm or tmpfs volume counted as file. The correct headroom estimate is file − shmem − file_dirty.
The second thing to read is active_anon (4.24 GB) against inactive_anon (0.25 GB). Almost all anonymous memory is on the active list, which under MGLRU is the expected steady state — Memory Reclaim Overview documents that MGLRU deliberately retains anonymous memory in the youngest generations — but under the classic two-list scanner it means reclaim has very little anonymous material it considers cold, so pressure will land on file cache regardless of swappiness.
The system-wide ratio, and what it tells you
/proc/vmstat keeps per-family scan and steal counters, and their ratio is the single most informative number about how a machine has actually been behaving since boot:
pgscan_anon 5174994 pgsteal_anon 3430896
pgscan_file 318568007 pgsteal_file 202484088
pswpin 809703 pswpout 3133298
workingset_refault_anon 822141
workingset_refault_file 39872964
Three readings. Steal ratio: 59:1 in favour of file. With swappiness = 10 that is exactly what the get_scan_count() arithmetic predicts — the kernel has done almost all of its reclaiming by dropping cache. Scan efficiency: pgsteal_anon / pgscan_anon is 66% and pgsteal_file / pgsteal_file is 64%, so both lists are yielding pages when scanned; a collapsing ratio on one side means the scanner is burning CPU on folios it cannot free (pinned, dirty, or under writeback). Refault counts: 39.9 M file refaults against 0.8 M anonymous ones says the file cache is being evicted and immediately needed again far more often than the anonymous working set is — the classic signature of a working set larger than RAM on the file side, and the argument for either more memory or a lower swappiness being wrong for this workload. pswpout (3.1 M pages, ~12 GiB) against pswpin (0.8 M) says most of what was swapped out has stayed out, which is the desired outcome: cold anonymous memory parked on a zram device.
flowchart TB Q["I am looking at N bytes of memory.<br/>Can the kernel get it back?"] Q --> U{"Is it Unevictable/Mlocked,<br/>or GUP-pinned?"} U -->|"yes"| NO1["**No.** Not at any price.<br/>Only the owner releasing it helps."] U -->|"no"| SB{"PG_swapbacked?<br/>(anonymous OR shmem)"} SB -->|"no — a real file<br/>backs it"| D{"Is it dirty?"} D -->|"no"| FREE["**Yes, for free.**<br/>Unmap and free the frame.<br/>This is what MemAvailable counts."] D -->|"yes"| WBK["**Yes, after writeback.**<br/>The bdi flusher usually got there first;<br/>reclaim skips dirty folios rather<br/>than blocking on I/O."] SB -->|"yes"| SW{"Is there swap<br/>with free slots?"} SW -->|"no"| NO2["**No.** Anonymous memory is not even<br/>scanned — get_scan_count() takes<br/>SCAN_FILE. Pressure goes to the<br/>OOM killer instead."] SW -->|"yes"| LF{"MADV_FREE'd and<br/>still clean?"} LF -->|"yes"| FREE2["**Yes, for free.**<br/>Drop it; a later read returns zeroes."] LF -->|"no"| SC{"Already in the swap cache<br/>with a valid on-disk copy?"} SC -->|"yes"| FREE3["**Yes, for free.**<br/>The swap copy is still valid —<br/>no second write."] SC -->|"no"| COST["**Yes, at the cost of a<br/>swap write now and a major<br/>fault later.** Weighted by swappiness."]
The reclaimability decision, as an operator would ask it. What it shows: five “yes” outcomes with wildly different prices, and two “no”s — and the family question (PG_swapbacked) is the first branch after pinning. The insight to take: “how much memory can I get back?” has no single answer; it has a cost curve. MemAvailable estimates only the cheap end (clean file pages plus reclaimable slab, minus watermarks), which is why it is conservative and why it is nonetheless the right number to alert on. The expensive end — anonymous memory needing a swap write — is capacity you have only if you configured swap, and the SCAN_FILE branch is the reason a swapless machine’s usable memory is much smaller than MemTotal suggests.
The runbook that follows from all of this is short:
# 1. What is the machine's split, and how much of "cache" is really droppable?
grep -E "^(MemTotal|MemAvailable|AnonPages|Cached|Shmem|SwapTotal|SwapFree|Unevictable)" /proc/meminfo
# 2. Which way has reclaim actually been going?
grep -E "^(pgscan|pgsteal)_(anon|file)|^pswp|^workingset_refault" /proc/vmstat
# 3. Per process, cheaply: the three-way split and the PSS version of it
grep -E "^(VmRSS|RssAnon|RssFile|RssShmem|VmSwap)" /proc/<pid>/status
grep -E "^Pss_(Anon|File|Shmem)" /proc/<pid>/smaps_rollup
# 4. Is a file-backed VMA quietly turning anonymous? (the COW seam)
awk '/^[0-9a-f]/{v=$0} /^Anonymous:/ && $2+0 > 0 && v ~ /\// {print v; print}' /proc/<pid>/smaps
# 5. In a container: subtract shmem before believing the cache figure
awk '/^(anon|file|shmem|file_dirty) /' /sys/fs/cgroup/<path>/memory.statStep 4 is the one people do not know about: it lists every named file mapping that contains anonymous pages, i.e. every place COW has fired inside a file mapping. On a process whose memory is mysteriously unreclaimable, that list is usually the answer.
See Also
- Demand Paging — how pages of either family are first populated (this note’s sibling): demand-zero for anonymous, demand-fill for file.
- Anonymous Shared Memory / memfd_create and Anonymous Memory Files — the shmem middle case from the API side.
- Folios and the Folio Conversion — the type both families are expressed in at 6.12; also walks
filemap_fault()and where folio vocabulary hands back to pages. - Transparent Huge Pages — large anonymous folios (mTHP), and why
AnonHugePagesdoes not count them. - The Zero Page and Lazy Allocation — the read-fault branch of
do_anonymous_page()that allocates nothing. - Kernel Samepage Merging — anonymous folios whose
mappingis not ananon_vma. - The Memory Cgroup memcg / Pressure Stall Information — where the split is charged and measured per container.
- 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).