文章目录
一、简介
1.1 简介
复合页是由两个或多个物理连续页组成的逻辑单元,其核心特征在于能够以单一实体形式被管理。虽然最常用于透明大页(THP)和hugetlbfs子系统,但复合页也出现在其他场景中:既可作为匿名内存使用,也能作为内核缓冲区。
需要注意的是,复合页不能存在于页缓存(page cache)中,因为该子系统仅处理独立页。
上述我觉得描述有误,复合页应该也是可以存在页缓存(page cache)中的,页缓存(page cache)都会加入到 LRU链表中,我在分析LRU链表: Linux 内存管理之LRU链表,都会看到对复合页的处理,处理复合页的第一个页 head page:
//处理复合页
page = compound_head(page);
又比如:https://kernelnewbies.org/Linux_4.8#Support_for_using_Transparent_Huge_Pages_in_the_page_cache
内核 4.8 引入对 tmpfs/shmem 的 THP 支持,扩展至文件缓存。
复合页相关结构体:
enum pageflags {
.......
PG_head, /* A head page */
}
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
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.
*/
struct list_head lru;
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
pgoff_t index; /* Our offset within mapping. */
/**
* @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;
};
......
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */
/* First tail page only */
unsigned char compound_dtor; /* 析构函数ID */
unsigned char compound_order; /* 分配阶数 */
atomic_t compound_mapcount; /* 复合页级映射计数 */
unsigned int compound_nr; /* = 1 << order */
};
struct { /* Second tail page of compound page */
unsigned long _compound_pad_1; /* compound_head */
atomic_t hpage_pinned_refcount;
/* For both global and memcg */
struct list_head deferred_list;
};
}
}
复合页第一个尾页(first tail page):1st tail page主要管理page[1].compound_dtor = compound_dtor page[1].compound_order page[1].compound_nr = 1U << order; 页大小相关信息;
复合页第二个尾页(Second tail page):2nd tail page 主要是&page[2].deferred_list的使用,涉及到memcg操作相关。
/* First tail page only */
unsigned char compound_dtor; /* 析构函数ID */
unsigned char compound_order; /* 分配阶数 */
atomic_t compound_mapcount; /* 复合页级映射计数 */
unsigned int compound_nr; /* = 1 << order */
复合页的布局如下图所示:
图片来自于:围绕HugeTLB的极致优化
复合页的head page表示复合页也可以用于Page cache,head page的 lru字段 链入 LRU链表,mapping字段指向该复合页的address_space。
元数据存储优化:
复合页的头部页(page[0])的 struct page 结构已被普通页面的元数据占满,没有额外空间存储复合页特有的信息。
因此,内核将复合页的全局信息(如析构函数、分配阶)存储在第一个尾部页(page[1])中。
1.2 复合页的使用
通过alloc_pages()分配复合页时需满足两个条件:
(1)设置__GFP_COMP标志位
(2)分配阶数(order)至少为1(即2页起)
例如:
// 分配4页组成的复合页
struct page *comp_page = alloc_pages(GFP_KERNEL | __GFP_COMP, 2);
这与普通高阶分配有本质区别:
// 仅分配4个连续页,不形成复合结构
struct page *normal_pages = alloc_pages(GFP_KERNEL, 2);
如下图所示:
复合页内存布局(以4页为例,order=2):
/*
* 复合页内存布局(以4页为例,order=2)
*
* Page[0] (Head Page):
* +---------------------+
* | flags | --> PG_head=1
* +---------------------+
*
* Page[1] (First Tail Page):
* +---------------------+
* | compound_head | --> Page[0]地址 | 0x1 (尾页标记)
* | compound_order=2 | --> 分配阶数
* | compound_dtor | --> 析构函数ID
* | compound_mapcount | --> 整个复合页的映射计数
* +---------------------+
*
* Page[2..3] (Other Tail Pages):
* +---------------------+
* | compound_head | --> Page[0]地址 | 0x1
* +---------------------+
*/
物理地址 页面索引 角色 关键元数据设置
=================================================================
0x10000000 page[0] 头部页 flags |= (1 << PG_head)
0x10010000 page[1] 第一个尾部页 compound_head = 0x10000001 (指向头部页+1)
|--> compound_order = 2
|--> compound_dtor = 析构函数ID
0x10020000 page[2] 尾部页 compound_head = 0x10000001
0x10030000 page[3] 尾部页 compound_head = 0x10000001
(1)在由4个4KB组成的compound page的第0个page结构体(page[0],即head page)上安置一个PG_head标记,逻辑如下:
page->flags |= (1UL << PG_head);
所以,如果传给PageHead() API的是第0个page结构体,由于PG_head为真,这个API返回true。
(2)在由4个4KB组成的compound page的第1~3的page结构体(page[1] ~ Page[3],即tail page)的compound_head上的最后一位设置1,逻辑如下:
page->compound_head |= 1UL;
(3)
在page[1]这个结构体的compound_order成员上,放置这个compound page的order,比如如果是连续4个4KB组成的复合页,则page[1].compound_order = 2。
在page[1]这个结构体的compound_dtor成员上,放置这个compound page的析构函数,此析构函数,在put_page[page[0]]并且refcount即将归0的时候会被执行。
目前有三种compound page的析构函数:
/* Keep the enum in sync with compound_page_dtors array in mm/page_alloc.c */
enum compound_dtor_id {
NULL_COMPOUND_DTOR,
COMPOUND_PAGE_DTOR,
#ifdef CONFIG_HUGETLB_PAGE
HUGETLB_PAGE_DTOR,
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
TRANSHUGE_PAGE_DTOR,
#endif
NR_COMPOUND_DTORS,
};
extern compound_page_dtor * const compound_page_dtors[NR_COMPOUND_DTORS];
枚举值含义:
compound_page_dtor * const compound_page_dtors[NR_COMPOUND_DTORS] = {
[NULL_COMPOUND_DTOR] = NULL,
[COMPOUND_PAGE_DTOR] = free_compound_page, //默认的复合页析构函数,用于普通复合页的释放。
#ifdef CONFIG_HUGETLB_PAGE
[HUGETLB_PAGE_DTOR] = free_huge_page, //大页(HugeTLB)的析构函数
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
[TRANSHUGE_PAGE_DTOR] = free_transhuge_page, //透明大页(Transparent Huge Pages)的析构函数
#endif
};
NULL_COMPOUND_DTOR:空析构函数(不执行任何操作)。
COMPOUND_PAGE_DTOR:默认的复合页析构函数,用于普通复合页的释放。
HUGETLB_PAGE_DTOR:大页(HugeTLB)的析构函数,仅当内核配置了 CONFIG_HUGETLB_PAGE 时存在。
TRANSHUGE_PAGE_DTOR:透明大页(Transparent Huge Pages)的析构函数,仅当内核配置了 CONFIG_TRANSPARENT_HUGEPAGE 时存在。
NR_COMPOUND_DTORS:枚举值的总数,用于定义数组大小。
1.3 相关函数
PageTail
static __always_inline int PageTail(struct page *page)
{
return READ_ONCE(page->compound_head) & 1;
}
PageTail() 函数判断page是否是tail page
位标记技巧:
利用compound_head指针的最后一位作为标记位(因为指针地址总是对齐的,最低位本应为0)。当该位为1时,表示当前页是尾页(tail page)。
PageCompound()
static __always_inline int PageCompound(struct page *page)
{
return test_bit(PG_head, &page->flags) || PageTail(page);
}
PageCompound()函数判断page是否是compound page
双条件检测:
检查PG_head标志位(判断是否为头页)。
或通过PageTail()判断是否为尾页。
compound_head
static inline unsigned long _compound_head(const struct page *page)
{
//读取 compound_head 字段
unsigned long head = READ_ONCE(page->compound_head);
//判断页面类型
//如果 head 的最低位为 1,表示当前传入的 page 是尾部页。
if (unlikely(head & 1))
//尾部页的 compound_head 字段存储的是头部页地址 + 1(最低位被置为 1 作为标记),因此返回时需减去 1 还原为真实地址。
return head - 1;
//如果最低位为 0,表示当前传入的 page 就是头部页。
return (unsigned long)page;
}
#define compound_head(page) ((typeof(page))_compound_head(page))
compound_head函数是从复合页的任意一个页面(无论是头部页还是尾部页)找到对应的头部页指针,获取复合页的头部页指针。
复合页的头部页和尾部页使用同一个 struct page 结构,但需要一种方式区分它们。通过复用 compound_head 字段的最低位作为标记位(1 表示尾部页,0 表示头部页),避免了额外字段的开销。(正常情况下page 的地址都是页对齐的)。
其中compound_head字段:
设计核心:通过位标记区分头部页和尾部页
复合页结构:
复合页由多个物理连续的页面组成,其中第一个页面是头部页,其余是尾部页。
所有尾部页都需要知道自己所属复合页的头部页地址。
地址复用与位标记:
头部页地址通常是偶数(最低位为 0,因为内存按页对齐,页大小通常是 4KB 或更大)。
通过将头部页地址加 1,内核在不额外占用空间的情况下,既存储了头部页地址,又用最低位标记了 “这是一个尾部页”。
快速判断页面类型:
检查 compound_head 的最低位:
如果为 1:说明这是尾部页,真实头部页地址为 compound_head - 1。
如果为 0:说明这是头部页本身(此时 compound_head 字段未被使用)。
1.4 复合页的用途
复合页是由两个或多个物理连续页组成的逻辑单元,其核心特征在于能够以单一实体形式被管理。虽然最常用于透明大页(THP)和hugetlbfs子系统,但复合页也出现在其他场景中:比如kmalloc。
(1)hugetlbfs子系统
(2)透明大页(THP)
(3)kmalloc()
当通过 kmalloc() 申请的内存超过 8K 时,系统会直接从 buddy 中分配,默认就是复合页。
二、分配过程
2.1 kmalloc_order
以kmalloc_order为例:当通过 kmalloc() 申请的内存超过 8K 时,系统会直接从 buddy 中分配,默认就是复合页。
void *kmalloc_order(size_t size, gfp_t flags, unsigned int order)
{
flags |= __GFP_COMP;
page = alloc_pages(flags, order);
}
alloc_pages()
--> __alloc_pages()
-->get_page_from_freelist()
-->prep_new_page()
static void prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags,
unsigned int alloc_flags)
{
if (order && (gfp_flags & __GFP_COMP))
prep_compound_page(page, order);
}
2.2 prep_compound_page
void prep_compound_page(struct page *page, unsigned int order)
{
int i;
int nr_pages = 1 << order; // 计算复合页包含的页面总数(2^order)
__SetPageHead(page); // 将第一个页面标记为头部页(设置 PG_head 标志)
// 遍历除头部页外的所有页面(即尾部页)
for (i = 1; i < nr_pages; i++) {
struct page *p = page + i; // 获取当前遍历到的页面
p->mapping = TAIL_MAPPING; // 设置 mapping 字段为 TAIL_MAPPING(特殊值,表示尾部页)
set_compound_head(p, page); // 设置该尾部页的 compound_head 字段指向头部页
}
// 设置复合页的析构函数(用于释放复合页时调用)
set_compound_page_dtor(page, COMPOUND_PAGE_DTOR);
// 设置复合页的分配阶(存储在第一个尾部页中)
set_compound_order(page, order);
// 初始化映射计数为 -1,表示复合页尚未被映射
atomic_set(compound_mapcount_ptr(page), -1);
// 如果支持大页锁定计数,则初始化锁定计数为 0
if (hpage_pincount_available(page))
atomic_set(compound_pincount_ptr(page), 0);
}
prep_compound_page() 的作用是将一组连续的物理页面初始化为一个复合页结构,设置头部页和尾部页的相关元数据,并配置复合页的析构函数、分配阶等信息。
2.2.1 设置头部页标志
__SetPageHead(page);
这会设置 将第一个页面 page->flags 中的 PG_head 标志,表示该页面是复合页的头部。
2.2.2 初始化尾部页
for (i = 1; i < nr_pages; i++) {
struct page *p = page + i;
p->mapping = TAIL_MAPPING;
set_compound_head(p, page);
}
p->mapping = TAIL_MAPPING:将尾部页的 mapping 字段设置为特殊值 TAIL_MAPPING,用于标识这是复合页的尾部页。
set_compound_head(p, page):设置尾部页的 compound_head 字段指向头部页,并将最低位设为 1 作为标记。例如,如果头部页地址为 0x1000,则 compound_head 会被设置为 0x1001。
static __always_inline void set_compound_head(struct page *page, struct page *head)
{
WRITE_ONCE(page->compound_head, (unsigned long)head + 1);
}
作用:将一个页面(page)标记为复合页的尾部页,并设置它指向头部页(head)的指针。
参数:
struct page *page:需要被设置为尾部页的页面。
struct page *head:该复合页的头部页指针。
实现:
将头部页的地址(head)转换为无符号长整型(unsigned long)。
将该地址值加 1(+ 1),使最低位(LSB)变为 1。
使用 WRITE_ONCE() 原子写入该值到 page->compound_head 字段。
2.2.3 设置析构函数
set_compound_page_dtor(page, COMPOUND_PAGE_DTOR);
这会将复合页的析构函数设置为 COMPOUND_PAGE_DTOR(通常是 free_compound_page()),用于在释放复合页时执行必要的清理操作。
/* Keep the enum in sync with compound_page_dtors array in mm/page_alloc.c */
enum compound_dtor_id {
NULL_COMPOUND_DTOR,
COMPOUND_PAGE_DTOR,
#ifdef CONFIG_HUGETLB_PAGE
HUGETLB_PAGE_DTOR,
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
TRANSHUGE_PAGE_DTOR,
#endif
NR_COMPOUND_DTORS,
};
extern compound_page_dtor * const compound_page_dtors[NR_COMPOUND_DTORS];
枚举值含义:
compound_page_dtor * const compound_page_dtors[NR_COMPOUND_DTORS] = {
[NULL_COMPOUND_DTOR] = NULL,
[COMPOUND_PAGE_DTOR] = free_compound_page, //默认的复合页析构函数,用于普通复合页的释放。
#ifdef CONFIG_HUGETLB_PAGE
[HUGETLB_PAGE_DTOR] = free_huge_page, //大页(HugeTLB)的析构函数
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
[TRANSHUGE_PAGE_DTOR] = free_transhuge_page, //透明大页(Transparent Huge Pages)的析构函数
#endif
};
NULL_COMPOUND_DTOR:空析构函数(不执行任何操作)。
COMPOUND_PAGE_DTOR:默认的复合页析构函数,用于普通复合页的释放。
HUGETLB_PAGE_DTOR:大页(HugeTLB)的析构函数,仅当内核配置了 CONFIG_HUGETLB_PAGE 时存在。
TRANSHUGE_PAGE_DTOR:透明大页(Transparent Huge Pages)的析构函数,仅当内核配置了 CONFIG_TRANSPARENT_HUGEPAGE 时存在。
NR_COMPOUND_DTORS:枚举值的总数,用于定义数组大小。
static inline void set_compound_page_dtor(struct page *page,
enum compound_dtor_id compound_dtor)
{
VM_BUG_ON_PAGE(compound_dtor >= NR_COMPOUND_DTORS, page);
page[1].compound_dtor = compound_dtor;
}
这段代码定义了复合页(compound page)的析构函数标识符枚举类型 enum compound_dtor_id,以及设置复合页析构函数的内联函数 set_compound_page_dtor()。
将析构函数标识符存储在第一个尾部页(page[1])的 compound_dtor 字段中。这是 Linux 内核复合页设计的一个关键细节:复合页的元数据(如析构函数、分配阶)存储在第一个尾部页,而非头部页。
元数据存储优化:
复合页的头部页(page[0])的 struct page 结构已被普通页面的元数据占满,没有额外空间存储复合页特有的信息。
因此,内核将复合页的全局信息(如析构函数、分配阶)存储在第一个尾部页(page[1])中。
灵活的析构机制:
通过枚举值和函数指针数组的结合,内核可以根据不同类型的复合页(普通复合页、大页、透明大页)选择不同的析构函数。
这种设计支持模块化扩展,当新的复合页类型加入时,只需添加新的枚举值和对应的函数实现,无需修改现有代码结构。
2.2.4 设置分配阶
set_compound_order(page, order);
分配阶 order 决定了复合页的大小(2^order 个页面)。该值存储在第一个尾部页的 compound_order 字段中。
static inline void set_compound_order(struct page *page, unsigned int order)
{
page[1].compound_order = order;
page[1].compound_nr = 1U << order;
}
2.2.5 初始化映射计数
atomic_set(compound_mapcount_ptr(page), -1);
compound_mapcount 是一个原子计数器,表示复合页被映射的次数。初始值为 -1 表示复合页尚未被任何进程映射。
static inline atomic_t *compound_mapcount_ptr(struct page *page)
{
return &page[1].compound_mapcount;
}
2.2.6 初始化锁定计数
if (hpage_pincount_available(page))
atomic_set(compound_pincount_ptr(page), 0);
如果系统支持大页锁定(例如通过 mlock() 锁定内存),则初始化锁定计数为 0。
参考资料
Linux 5.15
https://zhuanlan.zhihu.com/p/569517591
https://blog.csdn.net/feelabclihu/article/details/131485936
https://blog.csdn.net/shift_wwx/article/details/137789370
https://www.slideshare.net/slideshow/memory-management-with-page-folios/258148418