The Zero Page and Lazy Allocation
Linux has a single, system-wide, read-only page full of zeros — the
ZERO_PAGE— and it uses it to make freshly-allocated anonymous memory almost free until you write to it. When a process reads an anonymous page it has never written (the common case right aftermalloc/calloc/mmap(MAP_ANONYMOUS)), the fault handler does not allocate a real page: it maps the one shared zero page read-only into the faulting PTE, so the read returns zeros at the cost of a page-table entry and nothing more. Only the first write triggers a copy-on-write-style break that allocates a genuine private page and fills it. The kernel exports the globalzero_pfnfor exactly this purpose (mm/memory.c, v6.12). The same idea scales up to transparent huge pages via the huge zero page (kernel admin-guide, Transparent Hugepage). This is why allocating a multi-gigabyte zeroed buffer returns instantly and consumes no RAM until the bytes are touched — the topic of this note, verified against the Linux 6.12 LTS tree.
Mental Model — One Page of Zeros, Shared by Everyone
There is exactly one physical page of zeros in the kernel (per architecture; empty_zero_page on x86-64), and the kernel never lets anyone write to it. Any process that reads never-written anonymous memory has all of those reads satisfied by pointing its page tables at that one shared page. Because the mapping is read-only, no process can corrupt the shared zeros, and because it is shared, a thousand processes reading untouched memory cost the system one physical page total. The first store to such an address is a fault — the hardware refuses the write to a read-only PTE — and only then does the kernel allocate a real, private, writable page. So “allocating memory” splits into two events separated in time: reserving the address range (cheap, happens at mmap) and committing physical RAM (deferred to first write).
flowchart TB MAP["mmap(MAP_ANONYMOUS) / brk<br/>creates VMA, empty PTEs<br/>(no RAM committed)"] R["First READ of a page"] --> RF["do_anonymous_page:<br/>!FAULT_FLAG_WRITE branch"] RF --> ZP["map the shared ZERO_PAGE<br/>read-only (pte_mkspecial,<br/>my_zero_pfn) — NO allocation"] W["First WRITE of a page"] --> WF["do_anonymous_page:<br/>FAULT_FLAG_WRITE branch"] WF --> AL["alloc_anon_folio:<br/>allocate a real zeroed page,<br/>map it writable + exclusive"] ZPW["Later WRITE to a page<br/>currently mapped to ZERO_PAGE"] --> COW["write fault -> do_wp_page<br/>(PTE is RO) -> allocate copy"] MAP --> R MAP --> W ZP -.-> ZPW
The two faces of an untouched anonymous page. What it shows: after mmap, a page can be reached by either a read or a write fault. A read is handled by mapping the global zero page read-only with no allocation; a write (whether the very first access, or a write that follows an earlier zero-page read) ends in allocating a real private page. The insight to take: the zero page makes reads of fresh memory cost nothing, and lazy allocation makes writes the only event that actually consumes RAM. calloc of a huge buffer is fast precisely because it produces a sea of zero-page reads, not allocations.
What the Zero Page Is
The zero page is a statically-allocated, always-zero physical page set up at boot. On x86-64 it is empty_zero_page, and the ZERO_PAGE(vaddr) accessor returns its struct page (arch/x86/include/asm/pgtable.h, v6.12):
/* "ZERO_PAGE is a global shared page that is always zero" */
extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] ...;
#define ZERO_PAGE(vaddr) ((void)(vaddr), virt_to_page(empty_zero_page))The generic memory-management code captures this page’s page frame number (PFN) once at boot into a global, zero_pfn, so the fault path can cheaply form a PTE pointing at it (mm/memory.c, v6.12):
unsigned long zero_pfn __read_mostly;
EXPORT_SYMBOL(zero_pfn);
/* "CONFIG_MMU architectures set up ZERO_PAGE in their paging_init()" */
static int __init init_zero_pfn(void)
{
zero_pfn = page_to_pfn(ZERO_PAGE(0)); /* remember which PFN is the zero page */
return 0;
}
early_initcall(init_zero_pfn);my_zero_pfn(addr) returns that PFN (the indirection exists because some architectures colour the zero page across cache sets), and is_zero_pfn(pfn) tests whether a PFN is the zero page — a check the rest of the kernel uses to recognize the special page and avoid, for example, trying to swap it or COW-pin it (include/linux/mm.h, v6.12).
Lazy Allocation — the Read vs. Write Fork in do_anonymous_page
Both behaviors — zero page on read, real allocation on write — live in a single function, do_anonymous_page, the handler for a fault on an anonymous mapping with no existing page. The very first thing it does after allocating a page-table level is to branch on whether the fault was a write (mm/memory.c, v6.12):
/* 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; /* install the zero-page PTE; allocate nothing */
}
/* Allocate our own private page. */
ret = vmf_anon_prepare(vmf);
...
folio = alloc_anon_folio(vmf); /* a real, zeroed page (or large folio) */
...
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);
...
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE); /* owned by exactly one mm */Reading this carefully:
-
Read branch.
!(vmf->flags & FAULT_FLAG_WRITE)is true for a read fault.pfn_pte(my_zero_pfn(...), prot)builds a PTE that points at the zero page;pte_mkspecialmarks it special so the rest of the kernel knows this PTE does not own a normal page (it won’t try to free or migrate the zero page). The protection bits keep it read-only. Control jumps tosetpte, which installs the entry — no folio is allocated, noMM_ANONPAGEScounter is bumped. The read instruction restarts and sees zeros.mm_forbids_zeropageis the one escape hatch: a few configurations (on s390, address spaces using storage keys, where a shared read-only zero page would be incorrect) disable the zero-page optimization, in which case even a read allocates a real page. Note that theuserfaultfd_missing(vma)test inside this same branch is a separate check — it lets a registereduserfaultfdhandler intercept the first access rather than silently mapping the zero page. -
Write branch. A write fault skips the zero-page block and proceeds to allocate.
alloc_anon_folioreturns a genuinely zeroed page (kernel-allocated anonymous memory is always zeroed, so no leak of prior contents), the PTE is built writable and dirty (pte_mkwrite/pte_mkdirty), andfolio_add_new_anon_rmap(..., RMAP_EXCLUSIVE)records that this page is owned by exactly one process — setting thePageAnonExclusivestate that COW later relies on. NowMM_ANONPAGESis incremented; this is the moment RAM is actually committed.
There is a third path that is easy to miss: a page that was first read (and so is mapped to the shared zero page read-only) and is later written. That write does not re-enter do_anonymous_page — the PTE is already present, just read-only — so it faults into do_wp_page and is handled as a copy-on-write break, allocating a private page. So whether a write is the first access or follows a zero-page read, the end state is the same: a private, writable, zeroed page; only the code path differs.
The Huge Zero Page (for Transparent Huge Pages)
The single-4 KiB zero page does not help a region that is being mapped with 2 MiB huge pages: a PMD-level fault wants to install one entry covering 2 MiB, not 512 small PTEs. So the kernel keeps a huge zero page (also called the huge zero folio) — a single HPAGE_PMD_ORDER (2 MiB on x86-64) page of zeros that a PMD entry can point at. By default the kernel maps it on a read fault to a THP-eligible anonymous region: “By default kernel tries to use huge, PMD-mappable zero page on read page fault to anonymous mapping” (admin-guide, Transparent Hugepage).
Unlike the small zero page (statically allocated forever), the huge zero page is allocated lazily on first need and freed under memory pressure. It lives behind a refcount and a shrinker (mm/huge_memory.c, v6.12):
struct folio *huge_zero_folio __read_mostly;
unsigned long huge_zero_pfn __read_mostly = ~0UL;
static bool get_huge_zero_page(void)
{
...
zero_folio = mm_alloc_pages(... HPAGE_PMD_ORDER ...); /* allocate 2 MiB of zeros */
if (cmpxchg(&huge_zero_folio, NULL, zero_folio)) { ... } /* race-safe install */
WRITE_ONCE(huge_zero_pfn, folio_pfn(zero_folio));
...
}The first process to touch eligible memory with a read pays the 2 MiB allocation; everyone else shares it. A registered shrinker (shrink_huge_zero_page_*) frees the huge zero page once its refcount drops to one (nobody but the cache holds it), returning the 2 MiB to the system. The THP read-fault handler wires this up — the read-versus-write branch mirrors the small-page case (mm/huge_memory.c, v6.12):
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
...
if (!(vmf->flags & FAULT_FLAG_WRITE) && /* a READ fault... */
!mm_forbids_zeropage(vma->vm_mm) &&
transparent_hugepage_use_zero_page()) { /* ...and use_zero_page is on */
...
zero_folio = mm_get_huge_zero_folio(vma->vm_mm); /* lazily get the huge zero page */
...
set_huge_zero_folio(pgtable, vma->vm_mm, vma,
haddr, vmf->pmd, zero_folio); /* map it at PMD level, RO */
return ret; /* allocated nothing real */
}
/* WRITE fault: allocate a real 2 MiB folio */
folio = vma_alloc_folio(gfp, HPAGE_PMD_ORDER, vma, haddr, true);
return __do_huge_pmd_anonymous_page(vmf, &folio->page, gfp);
}The behavior is tunable. Writing 0 to /sys/kernel/mm/transparent_hugepage/use_zero_page disables it (forcing real 2 MiB allocations or fallback to small pages on read), and 1 re-enables it (admin-guide). Two vmstat/THP counters expose it: thp_zero_page_alloc “is incremented every time a huge zero page used for thp is successfully allocated. Note, it doesn’t count every map of the huge zero page, only its allocation,” and thp_zero_page_alloc_failed counts allocation failures that fell back to small pages (admin-guide).
Uncertain
Verify: the exact set of conditions under which
mm_forbids_zeropageis true on 6.12/6.18 LTS — in particular the claim that the s390 storage-key case is the canonical forbidder. Themm_forbids_zeropagemacro and its setters were not fetched from the v6.12 tree in this task, so the s390 attribution rests on prior knowledge, not a consulted primary source. Reason: the macro is arch-overridable and its definition lives outside the files fetched here. To resolve: grepmm_forbids_zeropageand its#define/override in the v6.12 tree (include/linux/mm.h,arch/s390/include/asm/). uncertain
Why calloc, mmap(MAP_ANONYMOUS), and Big Buffers Are “Cheap”
This mechanism explains a frequently-surprising performance fact: allocating an enormous zeroed buffer is nearly instant and shows almost no resident memory until you use it.
-
mmap(MAP_ANONYMOUS)returns memory whose “contents are initialized to zero” (mmap(2)), but per demand paging it installs no pages atmmaptime. Reads hit the zero page; writes allocate. Sommaping 100 GiB anonymous succeeds instantly and costs ~nothing until touched (subject to overcommit accounting). -
calloc(n, size)allocates and zeroes. For large requests, the C library (e.g. glibc) services them withmmap(MAP_ANONYMOUS)and skips the explicitmemsetentirely, because the kernel already guarantees fresh anonymous pages read as zero.calloc(3)itself notes the difference frommalloc: “The memory is set to zero” (calloc(3), in malloc(3)). The win is that the zeroing is lazy and shared: acallocof a gigabyte does not write a gigabyte of zeros up front — every untouched page just reads the one zero page. -
Sparse data structures (large hash tables, bitmap arenas,
MAP_NORESERVEregions) exploit the same property: you reserve a huge address range and only the cells you actually write consume RAM.
Uncertain
Verify: that current glibc’s
callocdefinitively omitsmemsetformmap-backed large allocations on Linux (the optimization is well-established but implementation-specific and version-dependent). Reason: this is a libc implementation detail, not a kernel guarantee, and was not confirmed against a current glibc source in this task. To resolve: check glibcmalloc/malloc.c__libc_callocfor themmap/MORECOREzero-already-cleared fast path. uncertain
Failure Modes and Common Misunderstandings
-
“Touching memory once is enough to commit it.” Only a write commits a private page. A program that allocates a big buffer and only ever reads it (e.g. to check it’s zero) keeps every page mapped to the shared zero page and uses essentially no RAM — Resident Set Size stays tiny. People debugging “where did my memory go?” sometimes write a touch-loop to force commitment; a read-loop will not do it.
-
The write storm after lazy allocation. Because commitment is deferred, a program can
mmapmore than physical RAM, pass overcommit checks, and then be OOM-killed later when it writes the pages and the kernel must actually find frames. The allocation “succeeded” long before the memory existed. -
Zero page defeats some measurements. Tools that estimate memory by counting mapped pages can over-count if they treat zero-page-backed PTEs as real pages. The kernel marks these PTEs special (
pte_special) precisely so the rest ofmm/does not mistake the shared zero page for an owned page. -
use_zero_pageand latency tuning. Disabling the huge zero page (use_zero_page=0) makes the first read of THP-eligible memory allocate a real 2 MiB page (or fall back to small pages), trading the read-side savings for more deterministic — but heavier — early allocation. This is occasionally done for workloads that read-then-write nearly all their memory, where the zero-page detour is wasted effort that is paid back as a COW break.
See Also
- Demand Paging — the broader lazy-population policy; this note is the demand-zero anonymous special case in depth.
- The Page Fault Handler —
handle_mm_fault/do_anonymous_page, the dispatcher these branches live under. - Copy-on-Write and fork — how a write to a zero-page-mapped PTE (and to shared pages after fork) breaks COW and allocates a private page.
- Anonymous vs File-Backed Memory — the zero page and lazy allocation apply to anonymous mappings; file mappings fill from the page cache instead.
- Transparent Huge Pages — the huge zero page is the THP analogue of the small zero page.
- Memory Overcommit and Accounting — why lazily-committed memory can be over-promised and OOM later.
- UP: Linux Memory Management MOC — §3, the page-fault handler and demand paging.