前情铺垫:
- 虚拟内存是内核为每个进程提供的独立线性地址空间的抽象,通过页表与物理内存解耦。
- 页表找到虚拟地址到物理地址的映射:虚拟地址(VA)→ 物理地址(PA)。不是
struct page, 可以这样想:MMU 只认识物理地址,不认识内核的数据结构。但是呢,页表项里有存页对齐的物理地址和标志位即:PFN + flags(present, dirty, rw, …)。可以将struct page看作是PFN 的软件侧描述符。struct page描述物理页面,指描述 PFN(一一对应关系)。物理页帧 PFN 是页编号,对应一段连续的物理空间,是一页大小(通常 4KB)的物理内存块的编号;struct page描述其引用计数、zone/node 信息、匿名页/文件页。- PFN = 物理地址 >> PAGE_SHIFT举个例子:PFN = 100 物理地址范围
[100 * 4KB, 101 * 4KB)也就是实际覆盖[0x190000, 0x191000)这一段物理地址空间,而不是单个地址。对应的物理页帧起始地址是0x190000。- “Node-Zone-伙伴系统”管理物理页面,通过
struct page维护物理空闲页框的分配与回收。涉及到NUMA,watermark,URL list这些经典内容。- SPARSEMEM 是一种稀疏的物理内存描述符组织机制,通过 Section 粒度的分层映射,实现 PFN 到
struct page的可扩展、可热插拔的寻址,同时避免为内存空洞分配不必要的管理开销。- 物理页帧,真实的RAM硅片,如MMU、DMA控制器直接使用物理地址。
1. flags字段
记录了物理内存页的section、node、zone、状态等信息,状态信息具体定义是linux/page-flags.h中的pageflags。每一位(bit)代表一个独立的开关或状态,具备极高的空间效率和原子操作可行性。
1.1 状态标志
| 标志名 | 位位置 | 描述 |
|---|---|---|
| PG_locked | 0 | 页面被锁定,表示正在进行I/O或防止并发访问 |
| PG_referenced | 1 | 页面最近被访问过,用于LRU回收算法 |
| PG_uptodate | 2 | 页面内容是最新的,与后端存储一致 |
| PG_dirty | 3 | 页面内容被修改,需要写回磁盘 |
| PG_lru | 4 | 页面在LRU链表上,可被回收 |
| PG_active | 5 | 页面在活动LRU链表中,表示热页面 |
| PG_slab | 6 | 页面用于slab分配器(内核对象缓存) |
| PG_owner_priv_1 | 7 | 页面所有者私有标志,不同子系统复用 |
| PG_arch_1 | 8 | 体系结构特定用途 |
| PG_reserved | 9 | 页面被保留,不可交换或回收(如内核镜像) |
| PG_private | 10 | 页面有私有数据,通常指向private指针 |
| PG_private_2 | 11 | 第二个私有标志,可能用于文件系统 |
| PG_writeback | 12 | 页面正在写回磁盘 |
| PG_head | 13 | 复合页(compound page)的头部页 |
| PG_mappedtodisk | 14 | 页面有对应的磁盘块映射 |
| PG_reclaim | 15 | 页面被标记为可回收 |
| PG_swapbacked | 16 | 页面由swap支持(匿名页或shmem) |
| PG_unevictable | 17 | 页面不可回收(如mlock锁定) |
| PG_mlocked | 18 | 页面被mlock()锁定在内存中 |
| PG_swapcache | 19 | 页面在swap缓存中 |
| PG_readahead | 20 | 预读页面,用于触发预读算法 |
| PG_isolated | 21 | 页面被隔离(如内存规整或迁移) |
| PG_reported | 22 | 页面已报告给hypervisor(用于超分) |
比较经典的PG_lru + PG_active这两个标志共同决定页面在LRU链表中的位置。PG_active=1, PG_referenced=1属于热页面;PG_active=0, PG_referenced=1属于冷但最近访问过的页面;两者都0,冷页面,候选回收。
PG_swapbacked区分匿名页和文件页,匿名页(堆、栈)可以有swap空间,文件页对应磁盘上的文件。
复合页相关(Compound Page)在位置 7 复用,如果是PG_compound表示页面是复合页的一部分。
1.2 操作flags字段
内核提供了一系列宏来操作这些标志:
- PageXXX(page):检查页面是否设置了XXX标志
- SetPageXXX(page):设置XXX标志
- ClearPageXXX(page):清除XXX标志
- TestSetPageXXX(page):测试并设置XXX标志
1.3 标志位布局
flags字段不仅存储页面状态(如PG_dirty、PG_locked等),还在高位存储:
- 页面属于哪个zone(DMA/NORMAL/HIGHMEM);
- 页面属于哪个NUMA节点;
- 在SPARSEMEM内存模型中的段编号(SECTION);
- 最后访问CPU ID(LAST_CPUPID),用于NUMA平衡;
- KASAN标签(KASAN_TAG)。
很多标志位不占用独立的位,而是通过组合或上下文来区分;并且内核通过page-flags.h动态分配位。// include\linux\mm.h /* Page flags: | [SECTION] | [NODE] | ZONE | [LAST_CPUPID] | ... | FLAGS | */ #define SECTIONS_PGOFF ((sizeof(unsigned long)*8) - SECTIONS_WIDTH) #define NODES_PGOFF (SECTIONS_PGOFF - NODES_WIDTH) #define ZONES_PGOFF (NODES_PGOFF - ZONES_WIDTH) #define LAST_CPUPID_PGOFF (ZONES_PGOFF - LAST_CPUPID_WIDTH) #define KASAN_TAG_PGOFF (LAST_CPUPID_PGOFF - KASAN_TAG_WIDTH) flags (64-bit): ┌──────────────────────────────────────────────────────────────┐ │ SECTION │ NODE │ ZONE │ CPUID │ KASAN │ FLAGS │ │ (24位) │(6位) │(2位) │ (4位) │ (8位) │ (20位) │ └──────────────────────────────────────────────────────────────┘ 高位 (63) 低位 (0) ↑ ↑ ↑ ↑ ↑ SECTIONS_ NODES_ ZONES_ LAST_ KASAN_ PGOFF PGOFF PGOFF CPUPID_ TAG_ PGOFF PGOFF
2. Page Migration Types 字段
页面的迁移类型,用来定义页面的迁移属性。是内存碎片管理的关键设计。
enum migratetype {
MIGRATE_UNMOVABLE, // 不可移动页面,内核直接使用的页面(如内核代码、数据结构)
MIGRATE_MOVABLE, // 可移动页面,用户空间页面可以通过页表重映射移动
MIGRATE_RECLAIMABLE, // 可回收页面,文件缓存等可以直接回收或写回后重用(比如dentry缓存、inode缓存 )
MIGRATE_PCPTYPES, // 每个CPU缓存页面的类型数量
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, // 高优先级原子分配
#ifdef CONFIG_CMA
MIGRATE_CMA, // 连续内存分配器区域,设备驱动DMA缓冲区
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, // 隔离区域(内存热插拔、CMA分配、正在迁移的页面等)
#endif
MIGRATE_TYPES // 类型总数
};
fallback机制,在某一种迁移类型的空闲列表耗尽时,可以在本zone内的另外迁移属性页面上获取足够多的内存页,并迁移到本类型中。fallbacks数组定义了按什么顺序借用其他类型的页面。
static int fallbacks[MIGRATE_TYPES][3] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, // RECLAIMABLE页面比MOVABLE更"固定",借用后影响较小
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, // 优先借用可回收的,最后才借用不可移动的
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, // UNMOVABLE和RECLAIMABLE性质更接近
// ...
};
3. Get Free Pages 标志
GFP Flags (gfp_t)是内存分配的最高层接口,由分配者(驱动、文件系统等)指定,描述了分配的内存应该具有什么特性。
// include/linux/gfp.h
typedef unsigned int __bitwise gfp_t;
// GFP标志的定义,不能直接用的
#define ___GFP_DMA 0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u
#define ___GFP_RECLAIMABLE 0x10u
#define ___GFP_HIGH 0x20u
#define ___GFP_IO 0x40u
#define ___GFP_FS 0x80u
#define ___GFP_ZERO 0x100u
#define ___GFP_ATOMIC 0x200u
#define ___GFP_DIRECT_RECLAIM 0x400u
#define ___GFP_KSWAPD_RECLAIM 0x800u
// ...
gfp_flags中包括了需要在哪个zone中分配、内存页是否清零、内存页是否可写等信息,经过转换后最终对应page flags的内容。
// 常用的GFP组合宏
#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO (__GFP_RECLAIM)
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA __GFP_DMA
#define GFP_DMA32 __GFP_DMA32
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)
4. Alloc Flags (alloc_flags)
alloc_flags 是内存分配器内部使用的标志,由页分配器(page allocator)在GFP标志的基础上进一步细化生成。它控制着分配过程中的具体行为。
// mm/internal.h
#define ALLOC_WMARK_MIN 0x01 /* 仅在最小水位water mark及以上限制页面分配 */
#define ALLOC_WMARK_LOW 0x02 /* 仅在低水位water mark及以上限制页面分配 */
#define ALLOC_WMARK_HIGH 0x04 /* 仅在高水位water mark及以上限制页面分配 */
#define ALLOC_NO_WATERMARKS 0x08 /* 分配时无视水位,用于紧急情况 */
#define ALLOC_WMARK_MASK 0x0f /* 水位掩码 */
// 尽力分配,如果是gfp_flags设置了__GFP_ATOMIC,该位会被使用;如果页面分配失败,则尽可能在MIGRATE_HIGHATOMIC中分配
#define ALLOC_HARDER 0x10 /* 尝试更努力,允许更多回收 */
#define ALLOC_HIGH 0x20 /* 高优先级调用者 */
#define ALLOC_CPUSET 0x40 /* 检查cpuset约束 */
#define ALLOC_CMA 0x80 /* 允许从CMA区域分配 */
#define ALLOC_NOFRAGMENT 0x100 /* 避免碎片化 */
#define ALLOC_KSWAPD 0x400 /* 分配内存不足时,唤醒kswapd回收内存 */
#define ALLOC_FAIR 0x800 /* 公平分配 */
alloc_flags的生成是根据GFP来的。
5. alloc_context 分配上下文
alloc_context 是分配过程中最详细的上下文结构,包含了所有分配决策所需的信息:
// mm/internal.h
struct alloc_context {
struct zonelist *zonelist; // 系统中所有zone列表,如果preferred_zoneref中无法满足分配,则从列表中寻找。
nodemask_t *nodemask; // 允许的NUMA节点掩码
struct zoneref *preferred_zoneref; // 优先的zone
int migratetype; // 迁移类型
enum zone_type highest_zoneidx; // 本次分配允许的最大zone下标,高于此下标的zone,不能用于本次分配
bool spread_dirty_pages; // 是否分散脏页,在某个zone的脏页超过预设的最大值,则本次分配不在该zone上进行
};
简化的伙伴系统分配流程:
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid)
{
struct page *page;
unsigned int alloc_flags;
struct alloc_context ac = { };
// 根据GFP标志准备分配上下文
ac.migratetype = gfpflags_to_migratetype(gfp);
ac.highest_zoneidx = gfp_zone(gfp);
ac.zonelist = node_zonelist(preferred_nid, gfp);
ac.nodemask = NULL;
ac.spread_dirty_pages = (gfp & __GFP_WRITE);
// 生成alloc_flags
alloc_flags = gfp_to_alloc_flags(gfp);
// 使用上下文和标志尝试分配
page = get_page_from_freelist(ac, alloc_flags, order);
// 如果失败,进行慢路径分配
if (!page)
page = __alloc_pages_slowpath(alloc_flags, order, &ac);
return page;
}
6. 透明大页扩展
透明大页作为复合页(Compound Page)的一种特定实现,内核用(2MB = 512 × 4KB) 512 个 struct page 组成的数组来描述它。这个数组也是一种复合页,可以将其看作元数据。传统内核中,作为透明大页的元数据仅 page[0] (头页) 和 第一、二个尾页(page[1],page[2])被充分利用,其余部分为空白填充。可以利用复合页尾页中原本未使用的 struct page 空间,存储页面热度元数据,可以在不增加额外内存开销的情况下存储大页和基础页面的访问数据。
6.1 数据结构
具体来说,利用复合页的 page[3](第三尾页)作为透明大页面的元数据页,记录透明大页面被访问的次数;page[4] 开始存储子页(组成透明大页面的基础页) 元数据。一个尾页可存储4个子页元数据,第 5-132 个尾页存 512 个透明大页子页的元数据。修改后的复合页结构如下图所示:
对于 4kb 基础页(不是复合页面了,没有复合页的元数据结构 padding 可用了),使用页表的 padding 记录访问元数据。一个 PTE 页表管理 512 个用户页,PTE 页表的 struct page 存一个 pginfo_t * 指针,指向额外分配的 4KB 元数据页,里面存 512 个 pginfo_t。这部分会有额外开销。
其中关键代码:
typedef struct {
uint32_t total_accesses;
uint16_t nr_accesses;
uint8_t cooling_clock;
bool may_hot;
} pginfo_t;
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
union {
……
#ifdef CONFIG_100
struct { /* Third tail page of compound page */
unsigned long __compound_pad_1; /* compound_head */
unsigned long total_accesses;
unsigned int hot_utils;
unsigned int idx;
uint32_t cooling_clock;
};
struct { /* Fourth~ tail pages of compound page */
unsigned long ___compound_pad_1;/* compound_head */
pginfo_t compound_pginfo[4]; /* 32 bytes */
};
#endif
struct { /* Page table pages */
unsigned long _pt_pad_1; /* compound_head */
pgtable_t pmd_huge_pte; /* protected by page->ptl */
#ifdef CONFIG_100
union {
pginfo_t *pginfo;
unsigned long _pt_pad_2;
};
#else
unsigned long _pt_pad_2; /* mapping */
#endif
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
};
}
}
6.2 获取元数据的方式
// THP 的访问信息
struct page *get_meta_page(struct page *page)
{
page = compound_head(page);
return &page[3];
}
// THP 中每个子页面的访问信息
pginfo_t *get_compound_pginfo(struct page *page, unsigned long address)
{
int idx, offset;
VM_BUG_ON_PAGE(!PageCompound(page), page);
idx = 4 + ((address & ~HPAGE_PMD_MASK) >> PAGE_SHIFT) / 4; // 落在哪个tail page
offset = ((address & ~HPAGE_PMD_MASK) >> PAGE_SHIFT) % 4; // 数组内偏移
return &(page[idx].compound_pginfo[offset]);
}
// 基础页的访问信息
static inline pginfo_t *get_pginfo_from_pte(pte_t *pte)
{
// 找到 PTE 条目所在的 PTE 页表页
struct page *page = virt_to_page((unsigned long)pte);
unsigned long idx;
// 计算是第几个 PTE(0-511)
idx = ((unsigned long)(pte) & ~PAGE_MASK) / 8;
return &page->pginfo[idx];
}
6.3 扩展后 THP 初始化
do_page_fault() → handle_mm_fault() → __handle_mm_fault() → do_anonymous_page()
↘
do_huge_pmd_anonymous_page()
- 检查是否允许使用 THP(根据内核配置、VMA 标志等)
- 分配一个 2MB 的匿名页(compound page)
- 清零页面内容,建立匿名映射,标记为 compound_head
- 设置 page->mapping、page->_mapcount 等元数据 -> 这一步需要初始化扩展的元数据内容
- 构造一个新的 PMD entry,指向这 2MB 页
- 建立 rmap 和 COW 支持
Linux 内核中用于分配并建立 Transparent Huge Page (THP) 映射的关键函数,出现在发生匿名页缺页异常(anon page fault)时,尝试用 PMD(2MB)级别的大页来映射用户空间。
// mm/huge_memory.c
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
……
#ifdef CONFIG_100
prep_transhuge_page_for_100(vma, page);
#else
prep_transhuge_page(page);
#endif
return __do_huge_pmd_anonymous_page(vmf, page, gfp);
}
void __prep_transhuge_page_for_100(struct mm_struct *mm, struct page *page)
{
int i, idx, offset;
pginfo_t pginfo = { 0, 0, 0, false, };
int hotness_factor = 0;
// 初始化 THP 的元数据信息
page[3].hot_utils = 0;
page[3].total_accesses = hotness_factor;
page[3].idx = 0;
if (hotness_factor < 0)
hotness_factor = 0;
pginfo.total_accesses = hotness_factor;
pginfo.nr_accesses = hotness_factor;
// 初始化 THP 子页面的元数据信息
for (i = 0; i < HPAGE_PMD_NR; i++) {
idx = 4 + i / 4;
offset = i % 4;
page[idx].compound_pginfo[offset] = pginfo;
}
……
}
6.4 基础页利用页表扩展的分配与回收
如果想记录基础页面元数据,之前提到需要页表项加一个指针,指向额外的一个存 512 个信息的 base page;在 PTE 页表创建时从 kmem_cache 申请 4KB 元数据页并建立关联;在页表销毁时回收元数据页,构成完整的引用计数。
// 分配入口
pgtable_t pte_alloc_one(struct mm_struct *mm)
{
struct page *pgtable;
pgtable = __pte_alloc_one(mm, __userpte_alloc_gfp);
#ifdef CONFIG_100
__pte_alloc_pginfo(pgtable);
#endif
return pgtable;
}
// 分配操作
static void __pte_alloc_pginfo(struct page *page)
{
/* __userpte_alloc_gfp contains __GFP_ZERO */
page->pginfo = kmem_cache_alloc(pginfo_cache,
__userpte_alloc_gfp);
}
// 回收入口
static inline void pte_free(struct mm_struct *mm, struct page *pte_page)
{
pgtable_pte_page_dtor(pte_page);
#ifdef CONFIG_100
free_pginfo_pte(pte_page);
#endif
__free_page(pte_page);
}
// 回收操作
void free_pginfo_pte(struct page *pte)
{
BUG_ON(pte->pginfo == NULL);
kmem_cache_free(pginfo_cache, pte->pginfo);
pte->pginfo = NULL;
}