Linux内核 物理内存管理
前言
本文来自:https://www.slideshare.net/slideshow/physical-memory-managementpdf/252219128
一、简介



(1)struct pglist_data
struct pglist_data是用于描述物理内存布局的核心数据结构,其设计针对 NUMA(非统一内存访问) 和 UMA(统一内存访问) 系统有不同的表现形式。
NUMA 系统:每个 NUMA 节点拥有独立的 pglist_data 实例,描述该节点的内存布局。
UMA 系统:全局只有一个 pglist_data,描述整个系统的内存。
Linux 内核的内存管理采用了分层结构:
系统 --> NUMA 节点 --> 内存区域(zone) --> 页面(page)
二、zone

(2)struct zone
每个 pglist_data 包含多个 struct zone,内存统计信息和页面置换算法的数据结构都是基于区域 (zone) 来维护的。
各Zone类型详解:
| Zone 类型 | 描述 |
|---|---|
| ZONE_DMA | 通常 0-16MB 旧式ISA设备DMA操作(需24位物理地址) |
| ZONE_DMA32 | 16MB-4GB(64位系统) 现代32位PCI设备DMA(如千兆网卡、USB 3.0控制器) |
| ZONE_NORMAL | 直接线性映射到内核虚拟地址空间的内存(64位系统通常包含大部分内存) |
| ZONE_MOVABLE | 包含可移动页,支持内存热插拔和碎片控制 |
| ZONE_DEVICE | 为持久化内存(PMEM)、GPU显存等设备内存设计 |
cat /proc/zoneinfo | grep Node
Node 0, zone DMA
Node 0, zone DMA32
Node 0, zone Normal
Node 0, zone Movable
Node 0, zone Device

这里多了个per_cpu_pageset:
/* Fields and list protected by pagesets local_lock in page_alloc.c */
struct per_cpu_pages {
int count; /* number of pages in the list */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */
......
/* Lists of pages, one per migrate type stored on the pcp-lists */
struct list_head lists[NR_PCP_LISTS];
};
struct zone {
struct per_cpu_pages __percpu *per_cpu_pageset;
}
per_cpu_pages 是一个per-cpu变量,即每个CPU都有一个本地的per_cpu_pages变量。这个per_cpu_pages结构体里面有一个单页面的链表,里面暂时存放了一小部分单个的物理页面。当系统需要单个物理页面时,就从本地CPU的per-cpu变量的链表中直接获取物理页面即可。这样分配单个物理页面效率高,而且能减少对zone中相关锁的操作。
2.1 e820

