背景
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
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 page
和 struct 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
细分优化:
- unevictable LRU 没有真正的 LRU 实体,因此复用为
mlock_count
计数用途。 - buddy 子系统当中的页面链表就直接改名为
buddy_list
/pcp_list
。
mapping
也是一个被复用的字段,它可以:
- 作为文件系统 page cache:就是字面意义的
struct address_space
类型(文件所属的页面)。 - 作为一个 VMA(虚拟地址空间):
struct anon_vma
,而且还会继续在低位打标记。详见注释。 - 作为页面迁移回调(non-LRU):
struct movable_operations
。见上方注释。 - 作为 DAX 特殊字段:PAGE_MAPPING_DAX_SHARED。
- 还找到不少子系统内部改用的其他用途,这里就不列出了(太多)。
index
和 share
都关联前面的 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