Folios and the Folio Conversion

A folio is a type-safe handle to a [[struct page Anatomy|struct page]] that is guaranteed never to be a tail page — it is always either a base page or the head of a compound page. Structurally it is almost nothing: struct folio { struct page page; ... }, an alias for the head page laid out so that a struct page * can be cast to a struct folio * for free. The point is not the bytes but the contract: a function taking struct folio * no longer has to wonder “did someone hand me a tail page?”, removing both the ambiguity and the defensive compound_head() calls that used to guard against it (Wilcox, via LWN 2021). (The honest framing of the payoff is clarity and cheapness, not a body count of fixed bugs — see Why the Conversion Was Worth the Pain, which quotes LWN’s contemporaneous scepticism on exactly that point.) The folio conversion is the multi-year, still-ongoing effort — begun by Matthew (Willy) Wilcox and merged starting in Linux 5.16 — to push struct folio through the page cache, reclaim, and the filesystems, replacing thousands of struct page interfaces. This note explains what the type is and why; its sibling Compound Pages and Large Folios explains the physical head/tail mechanism the type sits on.

This note pins claims to Linux 6.12 LTS (released 2024-11-17), a maintained long-term-support release; mainline has since moved into the 7.x series, and every fact dated later than 6.12 is labelled with the release it belongs to. The historical merge release was verified directly against the v5.15/v5.16/v5.17 source tags, and the post-6.12 markers against v6.15/v6.16/v6.17/v6.18/v7.0 (see Timeline and What Is Still Unconverted below). Where a number is described as “measured”, it was produced during the writing of this note by fetching the named file at the named tag and counting — not quoted from a summary.

Scope — this note and its neighbours

The folio material in this vault is split four ways, and the boundary matters because each note is deep:

NoteOwns
This noteThe type and the conversion: what struct folio is, the aliasing trick and the compile-time asserts that make it safe, the ambiguity it removes, the folio_* API, the mechanical conversion pattern, the release-by-release conversion status, and where it is heading
Compound Pages and Large FoliosThe physical mechanism underneath: head/tail pages, compound_head, where the order byte lives, folio_order()/folio_nr_pages() internals, the 6.12↔6.18 tail-metadata diff
struct page AnatomyThe per-frame descriptor a folio aliases — the five-word role union, the size budget, and the memdesc end-state in detail
The Page Cache and The Page Cache and address_spaceThe subsystem that consumed the conversion first: readahead folio orders, MAX_PAGECACHE_ORDER, mapping_set_large_folios(), and the full address_space_operations vtable

This note therefore does not re-derive how compound_order() reads the first tail page’s flags, and it does not enumerate the twenty a_ops methods. It uses both as evidence about the conversion’s shape.


Mental Model — A Type That Cannot Be a Tail

The deepest problem folios solve is terminological and type-level, not physical. For decades a struct page * could mean three different things: a base page, the head of a compound page, or a tail page of one. The compiler could not tell them apart — they are all struct page *. A function that dereferenced page->mapping was correct for a head or base page and wrong for a tail (whose mapping slot is repurposed as compound_head). The result, in Wilcox’s words: “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).

flowchart TB
  subgraph BEFORE["Before: struct page * is ambiguous"]
    P["struct page *"] --> B1["base page (ok)"]
    P --> H1["head page (ok)"]
    P --> T1["tail page (BUG if deref'd)"]
  end
  subgraph AFTER["After: struct folio * is a promise"]
    F["struct folio *"] --> B2["base page"]
    F --> H2["head page"]
    F -. "never " .-x T2["tail page"]
  end
  P -. "page_folio() normalizes to head" .-> F
  F -. "folio_page(f, n) selects a subpage" .-> P

The type contract folios introduce. What it shows: a struct page * may legally be a tail page, so any code dereferencing head-only fields needs a guard; a struct folio *, by construction and convention, is never a tail, so the same code is unconditionally safe. page_folio() normalizes any page to its containing folio (the head); folio_page() goes the other way to address an individual subpage. The insight: the conversion is fundamentally about moving an invariant the programmer used to enforce by hand (compound_head() everywhere) into the type system, where the compiler and the API shape enforce it instead.

The word “type” is doing real work in that sentence, and it is worth being precise about what kind of type safety this is, because it is not the kind a language with a strong type system would give you. C will happily let you write (struct folio *)some_tail_page. Nothing in the compiler stops it. What the conversion buys is weaker but, in a kernel, sufficient: every function that produces a folio produces a head, and every function that consumes one is written assuming that. The invariant is maintained at the boundariespage_folio() normalizes, folio_alloc() and filemap_alloc_folio() return heads, __filemap_get_folio() returns heads — and the type annotation then documents and propagates it. It is the same species of guarantee as struct sk_buff * always pointing at a valid skb: enforced by discipline at a small number of constructors, not by the compiler at every use.

The four behaviours a struct page * argument could mean

The clearest single statement of the problem is Wilcox’s own, from the first posting of the series in December 2020, quoted by LWN. He enumerated four distinct contracts that a function taking struct page * could be honouring, with no way for the caller to tell which:

flowchart TB
  CALL["caller has a struct page *<br/>(could be base, head, or tail)"]
  CALL --> F["callee: void f(struct page *p)"]
  F --> B1["(1) expects head or base<br/>BUGs if given a tail"]
  F --> B2["(2) accepts anything,<br/>operates on PAGE_SIZE bytes"]
  F --> B3["(3) accepts anything;<br/>page_size() bytes if head,<br/>PAGE_SIZE bytes if base or tail"]
  F --> B4["(4) accepts head or tail,<br/>operates on page_size() bytes"]
  B1 --> Q{"which one is it?<br/>the signature does not say"}
  B2 --> Q
  B3 --> Q
  B4 --> Q
  Q --> READ["read the implementation,<br/>every time"]
  Q --> DEFEND["or defensively call<br/>compound_head() and hope"]

The ambiguity Wilcox catalogued, drawn as the decision a caller cannot make. What it shows: four mutually incompatible contracts share one signature; “We have examples of all of these today,” he wrote (Wilcox, quoted by Corbet, LWN, March 2021). PAGE_SIZE is the base page size (4 KiB on x86-64); page_size() returns the size of the whole possibly-compound allocation. The insight to take: the cost was never a single spectacular bug — it was that the type carried no information, so every call site had to be resolved by reading code, and every defensive compound_head() was a programmer compensating for a signature that would not tell them anything. Replacing the argument with struct folio * collapses (1), (3) and (4) into one meaning and forces (2) to say so by taking a struct page * deliberately.


What struct folio Actually Is — The Offset-Aliasing Trick

A folio is not a separate object with its own storage. It is the same memory as the head struct page, viewed through a type that documents the head-only invariant and exposes the extra tail-page fields of a large folio. The mechanism is a hand-built union plus a wall of static assertions.

To see why the trick works you first have to see what it is aliasing onto. Here is struct page at v6.12 as it is actually laid out on x86-64 with CONFIG_MEMCG=y — 64 bytes, eight machine words. (The field names in the middle five words depend on the page’s current role; the names below are the page-cache/anonymous overlay, which is the one a folio cares about. struct page Anatomy walks the other overlays.)

---
config:
  packet:
    bitsPerRow: 64
---
packet-beta
0-63: "byte 0-7 — flags (page flags; low byte of a tail's flags holds the order)"
64-127: "byte 8-15 — lru.next | compound_head (bit 0 = 1 marks a TAIL)"
128-191: "byte 16-23 — lru.prev | mlock_count"
192-255: "byte 24-31 — mapping (address_space*, or anon_vma with bit 0 set)"
256-319: "byte 32-39 — index (file offset in pages)"
320-383: "byte 40-47 — private (buffer_heads / swp_entry_t / buddy order)"
384-415: "byte 48-51 — page_type | _mapcount"
416-447: "byte 52-55 — _refcount"
448-511: "byte 56-63 — memcg_data"

The 64-byte struct page on x86-64, drawn at byte accuracy from the v6.12 definition in include/linux/mm_types.h and confirmed against the running kernel’s BTF (see the measurement below). Ranges are bit offsets, as packet-beta requires; each row is one 8-byte word. What it shows: the whole descriptor is eight words, and words 1–5 are a role-dependent union — the same 40 bytes are lru/mapping/index/private for a page-cache page and a bare compound_head for a tail page. The insight to take: bit 0 of the second word is the load-bearing bit in this entire design. On a tail page it is set, which is how _compound_head() recognises a tail; on every other role it must be clear, which is why the comment in the source warns that other union users “MUST NOT use the bit”. The folio type exists to stop code from having to check that bit by hand.