一、内存探测:从固件获取物理内存布局
当 Linux 系统启动时,内核需要先了解物理内存的可用范围和属性,这一过程通过 BIOS/UEFI 固件交互 完成:
(1)E820 中断探测内存
内核向固件发起 INT 15h 中断,操作码设为 E820h,请求返回内存地址范围信息。
固件返回的信息包含每个内存段的起始地址、大小和类型(如 memory 、reserved、ACPI 等)。
memory :可被系统分配的物理内存。
reserved:被固件或内核保留的区域(如内核代码段、ACPI 表等)。
二、内存管理的双阶段机制:memblock 与伙伴系统
(1)启动阶段:memblock 分配器
特点:设计简单,仅支持分配 / 释放连续内存块,用于内核启动初期(如初始化页表、加载驱动)。
工作流程:
基于 e820_table 标记可用内存区域。
reserved — 为内核镜像、初始化数据结构等分配临时内存。
memory — 启动完成后,将未使用的内存移交给伙伴系统。
(2)运行阶段:伙伴系统(Buddy System)
特点:精细化管理,按 2 的幂次(4KB、8KB、16KB…)划分内存块,支持高效的分配与合并。
核心机制:
将物理内存按页(4KB)分组,维护不同大小空闲块的链表。
当申请内存时,查找最接近需求的空闲块,若不存在则分裂更大的块。
释放内存时,检查相邻块是否空闲,若空闲则合并以减少碎片。
三、应用程序的内存申请:缺页中断机制
当应用程序访问未映射的虚拟地址时,会触发 缺页中断,内核通过以下流程分配物理内存:
(1)中断触发:CPU 发现虚拟地址未映射到物理页,产生缺页异常,陷入内核。
(2)伙伴系统分配:
内核根据需求大小(如 4KB、2MB 大页)向伙伴系统申请物理页。
伙伴系统从对应大小的空闲块链表中分配页,并建立虚拟地址与物理页的映射。
(3)返回应用程序:映射完成后,CPU 重新执行被中断的指令,应用程序继续运行。
/*
* Determine if we were loaded by an EFI loader. If so, then we have also been
* passed the efi memmap, systab, etc., so we should use these data structures
* for initialization. Note, the efi init code path is determined by the
* global efi_enabled. This allows the same kernel image to be used on existing
* systems (with a traditional BIOS) as well as on EFI systems.
*/
/*
* setup_arch - architecture-specific boot-time initializations
*
* Note: On x86_64, fixmaps are ready for use even before this is called.
*/
void __init setup_arch(char **cmdline_p)
{
e820__memory_setup();
......
e820__memblock_setup();
}
(1)e820__memory_setup
首先内核需要获得系统的物理地址布局,x86下的物理地址布局称为e820表。
// arch/x86/kernel/e820.c
/*
* Calls e820__memory_setup_default() in essence to pick up the firmware/bootloader
* E820 map - with an optional platform quirk available for virtual platforms
* to override this method of boot environment processing:
*/
void __init e820__memory_setup(void)
{
char *who;
/* This is a firmware interface ABI - make sure we don't break it: */
BUILD_BUG_ON(sizeof(struct boot_e820_entry) != 20);
who = x86_init.resources.memory_setup();
memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));
memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));
pr_info("BIOS-provided physical RAM map:\n");
e820__print_table(who);
}
/*
* These are the E820 types known to the kernel:
*/
enum e820_type {
E820_TYPE_RAM = 1,
E820_TYPE_RESERVED = 2,
E820_TYPE_ACPI = 3,
E820_TYPE_NVS = 4,
E820_TYPE_UNUSABLE = 5,
E820_TYPE_PMEM = 7,
/*
* This is a non-standardized way to represent ADR or
* NVDIMM regions that persist over a reboot.
*
* The kernel will ignore their special capabilities
* unless the CONFIG_X86_PMEM_LEGACY=y option is set.
*
* ( Note that older platforms also used 6 for the same
* type of memory, but newer versions switched to 12 as
* 6 was assigned differently. Some time they will learn... )
*/
E820_TYPE_PRAM = 12,
/*
* Special-purpose memory is indicated to the system via the
* EFI_MEMORY_SP attribute. Define an e820 translation of this
* memory type for the purpose of reserving this range and
* marking it with the IORES_DESC_SOFT_RESERVED designation.
*/
E820_TYPE_SOFT_RESERVED = 0xefffffff,
/*
* Reserved RAM used by the kernel itself if
* CONFIG_INTEL_TXT=y is enabled, memory of this type
* will be included in the S3 integrity calculation
* and so should not include any memory that the BIOS
* might alter over the S3 transition:
*/
E820_TYPE_RESERVED_KERN = 128,
};
/*
* A single E820 map entry, describing a memory range of [addr...addr+size-1],
* of 'type' memory type:
*
* (We pack it because there can be thousands of them on large systems.)
*/
struct e820_entry {
u64 addr;
u64 size;
enum e820_type type;
} __attribute__((packed));
/*
* The whole array of E820 entries:
*/
struct e820_table {
__u32 nr_entries;
struct e820_entry entries[E820_MAX_ENTRIES];
};
e820表就是一个数组,它存储了系统物理地址中的内存、ACPI和一些保留区域。
/*
* The platform setup functions are preset with the default functions
* for standard PC hardware.
*/
struct x86_init_ops x86_init __initdata = {
.resources = {
.probe_roms = probe_roms,
.reserve_resources = reserve_standard_io_resources,
.memory_setup = e820__memory_setup_default,
},
......
}
e820__memory_setup_default用于处理系统的内存布局信息,特别是处理来自固件(如BIOS)提供的E820内存映射表。
/*
* Pass the firmware (bootloader) E820 map to the kernel and process it:
*/
char *__init e820__memory_setup_default(void)
{
char *who = "BIOS-e820";
/*
* Try to copy the BIOS-supplied E820-map.
*
* Otherwise fake a memory map; one section from 0k->640k,
* the next section from 1mb->appropriate_mem_k
*/
if (append_e820_table(boot_params.e820_table, boot_params.e820_entries) < 0) {
u64 mem_size;
/* Compare results from other methods and take the one that gives more RAM: */
if (boot_params.alt_mem_k < boot_params.screen_info.ext_mem_k) {
mem_size = boot_params.screen_info.ext_mem_k;
who = "BIOS-88";
} else {
mem_size = boot_params.alt_mem_k;
who = "BIOS-e801";
}
e820_table->nr_entries = 0;
e820__range_add(0, LOWMEMSIZE(), E820_TYPE_RAM);
e820__range_add(HIGH_MEMORY, mem_size << 10, E820_TYPE_RAM);
}
/* We just appended a lot of ranges, sanitize the table: */
e820__update_table(e820_table);
return who;
}
(2)e820__memblock_setup
用于设置内存块(memblock)信息,并根据E820表中的内存信息进行内存块(memblock)的分配和保留。
void __init e820__memblock_setup(void)
{
int i;
u64 end;
/*
* The bootstrap memblock region count maximum is 128 entries
* (INIT_MEMBLOCK_REGIONS), but EFI might pass us more E820 entries
* than that - so allow memblock resizing.
*
* This is safe, because this call happens pretty late during x86 setup,
* so we know about reserved memory regions already. (This is important
* so that memblock resizing does no stomp over reserved areas.)
*/
memblock_allow_resize();
for (i = 0; i < e820_table->nr_entries; i++) {
struct e820_entry *entry = &e820_table->entries[i];
end = entry->addr + entry->size;
if (end != (resource_size_t)end)
continue;
if (entry->type == E820_TYPE_SOFT_RESERVED)
memblock_reserve(entry->addr, entry->size);
if (entry->type != E820_TYPE_RAM && entry->type != E820_TYPE_RESERVED_KERN)
continue;
memblock_add(entry->addr, entry->size);
}
/* Throw away partial pages: */
memblock_trim_memory(PAGE_SIZE);
memblock_dump_all();
}
2.2 memblock
在linux内核的启动过程中在buddy系统正式工作之前,需要一个临时的内存分配机制来满足这个阶段的内存分配需求。最早的临时分配机制是bootmem,现在普遍使用的是memblock。
memblock 内存页帧分配器是 Linux 启动早期内存管理器,在伙伴系统(Buddy System)接管内存管理之前为系统提供内存分配、预留等功能。
memblock 将系统启动时获取的可用内存范围(如从E820表获取的内存范围)纳入管理,为内核启动阶段内存分配需求提供服务,直到 memblock 分配器将内存管理权移交给伙伴系统。同时 memblock 分配器也维护预留内存(reserved memory),使其不会被分配器直接用于分配,保证其不被非预留者使用。
/**
* struct memblock_type - collection of memory regions of certain type
* @cnt: number of regions
* @max: size of the allocated array
* @total_size: size of all regions
* @regions: array of regions
* @name: the memory type symbolic name
*/
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
/**
* struct memblock - memblock allocator metadata
* @bottom_up: is bottom up direction?
* @current_limit: physical address of the current allocation limit
* @memory: usable memory regions
* @reserved: reserved memory regions
*/
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
.memory.max = INIT_MEMBLOCK_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS,
.reserved.name = "reserved",
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};

memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合。
memory 表示系统可用的内存,后续用于给伙伴系统管理。
reserved 表示系统预留的内存:
(1)包括内存中的某些部分使永久分配给内核的,例如代码段和数据段,ramdisk和dtb占用的空间等,是系统内存的一部分,也不参与内存的分配。
(2)GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留。
memblock详细请参考:
https://www.cnblogs.com/LoyenWang/p/11440957.html
https://blog.csdn.net/u012489236/article/details/104643643
https://tinylab.org/riscv-memblock/

三、物理内存碎片整理

3.1 buddy system

伙伴系统分配物理页面失败后,内核会对物理页面进行页面迁移,然后用于内存规整。
可以迁移的物理页面:LRU链表上的页面,LRU链表上的页面通过是用户地址空间映射的页面,如匿名页和文件映射页。
页面迁移的本质是将页面的内容迁移到新的页面。这个过程会分配新页面,将旧的页面内容复制到新的页面,断开旧的页面的映射关系,并把映射关系映射到新页面,最后释放旧页面。
对于用户进程使用的页面,实际上通过用户页表的映射来访问。用户页表可以移动和修改映射关系,不会影响用户进程。




3.2 memory migration



(1)可以移动的页面(MIGRATE_MOVABLE):通常指用户态程序分配的内存,移动这些页面仅仅需要修改页表映射关系,代价很低。
(2)可以回收的页面(MIGRATE_RECLAIMABLE):不可以移动但可以回收的页面。
(3)不可以移动的页面(MIGRATE_UNMOVABLE):内核使用的页面。
3.3 physical memory defragmentation











