背景

Linux 内核使用 struct page 结构体描述页帧的元数据,本文简单整理其中的字段含义。

这些内容实质上非常复杂,本人不算懂,只是定期整理 OneNote 上的笔记。如有错误还请指正。

概览

struct page {
        unsigned long flags;            /* Atomic flags, some possibly
                                         * updated asynchronously */
        /*
         * Five words (20/40 bytes) are available in this union.
         * WARNING: bit 0 of the first word is used for PageTail(). That
         * means the other users of this union MUST NOT use the bit to
         * avoid collision and false-positive PageTail().
         */
        union {
                struct { /* ... */ };        /* Page cache and anonymous pages */
                struct { /* ... */ };        /* page_pool used by netstack */
                struct { /* ... */ };        /* Tail pages of compound page */
                struct { /* ... */ };        /* Page table pages */
                struct { /* ... */ };        /* ZONE_DEVICE pages */

                /** @rcu_head: You can use this to free a page by RCU. */
                struct rcu_head rcu_head;
        };

        union { /* This union is 4 bytes in size. */
                /*
                 * If the page can be mapped to userspace, encodes the number
                 * of times this page is referenced by a page table.
                 */
                atomic_t _mapcount;

                /*
                 * If the page is neither PageSlab nor mappable to userspace,
                 * the value stored here may help determine what this page
                 * is used for.  See page-flags.h for a list of page types
                 * which are currently stored here.
                 */
                unsigned int page_type;
        };

        /* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
        atomic_t _refcount;

#ifdef CONFIG_MEMCG
/* ... */
#endif
#if defined(WANT_PAGE_VIRTUAL)
/* ... */
#endif
#ifdef CONFIG_KMSAN
/* ... */
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
/* ... */
#endif
} _struct_page_alignment;

include/linux/mm_types.h 文件中的 struct page 结构体简化后如上所示。

可以看出,struct page 切分了五大块:

  • page flags。
  • 不同场景的 union。
  • map count。
  • ref count。
  • 不同特性(cgroup、HIGHMEM 等)支持。

page flags

