linux内存页块划分及位图存储机制

发布于:2025-03-12 ⋅ 阅读:(20) ⋅ 点赞:(0)

page_alloc.c - mm/page_alloc.c - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer

一. 什么是页块(Pageblock)?

  • 定义:页块是物理内存中的一个连续区域,由 2^pageblock_order 个物理页(Page)组成。

  • 作用:页块是内存碎片管理的最小单位,用于跟踪内存区域的 迁移类型(Migratetype)(如可移动、不可移动等),优化内存分配和碎片整理。


1.1. pageblock_order 的默认值

  • 常规配置

    #ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE
      #define pageblock_order HUGETLB_PAGE_ORDER
    #else
      #define pageblock_order (MAX_ORDER - 1)
    #endif
     
    • 默认情况:若未启用巨型页(HugeTLB)的动态大小配置,pageblock_order 通常设置为 MAX_ORDER - 1

    • MAX_ORDER:伙伴系统的最大分配阶数,通常为 11(对应 2^10 = 1024 页,即 4MB,假设页大小为 4KB)。

  • 结果

    • 默认页块大小为 2^(MAX_ORDER-1) 页(例如 MAX_ORDER=11 时,页块为 2^10 = 1024 页 = 4MB)。

    • 每个页块在全局位图 pageblock_flags 中占用若干位(如3位用于迁移类型)。


1.2. 为什么选择 MAX_ORDER - 1

设计目标
  • 对齐伙伴系统:确保页块大小与伙伴系统的最大连续内存块(MAX_ORDER)对齐,避免跨页块分配。

  • 减少碎片:较大的页块能更高效地隔离不可移动内存(如内核对象),减少长期内存碎片。

  • 迁移类型管理:每个页块独立维护迁移类型,方便内存碎片整理时批量移动页面。

权衡
  • 内存粒度:页块越大,管理更粗粒度,可能浪费内存;页块越小,管理更细粒度,但元数据开销增加。

  • 性能:较大的页块减少位图操作次数,提高效率。

二、什么是页块位图?

1. ​核心作用与背景

pageblock_flags 是 Linux 内存管理中的一个关键数据结构,主要用于跟踪和管理 ​内存块(pageblock)​ 的特性。通过 pageblock_flags,内核可以高效地记录每个内存块的属性,例如迁移类型、分配状态等,从而优化内存分配与回收策略。

2. ​数据结构与存储方式
  • 存储位置pageblock_flags 以位图(bitmap)形式存储在 struct zone 结构体中(字段 unsigned long *pageblock_flags),每个内存块对应位图中的若干比特位
    // https://elixir.bootlin.com/linux/v5.4.285/source/include/linux/mmzone.h#L448
    
    struct zone {
    #ifndef CONFIG_SPARSEMEM
    	/*
    	 * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
    	 * In SPARSEMEM, this map is stored in struct mem_section
    	 */
    	unsigned long		*pageblock_flags;  // 指向位图的指针
    #endif /* CONFIG_SPARSEMEM */
    }

    单个 pageblock_flags 元素可表示的页块和页的数量为:

    32位系统:32/4=8 个页块=8*1024页。

    64位系统:64/4=16 个页块=16*1024页。

  • 位图管理:每个内存块的标志位由 NR_PAGEBLOCK_BITS 定义,用于存储以下信息:
    • 迁移类型​(如 MIGRATE_UNMOVABLEMIGRATE_MOVABLE 等)。
    • 内存块的其他状态(如是否跳过内存碎片整理等)
      #define PB_migratetype_bits 3
      /* Bit indices that affect a whole block of pages */
      enum pageblock_bits {
      	PB_migrate,
      	PB_migrate_end = PB_migrate + PB_migratetype_bits - 1,
      			/* 3 bits required for migrate types */
      	PB_migrate_skip,/* 1位标记是否跳过压缩(PB_migrate_skip),避免重复处理低效内存块 */
      
      	/*
      	 * Assume the bits will always align on a word. If this assumption
      	 * changes then get/set pageblock needs updating.
      	 */
      	NR_PAGEBLOCK_BITS
      };

3. ​初始化


free_area_init_core -> setup_usemap

static void __ref setup_usemap(struct pglist_data *pgdat,
				struct zone *zone,
				unsigned long zone_start_pfn,
				unsigned long zonesize)
{
	unsigned long usemapsize = usemap_size(zone_start_pfn, zonesize);
	zone->pageblock_flags = NULL;
	if (usemapsize) {
		zone->pageblock_flags =
			memblock_alloc_node(usemapsize, SMP_CACHE_BYTES,
					    pgdat->node_id);
		if (!zone->pageblock_flags)
			panic("Failed to allocate %ld bytes for zone %s pageblock flags on node %d\n",
			      usemapsize, zone->name, pgdat->node_id);
	}
}

setup_usemap 的作用是为内存管理区(Zone)分配并初始化 pageblock_flags 位图,该位图用于跟踪每个内存块(Pageblock)的迁移类型(Migratetype)和其他状态。