调用链过程:
secondary_startup_64()
-->x86_64_start_kernel()
-->x86_64_start_reservations()
-->start_kernel()
-->mem_init()
-->preallocate_vmalloc_pages()
-->pud_alloc(&init_mm, p4d, addr)
-->__pud_alloc()
-->pud_alloc_one()
-->get_zeroed_page()
-->__get_free_pages(gfp_mask | __GFP_ZERO, 0)
-->alloc_pages()
-->__alloc_pages()
-->get_page_from_freelist()
-->rmqueue()
-->rmqueue_pcplist()
-->__rmqueue_pcplist()
-->rmqueue_bulk()
-->__rmqueue()
-->__rmqueue_fallback()
-->steal_suitable_fallback()
-->change_pageblock_range()
-->set_pageblock_migratetype()
get_page_from_freelist()从伙伴系统的空闲链表中分配内存,当发生内存碎片化的时候,发现没有分配出想要内存的物理内存,就会去从其他迁移类型中挪用内存,调用__rmqueue_fallback()。
回退列表(fallback list)上查找一个空闲的伙伴页(buddy page),并将其放置在请求的迁移类型(migratetype)的空闲列表上,根据碎片化避免启发式方法,可能会将来自同一块的其他页一起放置,具体取决于碎片化避免启发式方法。
这里就是将伙伴系统上的 MIGRATE_MOVABLE 类型的物理页迁移到 MIGRATE_UNMOVABLE类型的物理页。
enum migratetype {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
......
}
四、pageblock









五、page frame allocator:watermark

5.1 watermarks

内存回收是以zone为单位进行的,而系统判断一个zone需不需要进行内存回收,如上面所说,为zone设置一条线,当此zone的空闲页框不足以到达这条线时,就会对此zone进行内存回收,实际上一个zone有三条线,这三条线分别是最小阀值(WMARK_MIN),低阀值(WMARK_LOW),高阀值(WMARK_HIGH),它们都保存在zone的watermark[NR_WMARK]数组中,这个数组中保存的是各个阀值要求的页框数量,而每个阀值都会对内存回收造成影响。
内核为每个物理内存区域(zone)画了三条水位线:WMARK_MIN(页最小阈值), WMARK_LOW(页低阈值)和 WMARK_HIGH(页高阈值)。定义在 zone_watermarks 枚举中。
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
内存水位线是按区域(zone)维护的三个阈值,用于监控和管理系统内存使用状态:
(1)WMARK_MIN(最低水位线)
当可用内存低于此阈值时,系统进入紧急状态
触发直接内存回收(Direct Reclaim),可能导致应用程序短暂卡顿
优先保证关键内核操作的内存需求
(2)WMARK_LOW(低水位线)
当可用内存低于此阈值时,系统开始后台异步回收内存
唤醒 kswapd 内核线程进行页面回收(Page Reclaim)
维持系统内存的 “健康状态”
(3)WMARK_HIGH(高水位线)
当可用内存高于此阈值时,系统内存充足
停止后台内存回收,允许更宽松的内存分配策略
通常作为系统内存使用的 “安全上限”
内存回收策略
kswapd 后台回收:异步进行,优先回收不常用的匿名页和文件缓存。
直接内存回收:同步阻塞应用程序,强制回收内存以满足紧急需求。
内存不足(OOM):当所有回收手段无效时,触发 OOM 杀手选择进程终止。
阈值计算
水位线值通常基于区域总页数的百分比动态计算,例如:
min = zone->managed_pages * (sysctl_vm_min_free_kbytes / totalram_pages);
low = min * 5/4;
high = min * 3/2;
WMARK_MIN、WMARK_LOW和WMARK_HIGH 水位线都是通过内核参min_free_kbytes分别计算得到,使用sysctl可以动态设置这个参数,达到动态控制水位线的目的。
cat /proc/sys/vm/min_free_kbytes
67584
min_free_kbytes 是 Linux 内核中的一个关键参数,用于控制系统预留的最低空闲内存量。这个参数直接影响内存水位线机制的行为,对系统性能和稳定性有重要影响。
基本功能
min_free_kbytes 定义了系统中每个内存区域(zone)必须保留的最低空闲内存量(以千字节为单位)。
根据每个区域容量大小比例,从min_free_kbytes划分每个区域的 WMARK_MIN 水位线。
这个值直接决定了内存水位线中的 WMARK_MIN 阈值,而 WMARK_LOW 和 WMARK_HIGH 则基于 WMARK_MIN 按比例计算。
5.2 update watermark

