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 after malloc/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 global zero_pfn for 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_mkspecial marks 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 to setpte, which installs the entry — no folio is allocated, no MM_ANONPAGES counter is bumped. The read instruction restarts and sees zeros. mm_forbids_zeropage is 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 the userfaultfd_missing(vma) test inside this same branch is a separate check — it lets a registered userfaultfd handler 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_folio returns 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), and folio_add_new_anon_rmap(..., RMAP_EXCLUSIVE) records that this page is owned by exactly one process — setting the PageAnonExclusive state that COW later relies on. Now MM_ANONPAGES is 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_zeropage is true on 6.12/6.18 LTS — in particular the claim that the s390 storage-key case is the canonical forbidder. The mm_forbids_zeropage macro 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: grep mm_forbids_zeropage and 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 at mmap time. Reads hit the zero page; writes allocate. So mmaping 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 with mmap(MAP_ANONYMOUS) and skips the explicit memset entirely, because the kernel already guarantees fresh anonymous pages read as zero. calloc(3) itself notes the difference from malloc: “The memory is set to zero” (calloc(3), in malloc(3)). The win is that the zeroing is lazy and shared: a calloc of 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_NORESERVE regions) 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 calloc definitively omits memset for mmap-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 glibc malloc/malloc.c __libc_calloc for the mmap/MORECORE zero-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 mmap more 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 of mm/ does not mistake the shared zero page for an owned page.

  • use_zero_page and 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