Now the trick. struct folio declares its named fields in exactly the order and at exactly the widths above, and then unions the whole block with a literal struct page. Because the two arms of a union start at the same address, folio->mapping and page->mapping are the same 8 bytes. A cast between the two pointer types is therefore a pure reinterpretation — no copy, no allocation, no indirection. From include/linux/mm_types.h at v6.12, in skeleton:

struct folio {
    union {
        struct {
            unsigned long flags;            /* == page->flags */
            union { struct list_head lru; /* mlock slot */ };
            struct address_space *mapping;  /* == page->mapping */
            pgoff_t index;
            union { void *private; swp_entry_t swap; };
            atomic_t _mapcount;
            atomic_t _refcount;
            /* ... memcg_data, virtual, _last_cpupid ... */
        };
        struct page page;                   /* overlays the head page exactly */
    };
    union {                                 /* second descriptor: large folios */
        struct {
            unsigned long _flags_1;         /* == page[1].flags (holds order) */
            unsigned long _head_1;
            atomic_t _large_mapcount;
            atomic_t _entire_mapcount;
            atomic_t _nr_pages_mapped;
            atomic_t _pincount;
            unsigned int _folio_nr_pages;   /* 64-bit: cached 2^order */
        };
        struct page __page_1;
    };
    /* ... a third union (__page_2) for hugetlb/deferred-split fields ... */
};

The critical structural fact, and the one most descriptions get wrong, is that at v6.12 a struct folio is three struct pages wide, not one. The three unions overlay page[0], page[1] and page[2] — the head page and the first two tail pages of a large folio. A folio’s own storage is those three descriptors. That is only legal because a large folio, by definition, has tail pages whose descriptors are otherwise mostly wasted, so the kernel repurposes them as a place to put per-folio bookkeeping that would never fit in one 64-byte descriptor.

packet-beta can draw a linear field layout but cannot draw a correspondence between two type views of the same bytes, which is the actual idea here — so this one falls back to an ASCII box diagram, with byte offsets labelled:

        struct folio (192 B at v6.12)              the memory it aliases
        ============================               =====================
byte
   0  +--------------------------------+   <----   &mem_map[pfn]  ... struct page page;
      | flags        (also _flags_0)   |            page[0].flags
   8  | lru / mlock_count              |            page[0].lru        }
  16  |                                |                               } head page:
  24  | mapping                        |            page[0].mapping    } ordinary
  32  | index                          |            page[0].index      } page-cache
  40  | private / swap                 |            page[0].private    } fields
  48  | _mapcount        | _refcount   |            page[0]._mapcount / _refcount
  56  | memcg_data                     |            page[0].memcg_data
  64  +--------------------------------+   <----   &mem_map[pfn+1] ... struct page __page_1;
      | _flags_1                       |            page[1].flags   <-- low byte = ORDER
  72  | _head_1                        |            page[1].compound_head
  80  | _large_mapcount | _entire_map  |            (repurposed tail-page bytes:
  88  | _nr_pages_mapped | _pincount   |             large-folio-only bookkeeping
  96  | _folio_nr_pages  |    pad      |             that has nowhere else to live)
 ...  |            (unused)            |
 128  +--------------------------------+   <----   &mem_map[pfn+2] ... struct page __page_2;
      | _flags_2 / _flags_2a           |            page[2].flags
 136  | _head_2  / _head_2a            |            page[2].compound_head
 144  | _hugetlb_subpool  -or-         |            (hugetlb metadata, OR the
 152  | _hugetlb_cgroup   -or-         |             _deferred_list links used to
 160  | _hugetlb_cgroup_rsvd -or-      |             queue a THP for deferred split)
 168  | _hugetlb_hwpoison -or-         |
      | _deferred_list                 |
 191  +--------------------------------+

How struct folio overlays three consecutive struct page descriptors at v6.12. Fallback to ASCII because the diagram’s subject is a two-column correspondence, not a single field sequence — packet-beta has no notion of “these bytes are simultaneously X and Y”. What it shows: bytes 0–63 are the head page verbatim; bytes 64–127 and 128–191 are the first two tail pages, whose ordinary contents (flags, compound_head) are preserved at their natural offsets — that is what _flags_1 and _head_1 are — while the rest of each tail descriptor is reclaimed for folio-only fields. The insight to take: the folio is not “a page plus extra”; it is a window over a run of page descriptors, and every field beyond byte 63 only exists if the folio is large. This is precisely why folio_order() must check folio_test_large() before reading _flags_1: on an order-0 folio, the bytes at offset 64 belong to an unrelated page frame.

That structure has grown, and the growth is measurable. Fetching include/linux/mm_types.h at successive tags and counting gives the shape of the type’s own history:

Tagstruct folio defined?struct page overlaysFOLIO_MATCH assertssizeof(page)==sizeof(folio) asserted?
v5.15no0
v5.16yes (8 named fields)1 (page)8yes
v5.17yes19yes
v6.12yes3 (page, __page_1, __page_2)17no
v6.18yes4 (adds __page_3)19no
v7.0yes419no

