前情铺垫:

  • 虚拟内存是内核为每个进程提供的独立线性地址空间的抽象,通过页表与物理内存解耦。
  • 页表找到虚拟地址到物理地址的映射:虚拟地址(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)。
    // 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
    很多标志位不占用独立的位,而是通过组合或上下文来区分;并且内核通过page-flags.h动态分配位。

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;
}

文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
 上一篇
CPU架构的差异 CPU架构的差异
组合lscpu等命令输出的信息,判断底层隔离技术等其他和CPU紧密相关的设计是用的哪一种。记录遇到的被困扰过的差异点。
2025-05-11
下一篇 
内存压力 内存压力
lmbench套件以及其他程序用于构造内存压力,便于做一些和内存有关的基准测试
  目录