Linux内存管理之 compound pages

一、简介

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值