Measured by fetching each tag’s include/linux/mm_types.h and counting ^struct folio {, struct page __page_, ^FOLIO_MATCH, and the sizeof assertion. What it shows: the folio began in 5.16 as a strict same-size alias for one page — the kernel asserted the sizes were equal — and that assertion was deleted as the type grew to span multiple descriptors. The insight to take: “a folio is a struct page” was literally true in 5.16 and is false from at least 6.12 onward. A folio is a head page plus a claim on the descriptors that follow it. Any explanation still quoting struct folio { struct page page; }; is describing the December-2020 patch, not a shipping kernel.

Verify this on your own machine

If your kernel exports BTF (/sys/kernel/btf/vmlinux exists — it does on Fedora, Ubuntu and RHEL kernels), you can read the actual compiled layout rather than trusting a header. On the machine this note was written on, running Linux 7.1.8-200.fc44.x86_64, parsing BTF reports struct page at 64 bytes and struct folio at 256 bytes — four page descriptors — with _hugetlb_subpool at byte 208, i.e. inside __page_3. pahole -C folio /sys/kernel/btf/vmlinux (from the dwarves package) prints the same thing in a friendlier form. This is the honest way to answer “how big is it on my kernel”, because the answer depends on CONFIG_MEMCG, WANT_PAGE_VIRTUAL, LAST_CPUPID_NOT_IN_PAGE_FLAGS and the release.


The Compile-Time Guarantees — FOLIO_MATCH

The aliasing above is a claim about field offsets. Nothing in C enforces that the author of struct folio kept the fields in the same order as struct page; a well-meaning cleanup that reordered mapping and index in one struct and not the other would silently make folio->mapping read the file offset. What stops that is a wall of static_asserts — the mechanism that turns a convention into a build failure.

The first union makes folio->flags, folio->mapping, folio->_refcount, etc. occupy the identical offsets as the corresponding struct page fields. This is enforced at compile time by the FOLIO_MATCH macro:

/* Family 1: same offset within the head page. */
#define FOLIO_MATCH(pg, fl)						\
	static_assert(offsetof(struct page, pg) == offsetof(struct folio, fl))
FOLIO_MATCH(flags, flags);
FOLIO_MATCH(lru, lru);
FOLIO_MATCH(mapping, mapping);
FOLIO_MATCH(compound_head, lru);	/* a tail's compound_head aliases the head's lru */
FOLIO_MATCH(index, index);
FOLIO_MATCH(private, private);
FOLIO_MATCH(_mapcount, _mapcount);
FOLIO_MATCH(_refcount, _refcount);
#ifdef CONFIG_MEMCG
FOLIO_MATCH(memcg_data, memcg_data);
#endif
#if defined(WANT_PAGE_VIRTUAL)
FOLIO_MATCH(virtual, virtual);
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
FOLIO_MATCH(_last_cpupid, _last_cpupid);
#endif
#undef FOLIO_MATCH
 
/* Family 2: one whole struct page further on — i.e. inside page[1]. */
#define FOLIO_MATCH(pg, fl)						\
	static_assert(offsetof(struct folio, fl) ==			\
			offsetof(struct page, pg) + sizeof(struct page))
FOLIO_MATCH(flags, _flags_1);
FOLIO_MATCH(compound_head, _head_1);
#undef FOLIO_MATCH
 
/* Family 3: two struct pages further on — inside page[2]. */
#define FOLIO_MATCH(pg, fl)						\
	static_assert(offsetof(struct folio, fl) ==			\
			offsetof(struct page, pg) + 2 * sizeof(struct page))
FOLIO_MATCH(flags, _flags_2);
FOLIO_MATCH(compound_head, _head_2);
FOLIO_MATCH(flags, _flags_2a);
FOLIO_MATCH(compound_head, _head_2a);
#undef FOLIO_MATCH

Verbatim from include/linux/mm_types.h at v6.12, lines 402–435, with the family comments added. Seventeen assertions in three families.

Read them as three distinct promises, because they guard three distinct things:

FamilyAssertionWhat it protects
1 — head-page identityoffsetof(page, X) == offsetof(folio, X)That casting a head struct page * to struct folio * and reading flags, mapping, index, private, _mapcount, _refcount, memcg_data gives the same bytes. This is what makes the cast free.
2 — first-tail overlayoffsetof(folio, _flags_1) == offsetof(page, flags) + sizeof(struct page)That folio->_flags_1 really is page[1].flags. [[Compound Pages and Large Folios|folio_order()]] reads folio->_flags_1 & 0xff; if this assert did not hold, it would read the order out of the wrong frame.
3 — second-tail overlay… + 2 * sizeof(struct page)The same for page[2], where the hugetlb pointers and the deferred-split list live. Note that two pairs are asserted for page 2 (_flags_2/_head_2 and _flags_2a/_head_2a) because that union has two arms, and both must land on the tail page’s real flags and compound_head.

Two subtleties are worth pausing on. First, FOLIO_MATCH(compound_head, lru) is not a typo: on a tail page the second word is compound_head, on a head page it is lru.next. Asserting them equal is asserting that the tail bit (bit 0 of compound_head) lands where a head page keeps a list_head pointer — which is always even, hence never has bit 0 set. That single assertion is what makes PageTail() a correct test on any page regardless of role, and it is why the struct page source carries the warning that the other union members “MUST NOT use the bit to avoid collision and false-positive PageTail()”. Second, the assertions are wrapped in the same #ifdefs as the fields — CONFIG_MEMCG, WANT_PAGE_VIRTUAL, LAST_CPUPID_NOT_IN_PAGE_FLAGS — so the guarantee holds for every configuration, not just the one the author happened to build.

flowchart LR
  DEV["developer reorders a field<br/>in struct page (or in struct folio)"]
  DEV --> SA{"static_assert:<br/>offsetof(page, X)<br/>== offsetof(folio, X)?"}
  SA -->|"holds"| OK["build succeeds;<br/>(struct folio *)head is<br/>a free reinterpretation"]
  SA -->|"violated"| FAIL["**compile error**<br/>static assertion failed<br/>— caught before boot"]
  OK --> RT["runtime cost of the cast: zero<br/>(no code is emitted)"]
  FAIL -.->|"contrast: without the assert"| SILENT["folio->mapping silently<br/>reads folio->index —<br/>corruption at run time"]

What the assertions buy. What it shows: the alias is checked once, by the compiler, on every build and every config; the failure mode is a build break rather than a class of memory corruption that only appears under load. The insight to take: this is the entire justification for building a type-safety mechanism out of offsetof and union rather than out of a real abstraction — it costs nothing at run time (the cast emits no instructions) and it cannot drift, because drift is a build failure. It is also why the folio’s fields are declared in a strange, seemingly redundant order: the order is not a design choice, it is a constraint imposed by struct page.

Because the offsets are asserted equal, casting (struct folio *)head_page is a free, zero-cost reinterpretation — no copy, no allocation. If anyone ever reorders a field in struct page and breaks the correspondence, the kernel fails to compile. This static-assert aliasing is the mechanical heart of the whole design: the type-safety is real, but it costs nothing at runtime.


The folio_* API

The conversion replaces page-oriented helpers with folio-oriented ones that take a struct folio *. The two pivot functions convert between the views (include/linux/page-flags.h, v6.12):

/* Convert any page to its containing folio (the head). */
#define page_folio(p) (_Generic((p), \
    const struct page *: (const struct folio *)_compound_head(p), \
    struct page *:       (struct folio *)_compound_head(p)))
 
/* Address subpage n within a folio (n relative to folio start). */
#define folio_page(folio, n) nth_page(&(folio)->page, n)

page_folio() is just _compound_head() (clear bit 0 of a tail’s compound_head, or return the page itself) wrapped in a cast — so it normalizes any page to the head and reinterprets it as a folio. Its kdoc is explicit that “Every page is part of a folio” and warns about the split race: without a reference, the call “may race with a folio split, so it should re-check the folio still contains this page after gaining a reference.”

The rest of the API mirrors the old page helpers with folio_ prefixes and folio semantics:

  • Reference counting: folio_get(), folio_put(), folio_ref_count() — operate on the head’s single _refcount. The kdoc on struct folio is emphatic: “Do not access this member directly. Use folio_ref_count().”
  • Mapping/identity: folio_mapping(), folio_index(), folio_pos() (byte offset in the file), folio_file_page() (the subpage for a given file index).
  • Size: folio_order(), folio_nr_pages(), folio_size() — covered in Compound Pages and Large Folios; folio_test_large() is true iff order > 0.
  • Flags: folio_test_dirty(), folio_test_locked(), folio_test_uptodate(), folio_mark_dirty(), … generated by the same macro machinery as the PageFoo() flags but typed on folios, with FOLIO_PF_* policy constants controlling whether a flag is head-only, second-page, etc.
  • Locking: folio_lock(), folio_unlock(), folio_wait_locked() — the page-cache lock, now per-folio.
  • Mapcount: folio_mapcount(), folio_entire_mapcount() — the “how many times is this mapped into user page tables” accessors that correctly aggregate a large folio’s split PTE/PMD mappings.

Higher-level call sites that used to do page = find_get_page(mapping, index) now do folio = __filemap_get_folio(mapping, index, fgp_flags, gfp), and the page cache’s whole insert/lookup/dirty/writeback path is expressed in folios.

The surface is large. Counting distinct folio_* identifiers across just three v6.12 headers — include/linux/pagemap.h, include/linux/page-flags.h, include/linux/mm.h — gives 115 of them, and that excludes everything defined in mm/ internals, rmap.h, swap.h, memcontrol.h and the flag accessors generated by macro expansion. Grouping them by what question they answer is the only way to hold them in your head:

flowchart TB
  ROOT["folio_* API — v6.12<br/>115 distinct identifiers in<br/>pagemap.h + page-flags.h + mm.h"]
  ROOT --> C["**Conversion**<br/>the only family that<br/>mentions pages"]
  ROOT --> L["**Lifetime**"]
  ROOT --> I["**Identity**"]
  ROOT --> G["**Geometry**"]
  ROOT --> S["**State**"]
  ROOT --> X["**Exclusion**"]
  ROOT --> M["**Mapping / rmap**"]
  ROOT --> R["**Reclaim**"]
  C --> Cx["page_folio · folio_page<br/>folio_file_page · folio_pfn"]
  L --> Lx["folio_get · folio_put<br/>folio_ref_count · folio_try_get<br/>folio_maybe_dma_pinned"]
  I --> Ix["folio_mapping · folio_index<br/>folio_pos · folio_test_anon<br/>folio_inode"]
  G --> Gx["folio_order · folio_nr_pages<br/>folio_size · folio_test_large"]
  S --> Sx["folio_test_dirty · folio_test_uptodate<br/>folio_mark_dirty · folio_start_writeback<br/>folio_end_read"]
  X --> Xx["folio_lock · folio_unlock<br/>folio_trylock · folio_wait_locked<br/>folio_wait_writeback"]
  M --> Mx["folio_mapcount · folio_entire_mapcount<br/>folio_add_new_anon_rmap<br/>folio_add_file_rmap_ptes"]
  R --> Rx["folio_add_lru · folio_mark_accessed<br/>folio_referenced · folio_evictable"]