init_per_zone_wmark_min() 函数,负责初始化系统的最小空闲内存阈值 min_free_kbytes,并设置相关内存管理参数。
(1)计算低端内存大小(lowmem_kbytes)
(2)动态计算 min_free_kbytes,随着内存增加,min_free_kbytes 非线性增长。
(3)根据 min_free_kbytes 计算每个 zone 的 WMARK_MIN、WMARK_LOW、WMARK_HIGH。
(4)设置低端内存保留。
(5)若 THP 启用,调整 min_free_kbytes 以满足大页分配需求。
通过一个巧妙的算法动态计算系统的 min_free_kbytes 参数,确保内存管理策略与系统物理内存规模相匹配。
// v5.10/source/mm/page_alloc.c
/**
* nr_free_zone_pages - count number of pages beyond high watermark
* @offset: The zone index of the highest zone
*
* nr_free_zone_pages() counts the number of pages which are beyond the
* high watermark within all zones at or below a given zone index. For each
* zone, the number of pages is calculated as:
*
* nr_free_zone_pages = managed_pages - high_pages
*
* Return: number of pages beyond high watermark.
*/
static unsigned long nr_free_zone_pages(int offset)
{
struct zoneref *z;
struct zone *zone;
/* Just pick one node, since fallback list is circular */
unsigned long sum = 0;
struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);
for_each_zone_zonelist(zone, z, zonelist, offset) {
unsigned long size = zone_managed_pages(zone);
unsigned long high = high_wmark_pages(zone);
if (size > high)
sum += size - high;
}
return sum;
}
/**
* nr_free_buffer_pages - count number of pages beyond high watermark
*
* nr_free_buffer_pages() counts the number of pages which are beyond the high
* watermark within ZONE_DMA and ZONE_NORMAL.
*
* Return: number of pages beyond high watermark within ZONE_DMA and
* ZONE_NORMAL.
*/
unsigned long nr_free_buffer_pages(void)
{
return nr_free_zone_pages(gfp_zone(GFP_USER));
}
EXPORT_SYMBOL_GPL(nr_free_buffer_pages);
这段代码是 Linux 内核中用于计算高水位线(high watermark)之上的空闲内存页数的关键函数,主要用于内存管理和回收策略。
(1)nr_free_zone_pages(int offset)
功能
计算指定 zone 及其更低 zone 中,超过高水位线(high watermark)的空闲页数。
用于评估系统当前有多少内存可以安全使用(不会被回收)。
(2)nr_free_buffer_pages()
功能
计算 ZONE_DMA 和 ZONE_NORMAL 中,超过高水位线的空闲页数。
关键逻辑
调用 nr_free_zone_pages(gfp_zone(GFP_USER)):
gfp_zone(GFP_USER):获取 GFP_USER 标志对应的 zone 索引(通常是 ZONE_NORMAL)。
因此,此函数统计的是 ZONE_NORMAL 及更低 zone(如 ZONE_DMA)的超高水位页数。
返回值
ZONE_DMA + ZONE_NORMAL 中,超过高水位的总页数。
5.2.1 update min_free_kbytes and _watermark


(1)计算 lowmem_kbytes
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
nr_free_buffer_pages():获取当前空闲的页面数(不包括高端内存,仅低端内存)。
PAGE_SIZE >> 10:将页面大小(通常 4KB)转换为 KB(>> 10 即除以 1024)。
结果:lowmem_kbytes = 当前空闲的低端内存大小(KB)。
(2)计算 new_min_free_kbytes
new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
公式:min_free_kbytes = sqrt(低端内存大小_KB × 16)
例如:
如果 lowmem_kbytes = 16GB (16777216 KB),则 sqrt(16777216 × 16) = 16384(即 16MB)。
如果 lowmem_kbytes = 1GB (1048576 KB),则 sqrt(1048576 × 16) = 4096(即 4MB)。
作用:动态调整 min_free_kbytes,使其与系统内存规模匹配。

setup_per_zone_wmarks() 函数用于设置每个内存区域(zone)的水位标记(watermarks)。
核心功能
动态调整每个 zone 的水位值(WMARK_MIN、WMARK_LOW、WMARK_HIGH),使其与全局 min_free_kbytes 保持一致。
在以下情况下调用:
min_free_kbytes 被修改(如通过 /proc/sys/vm/min_free_kbytes)。
内存热插拔(hot-add/remove),即物理内存动态增加或减少时。


