struct page Anatomy
Every 4 KiB physical page frame in a Linux system is described by exactly one
struct page— a small, fixed-size kernel object living in a giant array (themem_map/vmemmap) that is the kernel’s model of physical RAM. Because there is one descriptor per frame across all of RAM, its size is sacred: on a 64-bit kernel it is about 64 bytes, so the descriptor array consumes on the order of 1.6 % of all memory it describes (Corbet, “The state of the page in 2024”, LWN). That brutal size budget is whystruct pagebecame one of the most heavily unioned, overloaded structures in the kernel — the same bytes mean completely different things depending on whether the frame is a page-cache page, an anonymous page, a slab, a compound tail page, aZONE_DEVICEpage, or a free buddy block. This note dissects the v6.12 LTS layout field by field, explains the reference counts and flags that every frame carries, and situates the ongoing effort to shrink the descriptor via folios and typed memory descriptors. (Definitions below are read from the pinned v6.12mm_types.h; v6.18 deltas are called out explicitly.)
Mental Model — One Descriptor Per Frame, Bytes Reused By Role
The right way to think about struct page is not as a struct with named fields you read freely. It is a fixed-size slab of bytes (five “words” of union, plus a flags word, plus the reference counts) whose interpretation is chosen by the current role of the physical frame. The kernel maintains the invariant “one frame ↔ one descriptor”; what those bytes mean is decided by page flags and the allocator that owns the frame. Reading a field that does not belong to the frame’s current role is a bug.
flowchart TB PFN["Physical Frame Number (PFN)<br/>e.g. frame 0x12abc"] PFN -->|"pfn_to_page()"| SP["struct page (≈64 bytes)<br/>one per 4 KiB frame"] SP --> FL["flags word<br/>PG_* bits + packed<br/>node / zone / section / cpupid"] SP --> UN["5-word union<br/>(role-dependent)"] SP --> RC["_mapcount / page_type<br/>+ _refcount"] UN -->|"page-cache / anon"| A["lru, mapping, index, private"] UN -->|"slab/SLUB"| B["reinterpreted as struct slab"] UN -->|"compound tail"| C["compound_head (bit 0 = 1)"] UN -->|"ZONE_DEVICE"| D["pgmap, zone_device_data"] UN -->|"page_pool (net)"| E["pp, dma_addr, pp_ref_count"] UN -->|"free"| F["buddy_list / pcp_list"]
The descriptor and its role-dependent overlays. What it shows: a physical frame number maps one-to-one to a struct page; the always-present parts are the flags word and the reference counts (_refcount, and _mapcount/page_type), while the five-word central union is reinterpreted depending on what the frame is being used for. The insight: the same bytes are a page-cache mapping+index for a file page, a compound_head pointer for a huge-page tail, a struct slab for kernel-object storage, or a buddy_list link when free. You must know the frame’s role (from its flags and owning allocator) before you may read any union field.
Why It Is So Small — The mem_map Size Budget
The constraint that shapes everything is arithmetic. There is one struct page for every 4 KiB frame in the machine. If the descriptor is S bytes, the descriptor array costs S / 4096 of RAM as pure overhead. With S ≈ 64, that is 64/4096 ≈ 1.56 % — the “1.6 %” figure Matthew Wilcox cited at the 2024 Linux Storage, Filesystem, Memory-Management and BPF Summit (LWN 973565). On a 1 TiB machine that is ~16 GiB of struct page array; under virtualization the array exists in both host and guest, doubling the toll. The doc summarizes this overhead as the array of descriptors that the physical memory model arranges as mem_map (FLATMEM) or per-section maps walked through vmemmap (SPARSEMEM) (kernel.org memory-model).
This is the entire reason struct page is union-packed rather than a clean struct with one field per purpose: adding a field is multiplied by every frame in the system. The source comment is blunt about it — when the DMA-pin tracking work needed somewhere to store a pin count, the rule was “struct page may not be increased in size for this, and all fields are already used,” forcing the [[get_user_pages and Page Pinning|pin count to be overloaded onto _refcount]] (pin_user_pages.rst).
Uncertain
Verify: the exact
sizeof(struct page)on x86-64 in 6.12/6.18. It is not a fixed constant — it depends on Kconfig (CONFIG_MEMCGaddsmemcg_data;WANT_PAGE_VIRTUAL,LAST_CPUPID_NOT_IN_PAGE_FLAGS, andCONFIG_KMSANeach add words). The “≈64 bytes” / “1.6 %” figures come from the LWN LSFMM 2024 report and back-of-envelope64/4096; I did not compile a config to readsizeof. Reason: config-dependent, not directly measured. To resolve: build a 6.12 defconfig and checkpahole/BUILD_BUG_ONonsizeof(struct page). uncertain
The Always-Present Fields
Three things are present regardless of role.
flags — the first word. In v6.12 it is unsigned long flags (atomic flags, “some possibly updated asynchronously”). It carries two distinct kinds of information packed into one machine word:
-
The
PG_*status bits (the lowNR_PAGEFLAGSbits):PG_locked,PG_uptodate,PG_dirty,PG_lru,PG_active,PG_reserved,PG_writeback,PG_swapcache,PG_reclaim, and so on. These are the booleans that drive the page cache and reclaim. -
Packed location identifiers in the upper bits — the section, node, and zone the frame belongs to, plus (optionally) a
last_cpupidfield for NUMA balancing and a KASAN tag.include/linux/page-flags-layout.hdefines the widths and documents the layouts explicitly (v6.12 page-flags-layout.h):No sparsemem / sparsemem-vmemmap: | NODE | ZONE | ... | FLAGS | " plus space for last_cpupid: | NODE | ZONE | LAST_CPUPID ... | FLAGS | classic sparse with space for node:| SECTION | NODE | ZONE | ... | FLAGS | " plus space for last_cpupid: | SECTION | NODE | ZONE | LAST_CPUPID ... | FLAGS | classic sparse no space for node: | SECTION | ZONE | ... | FLAGS |The build enforces
ZONES_WIDTH + LRU_GEN_WIDTH + SECTIONS_WIDTH + NODES_WIDTH + KASAN_TAG_WIDTH + LAST_CPUPID_WIDTH <= BITS_PER_LONG - NR_PAGEFLAGS, and#error "Not enough bits in page flags"if it overflows. This is whypage_to_nid(page),page_zone(page), and (under SPARSEMEM-vmemmap) the section lookup are cheap bit-extractions fromflagsrather than separate fields — there is no room for separate fields. When the bits genuinely don’t fit (e.g. NUMA on classic-sparse without vmemmap),NODE_NOT_IN_PAGE_FLAGSis set and the node is looked up out-of-line instead. The header comment notes the zone bits double as the hint the allocator reads from the GFP zone modifiers (see GFP Flags and Allocation Contexts).In v6.18 this field changes type: it is
memdesc_flags_t flags— a one-member wrappertypedef struct { unsigned long f; } memdesc_flags_t;(v6.18 mm_types.h). Semantically identical bits, but a distinct type so the compiler can police who touches the flags word as the memdesc refactor proceeds (see Folios and the Folio Conversion).
_refcount — atomic_t _refcount, marked “Usage count. DO NOT USE DIRECTLY. See page_ref.h.” This is the elevated-reference count that keeps a frame from being freed or reclaimed while anyone holds a reference. It is taken by the page cache, by every mapping, by get_page()/get_user_pages(), by I/O in flight, and so on; when it hits zero the frame returns to the buddy allocator. Critically, _refcount is also where the DMA-pin count is overloaded for small folios (via GUP_PIN_COUNTING_BIAS, covered in the pinning note). On allocation from alloc_pages() the refcount is positive (v6.12 mm_types.h comment).
_mapcount / page_type — a 4-byte union sharing the same slot:
atomic_t _mapcountcounts how many page-table entries map this page (for RMAP-tracked, non-typed folios). It is initialized to −1, not 0, so that the transitions both to −1 and from −1 can be detected atomically withatomic_add_negative(-1)/atomic_inc_and_test()— a value of 0 therefore means “mapped exactly once.”unsigned int page_typeis used instead for typed head pages (slab, page-table, etc.): the high bits encode “what kind of page is this,” with the convention that a typed page has_mapcount == page_type == -1for its tail pages. Owners may reuse the low 16 bits ofpage_typeafter setting the type but must restore them to all-ones before clearing it.
The Central Five-Word Union — Role Overlays
The comment is explicit: “Five words (20/40 bytes) are available in this union. WARNING: bit 0 of the first word is used for PageTail().” That single reserved bit is the linchpin of the whole compound-page scheme: if bit 0 of the first union word is set, the page is a tail of a compound page and the word is compound_head (a pointer to the head, with the low bit as the tag). Every other overlay must keep that bit clear to avoid a false-positive PageTail(). The major overlays, straight from v6.12:
- Page cache / anonymous pages (
struct):lru(the reclaim list linkage, protected bylruvec->lru_lock),mapping(thestruct address_space *for a file page, or theanon_vmafor an anonymous page — the low bits encode which, perPAGE_MAPPING_FLAGS),index(pgoff_toffset within the mapping), andprivate(opaque per-page data:buffer_heads ifPagePrivate, aswp_entry_tif in the swap cache, or the buddy order ifPageBuddy). Thelruslot is itself a sub-union — for the unevictable case it becomes{ void *__filler; unsigned int mlock_count; }(the__filleris “always even, to negatePageTail”), and for a free page it isbuddy_list/pcp_list. page_pool(network stack):pp_magic,pp(thestruct page_pool *),_pp_mapping_pad,dma_addr,pp_ref_count— the recycling metadata for the networking page pool.- Compound tail pages: just
compound_headwith bit 0 set. ZONE_DEVICEpages:pgmap(the hostingstruct dev_pagemap *) andzone_device_data— for device/PMEM memory that has descriptors but is not ordinary RAM.rcu_head: lets a page be freed by RCU.
The slab overlay is special: it is not a member of this union. Instead, struct slab is a separate type that reinterprets the bytes of struct page (which is why the comment notes “struct slab currently just reinterprets the bits of struct page,” and why all struct pages are double-word aligned via CONFIG_HAVE_ALIGNED_STRUCT_PAGE so SLUB’s cmpxchg_double() on its freelist+counters works). See The Slab Allocator and SLUB and Kmem Caches and Object Slabs.
struct folio — The Typed Head-Page Handle
Layered directly on top of struct page is struct folio, “a physically, virtually and logically contiguous set of bytes… a power-of-two in size, aligned to that same power-of-two, at least as large as PAGE_SIZE.” A folio is guaranteed not to be a tail page — it is the head — which untangles the head/tail confusion that plagued compound-page code. In v6.12 a folio is a union of either named fields (flags, lru, mapping, index, private/swap, _mapcount, _refcount, memcg_data, …) or an embedded struct page page — and a battery of FOLIO_MATCH() static_asserts enforce that each named folio field sits at exactly the same offset as the corresponding struct page field (v6.12 mm_types.h). For multi-page folios, the tail pages’ bytes are reused to store extra accounting the head needs: _large_mapcount, _entire_mapcount, _nr_pages_mapped, _pincount (the dedicated DMA-pin counter that large folios use instead of the _refcount bias trick), _folio_nr_pages, and the hugetlb/deferred-split fields. Every transitional member is annotated /* the union with struct page is transitional */. This note covers the page side; the folio refactor itself, including the spread of large folios through the page cache, lives in Folios and the Folio Conversion.
The Shrinking Effort — Memory Descriptors (“memdesc”)
The long-term plan, presented by Wilcox at LSFMM 2024 (LWN 973565), is to shrink struct page down to a single 8-byte memory descriptor whose bottom few bits say what type of page it is, with the real metadata moved into type-specific descriptors (a slab gets a slab descriptor, an anonymous folio gets a folio, a page-table page gets a page-table descriptor). Some typed descriptors already exist (slab and page-table descriptors). The payoff is the memory-map overhead dropping from ~1.6 % to ~0.2 % — multiple gigabytes saved on large systems — because one folio covers many base pages instead of one fat struct page per frame. The v6.18 memdesc_flags_t type is an early footprint of this work. The goal is explicitly not to delete struct page: it remains the granularity at which memory is mapped into user space.
Uncertain
The memdesc end-state (single 8-byte descriptor, ~0.2 % overhead) is a roadmap presented in May 2024, not a shipped 6.12/6.18 reality. As of v6.18
struct pageis still the ~5-word-union descriptor described above; onlymemdesc_flags_tand a couple of typed descriptors (slab, page-table) have landed. Reason: dated to a conference report; I read the v6.12/v6.18 source for current layout but not the full memdesc patch series. To resolve: track the memdesc series merge status in later kernels. Treat as plan, in progress, never “done.” uncertain
Common Misunderstandings
- “You can read any field of
struct page.” No. The union means a field is valid only for the frame’s current role. Readingpage->mappingon a slab page, orpage->indexon aZONE_DEVICEpage, returns garbage. The flags and the owning allocator tell you which overlay is live. - “
_refcountand_mapcountare the same kind of count.” They are not._mapcountcounts page-table mappings (how many processes map it);_refcountcounts all elevated references (mappings contribute, but so does I/O, the page cache holding it, GUP pins, etc.). A page can have_mapcount == -1(unmapped) yet_refcount > 0(e.g. clean page-cache page nobody has mapped). - “
_mapcountstarts at 0.” It starts at −1 so that the first map (→0) and the last unmap (→−1) are both detectable atomically. - “folios replaced
struct page.” They have not — covered in Folios and the Folio Conversion. A folio is a typed handle to the head page; thestruct pagearray still exists in 6.18. - “Each frame has lots of room for metadata.” The opposite: the per-frame size budget is the binding constraint behind every overload, and the whole memdesc effort exists to reduce it.
See Also
- Folios and the Folio Conversion — the typed head-page handle layered on
struct page, and the memdesc refactor that aims to shrink it. - The Physical Memory Model SPARSEMEM and vmemmap — how the
struct pagearray (mem_map/ per-section maps) is arranged and sized. - pfn to page Translation — the
pfn_to_page()/page_to_pfn()conversions that index the descriptor array. - Compound Pages and Large Folios — the head/tail scheme that the reserved bit-0
PageTail()andcompound_headoverlay implement. - get_user_pages and Page Pinning — why the DMA-pin count had to be overloaded onto
_refcount(small folios) or_pincount(large folios). - The Slab Allocator and SLUB — the slab overlay that reinterprets
struct pagebytes asstruct slab. - The LRU Lists — the
lrufield’s role in reclaim. - MOC: Linux Memory Management MOC (§6, struct page / folios / physical memory model).