The folio_* API grouped by the question each family answers, v6.12. What it shows: the API is not a flat rename of the Page* helpers — it is organised around eight concerns, and the conversion family (top-left) is the only one that mentions pages at all. The insight to take: when converting code, the right move is almost never “find the folio_ spelling of the Page function I was using”; it is “decide which of these eight questions I am asking, and use the folio-native answer.” A function that ends up calling folio_page(f, 0) to get back to a page is usually asking a geometry or state question in page vocabulary and should be rewritten.

A few of these deserve their kdoc quoted, because the documentation states contracts the names do not. folio_lock() — the page-cache lock, now per-folio — carries an explicit lock-ordering rule: “If you need to acquire the locks of two or more folios, they must be in order of ascending index, if they are in the same address_space. If they are in different address_spaces, acquire the lock of the folio which belongs to the address_space which has the lowest address in memory first” (include/linux/pagemap.h, v6.12). The same kdoc is candid that the lock “protects against many things, probably more than it should” — it is held while a folio is brought up to date from its backing file or from swap, while it is truncated from its address_space (so holding it keeps folio->mapping stable), and while write() modifies it to provide POSIX write atomicity. And folio_file_page(folio, index) exists precisely for the moment when folio vocabulary must hand back to page vocabulary: “Sometimes after looking up a folio in the page cache, we need to obtain the specific page for an index (eg a page fault).” That call is the seam between the two worlds, and the fault path is the canonical place it is crossed.


Why the Conversion Was Worth the Pain

Two motivations, of escalating importance.

First, disambiguation. Stop the head/tail confusion at the type level so that “a ‘page’ can refer either to a base page or a larger compound page” — the longstanding confusion the folio coverage calls out (Corbet, LWN 2022) — no longer leaks into every interface. Each removed compound_head() call also shrinks the kernel: those calls were inlined at thousands of sites.

Second, and the real prize: large pages everywhere. Wilcox’s framing is that the kernel must “manage memory in larger chunks than 4KB base pages” because “there are millions of those pages even on a typical laptop; that is a lot of pages to manage” (Corbet, LWN 2022). A folio “can have any size (as long as it is a power of two) and kernel code will do the right thing with it. That allows different sizes to be used in different settings” (Corbet, LWN 2023). The folio is the interface that makes large folios tractable in the page cache and in anonymous memory (multi-size THP) without rewriting every consumer to special-case “is this a huge page?” — they just operate on a folio of whatever order.

The bug-count claim, stated honestly

It is tempting — and this note said so in an earlier revision — to justify folios as “eliminating an entire class of bugs.” That overstates the primary-source record and should be corrected. LWN’s contemporaneous assessment, written while reviewing the fourth posting of the series, is deliberately careful: “There does not seem to be an extensive history of bugs resulting from this particular API, but an interface that is this poorly defined seems likely to encourage problems sooner or later” (Corbet, LWN, March 2021). Wilcox’s own framing in the first posting emphasised the cost of the defensive calls rather than a catalogue of CVEs: compound_head() “is relatively cheap, but it may be called many times over the course of a single operation on a page. That makes the kernel bigger (since it’s an inline function) and slows things down.”

So the accurate statement is: the tail-page ambiguity was a latent hazard and a measurable overhead, not a demonstrated epidemic. Later reporting reinforces this reading — by 2026 LWN describes the origin as Wilcox noticing that “a surprising amount of overhead went into ensuring, in many places in the kernel, that any passed-in struct page pointer referred to the head page” (Corbet, On pages and folios, April 2026). Overhead first; safety second; and the third motivation, large folios, only became the dominant one afterwards. A note that inverts that ordering is telling a tidier story than the record supports.

Uncertain

Verify: whether any specific, publicly-tracked kernel bug was caused by head/tail confusion in a struct page * interface and would have been prevented by folio typing. Reason: LWN in 2021 explicitly says no extensive history of such bugs was apparent, and I found no primary bug report to the contrary during this task; lore.kernel.org, where such a discussion would live, is blocked from this environment (Anubis proof-of-work challenge, verified 2026-09-04). To resolve: search lore.kernel.org/linux-mm for compound_head regression threads from 2018–2021 from a host that can reach it, or check the commit messages of fixes that added a defensive compound_head() call. Do not claim folios fixed known bugs without such a citation. uncertain

The debate, which is part of the technical record

The cost was real and contested, and the objections were substantive enough that they shaped the design’s future. Reading them is the fastest way to understand what a folio is not.

  • Andrew Morton, on churn. Reviewing v4: “Geeze it’s a lot of noise. More things to remember and we’ll forever have a mismash of page' and folio’ and code everywhere converting from one to the other… It’s unclear to me that it’s all really worth it” (LWN 2021). Hugh Dickins was similarly unenthusiastic. Kirill Shutemov and Michal Hocko supported the concept; Dave Chinner called the abstraction “absolutely necessary” for filesystem developers, particularly once the page cache could hold compound pages of several sizes.
  • Johannes Weiner, with the deepest objection — and a NAK. When Wilcox sent the pull request for 5.15, Weiner responded with “strong reservations” and, over a long thread, a coherent architectural complaint: a folio is just a physically contiguous power-of-two run of pages, and baking that into the API the filesystems see leaks memory-management internals into everything above it. He argued that a large API change is exactly the moment to introduce a more abstract memory representation that would not assume physical contiguity — because “the memory-management subsystem has never been good at allocating larger, contiguous chunks of memory… We’ve effectively declared bankruptcy on this already.” His second argument was about base-page size: the kernel would like a larger base page, but cannot have one because a one-line file would then waste a whole page in the cache (Al Viro’s back-of-envelope calculation had a 64 KiB base page quadrupling the memory needed to cache the kernel source). Weiner wanted the page cache able to work in sub-page units, which a page-derived folio cannot do. His summary was blunt: “As long as this is your ambition with the folio, I’m sorry but it’s a NAK from me” (Corbet, The folio pull-request pushback, LWN, September 2021).
  • Wilcox’s answer was that it makes little sense to manage memory in units other than the allocation size; if everything in the kernel uses larger pages, memory fragments less and larger pages become easier to allocate, and small files can simply use order-0 folios packed together. Weiner’s counter was that this pushes a problem the slab allocator already solves onto the page allocator: “The page allocator is good at cranking out uniform, slightly big memory blocks. The slab allocator is good at subdividing those into smaller objects.”
  • Linus Torvalds was lukewarm-positive on the API and worried about the churn: “So I don’t hate the patches. I think they are clever, I think they are likely worthwhile, but I also certainly don’t love them.” He also disliked the name, which produced the inevitable bikeshed — “sheaf”, “ream”, “head_page”, “mempages”, “pageset”, “cluster”, “superpage”, and Vlastimil Babka’s conclusion that the only suitable name was “pageshed”. Wilcox re-sent the entire pull request with everything renamed to “pageset” purely to demonstrate that he did not care about the name; it changed nothing.
  • The deadlock was procedural, not technical. David Howells eventually posted a plea for a decision because a queue of other memory-management work either depended on the folio patches or conflicted with them. Folios missed 5.15 and merged for 5.16.

The postscript matters: Weiner’s objection was not defeated, it was absorbed. By 2025 Wilcox described two readings of what a folio is — the “Ottawa interpretation” (his original: the head page of a compound page) and the “New York interpretation,” which he attributes largely to Weiner, in which folios are the lever for shrinking struct page to a single 8-byte memory descriptor (Corbet, The state of the page in 2025, LWN, March 2025). The memdesc work described later in this note is Weiner’s abstraction argument, arriving by a different route.

The work has been, in Wilcox’s own first-posting estimate, “a ton of work, and massively disruptive. It’ll touch every filesystem, and a good few device drivers! But I think it’s worth it.”


The Mechanical Conversion Pattern

“Converting a function to folios” sounds vague until you watch it happen. It is a small, repeatable, four-stage recipe, and the kernel keeps a dedicated file — mm/folio-compat.c — whose entire purpose is to hold the scaffolding for stage 2. Its header comment states the intent exactly: “Compatibility functions which bloat the callers too much to make inline. All of the callers of these functions should be converted to use folios eventually.