参数说明

  • struct pglist_data *pgdat: 指向 NUMA 节点的 pglist_data 结构,描述节点的内存布局。
  • struct zone *zone: 目标内存管理区(如 ZONE_NORMALZONE_DMA)。
  • unsigned long zone_start_pfn: 该 Zone 的起始物理页帧号(Page Frame Number)。
  • unsigned long zonesize: 该 Zone 的总页数。

代码逻辑分解

1. 计算 pageblock_flags 位图大小

#define PB_migratetype_bits 3
/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
	PB_migrate,
	PB_migrate_end = PB_migrate + PB_migratetype_bits - 1,
			/* 3 bits required for migrate types */
	PB_migrate_skip,/* 1位标记是否跳过压缩(PB_migrate_skip),避免重复处理低效内存块 */

	/*
	 * Assume the bits will always align on a word. If this assumption
	 * changes then get/set pageblock needs updating.
	 */
	NR_PAGEBLOCK_BITS
};

#define pageblock_nr_pages	(1UL << pageblock_order)


static unsigned long __init usemap_size(unsigned long zone_start_pfn, unsigned long zonesize)
{
    unsigned long usemapsize;
    zonesize += zone_start_pfn & (pageblock_nr_pages-1);       // 步骤1:对齐修正
    usemapsize = roundup(zonesize, pageblock_nr_pages);        // 步骤2:向上对齐到pageblock整数倍
    usemapsize = usemapsize >> pageblock_order;                // 步骤3:计算pageblock数量
    usemapsize *= NR_PAGEBLOCK_BITS;                           // 步骤4:总位数 = 块数 × 4位
    usemapsize = roundup(usemapsize, 8 * sizeof(unsigned long));// 步骤5:位对齐到unsigned long
    return usemapsize / 8;                                     // 步骤6:转换为字节
}

  • ​**usemap_size()**: 计算位图所需内存大小(单位:字节)。
    • 参数:根据 Zone 的起始页帧(zone_start_pfn)和总页数(zonesize)。
    • 内部逻辑:
      举例:某一个zone的start pfn = 0X1234;end pfn =  0X3600;zonesize = 0X23CC
      操作 目的 结果
      计算对齐修正值 处理Zone起始地址未按pageblock对齐的情况(如Zone起始于一个pageblock中间),修正总页数以包含不完整的起始pageblock 修正值:zone_start_pfn & (pageblock_nr_pages-1)=0x234
      zonesize += zone_start_pfn & (pageblock_nr_pages-1)=0x23CC+0x234=0x2600
      向上取整对齐 确保总页数是pageblock大小的整数倍(例如:1024页的倍数),避免部分pageblock无法被位图覆盖 usemapsize  = roundup(zonesize, pageblock_nr_pages) = roundup(0x2600, 1024)  = 0x2800页
      计算pageblock数量 右移pageblock_order位(等价于除以pageblock_nr_pages),得到Zone内完整的pageblock数量 usemapsize = usemapsize >> pageblock_order; =   0x2800 >> 10 = 10
      计算总位数 每个pageblock需要NR_PAGEBLOCK_BITS(4位)来存储状态,总位数 = pageblock数量 × 4 40
      最终位图大小(字节)​ ​总位数 / 8(1字节=8位) 40 / 8 = 5。(32bit系统上,需要两个pageblock_flags元素)

 


2. 分配 pageblock_flags 内存

if (usemapsize) {
    zone->pageblock_flags = memblock_alloc_node(usemapsize, SMP_CACHE_BYTES, pgdat->node_id);
    // ...
}
  • 条件判断:仅在 usemapsize > 0 时分配内存(Zone 包含至少一个完整内存块)。
  • 分配函数memblock_alloc_node
    • 用途: 在内核启动早期(伙伴系统未初始化时),从 memblock 分配器分配内存。
    • 参数:
      • usemapsize: 分配的字节数。
      • SMP_CACHE_BYTES: 对齐到缓存行(通常 64 字节),避免伪共享(False Sharing)。
      • pgdat->node_id: 在指定 NUMA 节点上分配内存,确保 NUMA 亲和性。
  • 结果zone->pageblock_flags 指向分配的位图内存。

三. 实际应用场景

3.1、内存碎片整理(Memory Compaction)

碎片整理器根据页块的迁移类型,将可移动页面(如用户态数据)迁移到其他页块,腾出连续内存。页块大小决定了迁移操作的最小单位。

3.1.1、设置迁移类型set_pageblock_migratetype(struct page *page, int migratetype),用于在页释放时将其所属内存块的迁移类型标记为正确值

#define PB_migratetype_bits 3
/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
	PB_migrate,
	PB_migrate_end = PB_migrate + PB_migratetype_bits - 1,
			/* 3 bits required for migrate types */
	PB_migrate_skip,/* If set the block is skipped by compaction */

	/*
	 * Assume the bits will always align on a word. If this assumption
	 * changes then get/set pageblock needs updating.
	 */
	NR_PAGEBLOCK_BITS
};