page-flags unsigned long flags; 的比特布局(图源

page flags 是常见的 bitmap 布局设计,但是会区分不同场景。在默认情况(SPARSEMEM VMEMMAP)下,page flags 字段会将高位提供给 node/zone 使用;其他情况可能会在高位继续为 memmap section 和 last cpuid 占用额外的比特位。因此只有剩下的比特位才能提供给 page 本身使用,page flags 位置是非常有限的(v6.4.8 只为 page 占用了 28 位)。具体布局详见 page-flags-layout.h 给出的注释。

标志位 描述(整理自注释和代码)
PG_locked 页面被锁定(发起 I/O 时设置,写回开始或读完成时清除)
PG_referenced 表示页面近期被访问过(用于页面回收算法)
PG_uptodate 页面数据与后备存储一致(PG_reported 复用此位)
PG_dirty 页面内容已被修改需要写回(PG_savepinned 在 Xen 中复用此位)
PG_lru 页面在(某个)LRU 链表中
PG_active 页面在活跃 LRU 链表中
PG_workingset 页面属于工作集(频繁访问,判定算法见之前的草稿
PG_waiters 页面有等待任务(必须与 PG_locked 同字节)
PG_error 页面发生 I/O 错误(PG_has_hwpoisoned 在复合页中复用此位)
PG_slab 页面由 SLAB/SLOB/SLUB 分配器管理
PG_owner_priv_1 复用位
- PG_checked:文件系统内部标记
- PG_swapcache:交换缓存页
- PG_pinned:Xen 只读页表页
- PG_foreign:Xen 跨域映射页
- PG_xen_remapped:swiotlb-xen 重映射页
- PG_vmemmap_self_hosted:自托管内存映射页
PG_arch_1 架构特定状态位(首次加入页缓存时清零)
PG_reserved 特殊保留页面,不会换出(内核镜像/启动保留页/零页/MMIO页等)
PG_private 文件系统页缓存中包含私有数据(位于page->private
PG_private_2 复用位
- 文件系统 aux data(不太懂,内部命名为 ordered,只在 btrfs 使用到)
- PG_fscache:FS-Cache 缓存页(似乎也是废弃设计)
PG_writeback 页面正在写回(写回开始前设置,结束后清除)
PG_head 复合页的头部页面
PG_mappedtodisk 复用位
- 页面在磁盘上有分配块
- PG_anon_exclusive:匿名页独占映射标记
PG_reclaim 复用位
- 页面待回收
- PG_readahead:预读页面标记
- PG_isolated:非 LRU 隔离页
PG_swapbacked 页面以 RAM/交换空间作为后备存储
PG_unevictable 页面不可回收
PG_mlocked 页面被 VM_LOCKED 内存区域锁定(需 CONFIG_MMU
PG_uncached 当前版本只有 IA64 在用,略
PG_hwpoison 硬件损坏页(含 ECC 错误),访问危险
PG_young 自上次检查后被访问(需 CONFIG_PAGE_IDLE_FLAG && CONFIG_64BIT
PG_idle 自上次检查后未被访问(需 CONFIG_PAGE_IDLE_FLAG && CONFIG_64BIT
PG_arch_2 架构特定扩展标志(需 CONFIG_ARCH_USES_PG_ARCH_X
PG_arch_3 架构特定扩展标志(需 CONFIG_ARCH_USES_PG_ARCH_X

具体的 page flags 可以参考 page-flags.h 文件。从源码可以看出,有部分 private bits 是会根据不同场景来重用不同 page flags,因为一个 unsigned long 字段的比特位数目是有限的,而 struct page 本身的大小是被限制在 cacheline 范围内的,因此不能随意扩展内部字段大小。

// 见:https://elixir.bootlin.com/linux/v6.4.8/source/fs/btrfs/ctree.h#L716
// 这里作为例子展示 page flags 常用的接口
/*
 * We use page status Private2 to indicate there is an ordered extent with
 * unfinished IO.
 *
 * Rename the Private2 accessors to Ordered, to improve readability.
 */
#define PageOrdered(page)               PagePrivate2(page)
#define SetPageOrdered(page)            SetPagePrivate2(page)
#define ClearPageOrdered(page)          ClearPagePrivate2(page)
#define folio_test_ordered(folio)       folio_test_private_2(folio)
#define folio_set_ordered(folio)        folio_set_private_2(folio)
#define folio_clear_ordered(folio)      folio_clear_private_2(folio)

通常会使用 Page*/SetPage*/ClearPage* 等宏来原子处理 page flags(非原子版本可能有下划线前缀),而 struct folio(可理解为 struct page 的另一种二进制兼容格式)也是类似的操作。但是需要注意有些 flag 会内部使用其他命名,比如前面提到的 PG_private_2 在某个场景被改名为 PG_ordered,这是只看 page-flags.h 头文件不会知道的事情……

map count

(先别看第一个 union,太复杂了)

_mapcount 用于统计被页表映射的次数。因为每个用户进程有单独的页表,因此会有计数需求。默认计数是 -1,但是读取接口(要考虑复合页)会在字段的基础上加一。使用场景为反向映射。

page type

// 见:https://elixir.bootlin.com/linux/v6.4.8/source/include/linux/page-flags.h#L904
/*
 * For pages that are never mapped to userspace (and aren't PageSlab),
 * page_type may be used.  Because it is initialised to -1, we invert the
 * sense of the bit, so __SetPageFoo *clears* the bit used for PageFoo, and
 * __ClearPageFoo *sets* the bit used for PageFoo.  We reserve a few high and
 * low bits so that an underflow or overflow of page_mapcount() won't be
 * mistaken for a page type value.
 */

#define PAGE_TYPE_BASE  0xf0000000
/* Reserve              0x0000007f to catch underflows of page_mapcount */
#define PAGE_MAPCOUNT_RESERVE   -128
#define PG_buddy        0x00000080
#define PG_offline      0x00000100
#define PG_table        0x00000200
#define PG_guard        0x00000400

page type 的使用场景就是用于区分页面的类型(废话)。类型的判定方式和前面 page flags 接口一致,比如测试 PG_buddy 就是 PageBuddy()。因此可以视 page type 为 page flags 的扩展,但是不同在于类型之间是互斥的。其他场景除了调试输出以外,zsmalloc 也短暂借用了 page type。

NOTE: Elixir 引擎不好反向查找这些 Page 宏的调用场景,还是本地源码搜关键字吧。

ref count

// 见:https://elixir.bootlin.com/linux/v6.4.8/source/include/linux/mm.h#L1419
// get_page 接口,增加引用计数
// 如果是 folio 版本那就是 folio_get
static inline void get_page(struct page *page)
{
        // #1
        folio_get(page_folio(page));
}

/**
 * folio_get - Increment the reference count on a folio.
 * @folio: The folio.
 *
 * Context: May be called in any context, as long as you know that
 * you have a refcount on the folio.  If you do not already have one,
 * folio_try_get() may be the right interface for you to use.
 */
static inline void folio_get(struct folio *folio)
{
        VM_BUG_ON_FOLIO(folio_ref_zero_or_close_to_overflow(folio), folio);
        // #2
        folio_ref_inc(folio);
}

static inline void folio_ref_inc(struct folio *folio)
{
        // #3
        page_ref_inc(&folio->page);
}

static inline void page_ref_inc(struct page *page)
{
        // #4
        atomic_inc(&page->_refcount);
        if (page_ref_tracepoint_active(page_ref_mod))
                __page_ref_mod(page, 1);
}

// put_page/folio_put 同理,减少引用计数
static inline void put_page(struct page *page);

只要使用到 struct page,那就会产生引用计数的需求。这里也可看出 struct pagestruct folio 的性能区别,因为后者通过类型保证了本身为单一页面或者复合页的头部,而前者还需要付出原子读操作的代价来判断,具体见 page_folio() 转换函数的实现(这里很难得用上了 C11「新」特性)。

// https://elixir.bootlin.com/linux/v6.4.8/source/include/linux/mm.h#L1522
/*
 * GUP_PIN_COUNTING_BIAS, and the associated functions that use it, overload
 * the page's refcount so that two separate items are tracked: the original page
 * reference count, and also a new count of how many pin_user_pages() calls were
 * made against the page. ("gup-pinned" is another term for the latter).
 *
 * With this scheme, pin_user_pages() becomes special: such pages are marked as
 * distinct from normal pages. As such, the unpin_user_page() call (and its
 * variants) must be used in order to release gup-pinned pages.
 *
 * 省略一部分注释……
 */
#define GUP_PIN_COUNTING_BIAS (1U << 10)

除此以外,GUP/pinning 特性会使得每次引用计数的变动会是 GUP_PIN_COUNTING_BIAS(固定值 1024)的倍数,因此该特性判断是否 pinned 也是特判该值。这算是混用字段了。

file/anon pages

// union 展开
struct {        /* Page cache and anonymous pages */
        /**
         * @lru: Pageout list, eg. active_list protected by
         * lruvec->lru_lock.  Sometimes used as a generic list
         * by the page owner.
         */
        union {
                struct list_head lru;

                /* Or, for the Unevictable "LRU list" slot */
                struct {
                        /* Always even, to negate PageTail */
                        void *__filler;
                        /* Count page's or folio's mlocks */
                        unsigned int mlock_count;
                };

                /* Or, free page */
                struct list_head buddy_list;
                struct list_head pcp_list;
        };
        /* See page-flags.h for PAGE_MAPPING_FLAGS */
        struct address_space *mapping;
        union {
                pgoff_t index;          /* Our offset within mapping. */
                unsigned long share;    /* share count for fsdax */
        };
        /**
         * @private: Mapping-private opaque data.
         * Usually used for buffer_heads if PagePrivate.
         * Used for swp_entry_t if PageSwapCache.
         * Indicates order in the buddy system if PageBuddy.
         */
        unsigned long private;
};

现在开始讨论 union 内部。其中首个匿名 struct 表示文件页(更准确点,page cache)和匿名页。

在早期的 Linux 实现中,lru 字段是名不副实的。如注释所指出,除了应用于(pglist_data 里面的)常规 LRU 以外,还可以作为一个通用链表去使用。因此后续有两个 commit 将 lru 细分优化:

mapping 也是一个被复用的字段,它可以:

  • 作为文件系统 page cache:就是字面意义的 struct address_space 类型(文件所属的页面)。
  • 作为一个 VMA(虚拟地址空间):struct anon_vma,而且还会继续在低位打标记。详见注释
  • 作为页面迁移回调(non-LRU):struct movable_operations。见上方注释。
  • 作为 DAX 特殊字段:PAGE_MAPPING_DAX_SHARED
  • 还找到不少子系统内部改用的其他用途,这里就不列出了(太多)。

indexshare 都关联前面的 mapping

  • index 作为 page cache 下标;
  • share 维护 PAGE_MAPPING_DAX_SHARED 场景。

private 见注释,像文件系统 VFS 场景就是藏一个 buffer head。

NOTE: 比较费解的是好像不少地方并不设置 PG_private?比如手动赋值 private

page pool

不知道。

tail page

struct {        /* Tail pages of compound page */
        unsigned long compound_head;    /* Bit zero is set */
};

用以设置和识别 tail page 当中的 head page,见 set_compound_head 和其他关联函数。

NOTES:

  • 为什么能在多个复用场景下进而识别出 tail page?在 git 记录作者表示他不确定,但是仔细考虑内存布局后好像没冲突……因此有了 union 注释那一段警告「bit 0 of the first word 不要碰」的说明(比如匿名页场景:unevictable LRU 会空出 __filler;而 lru/list_head 那种字段赋值会指针对齐,低位也是 0)。
  • 同样是指针对齐原因,设置 head 要 +1 来避开错误判断 PageTail
  • 当前版本有相当一部分的复合页元数据存放在 struct folio。早期版本的 struct page 也会继续区分 first tail page(析构函数)和 second tail page(延迟列表),感兴趣可自行查阅。

page table pages

struct {        /* Page table pages */
        unsigned long _pt_pad_1;        /* compound_head */
        pgtable_t pmd_huge_pte;         /* protected by page->ptl */
        unsigned long _pt_pad_2;        /* mapping */
        union {
                struct mm_struct *pt_mm; /* x86 pgds only */
                atomic_t pt_frag_refcount; /* powerpc */
        };
#if ALLOC_SPLIT_PTLOCKS
        spinlock_t *ptl;
#else
        spinlock_t ptl;
#endif
};

首先两个 pad 字段都是指的是防止与注释中的对应字段冲突,略。

不考虑 arch 目录下的特定实现,pmd_huge_pte 是在处理透明巨页的拆分(PMD 拆成若干 PTE)场景做临时存放使用。唯二的赋值调用流程在 pgtable_trans_huge_deposit 和 pgtable_trans_huge_withdraw 函数中(以 pmd_huge_pte 宏封装)。

pt_mm(对应 pgd_page_get_mm,见 git 记录)实际调用只与 Xen 有关。

ZONE_DEVICE pages

struct {        /* ZONE_DEVICE pages */
        /** @pgmap: Points to the hosting device page map. */
        struct dev_pagemap *pgmap;
        void *zone_device_data;
        /*
         * ZONE_DEVICE private pages are counted as being
         * mapped so the next 3 words hold the mapping, index,
         * and private fields from the source anonymous or
         * page cache page while the page is migrated to device
         * private memory.
         * ZONE_DEVICE MEMORY_DEVICE_FS_DAX pages also
         * use the mapping, index, and private fields when
         * pmem backed DAX files are mapped.
         */
};

ZONE_DEVICE 设备私有页本来只用于描述非传统 RAM 页面(持久内存 pmem,但是元数据还是挂在 RAM 上),也就是说这个区域只用于该特定 zone 作为标记,通过 pgmap(早期的 git 记录只有这一个字段) 记录设备私有维护的 pagemap。

但是现在的 MEMORY_DEVICE_* 扩展到更加广义的异构内存管理(HMM)。这一块我需要问问 GPU 领域大神……

rcu head

/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;

这个注释把历史信息隐藏了。参考早期的 git 记录:其实 rcu_head 字段是只用于 slab 子系统回收。(Used by SLAB when destroying via RCU)

// 这个实现早于 v6.4.8,只是拿来说明演变过程
/* Reuses the bits in struct page */More actions
struct slab {
        unsigned long __page_flags;
        union {
                struct list_head slab_list;
                struct {                /* Partial pages */
                        struct slab *next;
#ifdef CONFIG_64BIT
                        int slabs;      /* Nr of slabs left */
#else
                        short int slabs;
#endif
                };
                struct rcu_head rcu_head;
        };
        // 略……
};

// 见:https://elixir.bootlin.com/linux/v6.4.8/source/mm/slab.h#L119
/**
 * page_slab - Converts from first struct page to slab.
 * @p: The first (either head of compound or single) page of slab.
 *
 * A temporary wrapper to convert struct page to struct slab in situations where
 * we know the page is the compound head, or single order-0 page.
 *
 * Long-term ideally everything would work with struct slab directly or go
 * through folio to struct slab.
 *
 * Return: The slab which contains this page
 */
#define page_slab(p)            (_Generic((p),                          \
        const struct page *:    (const struct slab *)(p),               \
        struct page *:          (struct slab *)(p)))

从另一条 git 记录得知,内核从 struct page 分离出了 struct slab 结构体来单独处理 slab/slub/slob 流程。注意此时 struct slab 里面的 rcu_head 是保持与原有 struct page 同名字段相同的偏移量。总之两者是二进制兼容的,可以用 page_slab 或者 slab_page 双向转换(或者三向,加上 folio)。

// 见:https://elixir.bootlin.com/linux/v6.4.8/source/mm/slab.h#L10
/* Reuses the bits in struct page */
struct slab {
        unsigned long __page_flags;

#if defined(CONFIG_SLAB)
/* ... */
#elif defined(CONFIG_SLUB)

        struct kmem_cache *slab_cache;
        union {
                struct {
                        union { /* ... */ };
                        /* Double-word boundary */
                        void *freelist;         /* first free object */
                        union { /* ... */ };
                };
                struct rcu_head rcu_head;
        };
        unsigned int __unused;
        // 略……
#endif
};

直到近期版本,这条 git 记录表示现在已经不依赖偏移量做 slab 回收了,因此打破了二进制兼容。(仍保留 page flags 兼容。)

目前 struct page 对应的 rcu_head 字段虽然已经失去 slab 回收的意义,但是又发展出了用于页表回收的用途。感兴趣可以搜下源码。

References

The Linux Memory Manager
linux内核那些事之struct page – 微信
Minimizing struct page overhead – Oracle Linux Blog
Cramming more into struct page – LWN.net
mm/munlock: maintain page->mlock_count while unevictable
mm/page_alloc: add page->buddy_list and page->pcp_list
mm: make compound_head() robust
mm: add pt_mm to struct page
mm: ZONE_DEVICE for “device memory”
mm, dax, pmem: introduce {get|put}_dev_pagemap() for dax-gup
mm: add hmm_data to struct page
mm/ZONE_DEVICE: new type of ZONE_DEVICE for unaddressable memory
mm: combine LRU and main union in struct page
mm: Split slab into its own type
mm/sl[au]b: rearrange struct slab fields to allow larger rcu_head