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_UNMOVABLE
、MIGRATE_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_NORMAL
、ZONE_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数量 × 440 最终位图大小(字节) 总位数 / 8(1字节=8位) 40 / 8 = 5。(32bit系统上,需要两个 pageblock_flags
元素)
- 参数:根据 Zone 的起始页帧(
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_bitidx
到end_bitidx
的位宽(如3位),生成掩码0b111
pfn_to_bitidx:
将物理页帧号(PFN)转换为对应内存块(Pageblock)在位图(pageblock_flags
)中的起始位索引
示例:
- 参数:
zone_start_pfn = 0x1234
(未对齐到内存块大小)。pageblock_nr_pages = 1024
。pfn = 0x1500
(目标页 PFN)。- 计算过程:
- 对齐 Zone 起始 PFN:
round_down(0x1234, 1024) = 0x1000
。- PFN 偏移:
0x1500 - 0x1000 = 0x500
。- 内存块索引:
0x500 >> 10 = 1
(第 1 个内存块)。- 位索引:
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