Compound Pages and Large Folios
A compound page is the kernel’s way of treating a power-of-two run of physically contiguous base pages as a single allocation unit. The first page is the head page; the rest are tail pages. The bookkeeping that ties them together is brutally compact: every tail page’s
compound_headfield stores the address of the head page with bit 0 set, and the head page carries thePG_headflag plus, in the first tail page, the order of the allocation. Higher-order buddy-allocator allocations, huge pages, and every multi-page page-cache entry are compound pages under the hood. A large folio is simply the modern, type-safe view of an order > 0 compound page — a [[Folios and the Folio Conversion|struct folio]] whosefolio_order()is greater than zero. This note explains the physical mechanism (head/tail,compound_head, where the order is stored); its sibling Folios and the Folio Conversion explains the type abstraction layered on top.
This note pins every claim to the Linux 6.12 LTS source tree (released 2024-11-17), with 6.18 LTS (2025-11-30) deltas called out where the layout changed. All field offsets and accessor code below are quoted from include/linux/mm_types.h, include/linux/page-flags.h, and include/linux/mm.h at the v6.12 tag.
Mental Model — One Allocation, Many Page Descriptors
Recall from struct page Anatomy that the kernel keeps one struct page descriptor per physical 4 KiB page frame, packed into a giant array (mem_map / the SPARSEMEM vmemmap). When the buddy allocator hands out an order-n block — 2^n physically contiguous frames — it does not fuse those 2^n descriptors into one. There is still one struct page per frame. What the compound-page machinery does is annotate those descriptors so that, given any one of them, code can find the head and learn the size of the whole block.
flowchart LR subgraph CP["Order-2 compound page (4 contiguous frames)"] H["page[0] — HEAD<br/>PG_head set<br/>page[1] holds order=2"] T1["page[1] — TAIL #1<br/>compound_head = &head | 1<br/>flags low byte = order (2)"] T2["page[2] — TAIL #2<br/>compound_head = &head | 1"] T3["page[3] — TAIL #3<br/>compound_head = &head | 1"] end T1 -- "_compound_head() strips bit 0" --> H T2 -- "_compound_head()" --> H T3 -- "_compound_head()" --> H
An order-2 compound page: four contiguous page frames, four struct page descriptors. What it shows: only page[0] (the head) carries the PG_head flag; each tail’s compound_head field points back at the head with bit 0 set, which is simultaneously how PageTail() recognizes a tail (test bit 0) and how compound_head() recovers the head (clear bit 0). The allocation order is not stored on the head at all — it lives in the low byte of page[1].flags (the first tail). The insight: “compound page” is a convention painted onto an array of ordinary descriptors, not a distinct object. That convention is exactly what made plain struct page pointers ambiguous and motivated folios.
Why Compound Pages Exist
The base page on most architectures is 4 KiB (PAGE_SIZE). For a large object — a 2 MiB transparent huge page, a 64 KiB network buffer, a multi-page page-cache extent — managing it as 512 or 16 independent base pages is wasteful and error-prone: every operation (map, refcount, set dirty, free) would have to iterate. A compound page lets the kernel allocate, refcount, map, lock, and free the whole run as one unit, while still keeping the per-frame descriptors that hardware structures (the page tables, the pfn↔page map) require.
The canonical consumers are: the buddy allocator for any order > 0 allocation made with __GFP_COMP (which most high-order allocators set); huge pages (both hugetlbfs and THP); the SLUB slab allocator for multi-page slabs; and the page cache for large folios. The LWN folio coverage frames the underlying motivation bluntly: “The kernel really needs to manage memory in larger chunks than 4KB base pages. There are millions of those pages even on a typical laptop; that is a lot of pages to manage” (Corbet, A memory-folio update, LWN 2022).
Mechanical Walk-through — Head, Tail, and the compound_head Field
The compound_head field and the tail bit
In struct page, the union that overlays the lru/mapping words has one member dedicated to tail pages (include/linux/mm_types.h, v6.12):
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */
};The comment “Bit zero is set” is the whole trick. For a tail page, compound_head holds (unsigned long)head_page | 1. For a head or base page, this slot is occupied by the lru list head, whose first word is a pointer that is always at least 2-byte aligned, so bit 0 is naturally clear. Bit 0 is therefore an unambiguous “am I a tail?” flag that costs zero extra storage. set_compound_head() and the test functions make this explicit:
static __always_inline void set_compound_head(struct page *page, struct page *head)
{
WRITE_ONCE(page->compound_head, (unsigned long)head + 1); /* set bit 0 */
}
static __always_inline int PageTail(const struct page *page)
{
return READ_ONCE(page->compound_head) & 1 || page_is_fake_head(page);
}The page_is_fake_head() term handles the hugetlb vmemmap optimization, where many tail-page descriptors are deduplicated onto a single physical page; it is a corner case and can be ignored for the common path.
Recovering the head — compound_head() and _compound_head()
Given any page, compound_head() returns the head (itself, if it is already a head or base page):
static __always_inline unsigned long _compound_head(const struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);
if (unlikely(head & 1))
return head - 1; /* tail: clear bit 0 → head address */
return (unsigned long)page_fixed_fake_head(page); /* head/base: itself */
}
#define compound_head(page) ((typeof(page))_compound_head(page))This single function was, for a decade, the source of the ambiguity that folios fix: thousands of call sites took a struct page * and had to remember to call compound_head() first, or risk a BUG when handed a tail page. As Matthew Wilcox put it when proposing folios, “A function which has a struct page argument might be expecting a head or base page and will BUG if given a tail page… We have examples of all of these today” (LWN 2021).
PageHead and PageCompound
A head page is identified by the PG_head flag, which lives at a fixed bit (bit 6, per the v6.12 enum pageflags). PageCompound() is true for any page that is part of a compound page — head or tail:
static __always_inline int PageHead(const struct page *page)
{
return test_bit(PG_head, &page->flags) && !page_is_fake_head(page);
}
static __always_inline int PageCompound(const struct page *page)
{
return test_bit(PG_head, &page->flags) ||
READ_ONCE(page->compound_head) & 1;
}So a base page (order 0) is neither head nor tail: PG_head clear, compound_head bit 0 clear, PageCompound() false.
Where the order is stored — the first tail page’s flags
This is the detail most write-ups get wrong, and it is the bridge to large folios. The allocation order is not stored on the head page. It is stored in the low 8 bits of the first tail page’s flags word — i.e. page[1].flags. compound_order() (v6.12, include/linux/mm.h) reads it by reinterpreting the page as a folio:
static inline unsigned int compound_order(struct page *page)
{
struct folio *folio = (struct folio *)page;
if (!test_bit(PG_head, &folio->flags))
return 0; /* not a head → order 0 */
return folio->_flags_1 & 0xff; /* low byte of page[1].flags */
}Here folio->_flags_1 is, by the folio’s offset-aliasing definition, the same memory as page[1].flags — the static assertion FOLIO_MATCH(flags, _flags_1) in mm_types.h guarantees offsetof(struct folio, _flags_1) == offsetof(struct page, flags) + sizeof(struct page). The reason the order lives in the first tail rather than the head is purely a space problem: the head page’s own flags word is fully consumed by real page flags, but the tail pages’ flags words are otherwise unused and free to repurpose.
compound_nr() returns 2^order (the number of pages), and is defined to return 1 for a tail page or a base page — it never faults on the wrong input. The comment on compound_order() warns it can return “wild return values” during the brief window when PG_head is set before the order field is initialised; callers in compaction.c are written defensively for exactly this race.
Large Folios — The Folio View of an Order > 0 Compound Page
A folio is a struct folio * that is guaranteed not to be a tail page. A large folio is a folio whose order is greater than zero — i.e. it wraps the head of a multi-page compound page. The folio accessors read exactly the same bits as the page-level ones, because struct folio is byte-for-byte aliased onto struct page (see Folios and the Folio Conversion for the FOLIO_MATCH mechanism):
static inline unsigned int folio_order(const struct folio *folio)
{
if (!folio_test_large(folio))
return 0;
return folio->_flags_1 & 0xff; /* identical bits to compound_order() */
}
static inline long folio_nr_pages(const struct folio *folio)
{
if (!folio_test_large(folio))
return 1;
#ifdef CONFIG_64BIT
return folio->_folio_nr_pages; /* denormalized cache of 2^order */
#else
return 1L << (folio->_flags_1 & 0xff);
#endif
}Two things are worth pausing on. First, folio_test_large() is just PG_head on the head page — “large” and “order > 0 compound” are the same predicate. Second, on 64-bit builds the page count is stored redundantly in a dedicated field, _folio_nr_pages, rather than recomputed as 1 << order on every call. This field lives in the second page of the folio’s descriptor (__page_1), space that exists only because the folio is large — there is no room in a single base page’s descriptor. folio_nr_pages() is one of the hottest functions in reclaim and the page cache, so caching 2^order is a deliberate micro-optimization.
The first tail page of a large folio carries a cluster of large-folio-only metadata, defined in the second union of struct folio (v6.12):
union {
struct {
unsigned long _flags_1; /* == page[1].flags; low byte = order */
unsigned long _head_1; /* == page[1].compound_head */
atomic_t _large_mapcount;
atomic_t _entire_mapcount; /* times mapped as a single PMD entry */
atomic_t _nr_pages_mapped;
atomic_t _pincount; /* GUP/DMA pins, see get_user_pages */
#ifdef CONFIG_64BIT
unsigned int _folio_nr_pages; /* cached 2^order */
#endif
};
struct page __page_1;
};This is why a large folio’s descriptor is multiple struct pages wide even though it represents one logical object: the head page holds the normal fields, and the tail-page descriptors are repurposed to hold large-folio bookkeeping (order, page count, the various mapcounts that rmap needs to track how a 2 MiB folio is mapped — wholly via one PMD entry vs. piecemeal via PTEs).
How large folios reach the page cache
The page cache indexes file contents via an XArray keyed by page offset. When a large folio is inserted, the kernel stores the same folio pointer at each of the folio_nr_pages() consecutive slots it covers, so a lookup at any offset within the range returns the one folio. Allocation of large folios in the cache started narrow: as of the 2022 LSFMM report, “Only the readahead code allocates them now; the filesystem write path still does everything in terms of base pages” (Corbet, LWN 2022). That has expanded over subsequent releases, but the staged nature of it is exactly why the folio conversion is dated milestone-by-milestone rather than declared “done.”
Uncertain
Verify: the precise extent of large-folio use on the filesystem write path as of 6.12/6.18 LTS — the “write path still uses base pages” claim is dated to the May 2022 LSFMM report and has certainly advanced (iomap-based filesystems gained large-folio write support across the 6.x series). Reason: I did not trace the per-filesystem write path in the 6.12/6.18 tree. To resolve: read
mm/filemap.cfilemap_get_folio/__filemap_get_foliowithFGP_CREATcallers and the iomap buffered-write code at the v6.12/v6.18 tags. uncertain
Worked Example — Reading the Metadata by Hand
Suppose alloc_pages(GFP_KERNEL | __GFP_COMP, 9) returns a head page p for an order-9 (2 MiB, 512-frame) compound page. Then:
PageHead(p)is true (PG_headset onp);PageTail(p)is false (p->compound_headbit 0 clear).compound_order(p)reads((struct folio *)p)->_flags_1 & 0xff, i.e. the low byte ofp[1].flags, returning9.compound_nr(p)returns1 << 9 == 512.- For any tail, say
&p[300]:PageTail(&p[300])is true;compound_head(&p[300])readsp[300].compound_head, sees bit 0 set, and returnsp[300].compound_head - 1 == p. - Viewed as a folio,
folio = page_folio(p)(a no-op cast sincepis already a head);folio_order(folio) == 9,folio_nr_pages(folio) == 512(read from the cached_folio_nr_pageson 64-bit).
If you instead passed a tail into folio_order() you would get a wrong answer — which is precisely why the type discipline of [[Folios and the Folio Conversion|struct folio]] exists: a folio * is, by contract, never a tail, so the accessors are sound.
Point-in-Time Layout: 6.12 vs 6.18
The compound-page concept is stable, but the exact tail-page metadata layout shifts release to release as the rmap and folio work proceeds. Comparing struct folio’s large-folio union (include/linux/mm_types.h) between the two LTS trees actually fetched:
- 6.12 (2024-11-17): the first-tail block is
_large_mapcount,_entire_mapcount,_nr_pages_mapped,_pincount, and_folio_nr_pages(the cached page count, 64-bit only). - 6.18 (2025-11-30): the field is renamed
_nr_pages(matching its kdoc, “callfolio_nr_pages()”), and a new per-MM mapcount-tracking cluster appears —_mm_id[2],_mm_ids, and_mm_id_mapcount[2]— introduced to let rmap distinguish how many distinctmm_structs map a large folio (relevant to deciding when a COW folio is exclusively owned). Additionally,folio->flags(andpage->flags) changed type from a bareunsigned longtomemdesc_flags_t.
Uncertain
Verify: the rationale and full semantics of the 6.18
memdesc_flags_tflags-type change and the_mm_idper-MM mapcount cluster. Reason: I confirmed the field-level diff by comparing thev6.12andv6.18mm_types.hdirectly, but did not read the “memdesc” patch series or the per-MM mapcount series that introduced them, so I cannot explain why beyond the inline kdoc. To resolve: read the LWN coverage of the memdesc work and the rmap per-MM mapcount series, and the commit messages at the introducing tags. uncertain
Common Misunderstandings and Failure Modes
- “The order is on the head page.” No — it is in the low byte of the first tail page’s
flags(page[1].flags), read viafolio->_flags_1 & 0xff. The head’s flags word is full of real page flags. This is why a single-page allocation can’t be a “large folio”: there is no tail page to hold the order. - Passing a tail page to a head-expecting function. The classic pre-folio bug. A function that reads
page->mappingor refcounts the page must operate on the head; given a tail, the fields are garbage. The historical fix was a defensivecompound_head()at the top of the function; the structural fix is to take astruct folio *, which cannot be a tail by construction (LWN 2021). - Refcount/mapcount confusion. A compound page has one refcount (on the head,
_refcount), but mapping accounting is split:_entire_mapcountcounts times the whole folio is mapped by a single huge (PMD) entry, while per-base-page_mapcount/_nr_pages_mappedtrack piecemeal PTE mappings. Misreading a tail’s_mapcountas the folio’s mapcount is a bug — usefolio_mapcount(). - Assuming
compound_order()is always valid. During allocation there is a window wherePG_headis set before the order byte is written;compound_order()explicitly documents it may return wild values then. Compaction handles this with re-checks under the appropriate lock. __GFP_COMPvs. high-order non-compound. A high-order buddy allocation is only a compound page if__GFP_COMPwas requested. Rawalloc_pages()without it returns2^ordercontiguous but independent descriptors with no head/tail linkage —PageCompound()is false. Most callers that want a single managed object pass__GFP_COMP(or use higher-level helpers likefolio_alloc()that do).
Alternatives and When Each Applies
- Base pages (order 0). The default. No compound machinery, minimal metadata, maximal flexibility. Used everywhere the working set is small or fragmented.
- Compound pages via
__GFP_COMP. When the kernel wants a contiguous run treated atomically — huge pages, large network/DMA buffers, multi-page slabs. - Large folios. The modern, type-safe interface to the same compound pages for the page cache and anonymous memory. Multi-size THP (“mTHP”) uses large anonymous folios — documented in
transhuge.rstsince v6.8 (the doc string “Modern kernels support ‘multi-size THP’ (mTHP)” appears at v6.8 and not at v6.6) — to back anonymous memory in sizes between a base page and the PMD-size huge page (16K, 32K, 64K, …), reducing page faults and TLB pressure without the internal-fragmentation cost of always using 2 MiB (Corbet, Large folios for anonymous memory, LWN 2023). hugetlbfsgigantic pages. For order >MAX_ORDER(e.g. 1 GiB), allocated through the hugetlb pool rather than the buddy allocator’s normal path;MAX_FOLIO_NR_PAGESis bounded byPUD_ORDERon architectures withCONFIG_ARCH_HAS_GIGANTIC_PAGE.
Production Notes
The headline production payoff of large folios is fewer faults and better TLB reach. The anonymous-large-folio work reported “approximately 5% reduction in the time needed [to compile a kernel], with a reduction in kernel time of about 40%” — driven mostly by collapsing what would be 16 separate 4 KiB faults into a single 64 KiB fault, and by reduced page-table walk and TLB-miss cost (Corbet, LWN 2023). The same article notes large folios fall back gracefully: if a contiguous, suitably-aligned physical run is unavailable, or the folio would cross a VMA boundary, or part of the range is already mapped, the kernel drops to a smaller order rather than failing — so the feature degrades to base pages under fragmentation rather than causing allocation failures.
The flip side, visible in field reports, is memory fragmentation: sustaining a supply of high-order free runs requires compaction, and under heavy fragmentation large-folio allocations silently degrade to base pages, erasing the benefit. This is the same tension that governs THP — the gain is real but contingent on the buddy allocator being able to find contiguous runs.
See Also
- Folios and the Folio Conversion — the type abstraction (
struct folio, the offset-aliasing trick, thefolio_*API, the multi-release conversion) layered on the physical mechanism described here. - struct page Anatomy — the per-frame descriptor whose fields are repurposed for compound bookkeeping.
- The Buddy Allocator — produces the contiguous power-of-two runs;
__GFP_COMPmakes them compound. - Huge Pages Overview / Transparent Huge Pages / hugetlbfs and Reserved Huge Pages — the primary consumers of large compound pages.
- The Page Cache — where large folios become multi-page cache entries.
- The Translation Lookaside Buffer and TLB Shootdowns — why mapping more memory per entry (huge pages, large folios) matters.
- UP: Linux Memory Management MOC — §6, struct page, Folios, and the Physical Memory Model.