stateDiagram-v2
    [*] --> PageOnly
    PageOnly : Stage 0 — page-native<br/>f takes a struct page pointer<br/>and starts with compound_head
    FolioCore : Stage 1 — folio-native core<br/>folio_f takes a struct folio pointer<br/>and calls compound_head nowhere
    Shim : Stage 2 — compat shim<br/>the old page-named symbol survives<br/>as folio_f of page_folio of p<br/>living in mm/folio-compat.c
    Smell : Stage 2b — the code smell<br/>folio-typed signature whose body<br/>immediately casts the folio<br/>back to a struct page pointer
    Callers : **Stage 3 — callers converted**<br/>every caller now holds a folio<br/>and calls folio_f directly
    Done : **Stage 4 — shim deleted**<br/>the page spelling is removed<br/>from the tree
    PageOnly --> FolioCore : rewrite body in folio terms
    FolioCore --> Shim : keep old name working
    FolioCore --> Smell : rushed conversion
    Smell --> FolioCore : rework — the cast is<br/>a sign the job is unfinished
    Shim --> Callers : convert call sites,<br/>one subsystem at a time
    Callers --> Done : delete the shim
    Done --> [*]
    note right of Shim
      This is why "page" and "folio"
      spellings coexist for years:
      stage 2 is stable and shippable,
      and stage 3 is the expensive part.
    end note

The life cycle of one function through the conversion. What it shows: the conversion is not a flag day; each function passes through a stage where both spellings exist and the page spelling is a one-line wrapper. The insight to take: the “mishmash of page and folio” that Andrew Morton objected to is not sloppiness, it is stage 2 made visible — and the presence of mm/folio-compat.c in a tree is a direct measure of how much stage-3 work remains. It is still there at v6.12 (2,280 bytes) and still there at v7.0 (2,082 bytes), which is the most compact possible proof that the conversion is unfinished.

Stage 2 is what mm/folio-compat.c holds, and it is worth reading in full because every entry is the same three lines:

void unlock_page(struct page *page)
{
	return folio_unlock(page_folio(page));
}
EXPORT_SYMBOL(unlock_page);
 
void mark_page_accessed(struct page *page)
{
	folio_mark_accessed(page_folio(page));
}
EXPORT_SYMBOL(mark_page_accessed);
 
bool set_page_dirty(struct page *page)
{
	return folio_mark_dirty(page_folio(page));
}
EXPORT_SYMBOL(set_page_dirty);

Three of the eleven shims in mm/folio-compat.c at v6.12. Line by line: the page-named symbol is kept and exported (so out-of-tree modules and unconverted subsystems keep building); its body normalizes the page to its folio with page_folio() — which is just _compound_head() plus a cast — and tail-calls the folio-native implementation. The functions are deliberately not inline, per the file’s header comment, because inlining page_folio() at thousands of call sites is the code bloat the conversion is trying to remove.

The file at v6.12 holds exactly eleven such shims: unlock_page, end_page_writeback, wait_on_page_writeback, wait_for_stable_page, mark_page_accessed, set_page_writeback, set_page_dirty, clear_page_dirty_for_io, redirty_page_for_writepage, add_to_page_cache_lru, pagecache_get_page and grab_cache_page_write_begin. Two of those are interesting because they do more than cast: pagecache_get_page() calls __filemap_get_folio() and then returns folio_file_page(folio, index) — it has to pick the specific subpage corresponding to the requested index, because its page-typed caller cannot handle a large folio. That single line is the whole reason unconverted callers block large folios: they can only be handed one 4 KiB page at a time.

Stage 2b — the code smell. Wilcox described the failure mode himself at LSFMM 2022. In many cases the “conversion” of an address_space_operations method amounts to changing the prototype to take a struct folio * and then adding

struct page *p = (struct page *)folio;

at the top of the body. “This pattern is,” he said, “‘a bad code smell’; it is a sign that the code in question needs further work” (Corbet, A memory-folio update, LWN, May 2022). The cast is legal — that is what the whole aliasing design guarantees — but it means the function has a folio-shaped signature and page-shaped guts, and it will still break the moment it is handed an order-4 folio. When reviewing a conversion patch, that cast (and its 6.12-era spellings, &folio->page and folio_page(folio, 0)) is the single most useful thing to grep for. By 2025 Wilcox listed “removing any uses of the page member of struct folio” as a concrete way for volunteers to help (LWN, March 2025).

How a filesystem gets converted, in practice. The advice Wilcox gave repeatedly, and which filesystem maintainers acted on, has two halves. First, do not convert the page cache code yourself — move onto a layer that already speaks folios: iomap for block-based filesystems, David Howells’s netfs library for network filesystems. Both “insulate the filesystems from the page cache”, using byte counts rather than page counts, so the filesystem never has to learn what a folio is (Edge, A discussion on folios, LPC, September 2021). Second, convert incrementally: Ted Ts’o’s point at LSFMM 2022, which Wilcox endorsed, is that “a filesystem’s read path can be converted while leaving the write path unchanged for now” — useful in particular because iomap was at that time still missing capabilities (fs-verity, compression) that hurt more on the write side than the read side (LWN 2022).


Timeline — Verified Against the Source Tree

