2.3.2 结点管理
2.3.2.4 内存域水印(WaterMark)
内存域水印(WaterMark)是一种用于内存管理的机制,它帮助内核监控和调节物理内存的使用情况,以确保系统的稳定性和性能。
内存域水印是指每个内存区域(zone)中设定的三个关键水位线,分别是最低水线(WMARK_MIN)、低水线(WMARK_LOW)和高水线(WMARK_HIGH)。这些水位线以页(page)为单位,用于衡量内存区域的空闲页数,从而指导内存分配和回收策略。
2.3.2.4.1 内存域水印的作用
-
监控内存使用情况:通过比较内存区域的空闲页数与设定的水位线,内核可以判断内存的使用状态,是充足、轻微不足还是严重不足。
-
指导内存分配:当内存分配器(如buddy allocator)发现当前内存区域的空闲页数小于低水线,但大于最低水线时,会触发异步的内存回收操作(由kswapd守护进程执行),以释放足够的内存来满足分配请求。
如果空闲页数低于最低水线,则可能需要进行同步的内存回收(direct reclaim),这可能会阻塞内存分配请求直到回收足够的内存。
-
保障系统稳定性:通过设定最低水线,内核可以确保即使在内存压力较大的情况下,也能为关键任务保留足够的内存,从而避免系统崩溃或不稳定。
2.3.2.4.2 水印类型
- 最低水线(WMARK_MIN):是内存区域必须保留的最低空闲页数,是为关键性分配保留的内存空间,以确保系统的基本稳定运行。其值通常通过系统参数(全局变量)min_free_kbytes计算得出,该参数指定了系统保留的空闲内存的最低限(以KB为单位),位于/proc/sys/vm/min_free_kbytes,用户层可以读取和修改。
- 低水线(WMARK_LOW):当内存区域的空闲页数低于此水位线时,表明内存开始面临压力,此时会唤醒kswapd守护进程进行内存回收。低水线的值通常设置为最低水线的一定比例(如增加1/4)加上额外的预留空间。
- 高水线(WMARK_HIGH):当内存区域的空闲页数高于此水位线时,表明内存充足,不需要进行内存回收。高水线的值通常设置为最低水线的一定比例(如增加1/2)加上额外的预留空间。
2.3.2.4.3 计算水印
在计算各种水印之前,内核首先确定最低水线。下图为各水线(纵轴)与主内存大小(横轴)之间的关系,普通内存域,不涉及高端内存域。例如,pages_min为按页计算的min_free_kbytes。
- init_per_zone_pages_min():初始化struct zone中的pages_min、pages_low、pages_high水印值,该函数在内核启动期间调用(不止,每次通过proc文件系统修改某个控制参数时也会调用),无需显式调用。
- setup_per_zone_pages_min():设置struct zone的pages_min、pages_low、pages_high成员。在计算出高端内存域之外页面的总数(lowmem_pages)之后,内核遍历系统中的所有内存域并执行下列计算。
mm/page_alloc.c
void setup_per_zone_pages_min(void)
{
unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT -10);
unsigned long lowmem_pages = 0;
struct zone *zone;
unsigned long flags;
...
for_each_zone(zone) { //遍历所有zone
u64 tmp;
tmp = (u64)pages_min * zone->present_pages;
do_div(tmp,lowmem_pages);
if (is_highmem(zone)) {
int min_pages;
min_pages = zone->present_pages / 1024;
if (min_pages < SWAP_CLUSTER_MAX)
min_pages = SWAP_CLUSTER_MAX;
if (min_pages > 128)
min_pages = 128;
zone->pages_min = min_pages;
} else {
zone->pages_min = tmp;
}
zone->pages_low = zone->pages_min + (tmp >> 2);
zone->pages_high = zone->pages_min + (tmp >> 1);
}
}
SWAP_CLUSTER_MAX为高端内存域的下界(页面回收)。页面回收子系统经常对页进行分组式批处理操作,而SWAP_CLUSTER_MAX定义了分组的大小。
-
setup_per_zone_lowmem_reserve():计算lowmem_reserve。
lowmem_reserve是Linux内核中保留的一部分物理内存,这部分内存不被系统常规使用,而是作为备用资源,在系统内存压力极大时分配给关键任务。
内核在分配内存页时会检查各个内存区域(zone)中的可用内存量。如果某个zone的可用内存<预设的水线(watermark)+该zone的lowmem_reserve值,内核则认为其内存不足,会跳过这个zone。
lowmem_reserve的配置可以通过内核参数或系统启动后的proc接口文件进行调整,例如,可以通过内核参数/proc/sys/vm/lowmem_reserve_ratio调整各个zone的lowmem_reserve值。
lowmem_reserve_ratio是一个数组,每个元素对应一个内存区域(zone)的预留内存比例。内核会根据这个比例计算出每个zone需要预留的内存量,并在内存分配时考虑这些预留内存。这样,即使在高端内存紧张的情况下,低端内存也能得到一定的保护,不会被过度使用。
内核遍历系统的所有结点,对每个结点的各个内存域分别计算预留内存最小值,具体是将内存域中页帧的总数除以sysctl_lowmem_reserve_ratio[zone]。除数的默认设置对低端内存域是256,对高端内存域是32。
2.3.2.5 冷热页管理pageset[NR_CPUS](cpu高速缓存)
struct zone的pageset[NR_CPUS]成员用于实现冷热分配器(hot-n-cold allocator),NR_CPUS是内核能够支持CPU数量的最大值。
热页:页已经加载到CPU高速缓存,与在内存中的页相比,其数据能够更快地访问。
冷页:页不在CPU高速缓存中。
在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的。
NR_CPUS是可以在编译时间配置的宏常数。在单处理器系统上是1,针对SMP系统编译的内核中,在2和32(在64位系统上是64)之间,该值并不是系统中实际存在的CPU数目,而是内核支持的CPU的最大数目。
pageset[NR_CPUS]数组元素的类型为per_cpu_pageset。
<mmzone.h>
struct per_cpu_pageset {
struct per_cpu_pages pcp[2]; //索引0为热页,索引1为冷页
} ____cacheline_aligned_in_smp;
更新的版本已将管理冷页和热页的两个列表合并为一个列表。热页放置在列表头部,而冷页置于列表尾部。
<mmzone.h>
struct per_cpu_pages {
int count; //列表中的页数
int high; //页数上限水印,在需要的情况下清空列表
int batch; //添加/删除多页块的时候,块的大小(页数)
struct list_head list; //页的链表
};
- count:与该列表相关的页的数目。
- high:水印。如果count的值超出了high,则表明列表中的页太多了。对容量过低的状态没有显式使用水印。如果列表中没有成员,则重新填充列表。
- batch:CPU的高速缓存不是用单个页为单位来填充的,而是用多个页组成的块为单位。batch是每次添加页数的一个参考值,也就是每块的页数。
- list:是一个双链表,保存了当前CPU的冷页或热页。
下图展示了在双处理器系统上per-CPU缓存的数据结构是怎么填充的。
high的计算以及per_cpu_pageset的初始化后续讨论。
2.3.2.6 页帧page
页帧是系统内存的最小单位,对内存中的每个页都会创建struct page的一个实例,所以struct page结构要尽可能小,会有大量的页存在。例如,IA-32系统的标准页长度为4KB,当主内存大小为384MB时就会有100000页。
所以对page结构的小改动,也可能导致保存所有page实例所需的物理内存大幅增加。
2.3.2.6.1 page中特殊的union定义
内存管理的许多部分都使用页,有着广泛用途,所以内核的一个部分可能完全依赖于struct page提供的特定信息,而该信息对内核的另一部分可能完全无用。所以保持page的大小很难。
C语言的联合union类型很适合这种问题。
例如,一个物理内存页能够通过多个地方的不同页表映射到虚拟地址空间,内核想要跟踪有多少地方映射了该页。所以,page中有一个计数器用于计算映射的数目。如果某页用于slub分配器(slub将整页细分为更小部分),那么可以确保只有内核会使用该页,而其他地方不使用,所以映射计数信息就是多余的。这样内核可以重新解释该字段,用来表示该页被细分为多少个小的内存对象使用。
<mm_types.h>
struct page {
...
union { //双重解释
atomic_t _mapcount; //内存管理子系统中映射的页表项计数,用于表示页是否已经映射,还用于限制逆向映射搜索
unsigned int inuse; //用于slub分配器,为对象的数目
};
...
}
atomic_t允许以原子方式修改其值,是32bit,不受并发访问的影响。
unsigned int是普通整数。在Linux支持的每种体系结构上普通整数也是32bit。
假如将整个union换为atomic_t counter就不合理,因为slub代码在访问对象计数器时不需要原子性,应该反映在数据类型中。这样的定义会影响两个子系统中代码的可读性。_mapcount和inuse描述对应成员就很清晰,counter的含义则过于广泛。
2.3.2.6.2 struct page
与体系结构无关。
<mm.h>
struct page {
unsigned long flags; //原子标志,有些情况下会异步更新
atomic_t _count; //使用计数
union {
atomic_t _mapcount; //内存管理子系统中映射的页表项计数,用于表示页是否已经映射,还用于限制逆向映射搜索
unsigned int inuse; //用于slub分配器,对象的数目
};
union {
struct {
unsigned long private; /* 由映射私有,不透明数据:
* 如果设置了PagePrivate,通常用于buffer_heads;
* 如果设置了PageSwapCache,则用于swp_entry_t;
* 如果设置了PG_buddy,则用于表示伙伴系统中的阶。
*/
struct address_space *mapping; /* 如果最低位为0,则指向inode
* address_space,或为NULL。
* 如果页映射为匿名内存,最低位置位0,
* 而且该指针指向anon_vma对象:
* 参见下文的PAGE_MAPPING_ANON。
*/
};
...
struct kmem_cache *slab; //用于SLUB分配器,指向slab的指针
struct page *first_page; //用于复合页的尾页,指向首页
};
union {
pgoff_t index; //在映射内的偏移量
void *freelist; //SLUB: freelist req. slab lock
};
struct list_head lru; //换出页列表,例如由zone->lru_lock保护的active_list
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; //内核虚拟地址(如果没有映射则为NULL,即高端内存)
#endif /* WANT_PAGE_VIRTUAL */
};
-
flags:存储了体系结构无关的标志,用于描述页的属性。
-
_count:使用的计数,表示内核中引用该页的次数。==0时,说明page实例当前不使用,可以删除。如果>0,该实例则不会从内存删除。
-
_mapcount:在页表中有多少项指向该页。
-
inuse:用于slub分配器,对象的数目。
-
private:指向私有数据的指针,这样虚拟内存管理会忽略此数据。根据页的用途,用不同的方式使用该指针。多数情况下它用于将页与数据缓冲区关联起来,后续讨论。
-
mapping:指定了页帧所在的地址空间。pgoff_t类型的index是页帧在映射的地址空间内部的偏移量。由内存区的地址空间映射到物理内存(文件的内容所在)。
mapping不仅能够保存一个address_space指针,还能包含一些额外的信息,用于判断页是否属于未关联到地址空间的某个匿名内存区。
如果将mapping置为1,则说明该指针并不指向address_space的实例,而是指向anon_vma结构体,该结构对实现匿名页的逆向映射很重要(进程虚拟内存的用户空间缺页异常的校正)。对该指针也可以双重使用,因为address_space实例总是对齐到sizeof(long)。所以在Linux支持的所有计算机上,指向该实例的指针最低位总是0。
总结下来:
- mapping为0,表示该页面属于交换缓存(swapcache)。
- mapping不为0,但最低位为0,表示该页为匿名页,此时mapping指向一个anon_vma结构体。
- mapping不为0,且最低位不为0,表示该页与文件映射相关,此时mapping指向一个address_space结构体。
- slab:指向slab的指针。
- first_page:用于复合页的尾页,指向首页。
- freelist:
- lru:是一个表头,用于在各种链表上维护该页,以便将页按不同类别分组,最重要的类别是活动和不活动页(页面回收和页交换)。
- virtual:用于高端内存区域中的页,即无法直接映射到内核内存中的页。virtual用于存储该页的虚拟地址。按照预处理器语句#if defined(WANT_PAGE_VIRTUAL),只有定义了对应的宏,virtual才能成为struct page的一部分,仅部分体系结构是这样。其他体系结构都用别的方式来寻址虚拟内存页,主要是用来查找所有高端内存页帧的散列表(内核映射)。
2.3.2.6.3 page的flags及其宏(体系结构无关)
页的不同属性通过一系列页标志描述,是struct page的flags成员中的各个比特位。在page-flags.h中的宏定义,还有一些宏,用于标志的设置、删除、查询。有一种命名方案:
例如,PG_locked常数定义了标志中用于指定页是否锁定的比特位。对应的宏可以用来操作该比特位:
-
PageLocked:查询比特位是否置位;
-
SetPageLocked:设置PG_locked位,不考虑先前的状态;
-
TestSetPageLocked:设置比特位,而且返回原值;
-
ClearPageLocked:清除比特位,不考虑先前的状态;
-
TestClearPageLocked:清除比特位,返回原值。
其他的页标志,同样有这样的一组宏用来操作对应的比特位。这些宏是原子的,即这些语句无法中断,否则会导致竞态条件(锁)。
列出一些重要的标志:
-
PG_locked:指定页是否锁定。如果置位,则内核的其他部分不允许访问该页。防止了内存管理出现竞态条件,例如,从硬盘读取数据到页帧时。
-
PG_error:涉及该页的I/O操作期间是否发生错误。
-
PG_referenced、PG_active:表明系统使用该页的活跃程度。在页交换子系统选择换出页时有作用(页回收和页交换)。
-
PG_uptodate:表示页的数据已经从块设备读取,并且没有出错。
-
PG_dirty(脏页):与硬盘上的数据相比,页的内容是否已经改变(内存中的数据与外存储器介质如硬盘上的
数据)。出于性能考虑,页并不在每次改变后立即回写。所以内核使用该标志注明页已经改变,可以在稍后刷新。
-
PG_lru:有助于实现页面回收和切换。内核使用2个最近最少使用(least recently used,lru)链表来区别活动和不活动页(频繁使用的页排到链表最靠前的位置,而不活动的页向链表末尾方向移动)。如果页在其中一个链表中,则设置该位。此外,如果页在活动页链表中,也设置PG_active。(页回收和页交换)
-
PG_highmem:表示页在高端内存中,无法持久映射到内核内存中。
-
PG_private:如果page结构的private成员非空,则必须设置该位。用于I/O的页,可使用该字段将页细分为多个缓冲区(页缓存和块缓存)。
-
PG_writeback:如果页的内容处于向块设备回写的过程中,则设置该位。
-
PG_slab:如果页是slab分配器的一部分,则设置该位。
-
PG_swapcache:如果页处于交换缓存,则设置该位。在这种情况下,private包含一个类型为swap_entry_t的项。(页面回收和页交换)
-
PG_reclaim:在可用内存的数量变少时,内核尝试周期性地回收页,即剔除不活动、未用的页。(页面回收和页交换)在内核决定回收某个特定的页之后,则设置该位。
-
PG_buddy:如果页空闲且包含在伙伴系统的列表中,则设置该位,伙伴系统是页分配机制的核心。
-
PG_compound:表示该页属于一个更大的复合页,复合页由多个相连的普通页组成。
很多情况下,需要等待页的状态改变,才能恢复工作。内核有两个辅助函数来等待页:内核等待期间进入睡眠,解锁之后唤醒。
<pagemap.h>
void wait_on_page_locked(struct page *page); //等待锁定页,直至解锁。
void wait_on_page_writeback(struct page *page); //等待到与页面相关的所有待决回写操作结束,将页面包含的数据同步到块设备(例如,硬盘)为止
标签:zone,min,page,UMA,内存,Linux,pages,内核
From: https://blog.csdn.net/m0_45372381/article/details/144597438