前言

本文来自:https://www.slideshare.net/slideshow/physical-memory-managementpdf/252219128

一、简介

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
在这里插入图片描述
在这里插入图片描述
(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_DMA3216MB-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

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参考资料

https://blog.csdn.net/pwl999/article/details/112055498

Logo

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

更多推荐