The merge release is load-bearing and frequently misstated, so it was pinned by checking for the struct folio { definition across kernel tags directly:

  • v5.15: no struct folio definition (grep "^struct folio {" → 0 matches).
  • v5.16: struct folio first appears — and it is minimal, just the single-page head wrapper (flags, lru, mapping, index, private, _mapcount, _refcount, memcg_data) with static_assert(sizeof(struct page) == sizeof(struct folio)) and a short list of FOLIO_MATCHes on the first page only. No _flags_1 / large-folio union yet.
  • v5.17 onward: the type grows the second/third descriptor unions and the large-folio fields as more of the kernel is converted.

So the folio infrastructure merged in Linux 5.16 (the merge window opened late 2021). This corroborates the LWN account that, by the May 2022 LSFMM summit, “the folio project is not yet two years old, but it has already resulted in significant changes” (Corbet, LWN 2022) — the work began around mid-2020 and the first infrastructure landed in 5.16. Subsequent milestones the primary sources fix:

  • By 5.17 / mid-2022: page-cache read and readahead paths converted; large folios allocated only by readahead, with the filesystem write path still on base pages at that time (Corbet, LWN 2022).
  • 2023 → ~6.8: large folios for anonymous memory (“multi-size THP” / mTHP), defaulting to 64 KiB anonymous folios with graceful fallback, merged via Ryan Roberts’s series (Corbet, LWN 2023); the transhuge.rst documentation for mTHP appears at the v6.8 tag and not at v6.6.
timeline
  title The folio conversion, release by release
  Dec 2020 : First posting of the folio series<br/>struct folio { struct page page; }
  Mar 2021 : v4 posted, ~100 commits<br/>Morton and Dickins sceptical<br/>Chinner "absolutely necessary"
  Aug-Sep 2021 : Pull request for 5.15<br/>Weiner NAKs the abstraction<br/>naming bikeshed, no consensus
  5.16 (Jan 2022) : struct folio MERGES<br/>8 FOLIO_MATCH asserts<br/>same size as struct page
  5.18-5.19 (2022) : a_ops renamed to folio forms<br/>dirty_folio, read_folio<br/>page cache read path folio-native
  6.8 (Mar 2024) : multi-size THP (mTHP) documented<br/>large ANONYMOUS folios
  6.12 LTS (Nov 2024) : folio spans 3 page descriptors<br/>17 FOLIO_MATCH asserts<br/>one a_ops method still page-typed
  6.16 (2025) : -writepage removed from<br/>address_space_operations
  6.18 LTS / 7.0 (2025-26) : folio spans 4 descriptors<br/>page-flags become memdesc_flags_t<br/>a_ops fully folio-typed

The conversion as a sequence of release markers, each verified against the source tag named. What it shows: five years from first posting to an address_space_operations with no struct page in it, with the merge itself (5.16) sitting a full year after the first posting because of the design dispute. The insight to take: the interesting dates are not the merge but the long tail — the type kept growing (1 → 3 → 4 page descriptors) for years after it landed, which is what “the conversion” actually means. Anything dated after 6.12 LTS is later than this note’s pin and is labelled as such.

The conversion curve, measured

The most direct way to see the conversion’s shape is to count, in the files that matter, how many times each type is named. Fetching four files at eight tags and counting occurrences of the literal strings struct page and struct folio gives this:

Filev5.15v5.16v5.19v6.1v6.6v6.12v6.16v7.0
mm/filemap.c — page / folio94 / 065 / 2816 / 7812 / 828 / 907 / 907 / 937 / 96
include/linux/pagemap.h86 / 073 / 5760 / 7153 / 7443 / 7735 / 7825 / 7524 / 81
mm/vmscan.c21 / 021 / 39 / 165 / 391 / 421 / 421 / 491 / 48
mm/rmap.c48 / 047 / 120 / 2418 / 2615 / 3520 / 4124 / 4724 / 47

Measured during the writing of this note by fetching each file from raw.githubusercontent.com at each tag and running grep -c. What it shows: four different conversion trajectories. mm/filemap.c — the page cache core — flipped almost completely between 5.16 and 5.19 and has been flat since; mm/vmscan.c (reclaim) converted a release later and reached one residual mention; include/linux/pagemap.h is still shedding page-typed helpers a decade-equivalent later, because it is where the compatibility wrappers live; and mm/rmap.c went back up after 6.6. The insight to take: the rmap rise is the informative one. Reverse mapping is where folios meet page tables, and page tables map pages, not folios — so as rmap gained per-page-table-entry batching (folio_add_file_rmap_ptes() and friends) it legitimately acquired more struct page vocabulary, not less. Conversion progress is not monotonic, and a file with many struct page mentions is not necessarily unconverted.

Plotted, the page-cache core’s crossover is stark:

xychart-beta
    title "mm/filemap.c — lines mentioning each type, by kernel tag"
    x-axis [v5.15, v5.16, v5.19, v6.1, v6.6, v6.12, v6.16, v7.0]
    y-axis "lines containing the string" 0 --> 100
    line [94, 65, 16, 12, 8, 7, 7, 7]
    line [0, 28, 78, 82, 90, 90, 93, 96]

The same mm/filemap.c data as a curve: the falling line is struct page, the rising one is struct folio. What it shows: the crossover happens inside a single release window — between v5.16 (65 / 28) and v5.19 (16 / 78) — and both lines are essentially flat from v6.6 onward. The insight to take: for the page cache core, the folio conversion was not gradual; it was a concentrated effort in 2022 that finished and then stopped moving. The seven residual struct page lines at v6.12 are the seam described in the fault-path walkthrough, not unfinished work. When people describe the conversion as “still ongoing,” they mean the filesystems and the descriptor split, not mm/filemap.c.

A caveat on the method: grep -c counts lines containing the string, not occurrences, and it counts comments and kdoc alongside code. The numbers are therefore a shape, not a census. They are still the honest kind of evidence — reproducible in one command — and they agree with Wilcox’s own 2024 measurement, where he “put up a plot showing how many times struct page and struct folio are mentioned in the kernel since 2021. On the order of 30% of the page mentions have gone away over that time” (Corbet, The state of the page in 2024, LWN, May 2024).


What Is Still Unconverted at v6.12 — and What Changed After

A recurring error is to state that folios “replaced” struct page. They have not, and the tree says so out loud: at v6.12, v6.18 and v7.0, struct page is still defined, still the per-frame physical descriptor, and every folio union member is annotated /* the union with struct page is transitional */. The folio sits on top of struct page. Any note in this vault that says “folios replaced pages” is wrong; the accurate framing is “folios are the head-page handle that the page cache, reclaim and rmap now use, layered over a struct page array that still exists.”

Here is the concrete status, each row checked against the tag named:

ThingAt v6.12 LTSLaterHow it was checked
address_space_operationswritepage()still present, still takes struct page * — the last page-typed method in the vtableremoved in v6.16; absent at v6.17 and v7.0grep -c "int (\*writepage)" on include/linux/fs.h at v6.12/v6.15/v6.16/v6.17 → 1, 1, 0, 0
mm/folio-compat.c (stage-2 shims)present, 2,280 bytes, 11 shimsstill present at v7.0, 2,082 bytesfetched the file at both tags
write_begin / write_endalready folio-typed (struct folio **foliop)at v7.0 they additionally take const struct kiocb * instead of struct file *read the struct at v5.15 / v6.12 / v7.0
struct folio width3 struct page descriptors4 from v6.18 onwardcounted struct page __page_N members per tag
flags typeplain unsigned longmemdesc_flags_t from v6.18read mm_types.h at both tags
Large folios on the buffered write pathpartially — LSFMM 2022 reported readahead-only; by 2024 the VFS “is now allocating and using large folios through the entire write path”2025: large folios in generic_perform_write(), reported to “double write performance in some tests”LWN 2022, LWN 2024, LWN 2025
Filesystems supporting large foliosfew — XFS and bcachefs named as the exceptionsstill the top request for volunteers in 2025LWN 2024, LWN 2025

Two of these deserve elaboration.

->writepage was not converted; it was deleted. There is no write_folio method. The reason is that ->writepage — “write out this one specific page, right now, because reclaim asked” — encodes a model of reclaim that the memory-management developers no longer believe in. Wilcox proposed the test at LSFMM 2022: “perhaps filesystems should simply remove their implementation of the writepage() address-space operation,” on the argument that a modern filesystem is already keeping its devices busy with writeback and there is little useful it can do when asked to free one particular page. Howells had already done it in AFS “with seemingly good results” (LWN 2022). By 2024 the position was settled: “Filesystems as a whole are being moved away from the writepage() API; it was seen as harmful, so no folio version was created” (LWN 2024). The method is gone from address_space_operations at v6.16. This is the cleanest available marker for “the conversion has moved past 6.12”: if the vtable you are reading has a writepage, you are on 6.15 or older.

The buffer cache is where the aliasing assumption broke. Wilcox reported in 2024 that he had proceeded on the assumption that buffer heads are always attached to folios — and discovered that ext4 allocates slab memory and attaches that instead. “That usage isn’t wrong, Wilcox said, but he is ‘about to make it wrong’ and does not want to introduce bugs in the process.” Avoiding that required leaving information in struct page that would otherwise have moved out, and he was candid that “he would not have taken this direction with buffer heads had he known where it would lead, but he does not want to back it out now.” The current state is safe only by convention: “the ext4 code is careful not to call any functions on non-folio-backed buffer heads that might bring the system down. But there is nothing preventing that from happening in the future, and that is a bit frightening” (LWN 2024). This is the most honest single illustration of what the aliasing design costs: because a folio is bytes reinterpreted, an object that is not really a folio can be passed as one and the compiler will not object.


Where This Is Going — Memory Descriptors

The long-term destination is not “more folios”; it is a struct page small enough that having one per 4 KiB frame stops mattering. The plan, as Wilcox has presented it at three successive LSFMM summits, is to shrink struct page to a single eight-byte memory descriptor whose low bits say what type of memory this is, with the real metadata living in a type-specific structure — a folio for page-cache and anonymous memory, a struct slab for slab pages, a struct ptdesc for page-table pages, a zpdesc for zswap, and so on.

flowchart TB
  subgraph NOW["v6.12 — one fat descriptor per frame"]
    direction LR
    N0["page[0]<br/>64 B"] --- N1["page[1]<br/>64 B"] --- N2["page[2]<br/>64 B"] --- N3["page[3]<br/>64 B"]
    NF["a 4-page folio<br/>= a window over<br/>page[0..2]"]
  end
  subgraph FUTURE["memdesc goal — one 8-byte entry per frame"]
    direction LR
    D0["desc[0]<br/>8 B"] --- D1["desc[1]<br/>8 B"] --- D2["desc[2]<br/>8 B"] --- D3["desc[3]<br/>8 B"]
    FOL["ONE struct folio,<br/>allocated separately,<br/>larger than today"]
    D0 --> FOL
    D1 --> FOL
    D2 --> FOL
    D3 --> FOL
  end
  NOW -->|"memory-map overhead<br/>1.6% of RAM"| FUTURE
  FUTURE -.->|"target"| SAVE["0.2% of RAM<br/>— gigabytes on a large host,<br/>and doubled savings under<br/>virtualization, where the guest<br/>has its own memory map"]

Today’s memory map versus the memory-descriptor goal. What it shows: the win does not come from making the descriptor smaller in isolation — struct folio is expected to get bigger — but from needing one descriptor per folio instead of one per base page. The insight to take: the arithmetic Wilcox gave is the clearest statement of the trade. Caching a 1 GiB file today costs ~16 MB of struct page; with a shrunken memory map and base-page folios it would cost ~23 MB (worse, because each folio’s descriptor is bigger), but with four-page folios it drops to ~9 MB (Corbet, LWN, May 2024). The whole memdesc payoff is therefore contingent on large folios actually being used, which is why “add large-folio support to more filesystems” is at the top of Wilcox’s help-wanted list.

Three point-in-time facts anchor how far this has actually got, and none of them is “done”:

  • v6.18 renames page->flags and folio->flags to memdesc_flags_t. That is the first visible footprint of the descriptor work in the type system, and it is the change this note’s earlier revision spotted without being able to explain. It is a typed wrapper, not a shrunken descriptor.
  • Some typed descriptors already exist: struct slab for slab pages and struct ptdesc for page-table pages are both in the v6.12 tree — ptdesc is defined immediately after struct folio in mm_types.h, with the same overlay comment (“This struct overlays struct page for now”) and a static_assert(sizeof(struct ptdesc) <= sizeof(struct page)). The page-table descriptor is notably smaller than a folio because a page-table page cannot be mapped into user space and therefore needs no mapcount. zpdesc for zswap was added later (LWN, March 2025).
  • The 2025 objective was CONFIG_SEPARATE_FOLIO — a build option that would compile out any code not yet ready for struct folio to be a separate allocation rather than an overlay, so the separated design could be tested even in a kernel that could not yet do everything users need. Wilcox described this as a way around the fact that he “is getting tired of converting filesystems” (LWN, March 2025).

The end state is explicitly not the deletion of struct page. Wilcox: “the end goal is not to get rid of struct page entirely; it will always have its uses. Pages are, for example, the granularity with which memory is mapped into user space” (LWN, May 2024). LWN’s 2026 framing is that the memdesc work “may take years yet to complete,” comparing it to “replacing the foundation of a building that is in heavy use” (LWN, April 2026). struct page Anatomy carries the fuller account of the descriptor design; this note’s concern is only that the folio is the lever for it.

Uncertain

Verify: the current merge status of CONFIG_SEPARATE_FOLIO and of separately-allocated folios, as of mid-2026. Reason: the plan is sourced to LSFMM session reports from March 2025 and an April 2026 overview, both of which describe intent rather than a merged feature; I did not audit a 2026 tree for the config symbol. Note also that the machine this was written on runs 7.1.8, where struct folio is still a 256-byte overlay on four struct pages — i.e. not yet separate. To resolve: grep -r SEPARATE_FOLIO in a current mainline tree, and read the merge-window summaries for 7.0–7.2. uncertain


Worked Example — A Page-Cache Read, in Folio Terms

A buffered read() that hits the page cache today threads through folios end to end:

  1. The VFS read path calls into filemap_read(), which calls filemap_get_pages()__filemap_get_folio(mapping, index, ...).
  2. __filemap_get_folio() looks up the i_pages XArray at index. If a large folio covers that offset, the same folio pointer is stored at every slot it spans, so any in-range index returns it.
  3. On a miss with FGP_CREAT, it allocates a folio (possibly large, via readahead heuristics) with filemap_alloc_folio(), then filemap_add_folio() inserts it and charges it to the memcg.
  4. The data is copied out with copy_folio_to_iter() using folio_size() and folio_pos() to locate bytes — no per-base-page loop at the API level.
  5. Throughout, the folio’s single _refcount (via folio_get/folio_put) and per-folio lock (folio_lock) govern lifetime and exclusion, regardless of whether the folio is one page or 16.

The same code is correct for a 4 KiB folio and a 64 KiB folio because nothing in the path indexes a tail page — the type guarantees it never has one.

The other worked example: a page fault, where folio vocabulary must hand back to pages

The read path never needs a struct page. The fault path does, and watching where it crosses back is the most instructive thing in the whole API. filemap_fault() — the ->fault handler for every file-backed VMA — is folio-native from top to bottom and then, in its very last statement before returning, converts:

vm_fault_t filemap_fault(struct vm_fault *vmf)
{
	struct address_space *mapping = vmf->vma->vm_file->f_mapping;
	pgoff_t index = vmf->pgoff;
	struct folio *folio;
	...
	folio = filemap_get_folio(mapping, index);       /* folio in, folio out */
	if (IS_ERR(folio)) {
		count_vm_event(PGMAJFAULT);                  /* a major fault */
		...
		folio = __filemap_get_folio(mapping, index,
					  FGP_CREAT | FGP_FOR_MMAP, vmf->gfp_mask);
	}
	if (!lock_folio_maybe_drop_mmap(vmf, folio, &fpin))  /* per-folio lock */
		goto out_retry;
	if (unlikely(folio->mapping != mapping)) {           /* truncated under us? */
		folio_unlock(folio); folio_put(folio); goto retry_find;
	}
	VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);
	...
	vmf->page = folio_file_page(folio, index);        /* <-- the seam */
	return ret | VM_FAULT_LOCKED;
}