calculate_totalreserve_pages() 函数,用于计算系统需要保留的总内存页数,在 min_free_kbytes 或 sysctl_lowmem_reserve_ratio 修改时调用。
(1)核心功能
计算每个内存节点(pglist_data)及其 zone 的保留页数(totalreserve_pages),包括:
水位保留页(高水位 high_wmark_pages)。
低端内存保护页(lowmem_reserve)。
更新全局变量 totalreserve_pages,表示系统总共需要保留的页数。
(2)调用场景
修改 min_free_kbytes(通过 /proc/sys/vm/min_free_kbytes)。
修改 sysctl_lowmem_reserve_ratio(控制低端内存保留比例)。
totalreserve_pages:
系统范围内不能分配给普通进程的保留页数,用于紧急分配(如原子分配、内核线程等)。
totalreserve_pages 较大:
系统保留更多内存,减少 OOM 风险,但用户可用内存减少。
totalreserve_pages 较小:
用户可用内存增加,但可能因内存不足触发直接回收或 OOM Killer。


lowmem_reserve:
一个数组,表示当前 zone 需要为更高阶的 zone 保留的页数(防止高端内存占用低端内存)。

5.2.2 khugepaged_min_free_kbytes_update


// v5.10/source/mm/khugepaged.c
void khugepaged_min_free_kbytes_update(void)
{
mutex_lock(&khugepaged_mutex);
if (khugepaged_enabled() && khugepaged_thread)
set_recommended_min_free_kbytes();
mutex_unlock(&khugepaged_mutex);
}
static void set_recommended_min_free_kbytes(void)
{
struct zone *zone;
int nr_zones = 0;
unsigned long recommended_min;
//遍历所有已初始化的内存区域(如 ZONE_DMA、ZONE_NORMAL)
for_each_populated_zone(zone) {
/*
* We don't need to worry about fragmentation of
* ZONE_MOVABLE since it only has movable pages.
*/
if (zone_idx(zone) > gfp_zone(GFP_USER))
continue;
//nr_zones 统计低端内存(ZONE_DMA + ZONE_NORMAL)的数量。
nr_zones++;
}
//每个 zone 保留 2 个 pageblock 的空闲内存,用于避免内存碎片(满足 THP 的连续内存需求)。
/* Ensure 2 pageblocks are free to assist fragmentation avoidance */
recommended_min = pageblock_nr_pages * nr_zones * 2;
/*
* Make sure that on average at least two pageblocks are almost free
* of another type, one for a migratetype to fall back to and a
* second to avoid subsequent fallbacks of other types There are 3
* MIGRATE_TYPES we care about.
*/
//确保每种迁移类型有足够的后备内存,避免因迁移类型冲突导致 THP 分配失败。
recommended_min += pageblock_nr_pages * nr_zones *
MIGRATE_PCPTYPES * MIGRATE_PCPTYPES;
//限制最大值(不超过低端内存的 5%)
//转换为 KB 并更新 min_free_kbytes
/* don't ever allow to reserve more than 5% of the lowmem */
recommended_min = min(recommended_min,
(unsigned long) nr_free_buffer_pages() / 20);
recommended_min <<= (PAGE_SHIFT-10);
if (recommended_min > min_free_kbytes) {
if (user_min_free_kbytes >= 0)
pr_info("raising min_free_kbytes from %d to %lu to help transparent hugepage allocations\n",
min_free_kbytes, recommended_min);
min_free_kbytes = recommended_min;
}
//更新水位标记
//调用 setup_per_zone_wmarks() 根据新的 min_free_kbytes 重新计算各 zone 的水位值(WMARK_MIN、WMARK_LOW、WMARK_HIGH)。
setup_per_zone_wmarks();
}
khugepaged_min_free_kbytes_update() 函数,用于在透明大页(Transparent HugePages, THP)管理子系统 (khugepaged) 启用时,更新系统推荐的 min_free_kbytes 值。
核心作用
当 khugepaged 线程启用时,调用 set_recommended_min_free_kbytes() 重新计算并设置 min_free_kbytes。
确保系统为透明大页(THP)操作保留足够的内存。
调用场景
透明大页配置变更(如通过 /sys/kernel/mm/transparent_hugepage/enabled)。
khugepaged 线程启动或停止时
khugepaged_min_free_kbytes_update() 是透明大页管理的关键辅助函数,确保系统为 THP 操作保留足够内存。
通过动态调整 min_free_kbytes,平衡 THP 性能优势 和 内存稳定性。
开发者/管理员可通过透明大页配置参数间接影响此行为。
cat /sys/kernel/mm/transparent_hugepage/khugepaged/defrag
1

六、page frame allocator







参考资料
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)