Demand Paging
Demand paging is the lazy-population strategy at the heart of Linux memory management: when a process maps memory — with
mmap,brk, or by beingexec’d — the kernel creates only the bookkeeping (a virtual memory area describing the region) and installs no physical pages and no page-table entries for it. Physical memory is committed only on the first access to each page, which raises a page fault that the fault handler resolves by allocating and filling a page right then. The admin-guide calls this out as a primary purpose of virtual memory: it “allows to keep only needed information in the physical memory (demand paging)” (kernel admin-guide, Concepts overview). The payoff is enormous: a process canmmapa 100 GiB file or reserve a huge sparse heap and pay for only the pages it actually touches. This note explains the mechanism and its three concrete flavors — demand-zero (anonymous), demand-fill (file/page cache), and swap-in (previously evicted) — tracing the exact v6.12 code that decides between them.
What this note owns, and what its siblings own
This note owns the policy: why Linux populates memory lazily, which of the three populators runs, how much memory one fault populates, and how to override the laziness when its latency is unacceptable. It is deliberately about decisions and trade-offs.
- The Page Fault Handler owns the mechanism — the hardware trap and error code,
access_error, the per-VMA vsmmap_locklocking regimes, the page-table descent, theVM_FAULT_*return protocol, the retry machinery, and the kernel-fault exception table. The boundary readers trip over: “why is nothing allocated until I touch it, and what can I do about it” is here; “what code runs between the trap and the PTE being written” is there. Neither note repeats the other.- Minor and Major Faults owns the cost classification and the counters (
minflt/majflt,PGFAULT/PGMAJFAULT,getrusage). This note uses the minor/major distinction as a cost model; it does not re-derive the accounting rules.- Copy-on-Write and fork owns
do_wp_pageanddo_cow_fault. The Zero Page and Lazy Allocation owns the shared zero page. Anonymous vs File-Backed Memory owns the page-family split. The Page Cache owns whatfilemap_faultreads from. Linux Swap Subsystem owns the swap device and the reclaim side that creates the swap entries this note reads back.Everything here is pinned to Linux 6.12, this vault’s chosen tag, with file-and-line citations into
mm/memory.c. As of 2026-09-11, per kernel.org’sreleases.json, mainline is7.3-rc2, latest stable is7.2.5, and6.12.109is one of six longterm lines (alongside6.18.51,6.6.156,6.1.187,5.15.220,5.10.269) — so 6.12 is a pin, not “the” LTS. Where the code has changed since, it is called out with the release that changed it.
Mental Model — Promise Now, Pay on Touch
Think of a mapping as a promise of memory, not memory itself. mmap returns instantly with a valid pointer, but behind that pointer there is nothing — no RAM, no translation. The promise is recorded in a VMA: “the anonymous mappings only define virtual memory areas that the program is allowed to access” (Concepts overview). The bill comes due one page at a time, on the access that first dereferences each page. That access finds no valid page-table entry, the CPU’s MMU raises a fault, and the kernel pays the bill: it allocates a frame, fills it with the right contents, writes a PTE, and restarts the faulting instruction as if the memory had been there all along.
flowchart TB MAP["mmap / brk / exec<br/>creates VMA only<br/>(no pages, empty PTEs)"] --> ACC["CPU accesses an address"] ACC --> MMU{"MMU: valid PTE?"} MMU -->|"yes — page present"| RUN["instruction proceeds<br/>(no fault)"] MMU -->|"no — fault"| FH["page fault handler<br/>handle_mm_fault -> handle_pte_fault"] FH --> CL{"What is the PTE state<br/>and the VMA kind?"} CL -->|"none + anonymous VMA"| DZ["DEMAND-ZERO<br/>do_anonymous_page<br/>(zero page or new folio)"] CL -->|"none + file VMA"| DF["DEMAND-FILL<br/>do_fault -> read into page cache"] CL -->|"non-present (swap entry)"| SI["SWAP-IN<br/>do_swap_page<br/>(read back from swap)"] DZ --> SET["install PTE, restart instruction"] DF --> SET SI --> SET
Demand paging decision tree, mirroring handle_pte_fault in v6.12. What it shows: a mapping starts as a bare VMA; the first access to each page faults; the handler classifies the fault by the PTE’s state (empty vs a saved swap entry) and the VMA’s kind (anonymous vs file) and routes to one of three populators. The insight to take: “demand paging” is not one routine — it is the policy of populating lazily, realized by three distinct fault handlers, and which one runs is a two-question decision: is the PTE empty or a swap entry, and is the VMA anonymous or file-backed.
The word “demand” covers two different events
The terminology is older than Linux and worth pinning down, because the kernel’s own history uses it inconsistently. In the classic taxonomy a virtual-memory system has a fetch policy — when does a page get brought in? — and Linux’s answer is demand fetch: as Mel Gorman’s Understanding the Linux Virtual Memory Manager puts it, “the page is only fetched from backing storage when the hardware raises a page fault exception which the operating system traps and allocates a page” (Gorman, ch. 4 §4.6). Gorman then splits the two events the fault can service, and the split is still exactly right at v6.12 even though the function names have all changed:
- Demand allocation — the page has never existed. There is nothing to fetch; the kernel invents content (zeroes for anonymous memory) or reads it from a file. In 2.4 this was
do_no_page; in 6.12 it isdo_pte_missing. - Demand paging proper — the page did exist and was evicted to backing store. The kernel fetches it back. This was and still is
do_swap_page.
Colloquially “demand paging” has come to cover both, and this note uses it that way, but keeping the two apart explains something otherwise puzzling: the first touch of a freshly malloc’d page and the touch of a page that was swapped out both look like “a fault on memory I own,” yet one costs a microsecond and the other costs a disk read. They are different mechanisms wearing the same symptom.
Gorman also records how primitive the prefetch side was: “when a page is paged in from swap space, a number of pages after it, up to 2^page_cluster, are read in by swapin_readahead()… Unfortunately there is only a chance that pages likely to be used soon will be adjacent in the swap area making it a poor prepaging policy.” The page_cluster knob and swapin_readahead() are still there in 6.12; the observation still holds.
The four things a fault can be, and what each costs
| Fault class | What the PTE looked like | Handler | Backing-store I/O? | Counter it moves | Rough cost |
|---|---|---|---|---|---|
| Demand-zero (read) | empty | do_anonymous_page, zero-page branch | no | pgfault, minflt | Cheapest — no allocation at all, just a PTE pointing at the shared zero page |
| Demand-zero (write) | empty | do_anonymous_page, alloc branch | no | pgfault, minflt | One folio allocation + zeroing (folio_zero_user) + rmap insertion |
| Demand-fill, page-cache hit | empty | do_fault → filemap_fault | no | pgfault, minflt | Cheap — the folio is already resident; often amortised further by fault-around |
| Demand-fill, page-cache miss | empty | do_fault → filemap_fault | yes | pgfault, pgmajfault, majflt | A storage read. PGMAJFAULT is counted the moment the cache lookup misses, before the I/O is even issued |
| Swap-in, swap-cache hit | swap entry | do_swap_page | no | pgfault, minflt | The folio was reclaimed but not yet freed — a “near miss” that costs almost nothing |
| Swap-in, device read | swap entry | do_swap_page | yes | pgfault, pgmajfault, majflt | A swap-device read. On zram/zswap this is a decompression, not a disk seek |
| Protection fault (COW) | present, read-only | do_wp_page | no | pgfault, minflt (or pgreuse) | Not demand paging — the page already exists. See Copy-on-Write and fork |
| Invalid access | any | access_error rejects it | no | pgfault only | SIGSEGV. Counted in pgfault but not in minflt/majflt, which skip VM_FAULT_ERROR |
Table: every outcome a fault on a demand-paged region can have, and the counter that moves. Two entries routinely surprise people. First, PGMAJFAULT for a file mapping is counted on the page-cache miss, not on I/O completion — filemap_fault does count_vm_event(PGMAJFAULT); ret = VM_FAULT_MAJOR; immediately after filemap_get_folio returns an error, before calling do_sync_mmap_readahead (mm/filemap.c:3334-3343). Second, a failed fault still bumps pgfault but not the per-task counters, because mm_account_fault returns early on VM_FAULT_ERROR — so pgfault “is not a ‘this many hardware page faults’ counter,” in the source’s own words.
The life of one anonymous page
stateDiagram-v2 [*] --> NoVMA: process starts NoVMA --> Reserved: mmap(MAP_ANONYMOUS)<br/>VMA created, zero PTEs Reserved --> ZeroShared: first READ fault<br/>do_anonymous_page<br/>maps the shared zero page Reserved --> Private: first WRITE fault<br/>do_anonymous_page<br/>allocates + zeroes a folio ZeroShared --> Private: WRITE fault<br/>do_wp_page breaks COW<br/>on the zero page Private --> Private: further access<br/>no fault at all Private --> Swapped: reclaim writes it out<br/>PTE becomes a swap entry Swapped --> SwapCache: reclaim chose it but<br/>the folio is not freed yet SwapCache --> Private: fault — swap-cache hit<br/>MINOR Swapped --> Private: fault — device read<br/>do_swap_page, MAJOR Private --> Reserved: madvise(MADV_DONTNEED)<br/>PTEs torn down,<br/>back to zero-fill-on-demand Reserved --> [*]: munmap Private --> [*]: munmap / exit
The state machine of a single anonymous page under demand paging at v6.12. What it shows: Reserved — a VMA with no PTE — is not a transient state but the normal resting state of most of a process’s address space, and there are two different ways in and two different ways back out. The insight to take: the only expensive edge is Swapped → Private via a device read. Every other transition stays in RAM. This is why “my RSS grew” and “my process is slow” are different problems with different fixes: the first is caused by the Reserved → Private edge firing often, the second by the Swapped → Private edge firing at all. Note also that MADV_DONTNEED is the only edge that goes backwards — it is the explicit inverse of a fault.
The Mechanical Walk-through
Mapping creates VMAs, not pages
When a process calls mmap, the kernel allocates a struct vm_area_struct describing the new region — its start/end addresses, protection flags, and (for a file mapping) the backing file and offset — and inserts it into the process’s address space (indexed by the maple tree). It does not walk the region installing pages. The page tables for the region remain empty, and are usually not even allocated down to the PTE level: the kernel’s own page-table documentation states the design rationale plainly: a single flat page table “would be very sparse, because large portions of the virtual memory usually remains unused. By using hierarchical page tables large holes in the virtual address space does not waste valuable page table memory, because it will suffice to mark large areas as unmapped at a higher level in the page table hierarchy” (Documentation/mm/page_tables.rst). Demand paging is what keeps those holes holes.
That is a bigger saving than it first looks. On x86-64 with 4 KiB pages, a fully populated PTE table covers 2 MiB of virtual address space and costs one 4 KiB page. A process that reserves 1 TiB of address space and touches nothing pays zero page-table memory; if it were forced to materialise the tables it would pay 2 GiB of PTE pages alone, before a single byte of user data.
mmap(2) documents that for MAP_ANONYMOUS “the fd argument is ignored” and the contents “are initialized to zero” — but the zeroing, like the allocation, is deferred to first touch (mmap(2)). The same laziness applies to exec: loading a binary maps its text and data segments as file-backed VMAs and jumps to the entry point; the code pages are faulted in from the executable as execution reaches them. This is why process startup is fast for a large binary — you pay only for the pages the startup path actually executes.
sequenceDiagram autonumber participant App as application participant K as kernel (mmap path) participant PT as page tables participant PA as page allocator App->>K: mmap(NULL, 1 GiB, PROT_READ|PROT_WRITE,<br/>MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) K->>K: allocate one struct vm_area_struct K->>K: insert it into mm->mm_mt (the maple tree) K-->>App: returns a pointer, immediately Note over PT,PA: nothing here yet: no PTE table,<br/>no physical page, no accounting App->>App: p[0] = 1 App->>K: page fault K->>PT: pgd_offset / p4d_alloc / pud_alloc / pmd_alloc<br/>create each missing level K->>PT: pte_alloc — one 4 KiB PTE table,<br/>covering 2 MiB of this VMA K->>PA: alloc_anon_folio PA-->>K: folio K->>K: folio_zero_user, rmap, MM_ANONPAGES += nr_pages K->>PT: set_ptes K-->>App: retry the store — it succeeds Note over App,PT: the remaining 1 GiB minus one folio<br/>is still pure promise
What mmap does and does not do, at v6.12. What it shows: the syscall touches exactly one data structure — the VMA — and returns; every other cost is deferred to step 8 onward and is paid per page, not per mapping. The insight to take: step 10 is the cost people forget. A demand-zero fault allocates a page table as well as (sometimes) a data page, and that PTE table covers 2 MiB. A program that touches one byte in each of a thousand widely-scattered 2 MiB regions pays a thousand PTE tables — 4 MiB of kernel memory — for a few kilobytes of user data. Sparse access patterns are cheap in data pages and surprisingly expensive in page tables.
The first access faults
When the CPU executes an instruction that touches an address in the region, address translation walks the page tables and finds no valid translation. The MMU raises a page fault, the architecture’s entry point runs (traced in detail in The Page Fault Handler), and control reaches the generic handle_mm_fault, which descends the page-table levels — allocating them on the way down — and ultimately reaches handle_pte_fault (mm/memory.c:5732), the function that classifies the fault and dispatches it:
if (!vmf->pte)
return do_pte_missing(vmf); /* PTE empty: never populated */
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf); /* PTE holds a swap entry: swapped out */
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf); /* NUMA balancing hint */
/* ... write to a read-only present page ... */
return do_wp_page(vmf); /* copy-on-write */Reading this top to bottom: an empty PTE (!vmf->pte — either the PTE table does not exist, or the entry in it is pte_none) means the page has never been populated → do_pte_missing. A PTE that is non-empty but !pte_present holds an encoded swap entry — the page was here once and got evicted → do_swap_page. The remaining branches handle NUMA balancing and copy-on-write writes to present pages, which are not demand paging at all: the page already exists and only its permissions or its ownership are in question. So demand paging proper lives in two branches — do_pte_missing (Gorman’s “demand allocation”) and do_swap_page (Gorman’s “demand paging”).
That two-branch shape has been stable for a long time. Fetching mm/memory.c at v7.2 — the newest release as of 2026-09-11 — shows the identical sequence at lines 6379-6395, with the same four calls in the same order. The internals of each arm have changed a great deal; the decision has not.
do_pte_missing forks on VMA kind
do_pte_missing (mm/memory.c:3960) makes the anonymous-vs-file-backed decision in two lines:
static vm_fault_t do_pte_missing(struct vm_fault *vmf)
{
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); /* demand-zero */
else
return do_fault(vmf); /* demand-fill from file */
}vma_is_anonymous() is itself a one-liner — return !vma->vm_ops; (include/linux/mm.h:924-927): a VMA is anonymous exactly when it has no vm_ops. That is the whole test. A file mapping, a device mapping, a DAX mapping, a hugetlbfs mapping — all carry a vm_operations_struct supplied by the filesystem or driver, and all therefore route to do_fault, which calls that vm_ops->fault callback. Anonymous memory has nobody to ask, so the kernel invents the content itself.
There is one more entry into do_pte_missing worth knowing about, because it explains a case that otherwise looks impossible. A PTE can be non-empty yet still mean “nothing here”: a PTE marker left behind by userfaultfd write-protect. pte_marker_handle_uffd_wp (mm/memory.c:3972) recognises those and calls do_pte_missing anyway — “this is actually a page-missing access, but with uffd-wp special pte installed. It means this pte was wr-protected before being unmapped.”
The Three Flavors
Demand-zero (anonymous): do_anonymous_page
For an anonymous page being touched for the first time, what should it contain? The answer is zeroes — anonymous memory reads as zero until written, and it must, because handing a process another process’s stale data would be a catastrophic information leak. Linux exploits that requirement with an optimisation that costs nothing. From do_anonymous_page (mm/memory.c:4735):
/* File mapping without ->vm_ops ? */
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS; /* (1) an anon VMA cannot be shared */
if (pte_alloc(vma->vm_mm, vmf->pmd))
return VM_FAULT_OOM; /* (2) materialise the PTE table now */
/* 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)); /* (3) point at THE zero page */
...
if (userfaultfd_missing(vma)) { /* (4) checked under the ptl */
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte;
}
/* Allocate our own private page. */
ret = vmf_anon_prepare(vmf); /* (5) attach an anon_vma if needed */
folio = alloc_anon_folio(vmf); /* (6) may be a multi-page mTHP folio */
...
__folio_mark_uptodate(folio); /* (7) barrier: zeroes visible before PTE */
entry = mk_pte(&folio->page, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry), vma);
...
add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
folio_add_lru_vma(folio, vma);
setpte:
set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);Line (2) is the first real cost of a demand-zero fault and it is easy to overlook: even the read path allocates a PTE table if the PMD was empty, so the first touch anywhere in a 2 MiB-aligned span costs a page of kernel memory whether or not a data page is allocated. Line (7) is a correctness detail worth naming — __folio_mark_uptodate contains a memory barrier, and the comment says why: it “makes sure that preceding stores to the page contents become visible before the set_pte_at() write.” Without it, another CPU could observe the new PTE and read the page before the zeroing was visible.
flowchart TB IN["do_anonymous_page<br/>memory.c:4735"] --> SH{"VMA has VM_SHARED?"} SH -->|"yes"| BUS["VM_FAULT_SIGBUS<br/>an anon VMA must not be shared"] SH -->|"no"| ALLOCPT["pte_alloc — materialise<br/>the PTE table if the PMD is empty"] ALLOCPT -->|"fails"| OOM["VM_FAULT_OOM"] ALLOCPT --> W{"FAULT_FLAG_WRITE?"} W -->|"no (read)"| ZP{"mm_forbids_zeropage?"} ZP -->|"no"| ZERO["map my_zero_pfn,<br/>read-only, pte_mkspecial<br/>NO page allocated"] ZP -->|"yes (e.g. some KVM guests)"| ALLOC W -->|"yes (write)"| ALLOC["vmf_anon_prepare<br/>alloc_anon_folio"] ALLOC --> ORD["alloc_anon_folio: pick the largest<br/>enabled mTHP order whose aligned<br/>range is entirely pte_none"] ORD --> ZFILL["folio_zero_user()<br/>then __folio_mark_uptodate (barrier)"] ZFILL --> ACC["add_mm_counter(MM_ANONPAGES, nr_pages)<br/>folio_add_new_anon_rmap(RMAP_EXCLUSIVE)<br/>folio_add_lru_vma"] ZERO --> UFF{"userfaultfd_missing(vma)?<br/>checked under the ptl"} ACC --> UFF UFF -->|"yes"| HU["handle_userfault(VM_UFFD_MISSING)<br/>the fault goes to userspace"] UFF -->|"no"| SET["set_ptes(..., nr_pages)<br/>update_mmu_cache_range"]
do_anonymous_page at v6.12. What it shows: the read and write paths diverge completely — one installs a PTE pointing at a page that already exists and is shared system-wide, the other allocates, zeroes, accounts and reverse-maps. The insight to take: the nr_pages threaded through the bottom three boxes is the quiet change of the last few releases. set_ptes(..., nr_pages) means one anonymous fault can install many PTEs at once, because alloc_anon_folio may have returned a multi-page folio. The old mental model of “one fault, one 4 KiB page” is no longer accurate for anonymous memory.
The read case: the shared zero page. The kernel does not allocate anything. It builds a PTE pointing at my_zero_pfn — the single, system-wide, read-only zero page — marked pte_mkspecial so the rest of mm/ knows there is no real struct page ownership here. Every read-only anonymous page in every process can share that one physical frame. A subsequent write faults again (the PTE is read-only), lands in do_wp_page, and wp_page_copy notices is_zero_pfn(pte_pfn(vmf->orig_pte)) and allocates a pre-zeroed folio, skipping the copy entirely (mm/memory.c:3352-3353). The full treatment is in The Zero Page and Lazy Allocation.
This is why a process can calloc gigabytes, read all of it, and consume almost no physical RAM — every page points at the same frame. It is also why the RSS of such a process jumps the moment it starts writing.
The write case: how big is the page it allocates? Not necessarily 4 KiB. alloc_anon_folio (mm/memory.c:4653) implements multi-size THP (mTHP): it asks thp_vma_allowable_orders() which orders below PMD-order are enabled for this VMA, filters to those whose aligned range fits inside the VMA, then walks down from the largest, taking the first order whose whole aligned PTE range is still pte_none(). It bails to a plain order-0 folio if userfaultfd_armed(vma) — “if uffd is active for the vma we need per-page fault fidelity to maintain the uffd semantics.” The allocated folio is explicitly zeroed with folio_zero_user(folio, vmf->address) before being returned.
Why bother with an intermediate size at all, rather than just using PMD-sized huge pages? Because both extremes have a failure mode. Jonathan Corbet lays out the trade-off precisely: PMD-level huge pages “are large enough that they can be hard for the kernel to provide after the system has been running long enough to fragment memory. They are also subject to internal fragmentation; if only a small portion of a huge page is actually used by the process that owns it, the rest is simply wasted. Using huge pages in the wrong places can significantly increase a process’s total memory use.” Mid-sized folios “are easier for the memory-management subsystem to create and allocate; they are also more likely to be fully utilized and less subject to internal fragmentation” (LWN, Automatic mTHP creation in 7.2, 11 June 2026).
There is a hardware reason too, and it is the one most people miss: you do not need a PMD-sized page to save TLB entries. Per the same article, “more recent processors are capable of using a single TLB entry to cover eight (x86) or 16 (Arm) properly aligned, physically contiguous pages” — 32 KiB and 64 KiB respectively on a 4 KiB-page system. An mTHP of exactly that size gets most of the TLB benefit of a huge page with none of the allocation difficulty or internal fragmentation.
The kernel documents the payoff directly: mTHP means “page faults are significantly reduced (by a factor of e.g. 4, 8, 16, etc), but latency spikes are much less prominent because the size of each page isn’t as huge as the PMD-sized variant and there is less memory to clear in each page fault” (Documentation/admin-guide/mm/transhuge.rst). The orders are controlled per size under /sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/enabled, and the outcome is countable: anon_fault_alloc and anon_fault_fallback per order in the same sysfs tree. Depth on the huge-page machinery itself belongs to Transparent Huge Pages; what matters here is that “one fault, one page” is now a floor, not a rule.
Demand-fill (file-backed): do_fault
For a file-backed VMA the first access must bring the file’s contents into a page. do_fault (mm/memory.c:5401) makes a three-way choice on the access type and the sharing mode:
if (!vma->vm_ops->fault) {
/* a VMA with vm_ops but no ->fault: not populated on mmap and
missing VM_DONTEXPAND — SIGBUS unless a PTE appeared meanwhile */
} else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf); /* read: map the page-cache folio */
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf); /* private write: copy into anon */
else
ret = do_shared_fault(vmf); /* shared write: dirty the cache folio */flowchart TB IN["do_fault — memory.c:5401"] --> HASF{"vma->vm_ops->fault exists?"} HASF -->|"no"| SB["VM_FAULT_SIGBUS<br/>(or NOPAGE if a PTE raced in)"] HASF -->|"yes"| W{"FAULT_FLAG_WRITE?"} W -->|"no"| RF["do_read_fault — memory.c:5277"] W -->|"yes"| SHR{"VMA has VM_SHARED?"} SHR -->|"no"| CF["do_cow_fault — memory.c:5309<br/>read the file page, copy it into<br/>a fresh ANONYMOUS folio"] SHR -->|"yes"| SF["do_shared_fault — memory.c:5351<br/>->page_mkwrite, then dirty<br/>the page-cache folio itself"] RF --> FA{"should_fault_around?<br/>map_pages exists, no uffd,<br/>fault_around_pages > 1"} FA -->|"yes"| DFA["do_fault_around<br/>->map_pages maps every<br/>already-resident neighbour"] DFA -->|"PTE for our address installed"| DONE["return — one fault,<br/>many PTEs"] DFA -->|"our page was not ready"| DOF FA -->|"no"| DOF["__do_fault — memory.c:4855<br/>calls vm_ops->fault,<br/>usually filemap_fault"] DOF --> FF["filemap_fault — filemap.c:3301<br/>cache hit: minor<br/>cache miss: PGMAJFAULT + readahead"] FF --> FIN["finish_fault — memory.c:5071<br/>install the PTE(s), rmap, counters"] CF --> DOF SF --> DOF FIN --> DONE
The file-backed population paths at v6.12. What it shows: three destinations for what the user experiences as “touching a mapped file” — map the shared cache page, copy it into private anonymous memory, or dirty the cache page in place — and a fourth path, fault-around, that can satisfy the fault without calling ->fault at all. The insight to take: do_cow_fault is the exact moment a file page becomes an anonymous page. After it runs, the process’s copy has no relationship to the file: writing it will never reach disk, and reclaiming it requires swap. That transition is the subject of Anonymous vs File-Backed Memory.
do_read_fault (mm/memory.c:5277) tries fault-around first (next section), then __do_fault → the filesystem’s ->fault, which for almost every filesystem is filemap_fault (mm/filemap.c:3301). filemap_fault does three things in order that matter to demand paging:
- Bounds-check against
i_size.max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE); if (index >= max_idx) return VM_FAULT_SIGBUS;. This is the origin of the classic “SIGBUSon a truncated mapped file.” The check is repeated after the folio lock is taken, because the file can shrink mid-fault. - Look in the page cache.
filemap_get_folio(mapping, index). A hit is a minor fault, and if the folio is present the kernel opportunistically kicks offdo_async_mmap_readahead— unlessFAULT_FLAG_TRIEDis set, i.e. this is a retry, in which case a second readahead would be pointless. - On a miss, declare the fault major immediately.
count_vm_event(PGMAJFAULT); ret = VM_FAULT_MAJOR; fpin = do_sync_mmap_readahead(vmf);. Thenmaybe_unlock_mmap_for_io()dropsmmap_lock(pinning the file so it cannot vanish) and the fault returnsVM_FAULT_RETRY— see The Page Fault Handler for the replay protocol.
Finally finish_fault (mm/memory.c:5071) installs the PTE. It, too, can install more than one: if the folio from the page cache is large, nr_pages = folio_nr_pages(folio) and set_pte_range maps the whole thing — but only for anonymous shmem, and only when userfaultfd is not armed. The source is explicit about the reason for that restriction: “using per-page fault to maintain the uffd semantics, and same approach also applies to non-anonymous-shmem faults to avoid inflating the RSS of the process.” Mapping a whole large folio for an ordinary file would make RSS jump by the folio size on a single-byte read.
Swap-in: do_swap_page
The third flavour is re-population. A page that was demand-paged in earlier may have been reclaimed and written to swap; its PTE then holds an encoded swap entry instead of a present mapping. The next access takes the !pte_present branch into do_swap_page (mm/memory.c:4200).
flowchart TB IN["do_swap_page — memory.c:4200"] --> NS{"non_swap_entry(entry)?"} NS -->|"migration"| MIG["migration_entry_wait<br/>then return 0 and re-fault"] NS -->|"device exclusive"| DEX["remove_device_exclusive_entry"] NS -->|"device private"| DPR["under VMA lock: vma_end_read<br/>+ VM_FAULT_RETRY;<br/>else pgmap->ops->migrate_to_ram"] NS -->|"hwpoison"| HWP["VM_FAULT_HWPOISON — SIGBUS"] NS -->|"pte marker"| PMK["handle_pte_marker<br/>uffd-wp or POISONED"] NS -->|"none of these: a real swap entry"| SI["get_swap_device(entry)<br/>pins against swapoff"] SI --> SC{"swap_cache_get_folio hit?"} SC -->|"yes"| MINOR["MINOR fault —<br/>the folio never left RAM"] SC -->|"no"| SYNC{"SWP_SYNCHRONOUS_IO<br/>and __swap_count == 1?"} SYNC -->|"yes"| SKIP["skip the swap cache:<br/>alloc_swap_folio + swap_read_folio<br/>(zram / zswap / fast NVMe)"] SYNC -->|"no"| RA["swapin_readahead<br/>reads neighbours too"] SKIP --> MAJ RA --> MAJ["ret = VM_FAULT_MAJOR<br/>count PGMAJFAULT"] MAJ --> LOCK["folio_lock_or_retry<br/>may return VM_FAULT_RETRY"] MINOR --> LOCK LOCK --> EXCL{"write fault and<br/>exclusively owned?"} EXCL -->|"yes"| WR["map writable directly"] EXCL -->|"no, but write"| WP["fall through to do_wp_page"] EXCL -->|"read"| RO["map read-only"]
do_swap_page at v6.12. What it shows: only one of the six top-level branches is actually about swap; the “!pte_present” encoding is a general-purpose escape hatch that the kernel reuses for migration, device memory, poison and userfaultfd. The insight to take: the SWP_SYNCHRONOUS_IO branch is the modern common case on a machine using zram or zswap. For a compressed in-memory swap device the read is a decompression taking microseconds, so routing it through the swap cache — which exists to amortise slow, shared, asynchronous device I/O — is pure overhead; the kernel skips the cache entirely. It is still counted as a major fault, which is why pgmajfault can be enormous and yet harmless on a zram-backed system.
folio = swap_cache_get_folio(entry, vma, vmf->address); /* still in the swap cache? */
...
folio = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf); /* miss: read from swap */
...
/* Had to read the page from swap area: Major fault */
ret = VM_FAULT_MAJOR;
count_vm_event(PGMAJFAULT);
count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);Because reading from the swap device is real I/O, swap-in is counted as a major fault (VM_FAULT_MAJOR, PGMAJFAULT) — in contrast to demand-zero, which never touches a device and is always minor. That distinction is the subject of Minor and Major Faults; here the thing to take away is that it is a cost distinction, not a mechanism distinction. All three flavours run the same dispatch and end at the same set_ptes.
How much does one fault populate?
“Demand paging” implies one page per fault, and for most of Linux’s history that was true. At v6.12 it is not, and the gap between the two models is the single most useful thing to understand about modern demand paging. Three separate mechanisms amortise a fault over more than one page, and they apply to different kinds of memory.
| Mechanism | Applies to | How many pages per fault | Controlled by | Costs you |
|---|---|---|---|---|
| Plain demand paging | everything | 1 | — | One fault per 4 KiB touched |
Fault-around (do_fault_around) | file-backed read faults with a ->map_pages op | up to fault_around_pages, default 16 (64 KiB), clamped to the VMA and the PTE table | /sys/kernel/debug/fault_around_bytes | Nothing extra — it only maps folios already resident; it never starts I/O |
mTHP / large anon folios (alloc_anon_folio) | anonymous write faults | the largest enabled order whose aligned range is entirely empty | /sys/kernel/mm/transparent_hugepage/hugepages-<size>kB/enabled | Memory: you allocate and zero the whole folio for a one-byte write |
PMD-sized THP (create_huge_pmd) | anonymous and shmem, PMD-aligned | 512 (2 MiB on x86-64) | /sys/kernel/mm/transparent_hugepage/enabled + madvise(MADV_HUGEPAGE) | Memory and a latency spike to zero 2 MiB |
Large page-cache folios (finish_fault) | anonymous shmem only, unless uffd-armed | folio_nr_pages(folio) | filesystem / shmem folio-order policy | RSS inflation — which is why it is restricted |
Swap-in readahead (swapin_readahead) | swap entries, non-SWP_SYNCHRONOUS_IO | up to 2^page_cluster neighbours into the swap cache | /proc/sys/vm/page_cluster | Wasted I/O when swap locality is poor |
Table: the five ways one fault populates more than one page at v6.12, and what each costs. Note the asymmetry that makes fault-around uniquely safe: it is the only row that cannot cost you I/O or memory, because it maps only what is already in the page cache. Every other row trades memory or I/O for fewer faults.
What the fault does not do,
khugepagedmay do laterDemand paging decides how much to populate at fault time, with no knowledge of how the region will be used. The complementary mechanism runs afterwards:
khugepagedscans memory and collapses groups of base pages into huge pages once it can see that they are all actually being used. In v6.12 that thread only works at the PMD level; a patch series from Nico Pache extending it to build mTHPs at intermediate sizes was slated for the 7.2 development cycle (LWN, Automatic mTHP creation in 7.2, 11 June 2026). The algorithm builds a bitmap of which base pages in each 2 MiB region are “present, accessed relatively recently, and contain something other than zeroes,” tries a PMD collapse first, and falls back to progressively smaller mTHPs. The division of labour is worth internalising: the fault path guesses optimistically from alignment and emptiness;khugepagedcorrects the guess using observed behaviour. That is why an mTHP policy that looks wasteful at fault time can still be right overall. The collapse machinery belongs to Transparent Huge Pages.
Fault-around, the free amortisation
do_fault_around (mm/memory.c:5231) is the cheapest win in the whole fault path, and it works because of one asymmetry: the expensive part of a minor file fault is the fault, not the mapping. If ten neighbouring pages of a mapped file are already in the page cache, mapping all eleven under one page-table lock costs barely more than mapping one — and saves ten traps.
flowchart LR subgraph WO["without fault-around"] F1["fault on page N"] --> P1["map N"] F2["fault on N+1"] --> P2["map N+1"] F3["fault on N+2"] --> P3["map N+2"] F4["... 16 traps for 64 KiB"] end subgraph WI["with fault-around (default)"] G1["fault on page N"] --> CALC["clamp a 16-page window:<br/>ALIGN_DOWN to the window,<br/>clamped to the VMA and to PTRS_PER_PTE"] CALC --> MP["vm_ops->map_pages(from, to)<br/>one ptl acquisition"] MP --> R["maps every folio that is<br/>already resident, uptodate<br/>and unlocked; skips the rest"] R --> G2["N+1 .. N+15: no fault at all"] end
Fault-around, contrasting the same 64 KiB of sequential access with and without it. What it shows: the window is computed from the fault address, aligned down, then clamped so it can cross neither the VMA boundary nor the PTE-table boundary — the source says why: “this function doesn’t cross VMA or page table boundaries, in order to call map_pages() and acquire a PTE lock only once.” The insight to take: ->map_pages deliberately skips any page that is “not ready to be mapped: not up-to-date, locked, etc.” Fault-around never blocks and never starts I/O; it is pure opportunism over what the page cache already holds. If it did not install a PTE for the faulting address itself, do_read_fault falls through to the normal __do_fault path.
The window size is fault_around_pages, initialised to 65536 >> PAGE_SHIFT (mm/memory.c:5171-5172) — 16 pages, 64 KiB, on a 4 KiB-page system. It is tunable at runtime through /sys/kernel/debug/fault_around_bytes (debugfs, so CONFIG_DEBUG_FS must be on). The setter (mm/memory.c:5185-5197) rejects anything larger than one PTE table’s worth (val / PAGE_SIZE > PTRS_PER_PTE → -EINVAL, i.e. 2 MiB on x86-64), clamps the value up to a minimum of one page, and rounds it down to a power of two. Setting it to one page effectively disables the feature — the comment says so: “the minimum value is 1 page, however this results in no fault-around at all”, because should_fault_around() returns false when fault_around_pages is not greater than 1.
Two conditions disable fault-around per-VMA. There must be a ->map_pages operation — filesystems that do not implement it (many network and FUSE filesystems) get no fault-around — and uffd_disable_fault_around(vma) must be false, because a userfaultfd monitor registered for missing-page events must see every fault individually.
# inspect and tune the window (needs debugfs mounted and CONFIG_DEBUG_FS)
cat /sys/kernel/debug/fault_around_bytes # 65536 by default
echo 4096 > /sys/kernel/debug/fault_around_bytes # effectively off: 1 page
echo 131072 > /sys/kernel/debug/fault_around_bytes # 32 pagesTurning it down is occasionally useful when diagnosing: with fault-around off, minflt counts real first-touches one-for-one, which makes it much easier to tell how much of a mapping a workload genuinely uses. Leaving it off in production trades that clarity for up to 16× more traps on sequentially-read mapped files — notably on program text, which is exactly the access pattern fault-around was written for.
Worked Example — Watching Demand Paging Happen
You can observe every claim in this note directly, and the experiment is worth actually running because the numbers are more surprising than the prose.
Uncertain — the numbers in this section are derived, not measured
Verify: the fault counts, RSS figures and
smapsvalues below were computed from the mechanism (pages = bytes /PAGE_SIZE, two passes = two faults per page) and from the code paths cited above, not captured from a run on this machine during this task. Reason: no kernel was available to execute against while writing. To resolve: compile and run the program under/usr/bin/time -von a 4 KiB-page x86-64 kernel with multi-size THP disabled, and replace the figures with the observed ones. The shape of the result — two faults per page, RSS flat during the read pass, zero major faults — follows directly fromdo_anonymous_pageand is not in doubt; only the exact constants are. uncertain
Anonymous: read versus write
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#define GIB (1UL << 30)
int main(void)
{
char *p = mmap(NULL, GIB, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); /* (1) reserve 1 GiB */
if (p == MAP_FAILED) { perror("mmap"); return 1; }
/* (2) RSS is still a few hundred KiB here. Nothing is committed.
* /proc/self/smaps shows Size: 1048576 kB, Rss: 0 kB. */
volatile char sink = 0;
for (size_t i = 0; i < GIB; i += 4096)
sink = p[i]; /* (3) READ one byte per page */
/* (4) ~262144 minor faults, and RSS is STILL near zero:
* every PTE points at the one shared zero page. */
for (size_t i = 0; i < GIB; i += 4096)
p[i] = 1; /* (5) WRITE one byte per page */
/* (6) another ~262144 minor faults (COW breaks on the zero page),
* and now RSS really is ~1 GiB. */
return 0;
}Line by line: (1) returns instantly regardless of how much RAM the machine has — mmap only builds a VMA. (3) faults 262,144 times but allocates nothing but page tables; the read path of do_anonymous_page maps my_zero_pfn. (5) faults again on every page — the zero-page PTE is read-only, so each write takes a second fault through do_wp_page → wp_page_copy, which sees is_zero_pfn and allocates a pre-zeroed folio.
That “fault twice” behaviour is the single most useful thing this experiment demonstrates, and it has a practical consequence: touching memory with a read before you write it doubles your fault count. Zeroing a buffer with memset is one fault per page; checking whether it is already zero and then writing is two.
# what to run; the fault counts below are what the mechanism predicts,
# not a transcript from a specific machine
$ /usr/bin/time -v ./demandzero
Maximum resident set size (kbytes): ~1048576 # ~1 GiB, all from the write pass
Minor (reclaiming a frame) page faults: ~524288 # 2 x 262144 = read pass + write pass
Major (requiring I/O) page faults: 0The arithmetic is the point: 1 GiB / 4 KiB = 262,144 pages, faulted twice — once by the read pass and once by the write pass — so roughly 524,288 minor faults, plus a small constant for the program’s own text and stack. Zero major faults, because nothing ever touched a device. Watch the RSS split in real time with grep -E 'Rss|AnonHugePages' /proc/<pid>/smaps_rollup, or count the faults per call site with perf record -e minor-faults -g ./demandzero.
Expect the fault counts to be lower than 4 KiB × pages on a modern kernel
The ~524,288 figure above assumes order-0 anonymous folios. With multi-size THP enabled for, say, 64 KiB (
/sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabledset toalways),alloc_anon_foliowill back the write pass with 16-page folios and the write-pass fault count drops by roughly 16×. Checkanon_fault_allocandanon_fault_fallbackunder the same sysfs directory to see whether it actually happened. The read pass is unaffected — the shared zero page is not a folio you can enlarge, thoughuse_zero_pagedoes enable a PMD-sized huge zero page for PMD-aligned read faults.
File-backed: major, then minor, then fault-around
# drop caches so the file is genuinely cold (needs root; destroys other cached data)
sync; echo 3 > /proc/sys/vm/drop_caches
# read a 256 MiB file through a mapping and count faults
perf stat -e minor-faults,major-faults ./mmap_read bigfile # cold: major faults appear
perf stat -e minor-faults,major-faults ./mmap_read bigfile # warm: all minorOn the cold pass you see major faults; on the warm pass none, because the folios are in the page cache. Now compare fault counts between the two passes with fault-around off and on:
cat /sys/kernel/debug/fault_around_bytes # 65536
echo 4096 > /sys/kernel/debug/fault_around_bytes
perf stat -e minor-faults ./mmap_read bigfile # ~65536 faults (256 MiB / 4 KiB)
echo 65536 > /sys/kernel/debug/fault_around_bytes
perf stat -e minor-faults ./mmap_read bigfile # expect roughly 16x fewer (64 KiB window)The warm-pass minor-fault count is the cleanest possible measurement of fault-around’s effect, because nothing else differs between the two runs.
Reading the mapping’s state directly
/proc/<pid>/smaps gives the per-VMA truth that RSS alone hides. A 1 GiB anonymous mapping shortly after mmap, with only a little touched, looks like this (field names verbatim from smaps; the values are illustrative):
7f2c00000000-7f2c40000000 rw-p 00000000 00:00 0
Size: 1048576 kB <- the promise (the VMA)
Rss: 132 kB <- what has actually been faulted in
Pss: 132 kB
Anonymous: 132 kB
AnonHugePages: 0 kB <- how much of it is PMD-sized THP
Swap: 0 kB <- how much got evicted to swap
Locked: 0 kB <- mlock'd, so never reclaimable
Size minus Rss is exactly the memory demand paging has saved you. Swap being non-zero on an anonymous VMA means some of those pages have already made the Private → Swapped transition and will cost a major fault to get back.
Failure Modes and Common Misunderstandings
“mmap succeeded, so the memory is mine.” No. Demand paging defers not just the allocation but the possibility of failure. Under overcommit, mmap can hand back a mapping the system cannot back with RAM plus swap; the shortfall surfaces at the faulting write, where do_anonymous_page cannot get a folio and returns VM_FAULT_OOM. mmap(2) says as much about MAP_NORESERVE: “when swap space is not reserved one might get SIGSEGV upon a write if no physical memory is available.” In practice on a modern kernel the process is much more likely to be chosen by the OOM killer from inside the allocator than to receive SIGSEGV — note that pagefault_out_of_memory() itself does not invoke the global OOM killer, as The Page Fault Handler details. Either way, the lesson holds: under overcommit a successful mmap is a promise the kernel may break at fault time.
“The first pass over my array is slow, later passes are fast.” That first pass is paying the demand-paging tax — one fault per page (minor for anonymous and cached-file, major for cold file or swap). Once populated, the PTEs are present and subsequent accesses never fault. This is why benchmark harnesses that do not warm up measure the fault path instead of the code under test.
“I madvise(MADV_DONTNEED)’d a region and it lost my data.” That is the documented behaviour, not a bug. Per madvise(2), after a successful MADV_DONTNEED “subsequent accesses of pages in the range will succeed, but will result in either repopulating the memory contents from the up-to-date contents of the underlying mapped file (for shared file mappings, shared anonymous mappings, and shmem-based techniques such as System V shared memory segments) or zero-fill-on-demand pages for anonymous private mappings.” So the region reverts to its unpopulated, demand-zero state: it is the exact inverse of a fault. Note two further details from the same page: RSS drops immediately even for shared mappings where the kernel may defer the actual free, and MADV_DONTNEED “cannot be applied to locked pages, or VM_PFNMAP pages.”
“MADV_FREE is the same thing, just newer.” It is not. MADV_DONTNEED (value 4) frees immediately; MADV_FREE (value 8, “free pages only if memory pressure” per mman-common.h) marks the pages as reclaimable-on-demand but leaves the contents intact until the kernel actually needs the memory. A read after MADV_FREE may return the old data or zeroes depending on whether reclaim got there first, which makes it excellent for allocator free-lists and a trap for anything that assumes one behaviour.
“Demand paging means one fault per page.” Not since large anonymous folios and fault-around. See How much does one fault populate? above — anonymous writes may install 16 or 512 PTEs at a time, and a file read fault may install 16.
“MAP_POPULATE guarantees the mapping is fully populated.” It does not report failures. madvise(2) is explicit that, unlike MAP_POPULATE, MADV_POPULATE_READ/WRITE “do not hide errors” and return an error rather than raising SIGBUS. If you need to know that pre-faulting succeeded, use the madvise forms. Also note MAP_POPULATE combined with MAP_NONBLOCK has done nothing since Linux 2.6.23 — the man page says so outright.
“Pre-faulting with MAP_POPULATE means no more faults.” It means no more first-touch faults. The pages are ordinary reclaimable anonymous pages; under memory pressure they can still be swapped out and cost a major fault later. Only mlock/mlockall — which makes them unevictable — removes that possibility.
“Major faults are always bad.” Not on a system using zram or zswap. There, a “major” fault is a decompression from RAM taking single-digit microseconds, not a disk seek. pgmajfault can be enormous and the system perfectly healthy. Check what your swap device is before reading the counter as a pressure signal; Pressure Stall Information measures actual stall time and is the better metric.
“A large Size in smaps means the process is using that memory.” It means the process promised itself that memory. Rss is usage. Virtual-size-based alerting on JVM, Go, or sanitizer-instrumented processes — all of which reserve enormous sparse address spaces deliberately — produces nothing but false pages.
Alternatives and When to Override the Default
Demand paging is the default policy, not the only one. Every override buys predictability with memory or up-front time.
| Mechanism | What it does | Populates? | Pins? | Reports failure? | Use it when |
|---|---|---|---|---|---|
| (default) | Nothing — populate on touch | no | no | at fault time, as SIGSEGV/OOM | Almost always |
mmap(..., MAP_POPULATE) | Pre-faults the whole mapping at mmap time | yes | no | no — errors are hidden | You want the cost at a moment you choose, and the mapping is new |
madvise(MADV_POPULATE_READ) (Linux 5.14+) | Pre-faults readable, “just as if manually reading from each page,” without dirtying | yes | no | yes, as an error return | Warming a file mapping you will only read — pages stay clean, so eviction needs no writeback |
madvise(MADV_POPULATE_WRITE) (Linux 5.14+) | Pre-faults writable | yes | no | yes | Warming a heap you will write; avoids the read-then-write double fault |
madvise(MADV_WILLNEED) | Starts asynchronous readahead into the page cache | page cache only, not PTEs | no | no | You want later faults to be minor rather than major, without blocking now |
mlock / mlockall(MCL_CURRENT|MCL_FUTURE) | Pre-faults and makes pages unevictable | yes | yes | yes (ENOMEM against RLIMIT_MEMLOCK) | Hard real-time, crypto key material, anything that must never take a major fault |
madvise(MADV_HUGEPAGE) | Biases the region toward PMD-sized THP | no (still lazy) | no | n/a | Large contiguous working sets where TLB pressure dominates |
| mTHP sysfs orders | Biases anonymous faults toward mid-sized folios | no (still lazy) | no | n/a | You want fewer faults without 2 MiB zeroing spikes |
madvise(MADV_DONTNEED) | The inverse: de-populates, reverting to zero-fill-on-demand | removes | no | n/a | Returning memory to the kernel from an allocator |
madvise(MADV_FREE) | Lazy de-population: reclaimable, contents kept until pressure | removes lazily | no | n/a | Allocator free-lists where you may reuse the page soon |
Table: the demand-paging override knobs at v6.12, with the madvise values taken from include/uapi/asm-generic/mman-common.h and the semantics from madvise(2) and mmap(2). The two columns people conflate are Populates and Pins. MAP_POPULATE populates without pinning, so the pages can be reclaimed five minutes later and cost you a major fault anyway; only mlock does both.
flowchart TB Q0{"Do you need the pages<br/>to survive memory pressure?"} Q0 -->|"yes — a major fault is<br/>unacceptable, ever"| MLOCK["mlock / mlockall(MCL_CURRENT|MCL_FUTURE)<br/>populates AND pins<br/>costs: RLIMIT_MEMLOCK, permanent RSS"] Q0 -->|"no"| Q1{"Do you need the PTEs<br/>installed now, or just the<br/>data in the page cache?"} Q1 -->|"just cached"| WN["madvise(MADV_WILLNEED)<br/>async readahead;<br/>later faults become MINOR"] Q1 -->|"PTEs installed"| Q2{"Is the mapping new,<br/>or already exists?"} Q2 -->|"new, at mmap time"| Q3{"Do you need to know<br/>if it failed?"} Q3 -->|"no"| MP["mmap(..., MAP_POPULATE)<br/>errors are hidden"] Q3 -->|"yes"| MPW Q2 -->|"existing, or part of one"| Q4{"Will you write it?"} Q4 -->|"read only"| MPR["madvise(MADV_POPULATE_READ)<br/>pages stay clean —<br/>no writeback on eviction"] Q4 -->|"write"| MPW["madvise(MADV_POPULATE_WRITE)<br/>avoids the read-then-write<br/>double fault"] Q0 -->|"the opposite: you want to<br/>give memory back"| Q5{"Might you reuse it soon?"} Q5 -->|"no"| DN["madvise(MADV_DONTNEED)<br/>frees now; reverts to<br/>zero-fill-on-demand"] Q5 -->|"maybe"| FR["madvise(MADV_FREE)<br/>reclaimable, contents kept<br/>until pressure"]
Choosing a demand-paging override, as a decision tree. What it shows: the questions that actually discriminate between the knobs are “must it survive reclaim,” “do you need PTEs or just cache,” “does it already exist,” and “will you write it” — in that order. The insight to take: the two branches people most often get wrong are the top one and the bottom-right one. MAP_POPULATE does not pin, so it does not protect against a later major fault; only mlock does. And MADV_POPULATE_READ leaves the pages clean, so evicting them later costs no writeback — a real saving for a read-only warm-up of a large file mapping that MADV_POPULATE_WRITE would throw away.
Mechanically, all the pre-faulting forms converge on the same code. MAP_POPULATE, mlock and MADV_POPULATE_* all end up in populate_vma_page_range (mm/gup.c:1876), which drives get_user_pages-style synthetic faults — one call into handle_mm_fault per page, with regs == NULL. That last detail has an observable consequence: pre-faulted pages bump minflt in /proc/<pid>/stat but do not appear in perf stat -e page-faults, because mm_account_fault skips the perf events when regs is NULL.
The trade-off never changes shape: demand paging minimises committed memory and startup cost at the price of first-access latency and deferred failure; every override spends memory and up-front time to buy predictable, fault-free steady-state access.
Production Notes
Overcommit is only viable because of demand paging. Linux servers routinely run with total mapped virtual memory far exceeding RAM plus swap, and it works because most reserved memory is never touched. The accounting rules that decide when the kernel refuses a mapping are in Memory Overcommit and Accounting; the reason those rules can be so permissive is this note’s subject.
Major faults still bottleneck multi-threaded processes, and the kernel knows it. On v6.12, a fault that needs I/O releases the per-VMA lock and is replayed with mmap_lock held, which “can create significant mmap_lock contention, causing threads to stall” — the subject of a whole session at the 2026 LSFMM+BPF summit led by Barry Song (LWN, Toward better handling of major page faults, 22 May 2026). Two findings from that session are directly actionable. The contention Song measured was mostly on file-backed pages, not anonymous ones — so a service that mmaps a large shared data file and takes major faults on it from many threads is the shape most affected. And the failure it produces is a priority inversion: on Android, “one thread holding mmap_lock could block the high-priority user-interface thread, causing visible stalls,” and it happens “even if the high-priority thread is not accessing the same VMA.” If you are chasing tail latency in a threaded process that major-faults, pre-faulting the mapping is not just avoiding I/O — it is avoiding a lock. No fix had been settled on as of that summit.
Fault rate is a first-class production metric, but the two classes mean different things. A rising minor fault rate is usually workload shape — a heap growing, a fork-heavy service paying COW, a process re-mmaping instead of reusing mappings. A rising major fault rate means the working set no longer fits and the system is reading from backing store to make progress. Alert on the second, investigate the first. Pressure Stall Information converts both into the thing you actually care about — stalled time — and is more robust than either counter, especially on compressed-swap systems where a “major” fault is cheap.
Latency-sensitive services pre-fault, and accept the footprint. Databases, JVMs with large heaps, low-latency trading systems and real-time audio all routinely use mlockall(MCL_CURRENT|MCL_FUTURE) or MADV_POPULATE_WRITE to move every fault out of the hot path. The cost is a higher and less flexible resident footprint and a longer startup; the benefit is that a p99.9 latency target is not at the mercy of a page fault. Note that mlockall(MCL_FUTURE) changes the meaning of every later allocation in the process — mmap now populates and pins immediately, so an allocation that used to be instant can now block for a long time or fail against RLIMIT_MEMLOCK.
Container and serverless cold start is a demand-paging problem. A cold container’s startup latency is dominated by demand-faulting the executable and its shared libraries in from a page cache that is empty — and, on overlay or network-backed images, from storage that is slow. This is why image pre-pulling, MADV_WILLNEED warm-up passes, and checkpoint/restore (CRIU) exist, and why userfaultfd-based lazy restore is the standard technique for restoring a large process image quickly: you resume the process immediately and let its own fault pattern drive what gets pulled in. That mechanism is the fault handler’s handle_userfault arm, covered in The Page Fault Handler.
The read-then-write double fault is a real, avoidable cost. Any code that probes memory before writing it — a zero-check before a memset, a checksum before an update, a copy-on-read cache — pays two faults per page on first touch instead of one, and the second one goes through wp_page_copy. If you control the allocation, MADV_POPULATE_WRITE or simply writing first eliminates it.
Tuning fault-around is a diagnostic tool more often than a performance one. The default 64 KiB window has stood for a long time and is well matched to program text. Setting /sys/kernel/debug/fault_around_bytes to 4096 during an investigation makes minflt a faithful count of distinct pages touched, which is the easiest way to measure how much of a large mapping a workload genuinely uses. Put it back afterwards.
See Also
- The Page Fault Handler — the mechanism this note’s policy rides on: the hardware trap and error code,
access_error, the per-VMA andmmap_lockregimes, the page-table descent,handle_pte_fault’s dispatch, theVM_FAULT_*protocol and the retry dance. The boundary: “why is nothing allocated until I touch it, and what can I do about it” is here; “what code runs between the trap and the PTE being written” is there. - Minor and Major Faults — the cost classification and every counter (
minflt/majflt,PGFAULT/PGMAJFAULT,getrusage, cgroupmemory.stat). This note treats minor-vs-major as a given; that note derives it. - Anonymous vs File-Backed Memory — the page-family split
do_pte_missingmakes, and what each family costs to reclaim. - The Zero Page and Lazy Allocation — the read-fault branch of
do_anonymous_pagein full. - Copy-on-Write and fork —
do_wp_pageanddo_cow_fault: the write-triggered population that is demand paging’s cousin, including the zero-page COW break this note’s worked example exercises. - The Page Cache · The Page Cache and address_space · Readahead — where
do_faultfills file pages from.do_fault_aroundis fault-driven readahead over what the cache already holds;MADV_WILLNEEDanddo_sync_mmap_readaheadare the I/O-issuing kind. - Linux Swap Subsystem · The Swap Cache — the backing store
do_swap_pagereads from, and the reclaim side that creates the swap entries. - Transparent Huge Pages —
create_huge_pmdand the mTHP orders that decide how much one anonymous fault populates. - Memory Overcommit and Accounting · The OOM Killer — why a successful
mmapcan still fail at fault time. - Direct Reclaim — what happens inside the allocator when
do_anonymous_pagecannot get a folio. - The Unevictable LRU and mlock — the only override that both populates and pins.
- Pressure Stall Information — the metric that beats raw fault counters for detecting genuine memory pressure.
- Page Table Entry · Linux Page Table Hierarchy — the PTE states
handle_pte_faultbranches on, and why a sparse address space costs no page-table memory. - Memory Management Unit — the hardware that raises the fault in the first place.
- MOC: Linux Memory Management MOC (§3 The Page-Fault Handler and Demand Paging).