Condensed from mm/filemap.c at v6.12. Commentary, line by line: the lookup, the allocation, the lock, the truncate re-check and the containment assertion are all expressed on folios and are all correct for any order. Then vmf->page = folio_file_page(folio, index) picks out the one base page whose contents belong at the faulting virtual address and hands it to the generic fault code, which will install exactly one page-table entry pointing at exactly one frame.

sequenceDiagram
    autonumber
    participant U as user process
    participant FH as handle_mm_fault
    participant FF as filemap_fault
    participant XA as i_pages XArray
    participant FS as filesystem a_ops
    U->>FH: touches a mapped file address
    FH->>FF: vma->vm_ops->fault(vmf)
    FF->>XA: filemap_get_folio(mapping, index)
    alt folio already cached
        XA-->>FF: struct folio * (order 0..N)
    else miss
        FF->>FF: count_vm_event(PGMAJFAULT)
        FF->>FS: do_sync_mmap_readahead()
        FS-->>XA: allocate + insert folio(s)
        XA-->>FF: struct folio *
        opt not uptodate
            FF->>FS: a_ops->read_folio(file, folio)
            FS-->>FF: folio marked uptodate, unlocked
        end
    end
    FF->>FF: folio_lock, re-check folio->mapping, folio_contains(index)
    Note over FF: everything so far is order-agnostic —<br/>no tail page has been named
    FF->>FF: vmf->page = folio_file_page(folio, index)
    Note over FF,FH: THE SEAM: one folio in, one struct page out
    FF-->>FH: VM_FAULT_LOCKED
    FH->>U: set_pte_at() — one PTE, one 4 KiB frame

A file-backed page fault at v6.12, showing where folio vocabulary ends. What it shows: eleven of the twelve steps are expressed on folios of arbitrary order; only the final handoff names an individual page, because a page-table entry references a frame, not a folio. The insight to take: this is the general rule for the whole conversion — folios go as deep as the software abstraction goes, and stop exactly where hardware granularity begins. Page tables, DMA addresses, GUP pins and MMU permissions are all per-frame concepts; the folio cannot and should not replace struct page there. That is why Wilcox says the goal is not to delete struct page: “Pages are, for example, the granularity with which memory is mapped into user space.”

Note in passing what filemap_fault() does not do: it never loops over subpages, never calls compound_head(), and its truncate re-check (folio->mapping != mapping) is a single dereference that would have been wrong on a tail page. That is the conversion’s payoff written out as code.


Common Misunderstandings

  • “A folio is a new allocation / a separate object.” No — not at v6.12, and not yet at v7.0. It is the same bytes as the head struct page (and, for a large folio, the two or three descriptors after it), reinterpreted. page_folio() is a cast, not a copy. The 2025 plan is to make it a separate allocation; when that lands this bullet flips, which is exactly why the note dates it.
  • struct folio { struct page page; }; — the definition quoted in most secondary write-ups. That was true of the December-2020 posting and roughly true at v5.16, where the kernel asserted sizeof(struct page) == sizeof(struct folio). That assertion no longer exists at v6.12; the type spans three page descriptors there and four from v6.18.
  • “Folios replaced struct page.” No — see What Is Still Unconverted. struct page is still defined at v7.0 and every folio union arm is annotated /* the union with struct page is transitional */. Wilcox’s stated goal is explicitly not to delete it.
  • “folio == huge page.” No. A folio of order 0 is a single base page. “Large folio” (order > 0) is the multi-page case; a huge page is a particular large folio (PMD-order). All huge pages are large folios; not all folios are huge.
  • folio->page lets me grab the first subpage as a normal page.” It is the head page — fine for head-only operations, but to address subpage n use folio_page(folio, n), and remember the result is a tail for n > 0 and must not be passed to head-expecting code. Reaching for &folio->page at all is the stage-2b code smell described above; by 2025 removing such uses was on Wilcox’s help-wanted list.
  • “Folios are a C type-safety feature; the compiler stops me passing a tail.” It does not. C will compile (struct folio *)tail_page without complaint. The guarantee is maintained by a small set of constructors (page_folio(), folio_alloc(), __filemap_get_folio()) that only ever produce heads, plus the discipline of not casting. What the compiler does enforce is the field aliasing, via FOLIO_MATCH.
  • “The conversion fixed a pile of known bugs.” The primary record does not support this; LWN in 2021 said there did not appear to be an extensive history of such bugs. The demonstrated wins are removed compound_head() overhead, clearer interfaces, and — much the largest — the ability to use large folios at all.
  • page_folio() is always safe to call.” Its own kdoc adds a condition: “If the caller does not hold a reference, this call may race with a folio split, so it should re-check the folio still contains this page after gaining a reference on the folio.” A folio can be split under you; folio_contains() is the re-check.
  • Merged in 5.17. A common misremembering; the struct folio definition first appears at the v5.16 tag, not v5.17 (verified by fetching mm_types.h at v5.15, v5.16 and v5.17 and grepping for the definition).
  • “There is a write_folio() method now.” There is not. ->writepage was judged harmful and deleted in v6.16 rather than converted; no folio equivalent was ever created.