#define set_pageblock_flags_group(page, flags, start_bitidx, end_bitidx) \
	set_pfnblock_flags_mask(page, flags, page_to_pfn(page),		\
			end_bitidx,					\
			(1 << (end_bitidx - start_bitidx + 1)) - 1)

void set_pageblock_migratetype(struct page *page, int migratetype)
{
	if (unlikely(page_group_by_mobility_disabled &&
		     migratetype < MIGRATE_PCPTYPES))
		migratetype = MIGRATE_UNMOVABLE;

	set_pageblock_flags_group(page, (unsigned long)migratetype,
					PB_migrate, PB_migrate_end);
}

/**
 * set_pfnblock_flags_mask - Set the requested group of flags for a pageblock_nr_pages block of pages
 * @page: The page within the block of interest
 * @flags: The flags to set
 * @pfn: The target page frame number
 * @end_bitidx: The last bit of interest
 * @mask: mask of bits that the caller is interested in
 */
void set_pfnblock_flags_mask(struct page *page, unsigned long flags,
					unsigned long pfn,
					unsigned long end_bitidx,
					unsigned long mask)
{
	unsigned long *bitmap;
	unsigned long bitidx, word_bitidx;
	unsigned long old_word, word;

	BUILD_BUG_ON(NR_PAGEBLOCK_BITS != 4);
	BUILD_BUG_ON(MIGRATE_TYPES > (1 << PB_migratetype_bits));
    // 获取目标页块对应的位图指针及位偏移
	bitmap = get_pageblock_bitmap(page, pfn); //page_zone(page)->pageblock_flags
	bitidx = pfn_to_bitidx(page, pfn);
    // 计算位图中的具体位置(原子操作)
	word_bitidx = bitidx / BITS_PER_LONG;
	bitidx &= (BITS_PER_LONG-1);

	VM_BUG_ON_PAGE(!zone_spans_pfn(page_zone(page), pfn), page);
    // 位操作:对齐掩码和标志值
	bitidx += end_bitidx;
	mask <<= (BITS_PER_LONG - bitidx - 1);
	flags <<= (BITS_PER_LONG - bitidx - 1);
    // 原子更新位图
	word = READ_ONCE(bitmap[word_bitidx]);
	for (;;) {
		old_word = cmpxchg(&bitmap[word_bitidx], word, (word & ~mask) | flags);
		if (word == old_word)
			break;
		word = old_word;
	}
}

static inline int pfn_to_bitidx(struct page *page, unsigned long pfn)
{
    // 步骤1:计算对齐后的Zone起始PFN,并调整pfn为相对于该起始的偏移
    pfn = pfn - round_down(page_zone(page)->zone_start_pfn, pageblock_nr_pages);
    // 步骤2:将偏移转换为内存块索引,再乘以每块占用的位数,得到位索引
    return (pfn >> pageblock_order) * NR_PAGEBLOCK_BITS;
}

迁移特性开关检查
page_group_by_mobility_disabled全局标志(由系统内存状态决定)若为真,表示禁用按迁移类型分组。此时强制将基础迁移类型(如MIGRATE_MOVABLE)设为MIGRATE_UNMOVABLE,避免碎片整理操作。

生成掩码:

(1 << (end_bitidx - start_bitidx + 1)) - 1

计算start_bitidxend_bitidx的位宽(如3位),生成掩码0b111

pfn_to_bitidx:将物理页帧号(PFN)转换为对应内存块(Pageblock)在位图(pageblock_flags)中的起始位索引

示例:
  • 参数
    • zone_start_pfn = 0x1234(未对齐到内存块大小)。
    • pageblock_nr_pages = 1024
    • pfn = 0x1500(目标页 PFN)。
  • 计算过程
    1. 对齐 Zone 起始 PFN
      round_down(0x1234, 1024) = 0x1000
    2. PFN 偏移
      0x1500 - 0x1000 = 0x500
    3. 内存块索引
      0x500 >> 10 = 1(第 1 个内存块)。
    4. 位索引
      1 * 4 = 4
  • 结果
    该页位于第 1 个内存块,其状态位在位图的第 4 位。

3.1.2、​获取迁移类型get_pageblock_migratetype(struct page *page),从页的元数据中提取所属内存块的迁移类型

3.1.3、​初始化与校验:在系统启动时,内核会检查每个迁移类型的内存块是否达到最小数量(pageblock_nr_pages),以决定是否启用迁移优化特性

3.2、连续内存分配器(CMA)
  • CMA 预留的连续内存以页块为单位管理,pageblock_order 影响 CMA 区域的最小粒度。

3.3、巨型页(HugeTLB)
  • 若启用动态巨型页大小(CONFIG_HUGETLB_PAGE_SIZE_VARIABLE),pageblock_order 可能与巨型页大小对齐。

3.4、内存热插拔
  • 动态调整内存区域时,通过 pageblock_flags 快速定位可迁移或可回收的内存块

page_alloc.c - mm/page_alloc.c - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer

page_alloc.c - mm/page_alloc.c - Linux source code v5.4.285 - Bootlin Elixir Cross Referencer