Alternatives and When the Distinction Matters

For most application and even most driver code there is no choice to make — you receive folios from the page cache or pages from alloc_pages() and use the matching API. The distinction matters when writing filesystem or memory-management code:

  • Use folios for anything touching the page cache, reclaim, or rmap — that is where the conversion lives and where the type safety pays off.
  • Drop to struct page only for genuinely per-frame work: DMA on a specific physical page, GUP pinning of individual pages, page-table entries (which reference frames, not folios), and hardware that operates at 4 KiB granularity.
  • The folio_page() / page_folio() pair is the documented bridge for crossing between the two views; reaching into folio->page or doing pointer arithmetic on subpages directly is fragile and discouraged.
flowchart TB
  START{"I have a handle on some<br/>memory. Which type do I want?"}
  START --> HW{"Is the operation<br/>defined by hardware<br/>at 4 KiB granularity?"}
  HW -->|"yes — PTE install,<br/>DMA address, GUP pin,<br/>MMU permissions,<br/>pfn arithmetic"| PAGE["**struct page ***<br/>and say why in a comment"]
  HW -->|"no"| SUB{"Does the operation<br/>belong to the page cache,<br/>reclaim, rmap, writeback<br/>or a filesystem?"}
  SUB -->|"yes"| FOLIO["**struct folio ***<br/>the conversion lives here"]
  SUB -->|"no"| ALLOC{"Am I allocating raw<br/>physical memory for<br/>kernel-internal use?"}
  ALLOC -->|"yes"| BUDDY["**alloc_pages / struct page ***<br/>or folio_alloc if the object<br/>will become a folio"]
  ALLOC -->|"no"| TYPED["**a typed descriptor**<br/>struct slab, struct ptdesc,<br/>zpdesc — the memdesc direction"]
  FOLIO --> CROSS{"Do I need one<br/>specific subpage?"}
  CROSS -->|"yes"| BRIDGE["folio_file_page(f, index)<br/>or folio_page(f, n)<br/>— the documented seam"]
  CROSS -->|"no"| STAY["stay in folio vocabulary;<br/>folio_size, folio_pos,<br/>copy_folio_to_iter"]
  PAGE -.->|"need the whole object?"| UP["page_folio(p)<br/>normalizes to the head"]

Choosing between the two views, as a decision procedure. What it shows: the branch is decided by whether the operation is defined at frame granularity by hardware, not by which type is more modern. The insight to take: the two arrows at the bottom are the only sanctioned crossings — folio_file_page()/folio_page() downward and page_folio() upward — and a conversion that needs a third kind of crossing (a raw cast, &folio->page) is signalling that the code has not decided which question it is asking. Note the fourth branch: for memory that is neither user-mappable nor a raw allocation, the modern answer is increasingly a typed descriptor rather than either struct page or struct folio, which is the memdesc direction arriving in practice.

One more distinction is worth stating because it trips people converting driver code: folio_alloc() is not a drop-in replacement for alloc_pages(). A folio allocation produces a compound allocation with the head/tail bookkeeping set up (__GFP_COMP semantics), whereas a bare high-order alloc_pages() returns 2^order independent descriptors with no linkage at all — PageCompound() is false and there is no head to make a folio out of. If a driver allocates high-order memory without __GFP_COMP and later hands one of those pages to code that calls page_folio(), the result is the page itself, treated as an order-0 folio, which is silently wrong for anything that asks its size. This is one of the “eliminate all higher-order memory allocations that do not use compound pages” items on Wilcox’s list; he named the crypto layer as a place with many of them (LWN, May 2024).


Production Notes

The conversion’s user-visible payoff is the large-folio features it unlocked, and the reported numbers are worth collecting in one place because they are scattered across four years of conference reports. Every figure below is a reported result from a named source, not a benchmark run here.

Reported effectMagnitudeSource and date
Kernel compile time with large anonymous folios (mTHP)~5% faster overall; ~40% less kernel timeCorbet, LWN, July 2023
Kernel compile with large folios generally5% fasterWilcox, LSFMM+BPF 2024
Buffered write throughput, large folios in generic_perform_write()up to 2× in some testsWilcox, LSFMM+BPF 2025
LRU list length in some benchmark runs~1000× shorter (“just insane”)Wilcox, LSFMM 2022
Memory-map overhead, today vs. the memdesc goal1.6% → 0.2% of RAMWilcox, LSFMM+BPF 2024
struct page mentions removed from the kernel, 2021–2024~30%Wilcox, LSFMM+BPF 2024
folio_end_read() — sets uptodate, clears the lock, checks waiters, acts as a barrierone instruction on x86Wilcox, LSFMM+BPF 2024

Reported wins from the folio conversion. What it shows: the gains cluster into three kinds — fewer faults and better TLB reach (the compile-time numbers), less per-object bookkeeping (LRU length, memory-map overhead), and larger I/O per operation (write throughput). The insight to take: none of these come from the type — they come from large folios, which the type made tractable. The type’s own contribution is the removed compound_head() calls and the interface clarity, which nobody has published a number for. Treat conference-reported figures as directional; they are single-workload results announced by the author of the change.

The costs are equally documented, and two are still open. The first is churn, the friction Andrew Morton flagged in 2021, which materialised exactly as he predicted: page and folio spellings have coexisted for over a decade of releases and mm/folio-compat.c is still in the tree at v7.0. The second is write amplification: dirty state is tracked per folio, not per base page, so a one-byte store into a 64 KiB folio dirties the whole folio and the whole folio is written back. Wilcox raised this himself at LSFMM 2022 and did not expect “serious trouble”, but Chris Mason pointed out that Jens Axboe was simultaneously working hard to make small I/O cheap in io_uring precisely for write-bandwidth reasons; Axboe’s own read was that bandwidth is a bigger concern on the read side than the write side. The session ended with “a general agreement that better metrics are needed” (LWN 2022). If you are running a write-heavy workload on a filesystem that has recently gained large-folio support and your device-level write bandwidth has risen without your application changing, this is the mechanism to suspect.

What this means when you are reading mm/ code. At 6.12 the page cache, reclaim and rmap are folio-native, while the lowest physical layers — the buddy allocator, pfn↔page translation, and the page tables themselves — remain page-native, and the two meet through page_folio()/folio_page(). A practical reading rule follows: if a function takes a struct page * in a 6.12 tree, ask why. There are exactly three good answers — it is doing per-frame hardware work, it is an unconverted caller (stage 0), or it is a compat shim (stage 2). Anything else is a conversion that is not finished.

And a warning about documentation drift. Because the conversion moved through the tree function by function over five years, third-party material about the page cache and reclaim ages in a specific, detectable way. Descriptions that say the cache stores struct page predate 5.16; ones that name readpage, set_page_dirty or invalidatepage as a_ops methods predate 5.18–5.19; ones that describe ->writepage as part of the writeback contract predate 6.16. The kernel’s own in-tree documentation lags too — Documentation/core-api/mm-api.rst at v6.12 is mostly kernel-doc directives pointing at the source, which is the safest kind of documentation precisely because it cannot drift. When in doubt, read the header.


See Also