伙伴分配器的内核实现

主要参考了《深入linux内核》和《Linux内核深度解析》,另外简单浅析了一下相关内容

伙伴分配器

当系统内核初始化完毕后,使用页分配器管理物理页,使用的页分配器是伙伴分配器,伙伴分配器的特点是算法简单且高效。

基本的伙伴分配器

连续的物理页称为页块(page block)。阶(order)是伙伴分配器的一个专业术语,是页的数量单位,2 ^ n个连续页称为 n 阶页块。 满足以下条件的两个n阶页块称为伙伴(buddy):

1、两个页块是相邻的,即物理地址是连续的;

2、页块的第一页的物理页号必须是2n的整数倍;

3、如果合并成(n+1)阶页块,第一页的物理页号必须是2n+1的整数倍。

伙伴分配器分配和释放物理页的数量单位为阶。分配n阶页块的过程如下:

1、查看是否有空闲的n阶页块,如果有直接分配;否则,继续执行下一步;

2、查看是否存在空闲的 n+1 阶页块,如果有,把 n+1 阶页块分裂为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;否则继续执行下一步。

3、查看是否存在空闲的 n+2 阶页块,如果有把 n+2 阶页块分裂为两个(n+2)阶页块,一个插入空闲 n+1 阶页块链表,另一个分裂为两个n阶页块,一个插入空间n阶页块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。

分区的伙伴分配器

内核在基本的伙伴分配器基础改进扩展

  • 支持内存节点和区域,称为分区的伙伴分配器(zond buddy allocator) ;

  • 为了预防内存碎片,把物理页根据可移动性分组;(分成不同的链表管理)

  • 针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域增加1个每处理器页集合。

管理分区内存的链表

分区的伙伴分配器专注于某个内存节点的某个区域。内存区域的结构体成员free_area用来维护空闲页块,数组下标对应页块的阶数。 系统内存中的每个物理内存页(页帧),都对应于一个struct page实例,,每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数数组。

struct zone {
	/* Read-mostly fields */
	
	...
	/* free areas of different sizes */
	struct free_area	free_area[MAX_ORDER]; // 不同长度的空闲区域
	...
};

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES]; // MIGRATE_TYPES
	unsigned long		nr_free;
};

image-20220804162645113

MAX_ORDER是最大阶数,实际上是可分配的最大阶数减1,默认值是11,意味着伙伴分配器一次最多可以分配2^10页。

#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif

根据分配标志获取首选区域类型

申请页时,最低的4个标志位用来指定首选的内存区域类型,内核源码如下:

include\linux\gfp.h

标志组合

#define ___GFP_DMA		0x01u
#define ___GFP_HIGHMEM		0x02u
#define ___GFP_DMA32		0x04u
#define ___GFP_MOVABLE		0x08u

内存区域类型

为什么要使用OPT_ZONE_DMA,而不使用ZONE_DMA?

因为DMA区域是可选的,如果不存在只能访问16MB以下物理内存的外围设备,那么不需要定义DMA区域,OPT_ZONE_DMA就是ZONE_NORMAL,
从普通区域申请页。高端内存区域和DMA32区域也是可选的。

#ifdef CONFIG_HIGHMEM
#define OPT_ZONE_HIGHMEM ZONE_HIGHMEM
#else
#define OPT_ZONE_HIGHMEM ZONE_NORMAL
#endif

#ifdef CONFIG_ZONE_DMA
#define OPT_ZONE_DMA ZONE_DMA
#else
#define OPT_ZONE_DMA ZONE_NORMAL
#endif

#ifdef CONFIG_ZONE_DMA32
#define OPT_ZONE_DMA32 ZONE_DMA32
#else
#define OPT_ZONE_DMA32 ZONE_NORMAL
#endif

内核使用宏GFP_ZONE_TABLE定义了标志组合到区域类型的映射表,其中GFP_ZONES_SHIFT是区域类型占用的位数,GFP_ZONE_TABLE把每种标志组合映射到32位整数的某个位置,偏移是(标志组合*区域类型位数),从这个偏移开始的GFP_ZONES_SHIFT个二进制位存放区域类型。

宏GFP_ZONE_TABLE是一个常量,编译器在编译时会进行优化,直接计算出结果,不会等到运行程序的时候才计算数值。

#define GFP_ZONE_TABLE ( \
	(ZONE_NORMAL << 0 * GFP_ZONES_SHIFT)				       \
	| (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT)		       \
	| (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT)	       \
	| (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT)		       \
	| (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT)		       \
	| (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT)    \
	| (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\
	| (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT)\
)

内核使用函数gfp_zone()根据分配标志得到首选的区域类型

  • 先分离出区域标志位
  • 然后算出在映射表中的偏移(区域标志位 * 区域类型位数)
  • 接着把映射表右移偏移值,最后取出最低的区域类型位数(作为zone_type所对应的类型)。
static inline enum zone_type gfp_zone(gfp_t flags)
{
	enum zone_type z;
	int bit = (__force int) (flags & GFP_ZONEMASK);

	z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
					 ((1 << GFP_ZONES_SHIFT) - 1);
	VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
	return z;
}

备用区域列表(从其他节点或区域借用物理页)

如果首选的内存节点或区域不能满足分配请求,可以从备用的内存区域借用物理页。

借用必须遵守相应的规则:

  • 一个内存节点的某个区域类型可以从另一个内存节点的相同区域类型借用物理页,比如节点0的普通区域可以从节点1的普通区域借用物理页;
  • 高区域类型**(高地址的区域类型)可以从低区域类型(低地址的区域类型)**借用物理页,比如普通区域可以从DMA区域借用物理页;
  • 低区域类型不能从高区域类型借用物理页,比如DMA区域不能从普通区域借用物理页。

内存节点的pg_data_t实例已定义备用区域列表,内核源码如下:

include\linux\mmzone.h

typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES]; // 内存区域数组
	struct zonelist node_zonelists[MAX_ZONELISTS]; // 备用区域列表
	int nr_zones; // 该节点包含的内存区域数量
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	struct page *node_mem_map; // 页描述符数组
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *node_page_ext; // 页的扩展属性
#endif
#endif
	...
	unsigned long node_start_pfn; // 该节点的起始物理页号
	unsigned long node_present_pages; /// 物理页的总数
	unsigned long node_spanned_pages; // 物理页的总长度,包括空洞
    
	int node_id; // 节点标识符
	...
} pg_data_t;

struct zonelist node_zonelists[MAX_ZONELISTS]; 中的成员项

enum {
	ZONELIST_FALLBACK,	// 包含所有内存点的备用区域列表
#ifdef CONFIG_NUMA
	/*
	 * The NUMA zonelists are doubled because we need zonelists that
	 * restrict the allocations to a single node for __GFP_THISNODE.
	 */
	ZONELIST_NOFALLBACK,	// 只包含当前内存节点的备用区域列表
#endif
	MAX_ZONELISTS
};
struct zoneref {
	struct zone *zone;	// 内存区的数据结构
	int zone_idx;		// 成员zone指向的内存区域的类型
};

UMA系统只有一个备用区域列表,按区域类型从高到低排序。假设UMA系统包含普通区域和DMA区域,那么备用区域列表:{普通区域,DMA区域}。

**NUMA系统每个内存节点有两个备用区域列表:一个包含所有内存节点的区域,另一个只包含当前内存节点的区域。**如果申请页时指定标志
__GFP_THISNODE,要求只能从指定内存节点分配物理页,就需要使用指定内存节点的第二个备用区域列表。

包含所有内存节点的备用区域列表有两种排序方法:
a.节点优先顺序

  • 先根据节点距离从小到大排序,然后在每个节点里面根据区域类型从高到低排序。
  • 优点是优先选择距离近的内存,缺点是在高区域耗尽以前使用低区域。

b.区域优先顺序

  • 先根据区域类型从高到低排序,然后在每个区域类型里面根据节点距离从小到大排序。
  • 优点是减少低区域耗尽的概率,缺点是不能保证优先选择距离近的内存。

默认的排序方法就是自动选择最优的排序方法:比如是64位系统,因为需要DMA和DMA32区域的备用相对少,所以选择节点优先顺序;如果是32位系统,选择区域优先顺序。

区域水线

首选的内存区域什么情况下从备用区域借用物理页呢?

每个内存区域有3个水线

img

a.高水线(high):如果内存区域的空闲页数大于高水线,说明内存区域的内存充足;

b.低水线(low):如果内存区域的空闲页数小于低水线,说明内存区域的内存轻微不足;

c.最低水线(min):如果内存区域的空闲页数小于最低水线,说明内存区域的内存严重不足。(需要开启回收内存的工作)

struct zone {
	/* Read-mostly fields */

	/* zone watermarks, access with *_wmark_pages(zone) macros */
	unsigned long watermark[NR_WMARK]; // 页分配器使用的水线
  • 最低水线以下的内存称为紧急保留内存,在内存严重不足的紧急情况下,给承诺“分给我们少量的紧急保留内存使用,我可以释放更多的内存”的进程使用。
    • 设置了进程标志位PF_MEMALLOC的进程可以使用紧急保留内存,标志位PF_MEMALLOC表示承诺“给我少量紧急保留内存使用,我可以释放更多的内存”。内存管理子系统以外的子系统不应该使用这个标志位,典型的例子是页回收内核线程kswapd,在回收页的过程中可能需要申请内存。
    • 如果申请页时设置了标志位__GFP_MEMALLOC,即调用者承诺“给我少量紧急保留内存使用,我可以释放更多的内存”,那么可以使用紧急保留内存。
相关数据结构

watermark水位控制内核源码重要数据参数

enum zone_watermarks {
	WMARK_MIN,
	WMARK_LOW,
	WMARK_HIGH,
	NR_WMARK
};

#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])
struct zone {
	/* Read-mostly fields */

	/* zone watermarks, access with *_wmark_pages(zone) macros */
	unsigned long watermark[NR_WMARK]; // 页分配器使用的水线
    
    ...
	unsigned long		managed_pages; // 伙伴分配器管理的物理页的数量
	unsigned long		spanned_pages; // 当前区域跨越的总页数,包括空洞
	unsigned long		present_pages; // 当前区域存在的物理页的数量,不包括空洞
  • spanned_pages: 代表的是这个zone中所有的页,包含空洞,计算公式是: zone_end_pfn - zone_start_pfn
  • present_pages: 代表的是这个zone中可用的所有物理页,计算公式是:spanned_pages-hole_pages
  • managed_pages: 代表的是通过buddy管理的所有可用的页,计算公式是:present_pages - reserved_pages
  • 三者的关系是: spanned_pages > present_pages > managed_pages

它们三者之间的关系: spanned_pages > present_pages > managed_pages。

计算水位线

内核在初始化阶段会调用 init_per_zone_wmark_min 来进行每个zone 的内存水位线初始化,同时也会设置zone的lowmem_reserve 值

计算水线时,有两个重要的参数。

min_free_kbytes代表的是系统保留空闲内存的最低限,watermark[WMARK_MIN]的值是通过min_free_kbytes计算出来。

(1)min_free_kbytes是最小空闲字节数。默认值 = 4 * sqrt(lowmem_kbytes),并且限制在范围[128,65536]以内。
其中lowmem_kbytes是超过high的水位的页和,单位是KB。参考文件“mm/page_alloc.c”中的函数init_per_zone_wmark_min。可以通过文件“/proc/sys/vm/min_free_kbytes”设置最小空闲字节数。

  • int __meminit init_per_zone_wmark_min(void)
    ...
        // lowmem中超过高水位的页的总和,单位kbytes,就是lowmem中超过high的水位的页乘以4得到lowmem_kybytes
    	lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
    	new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
    
        // min_free_kybtes最小不能小于128k,最大超过65535k
    	if (new_min_free_kbytes > user_min_free_kbytes) { // int user_min_free_kbytes = -1;
    		min_free_kbytes = new_min_free_kbytes;
    		if (min_free_kbytes < 128)
    			min_free_kbytes = 128;
    		if (min_free_kbytes > 65536)
    			min_free_kbytes = 65536;
    ...
    

(2)“watermark_scale_factor"这个系数,其默认值为10,对应内存占比0.1%(10/10000),可通过”/proc/ sys/vm/watermark_scale_factor"设置,最大为1000。当它的值被设定为1000时,意味着"low"与"min"之间的差值,以及"high"与"low"之间的差值都将是内存大小的10%(1000/10000)。

文件“mm/page_alloc.c”中的函数__setup_per_zone_wmarks()负责计算每个内存区域的最低水线、低水线和高水线。

计算最低水线的方法如下:

高端内存区域(ZONE_HIGHMEM):这是32位时代的产物,内核和用户地址空间按1 : 3划分,内核地址空间只有1GB,不能把1GB以上的内存直接映射到内核地址空间,把不能直接映射的内存划分到高端内存区域(采用间接映射)。通常把DMA区域、DMA32区域和普通区域统称为低端内存区域。64位系统的内核虚拟地址空间非常大,不再需要高端内存区域(内核虚拟地址空间够用)。

Linux的内核空间(低端内存、高端内存)

  • (1)min_free_pages = min_free_kbytes对应的页数==(动态变化)==。

    • min_free_kbytes = 4 * sqrt(lowmem_kbytes),lowmem_kbytes中和 managed 与 当前high水位的差值,详解nr_free_zone_pages函数(mm\page_alloc.c)
    • 高水线比较大的时候,最低水线比较小,因为kswap线程在很早就开启了
    • 高水线比较小的时候,最低水线比较大,因为kswap线程开启比较晚,需要保留足够内存给它用
  • (2)lowmem_pages = 所有低端内存区域中伙伴分配器管理的页数总和。

  • (3)高端内存区域的最低水线 = zone->managed_pages/1024,并且限制在范围[32, 128]以内(zone->managed_pages是该内存区域中伙伴分配器管理的页数,在内核初始化的过程中引导内存分配器分配出去的物理页,不受伙伴分配器管理)。

  • (4)低端内存区域的最低水线 = min_free_pages * zone->managed_pages / lowmem_pages,即把min_free_pages按比例分配到每个低端内存区域。

计算低水线和高水线的方法如下:

  • (1)增量 = (最低水线 / 4, zone->managed_pages * watermark_scale_factor / 10000)取最大值。

  • (2)低水线 = 最低水线 + 增量。

  • (3)高水线 = 最低水线 + 增量 * 2。

如果(最低水线 / 4)比较大,那么计算公式简化如下:

(1)低水线 = 最低水线 * 5/4。

(2)高水线 = 最低水线 * 3/2。

如下图

image-20220529011240322
水位的变化

下面这张时序图能很好地表示水位的变化:

image-20220529011312206

剩余内存高于pages_high,说明剩余内存比较多,没有内存压力;
剩余内存小于pages_high,说明内存有一定压力,但还可以满足新内存请求;
剩余内存小于pages_low,说明内存压力比较大,剩余内存不多了。这时,kswapd0 会被唤醒,执行内存回收,直至剩余内存大于pages_high;
剩余内存小于pages_min,说明进程可用的内存都耗尽,仅内核才可以分配内存;

如果内存消耗导致剩余内存达到或超过了pages_min时,就会触发直接回收(direct page reclaim);

防止过度借用

和高区域类型相比,低区域类型的内存相对少,是稀缺资源,而且有特殊用途,例如DMA区域用于外围设备和内存之间的数据传输。为了防止高区域类型过度借用低区域类型的物理页,低区域类型需要采取防卫措施,保留一定数量的物理页。

一个内存节点的某个区域类型从另一个内存节点的相同区域类型借用物理页,后者应该毫无保留地借用。

内存区域有一个数组用于存放保留页数:

include/linux/mmzone.h
struct zone {long lowmem_reserve[MAX_NR_ZONES];} ____cacheline_internodealigned_in_smp;

zone[i]->lowmem_reserve[j]表示区域类型i应该保留多少页不能借给区域类型j,仅当j大于i时有意义。
zone[i]->lowmem_reserve[j]的计算规则如下:

(i < j):
 zone[i]->lowmem_reserve[j]
 = (当前内存节点上从zone[i + 1] 到zone[j]伙伴分配器管理的页数总和)
    / sysctl_lowmem_reserve_ratio[i]
(i = j):
   zone[i]->lowmem_reserve[j]= 0(相同的区域类型不应该保留)
(i > j):
   zone[i]->lowmem_reserve[j]= 0(没意义,不会出现低区域类型从高区域类型借用物理页的情况

数组sysctl_lowmem_reserve_ratio存放各种区域类型的保留比例,因为内核不允许使用浮点数,所以使用倒数值。DMA区域和DMA32区域的默认保留比例都是256,普通区域和高端内存区域的默认保留比例都是32。

mm/page_alloc.c
int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES-1] = {
#ifdef CONFIG_ZONE_DMA
       256,
#endif
#ifdef CONFIG_ZONE_DMA32
       256,
#endif
#ifdef CONFIG_HIGHMEM
       32,
#endif
       32,
};

可以通过文件“/proc/sys/vm/lowmem_reserve_ratio”修改各种区域类型的保留比例。

/proc/zoneinfo

Linux系统通常将主内存划分为三个区域。大多数内存分配到ZONE_NORMAL区域 。 在低端,有16MB的内存被分区到DMA区域ZONE_DMA中,该内存被保留用于特定需要的情况。DMA内存最常见的用户是较旧的外设,它只能寻址24位内存。在高端,ZONE_HIGHMEM包含内核无法直接寻址的所有内存。

并非所有系统都实现所有这些区域。一些较新的体系结构不支持古老的外围设备,而忽略了区域 ZONE_DMA。
一般来说,64位系统没有寻址问题,也不需要ZONE_HIGHMEM。
IA64体系结构决定了ZONE_DMA的另一种实现方式,将其定义为覆盖4GB以下的所有内存。

事实证明,4GB区域有很多用途。相当多的设备在访问不能用32位寻址的内存时遇到问题。这些设备的驱动程序已经被强制使用ZONE_DMA、I/O存储器管理单元(在有ZONE_DMA的系统上)或缓冲区。这些解决方案都不是理想的:ZONE_DMA是一个小而稀缺的资源,IOMMU空间也可能稀缺,反弹缓冲区也很慢。如果在4GB边界下可靠地分配DMA内存,所有这些问题都可以避免。

在64位Linux操作系统上,分区如下:
最开始的16M内存是DMA ZONE 内存,DMA32 ZONE为16M~4G,高于4G的内存为Normal ZONE。

root:/ # cat /proc/zoneinfo
pages free     87512
        min      2702
        low      10899
        high     11574
        spanned  2094976     ==>  spanned_pages
        present  1959807     ==>  present_pages
        managed  1911161     ==>  managed_pages
        protection: (0, 0)
...
Node 0, zone    DMA32
...
Node 0, zone   Normal
...
Node 0, zone  Movable
...
Node 0, zone   Device
...

内存水位watermark

根据可移动性分组

在系统长时间运行后,物理内存可能出现很多碎片,可用物理页很多,但是最大的连续物理内存可能只有一页。

  • 内存碎片对用户程序不是问题,因为用户程序可以通过页表把连续的虚拟页映射到不连续的物理页。但是内存碎片对内核是一个问题,因为内核使用直接映射的虚拟地址空间,连续的虚拟页必须映射到连续的物理页。内存碎片是伙伴分配器的一个弱点。

为了预防内存碎片,内核根据可移动性把物理页分为3种类型。

  • (1)不可移动页:位置必须固定,不能移动,直接映射到内核虚拟地址空间的页属于这一类。
  • (2)可移动页:使用页表映射的页属于这一类,可以移动到其他位置,然后修改页表映射。
  • (3)可回收页:不能移动,但可以回收,需要数据的时候可以重新从数据源获取。后备存储设备支持的页属于这一类。

内核把具有相同可移动性的页分组。为什么这种方法可以减少碎片?试想:如果不可移动页出现在可移动内存区域的中间,会阻止可移动内存区域合并。这种方法把不可移动页聚集在一起,可以防止不可移动页出现在可移动内存区域的中间。

内核定义了以下迁移类型:

include/linux/mmzone.h

enum migratetype {
    MIGRATE_UNMOVABLE,         /* 不可移动 */
    MIGRATE_MOVABLE,           /* 可移动 */
    MIGRATE_RECLAIMABLE,       /* 可回收 */
    MIGRATE_PCPTYPES,          /* 定义内存区域的每处理器页集合中链表的数量 */
    MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
                               /* 高阶原子分配,即阶数大于0,并且分配页时不能睡眠等待 */
#ifdef CONFIG_CMA
    MIGRATE_CMA,               /* 连续内存分配器 */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,           /* 隔离,不能从这里分配 */
#endif
    MIGRATE_TYPES
};

前面3种是真正的迁移类型,后面的迁移类型都有特殊用途:

  • MIGRATE_HIGHATOMIC用于高阶原子分配(参考3.7.5节的“对高阶原子分配的优化处理”),
  • MIGRATE_CMA用于连续内存分配器(参考3.20节),
  • MIGRATE_ISOLATE用来隔离物理页(由连续内存分配器、内存热插拔和从内存硬件错误恢复等功能使用)。

对伙伴分配器的数据结构的主要调整是把空闲链表拆分成每种迁移类型一条空闲链表。

struct free_area {
     struct list_head  free_list[MIGRATE_TYPES];
     unsigned long     nr_free;
};

只有当物理内存足够大且每种迁移类型有足够多的物理页时,根据可移动性分组才有意义。

  • 全局变量page_group_by_mobility_disabled表示是否禁用根据可移动性分组。
  • vm_total_pages是所有内存区域里面高水线以上的物理页总数,
  • pageblock_order是按可移动性分组的阶数,pageblock_nr_pages是pageblock_order对应的页数。

如果所有内存区域里面高水线以上的物理页总数小于(pageblock_nr_pages * 迁移类型数量),那么禁用根据可移动性分组。

mm/page_alloc.c
void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone)
{if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
         page_group_by_mobility_disabled = 1;
    else
         page_group_by_mobility_disabled = 0;

pageblock_order是按可移动性分组的阶数,简称分组阶数,可以理解为一种迁移类型的一个页块的最小长度。如果内核支持巨型页,那么pageblock_order是巨型页的阶数,否则pageblock_order是伙伴分配器的最大分配阶。

申请页时,可以使用标志__GFP_MOVABLE指定申请可移动页,使用标志__GFP_RECLAIMABLE指定申请可回收页,如果没有指定这两个标志,表示
申请不可移动页。函数gfpflags_to_migratetype用来把分配标志转换成迁移类型:

include/linux/gfp.h

/* 把分配标志转换成迁移类型 */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3

static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{if (unlikely(page_group_by_mobility_disabled))
           return MIGRATE_UNMOVABLE;
     /* 根据可移动性分组 */
     return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}

如果禁用根据可移动性分组,那么总是申请不可移动页。

申请某种迁移类型的页时,如果这种迁移类型的页用完了,可以从其他迁移类型盗用(steal)物理页。内核定义了每种迁移类型的备用类型优先级列表:

static int fallbacks[MIGRATE_TYPES][4] = {
     [MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_TYPES },
     [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_TYPES },
     [MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
     [MIGRATE_CMA]         = { MIGRATE_TYPES }, /* 从不使用 */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
     [MIGRATE_ISOLATE]     = { MIGRATE_TYPES }, /* 从不使用 */
#endif
};

不可移动类型的备用类型按优先级从高到低是:可回收类型和可移动类型。
可回收类型的备用类型按优先级从高到低是:不可移动类型和可移动类型。
可移动类型的备用类型按优先级从高到低是:可回收类型和不可移动类型。
如果需要从备用类型盗用物理页,那么从最大的页块开始盗用,以避免产生碎片。

释放物理页的时候,需要把物理页插入物理页所属迁移类型的空闲链表,内核怎么知道物理页的迁移类型?
内存区域的zone结构体的成员pageblock_flags指向页块标志位图,页块的大小是分组阶数pageblock_order (MAX_ORDER-1),我们把这种页块称为分组页块。

struct zone {#ifndef CONFIG_SPARSEMEM
     /*
      * 分组页块的标志参考文件pageblock-flags.h。
      * 如果使用稀疏内存模型,这个位图在结构体mem_section中。
      */
     unsigned long   *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */} ____cacheline_internodealigned_in_smp;

每个分组页块在位图中占用4位,其中3位用来存放页块的迁移类型。

include/linux/pageblock-flags.h

/* 影响一个页块的位索引 */
enum pageblock_bits {
   PB_migrate, // 0 ~ 2
   PB_migrate_end = PB_migrate + 3 - 1,   /* 迁移类型需要3位 */
   PB_migrate_skip,/* 如果被设置,内存碎片整理跳过这个页块。*/
   NR_PAGEBLOCK_BITS
};

函数 set_pageblock_migratetype()用来在页块标志位图中设置页块的迁移类型,函数get_pageblock_migratetype()用来获取页块的迁移类型。
内核在初始化时,把所有页块初始化为可移动类型,其他迁移类型的页是盗用(被拿走)产生的。

mm/page_alloc.c
free_area_init_core() -> free_area_init_core() -> memmap_init() -> memmap_init_zone()

void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
        unsigned long start_pfn, enum memmap_context context)
{for (pfn = start_pfn; pfn < end_pfn; pfn++) {if (!(pfn & (pageblock_nr_pages - 1))) {   /* 如果是分组页块的第一页 */
               struct page *page = pfn_to_page(pfn);
               __init_single_page(page, pfn, zone, nid);
               set_pageblock_migratetype(page, MIGRATE_MOVABLE); // 设置迁移类型
          } else {
               __init_single_pfn(pfn, zone, nid);
          }
     }
}

可以通过文件“/proc/pagetypeinfo”查看各种迁移类型的页的分布情况。

每处理器页集合(分配单页优化)

内核针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域增加 1个每处理器页集合

include/linux/mmzone.h
struct zone {struct per_cpu_pageset __percpu *pageset;  /* 在每个处理器上有一个页集合 */} ____cacheline_internodealigned_in_smp;
struct per_cpu_pageset {
     struct per_cpu_pages pcp;};
struct per_cpu_pages {
     int count;      /* 链表里面页的数量 */
     int high;       /* 如果页的数量达到高水线,需要返还给伙伴分配器 */
     int batch;      /* 批量添加或删除的页数量 */
     struct list_head lists[MIGRATE_PCPTYPES]; /* 每种迁移类型一个页链表 */
};

内存区域在每个处理器上有一个页集合,页集合中每种迁移类型有一个页链表。

页集合有高水线和批量值,页集合中的页数量不能超过高水线。申请单页加入页链表,或者从页链表返还给伙伴分配器,都是采用批量操作,一次
操作的页数量是批量值。

默认的批量值batch的计算方法如下。
(1)batch = zone->managed_pages / 1024,其中zone->managed_pages是内存区域中由伙伴分配器管理的页数量。
(2)如果batch超过(512 * 1024) / PAGE_SIZE,那么把batch设置为(512 * 1024) / PAGE_SIZE,其中PAGE_SIZE是页长度。
(3)batch = batch / 4。
(4)如果batch小于1,那么把batch设置为1。
(5)batch = rounddown_pow_of_two(batch * 1.5) − 1,其中rounddown_pow_of_two()用来把数值向下对齐到2的n次幂。

默认的高水线是批量值的6倍。

  • 可以通过文件“/proc/sys/vm/percpu_pagelist_fraction”修改比例值,最小值是8,默认值是0。高水线等于(伙伴分配器管理的页数量 / 比例值),同时把批量值设置为高水线的1/4。

从某个内存区域申请某种迁移类型的单页时,从当前处理器的页集合中该迁移类型的页链表分配页,如果页链表是空的,先批量申请页加入页链表,然后分配一页。

缓存热页是指刚刚访问过物理页,物理页的数据还在处理器的缓存中。如果要申请缓存热页,从页链表首部分配页;如果要申请缓存冷页,从页链表尾部分配页。

释放单页时,把页加入当前处理器的页集合中。如果释放缓存热页,加入页链表首部;如果释放缓存冷页,加入页链表尾部。如果页集合中的页数量大于或等于高水线,那么批量返还给伙伴分配器。

分配标志位

分配页的函数都带一个分配标志位参数,分配标志位分为以下5类(标志位名称中的GFP是Get Free Pages的缩写)。

(1)区域修饰符:指定从哪个区域类型分配页,“ 根据可移动性分组” 已经描述了根据分配标志得到首选区域类型的方法。

(2)页移动性和位置提示:指定页的迁移类型和从哪些内存节点分配页。

(3)高优先级修饰符。

(4)回收修饰符。

(5)行动修饰符。
__GFP_COLD:调用者不期望分配的页很快被使用,尽可能分配缓存冷页(数据不在处理器的缓存中)。
__GFP_NOWARN:如果分配失败,不要打印警告信息。
__GFP_COMP:把分配的页块组成复合页(compound page)。
__GFP_ZERO:把页用零初始化。

因为这些标志位总是组合使用,所以内核定义了一些标志位组合。常用的标志位组合如下。

(1)GFP_ATOMIC:原子分配,分配内核使用的页,不能睡眠。调用者是高优先级的,允许异步回收页。

(2)GFP_KERNEL:分配内核使用的页,可能睡眠。从低端内存区域分配页,允许异步回收页和直接回收页,允许读写存储设备,允许调用到底层文件系统。

(3)GFP_NOWAIT:分配内核使用的页,不能等待。允许异步回收页,不允许直接回收页,不允许读写存储设备,不允许调用到底层文件系统。

(4)GFP_NOIO:不允许读写存储设备,允许异步回收页和直接回收页。
请尽量避免直接使用这个标志位,应该使用函数memalloc_noio_save和memalloc_noio_restore标记一个不能读写存储设备的范围,前者设置进程标志位PF_MEMALLOC_NOIO,后者清除进程标志位PF_MEMALLOC_NOIO。

(5)GFP_NOFS:不允许调用到底层文件系统,允许异步回收页和直接回收页,允许读写存储设备。请尽量避免直接使用这个标志位,应该使用函数memalloc_nofs_save和memalloc_nofs_restore标记一个不能调用到文件系统的范围,前者设置进程标志位PF_MEMALLOC_NOFS,后者清除进程标志位PF_MEMALLOC_NOFS。

(6)GFP_USER:分配用户空间使用的页,内核或硬件也可以直接访问,从普通区域分配,允许异步回收页和直接回收页,允许读写存储设备,允许调用到文件系统,允许实施cpuset内存分配策略。

(7)GFP_HIGHUSER:分配用户空间使用的页,内核不需要直接访问,从高端内存区域分配,物理页在使用的过程中不可以移动。

(9)GFP_TRANSHUGE_LIGHT:分配用户空间使用的巨型页,把分配的页块组成复合页,禁止使用紧急保留内存,禁止打印警告信息,不允许异步回收页和直接回收页。

(10)GFP_TRANSHUGE:分配用户空间使用的巨型页,和GFP_TRANSHUGE_LIGHT的区别是允许直接回收页。

复合页

如果设置了标志位__GFP_COMP并且分配了一个阶数大于0的页块,页分配器会把页块组成复合页(compound page)。复合页最常见的用处是创建巨型页。

复合页的第一页叫首页(head page),其他页都叫尾页(tail page)。一个由n阶页块组成的复合页的结构如图3.19所示。

image-20220802001727530

(1)首页设置标志PG_head。

(2)第一个尾页的成员compound_mapcount表示复合页的映射计数,即多少个虚拟页映射到这个物理页,初始值是−1。这个成员和成员mapping组成一个联合体,占用相同的位置,其他尾页把成员mapping设置为一个有毒的地址。

(3)第一个尾页的成员 compound_dtor 存放复合页释放函数数组的索引,成员compound_order存放复合页的阶数n。这两个成员和成员lru.prev占用相同的位置。

(4)所有尾页的成员compound_head存放首页的地址,并且把最低位设置为1。这个成员和成员lru.next占用相同的位置。判断一个页是复合页的成员的方法是:页设置了标志位PG_head(针对首页),或者页的成员compound_head的最低位是1(针对尾页)。

include/linux/mm_types.h

结构体page中复合页的成员如下:

struct page {
     unsigned long flags;
     union {
         struct address_space *mapping;
         atomic_t compound_mapcount;   /* 映射计数,第一个尾页 */
         /* page_deferred_list().next    -- 第二个尾页 */
     };union {
         struct list_head lru;
         /* 复合页的尾页 */
         struct {
              unsigned long compound_head; /* 首页的地址,并且设置最低位 */
              /* 第一个尾页 */
#ifdef CONFIG_64BIT
              unsigned int compound_dtor;  /* 复合页释放函数数组的索引 */
              unsigned int compound_order; /* 复合页的阶数 */
#else
              unsigned short int compound_dtor;
              unsigned short int compound_order;

#endif
         };
     };}

对高阶原子分配的优化处理(MIGRATE_HIGHATOMIC)

高阶原子分配:阶数大于0,并且调用者设置了分配标志位__GFP_ATOMIC,要求不能睡眠。

页分配器对高阶原子分配做了优化处理==(保留一些页专门供其分配加快分配速度)==,增加了高阶原子类型(MIGRATE_HIGHATOMIC),在内存区域的结构体中增加1个成员“nr_reserved_highatomic”,用来记录高阶原子类型的总页数,并且限制其数量:zone->nr_reserved_highatomic < (zone->managed_pages / 100) + pageblock_nr_pages,即必须小于(伙伴分配器管理的总页数 / 100 + 分组阶数对应的页数)。

include/linux/mmzone.h

struct zone {unsigned long nr_reserved_highatomic;} ____cacheline_internodealigned_in_smp;

执行高阶原子分配时,先从高阶原子类型分配页,如果分配失败,从调用者指定的迁移类型分配页。分配成功以后,如果内存区域中高阶原子类型的总页数小于限制,并且页块的迁移类型不是高阶原子类型、隔离类型和CMA迁移类型,那么把页块的迁移类型转换为高阶原子类型,并且把页块中没有分配出去的页移到高阶原子类型的空闲链表中。

当内存严重不足时,直接回收页以后仍然分配失败,针对高阶原子类型的页数超过pageblock_nr_pages的目标区域,把高阶原子类型的页块转换成申请的迁移类型,然后重试分配,其代码如下:

mm/page_alloc.c

static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
         unsigned int alloc_flags, const struct alloc_context *ac,
         unsigned long *did_some_progress)
{
    struct page *page = NULL;
    bool drained = false;
    *did_some_progress = __perform_reclaim(gfp_mask, order, ac);/* 直接回收页 */
    if (unlikely(!(*did_some_progress)))
         return NULL;
retry:
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (!page && !drained) {
         /* 把高阶原子类型的页块转换成申请的迁移类型 */
         unreserve_highatomic_pageblock(ac, false);
         drain_all_pages(NULL);
         drained = true;
         goto retry;
    }
    return page;
}

如果直接回收页没有进展超过16次,那么针对目标区域,不再为高阶原子分配保留页,把高阶原子类型的页块转换成申请的迁移类型,其代码如下

mm/page_alloc.c

static inline bool
should_reclaim_retry(gfp_t gfp_mask, unsigned order,
               struct alloc_context *ac, int alloc_flags,
               bool did_some_progress, int *no_progress_loops)
{if (did_some_progress && order <= PAGE_ALLOC_COSTLY_ORDER)
          *no_progress_loops = 0;
    else
          (*no_progress_loops)++;
    if (*no_progress_loops > MAX_RECLAIM_RETRIES) {
          /* 在调用内存耗尽杀手之前,用完为高阶原子分配保留的页 */
          return unreserve_highatomic_pageblock(ac, true);
    }}

分配页

分配接口

页分配器提供了以下分配页的接口。

(1)alloc_pages(gfp_mask, order)请求分配一个阶数为order的页块,返回一个page实例。

(2)alloc_page(gfp_mask)是函数alloc_pages在阶数为0情况下的简化形式,只分配一页。

(3)__get_free_pages(gfp_mask, order)对函数alloc_pages做了封装,只能从低端内存区域分配页,并且返回虚拟地址。

(4)__get_free_page(gfp_mask)是函数__get_free_pages在阶数为0情况下的简化形式,只分配一页。

(5)get_zeroed_page(gfp_mask)是函数__get_free_pages在为参数gfp_mask设置了标志位__GFP_ZERO且阶数为0情况下的简化形式,只分配一页,并且用零初始化。

核心函数的实现__alloc_pages_nodemask

在Linux内核中,所有分配页的函数最终都会调用到__alloc_pages_nodemask,此函数被称为分区的伙伴分配器的心脏

struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, 
              struct zonelist *zonelist, nodemask_t *nodemask)

(1)gfp_mask:分配标志位。
(2)order:阶数。
(3)zonelist:首选内存节点的备用区域列表。如果指定了标志位__GFP_THISNODE,选择pg_data_t.node_zonelists[ZONELIST_NOFALLBACK],否则选择
pg_data_t.node_zonelists [ZONELIST_FALLBACK]。
(4)nodemask:允许从哪些内存节点分配页,如果调用者没有要求,可以传入空指针

算法流程:

1、根据分配标志位得到首选区域类型和迁移类型;

2、执行快速路径,使用低水线尝试第一次分配;

3、如果快速路径分配失败,才执行慢速路径

页分配器内部的标志位:

mm\internal.h

/* The ALLOC_WMARK bits are used as an index to zone->watermark */
#define ALLOC_WMARK_MIN		WMARK_MIN // 使用最低水线
#define ALLOC_WMARK_LOW		WMARK_LOW // 低水线
#define ALLOC_WMARK_HIGH	WMARK_HIGH // 高水线
#define ALLOC_NO_WATERMARKS	0x04 // 完全不检查水线

/* Mask to get the watermark bits */
#define ALLOC_WMARK_MASK	(ALLOC_NO_WATERMARKS-1) // 得到水线位的掩码

#define ALLOC_HARDER		0x10 // 试图更努力分配
#define ALLOC_HIGH		0x20 // 调用者是高优先级
#define ALLOC_CPUSET		0x40 // 检查cpuset是否允许进程从某个内存节点分配页
#define ALLOC_CMA		0x80 // 允许从CMA(连续内存分配器)迁移类型分配

快速分配 get_page_from_freelist

/*
 * get_page_from_freelist goes through the zonelist trying to allocate
 * a page.
 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
	struct zoneref *z = ac->preferred_zoneref;
	struct zone *zone;
	struct pglist_data *last_pgdat_dirty_limit = NULL;

	/*
	   扫描备用区域列表中每个满足条件的区域:区域类型小于或等待首选区域类型,
	   并且内存节点在节点掩码中的相应位被设置处理。
	*/
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
								ac->nodemask) {
		struct page *page;
		unsigned long mark;
		
        /*
        	如果编译了cpuset功能,调用者设置ALLOC_CPUSET要求使用cpuset检查,
			并且cpuset不允许当前进程从这个内存节点分配,那么不能从这个区域分配页
        */
		if (cpusets_enabled() &&
			(alloc_flags & ALLOC_CPUSET) &&
			!__cpuset_zone_allowed(zone, gfp_mask))
				continue;
		
        /*
        	如果调用者设置标志位_GFP_MRITE,表示文件系统申请分配一个页页缓存页用来写文件,
			那么检查内存节点的脏页数量是否超过限制。如果超过就不能从这个区域分配页
        */
		if (ac->spread_dirty_pages) {
			if (last_pgdat_dirty_limit == zone->zone_pgdat)
				continue;

			if (!node_dirty_ok(zone->zone_pgdat)) {
				last_pgdat_dirty_limit = zone->zone_pgdat;
				continue;
			}
		}

        /* 检查水线,如果(区域的空闲页数 - 申请的页数)小于水线 */
		mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
		if (!zone_watermark_fast(zone, order, mark,
				       ac_classzone_idx(ac), alloc_flags)) {
			int ret;

			/* Checked here to keep the fast path fast */
			BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
			if (alloc_flags & ALLOC_NO_WATERMARKS)
				goto try_this_zone;

            /* 如果没有开启节点回收功能,或者当前节点和首选节点之间的距离
            大于回收距离,不能从这个区域分配页 */

			if (node_reclaim_mode == 0 ||
			    !zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
				continue;
            
			/* 从节点回收没有映射到里进程虚拟地址空间的文件页的块
			分配器申请的页,然后重新检查水线, */
			ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
			switch (ret) {
			case NODE_RECLAIM_NOSCAN:
				/* did not scan */
				continue;
			case NODE_RECLAIM_FULL:
				/* scanned but unreclaimable */
				continue;
			default:
				/* did we reclaim enough */
				if (zone_watermark_ok(zone, order, mark,
						ac_classzone_idx(ac), alloc_flags))
					goto try_this_zone;

				continue;
			}
		}

try_this_zone:
        /* 直接从当前区域分配页,调用rmqueue来分配 */
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
        
        // 如果分配成功,调用函数prep_new_page以初始化页。如果是高阶原子分配,并且区域中高阶原子类型的页数没有超过限制,那么把分配的页所属的页块转换为高阶原子类型。
		if (page) {
			prep_new_page(page, order, gfp_mask, alloc_flags);

			/*
			 * If this is a high-order atomic allocation then check
			 * if the pageblock should be reserved for the future
			 */
			if (unlikely(order && (alloc_flags & ALLOC_HARDER)))
				reserve_highatomic_pageblock(page, zone, order);

			return page;
		}
	}

	return NULL;
}

慢速分配 __alloc_pages_slowpath

如果低水线分配失败,则执行慢速路径,慢速路径是在函数__alloc_pages_slowpath中实现的,执行流程如下图

image-20220802002636675

1)如果允许异步回收页,那么针对每个目标区域,唤醒区域所属内存节点的页回收线程。
2)使用最低水线尝试分配。
3)针对申请阶数大于0:如果允许直接回收页,那么执行异步模式的内存碎片整理,然后尝试分配。
4)如果调用者承诺“给我少量紧急保留内存使用,我可以释放更多的内存”,那么在忽略水线的情况下尝试分配。
5)直接回收页,然后尝试分配。
6)针对申请阶数大于0:执行同步模式的内存碎片整理,然后尝试分配。
7)如果多次尝试直接回收页和同步模式的内存碎片整理,仍然分配失败,那么使用杀伤力比较大的内存耗尽杀手选择一个进程杀死,然后尝试分配。

页分配器认为阶数大于3是昂贵的分配,有些地方做了特殊处理。

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
						struct alloc_context *ac)
{
	bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
	const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
	struct page *page = NULL;
	unsigned int alloc_flags;
	unsigned long did_some_progress;
	enum compact_priority compact_priority;
	enum compact_result compact_result;
	int compaction_retries;
	int no_progress_loops;
	unsigned long alloc_start = jiffies;
	unsigned int stall_timeout = 10 * HZ;
	unsigned int cpuset_mems_cookie;

	// 申请阶数不能超过页分配器支持的最大分配阶
	if (order >= MAX_ORDER) {
		WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
		return NULL;
	}

	/*
	 * We also sanity check to catch abuse of atomic reserves being used by
	 * callers that are not in atomic context.
	 */
	if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
				(__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
		gfp_mask &= ~__GFP_ATOMIC;

retry_cpuset:
	compaction_retries = 0;
	no_progress_loops = 0;
	compact_priority = DEF_COMPACT_PRIORITY;
    
    /* 后面可能检查cpuset是否允许当前进程从哪些内存节点申请页,
	需要读当前进程的成员mems_allowed,使用顺序锁保护 */
	cpuset_mems_cookie = read_mems_allowed_begin();

	/*
	 * The fast path uses conservative alloc_flags to succeed only until
	 * kswapd needs to be woken up, and to avoid the cost of setting up
	 * alloc_flags precisely. So we do that now.
	 */
    // 把分配标志位转换成内部分配标志位
	alloc_flags = gfp_to_alloc_flags(gfp_mask);

	/*
	 * We need to recalculate the starting point for the zonelist iterator
	 * because we might have used different nodemask in the fast path, or
	 * there was a cpuset modification and we are retrying - otherwise we
	 * could end up iterating over non-eligible zones endlessly.
	 */
    // 获取首选的内存区域
	ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->high_zoneidx, ac->nodemask);
	if (!ac->preferred_zoneref->zone)
		goto nopage;
	
    // 异步回收页,唤醒页回收机制
	if (gfp_mask & __GFP_KSWAPD_RECLAIM)
		wake_all_kswapds(order, ac);

	/*
	 * The adjusted alloc_flags might result in immediate success, so try
	 * that first
	 */
    
    // 使用最低水线分配页
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;

	/*
	 * For costly allocations, try direct compaction first, as it's likely
	 * that we have enough base pages and don't need to reclaim. For non-
	 * movable high-order allocations, do that as well, as compaction will
	 * try prevent permanent fragmentation by migrating from blocks of the
	 * same migratetype.
	 * Don't try this for allocations that are allowed to ignore
	 * watermarks, as the ALLOC_NO_WATERMARKS attempt didn't yet happen.
	 */
    
    // 针对申请的阶数大于0,满足3个条件 
	if (can_direct_reclaim &&
			(costly_order ||
			   (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
			&& !gfp_pfmemalloc_allowed(gfp_mask)) {
		page = __alloc_pages_direct_compact(gfp_mask, order,
						alloc_flags, ac,
						INIT_COMPACT_PRIORITY,
						&compact_result);
		if (page)
			goto got_pg;

		/*
		 * Checks for costly allocations with __GFP_NORETRY, which
		 * includes THP page fault allocations
		 */
		if (costly_order && (gfp_mask & __GFP_NORETRY)) {
			/*
			 * If compaction is deferred for high-order allocations,
			 * it is because sync compaction recently failed. If
			 * this is the case and the caller requested a THP
			 * allocation, we do not want to heavily disrupt the
			 * system, so we fail the allocation instead of entering
			 * direct reclaim.
			 */
			if (compact_result == COMPACT_DEFERRED)
				goto nopage;

			/*
			 * Looks like reclaim/compaction is worth trying, but
			 * sync compaction could be very expensive, so keep
			 * using async compaction.
			 */
			compact_priority = INIT_COMPACT_PRIORITY;
		}
	}

retry:
	/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
	// 确保页回收线程在我们循环的时候不会意外地睡眠
    if (gfp_mask & __GFP_KSWAPD_RECLAIM)
		wake_all_kswapds(order, ac);

	if (gfp_pfmemalloc_allowed(gfp_mask))
		alloc_flags = ALLOC_NO_WATERMARKS;

	/*
	 * Reset the zonelist iterators if memory policies can be ignored.
	 * These allocations are high priority and system rather than user
	 * orientated.
	 */
	if (!(alloc_flags & ALLOC_CPUSET) || (alloc_flags & ALLOC_NO_WATERMARKS)) {
		ac->zonelist = node_zonelist(numa_node_id(), gfp_mask);
		ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->high_zoneidx, ac->nodemask);
	}

	/* Attempt with potentially adjusted zonelist and alloc_flags */
    // 使用可能调整过的区域列表和分配标志
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;

	/* Caller is not willing to reclaim, we can't balance anything */
	if (!can_direct_reclaim)
		goto nopage;

	/* Make sure we know about allocations which stall for too long */
	if (time_after(jiffies, alloc_start + stall_timeout)) {
		warn_alloc(gfp_mask & ~__GFP_NOWARN, ac->nodemask,
			"page allocation stalls for %ums, order:%u",
			jiffies_to_msecs(jiffies-alloc_start), order);
		stall_timeout += 10 * HZ;
	}

	/* Avoid recursion of direct reclaim */
	if (current->flags & PF_MEMALLOC)
		goto nopage;

	/* Try direct reclaim and then allocating */
    // 直接回收页
	page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
							&did_some_progress);
	if (page)
		goto got_pg;

	/* Try direct compaction and then allocating */
    // 针对申请阶数大于0,执行同步的内存碎片整理
	page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
					compact_priority, &compact_result);
	if (page)
		goto got_pg;

	/* Do not loop if specifically requested */
    // 如果调用者不要求重试,则放弃
	if (gfp_mask & __GFP_NORETRY)
		goto nopage;

	/*
	 * Do not retry costly high order allocations unless they are
	 * __GFP_REPEAT
	 */
	if (costly_order && !(gfp_mask & __GFP_REPEAT))
		goto nopage;

	if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
				 did_some_progress > 0, &no_progress_loops))
		goto retry;

	/*
	 * It doesn't make any sense to retry for the compaction if the order-0
	 * reclaim is not able to make any progress because the current
	 * implementation of the compaction depends on the sufficient amount
	 * of free memory (see __compaction_suitable)
	 */
    // 申请阶数大于0: 判断是否应该重试内存碎片整理
	if (did_some_progress > 0 &&
			should_compact_retry(ac, order, alloc_flags,
				compact_result, &compact_priority,
				&compaction_retries))
		goto retry;

	/*
	 * It's possible we raced with cpuset update so the OOM would be
	 * premature (see below the nopage: label for full explanation).
	 */
    
    // 如果cpuset修改允许当前进程从那些内存节点申请页
	if (read_mems_allowed_retry(cpuset_mems_cookie))
		goto retry_cpuset;

	/* Reclaim has failed us, start killing things */
    // 使用内存耗尽杀手选择一个进程杀死
	page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
	if (page)
		goto got_pg;

	/* Avoid allocations with no watermarks from looping endlessly */
    
    // 如果当前进程正在被内存耗尽杀手杀死,并且忽略水线或者不允紧急保留内存
	if (test_thread_flag(TIF_MEMDIE) &&
	    (alloc_flags == ALLOC_NO_WATERMARKS ||
	     (gfp_mask & __GFP_NOMEMALLOC)))
		goto nopage;

	/* Retry as long as the OOM killer is making progress */
    // 如果内存耗尽杀手取得进展,则重试
	if (did_some_progress) {
		no_progress_loops = 0;
		goto retry;
	}

nopage:
	/*
	 * When updating a task's mems_allowed or mempolicy nodemask, it is
	 * possible to race with parallel threads in such a way that our
	 * allocation can fail while the mask is being updated. If we are about
	 * to fail, check if the cpuset changed during allocation and if so,
	 * retry.
	 */
	if (read_mems_allowed_retry(cpuset_mems_cookie))
		goto retry_cpuset;

	/*
	 * Make sure that __GFP_NOFAIL request doesn't leak out and make sure
	 * we always retry
	 */
	if (gfp_mask & __GFP_NOFAIL) {
		/*
		 * All existing users of the __GFP_NOFAIL are blockable, so warn
		 * of any new users that actually require GFP_NOWAIT
		 */
		if (WARN_ON_ONCE(!can_direct_reclaim))
			goto fail;

		/*
		 * PF_MEMALLOC request from this context is rather bizarre
		 * because we cannot reclaim anything and only can loop waiting
		 * for somebody to do a work for us
		 */
		WARN_ON_ONCE(current->flags & PF_MEMALLOC);

		/*
		 * non failing costly orders are a hard requirement which we
		 * are not prepared for much so let's warn about these users
		 * so that we can identify them and convert them to something
		 * else.
		 */
		WARN_ON_ONCE(order > PAGE_ALLOC_COSTLY_ORDER);

		/*
		 * Help non-failing allocations by giving them access to memory
		 * reserves but do not use ALLOC_NO_WATERMARKS because this
		 * could deplete whole memory reserves which would just make
		 * the situation worse
		 */
		page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
		if (page)
			goto got_pg;

		cond_resched();
		goto retry;
	}
fail:
	warn_alloc(gfp_mask, ac->nodemask,
			"page allocation failure: order:%u", order);
got_pg:
	return page;
}

释放页

在CPU 访问内存时,因于这个操作比较慢,为了加快速度,
根据本地性原则,CPU在访问主内存时的时候会把附近的一块数据都加载到CPU 的Cache 里,之后读这与这块数据都是在Cache 里做。

Linux 本来有伙伴系统分配内存页,为了加快单个内存页的分配
Linux 在每个 inode 里 为每个CPU 分配了一个per_cpu_pageset (暂且叫页缓存吧)。
每个 页缓存包含一个冷页缓存 和 一个热页缓存。

  1. 如果申请完一个内存页就立即用来写数据,用热页缓存。
  2. 如果申请完一个内存页,暂时用不到 或者 给 DMA用,用冷页缓存。

这主要是因为 内核 用free_page 释放单个内存页的时候会调用 free_hot_page。
刚释放的内存页大概率还在CPU 的Cache 里,也就是说热页缓存里的页很可能还在CPU 的 Cache里,
所以申请热页缓存并且立即使用会直接访问 CPU 的Cache 速度会比较快。

页分配器提供释放页的接口:

(1)void __free_pages(struct page *page, unsigned int order),第一个参数是第一个物理页的page实例的地址,第二个参数是阶数。

(2)void free_pages(unsigned long addr, unsigned int order),第一个参数是第一个物理页的起始内核虚拟地址,第二个参数是阶数。

函数__free_pages的代码如下:

void __free_pages(struct page *page, unsigned int order)
{
    // 引用计数减一,为0 执行操作
	if (put_page_testzero(page)) {
		if (order == 0)
			free_hot_cold_page(page, false); // 返还给伙伴分配器
		else
			__free_pages_ok(page, order); // 返还
	}
}

首先把页的引用计数减1,只有页的引用计数变成零,才真正释放页:如果阶数是0,不还给伙伴分配器,而是当作缓存热页添加到每处理器页集合中;如果阶数大于0,调用函数__free_pages_ok以释放页。

free_hot_cold_page

函数free_hot_cold_page把一页添加到每处理器页集合中,如果页集合中的页数量大于或等于高水线,那么批量返还给伙伴分配器。第二个参数cold表示缓存冷热程度,主动释放的页作为缓存热页,回收的页作为缓存冷页,因为回收的是最近最少使用的页。

mm/page_alloc.c
void free_hot_cold_page(struct page *page, bool cold)
{
     struct zone *zone = page_zone(page);
     struct per_cpu_pages *pcp;
     unsigned long flags;

     unsigned long pfn = page_to_pfn(page);
     int migratetype;
     if (!free_pcp_prepare(page))
           return;
     migratetype = get_pfnblock_migratetype(page, pfn);/* 得到页所属页块的迁移类型 */
     set_pcppage_migratetype(page, migratetype);/* page->index保存真实的迁移类型 */
     local_irq_save(flags);
     __count_vm_event(PGFREE);
     /*
      * 每处理器集合只存放不可移动、可回收和可移动这3种类型的页,
      * 如果页的类型不是这3种类型,处理方法是:
      * (1)如果是隔离类型的页,不需要添加到每处理器页集合,直接释放;
      * (2)其他类型的页添加到可移动类型链表中,page->index保存真实的迁移类型。
      */
     if (migratetype >= MIGRATE_PCPTYPES) {
          if (unlikely(is_migrate_isolate(migratetype))) {
                free_one_page(zone, page, pfn, 0, migratetype);
                goto out;
          }
          migratetype = MIGRATE_MOVABLE;
     }
     /* 添加到对应迁移类型的链表中,如果是缓存热页,添加到首部,否则添加到尾部 */
     pcp = &this_cpu_ptr(zone->pageset)->pcp;
     if (!cold)
           list_add(&page->lru, &pcp->lists[migratetype]);
     else
           list_add_tail(&page->lru, &pcp->lists[migratetype]);
     pcp->count++;
     /* 如果页集合中的页数量大于或等于高水线,那么批量返还给伙伴分配器 */
     if (pcp->count >= pcp->high) {
           unsigned long batch = READ_ONCE(pcp->batch);
           free_pcppages_bulk(zone, batch, pcp);
           pcp->count -= batch;
     }
out:
     local_irq_restore(flags);
}

__free_pages_ok

函数__free_pages_ok负责释放阶数大于 0 的页块,最终调用到释放页的核心函数__free_one_page,算法是:如果伙伴是空闲的,并且伙伴在同一个内存区域,那么和伙伴合并,注意隔离类型的页块和其他类型的页块不能合并。算法还做了优化处理:

假设最后合并成的页块阶数是order,如果order小于(MAX_ORDER−2),则检查(order+1)阶的伙伴是否空闲,如果空闲,那么order阶的伙伴可能正在释放,很快就可以合并成(order+2)阶的页块。为了防止当前页块很快被分配出去,把当前页块添加到空闲链表的尾部。

mm/page_alloc.c
__free_pages_ok() -> free_one_page() -> __free_one_page()
static inline void __free_one_page(struct page *page,
            unsigned long pfn,
            struct zone *zone, unsigned int order,
            int migratetype)
{
     unsigned long combined_pfn;
     unsigned long uninitialized_var(buddy_pfn);
     struct page *buddy;
     unsigned int max_order;
     /* pageblock_order是按可移动性分组的阶数 */
     max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1);
     …

continue_merging:
     /*如果伙伴是空闲的,和伙伴合并,重复这个操作直到阶数等于(max_order-1)。*/
     while (order < max_order - 1) {
         buddy_pfn = __find_buddy_pfn(pfn, order);/* 得到伙伴的起始物理页号 */
         buddy = page + (buddy_pfn - pfn);        /* 得到伙伴的第一页的page实例 */
         if (!pfn_valid_within(buddy_pfn))
               goto done_merging;
         /* 检查伙伴是空闲的并且在相同的内存区域 */
         if (!page_is_buddy(page, buddy, order))
               goto done_merging;
         /*
          * 开启了调试页分配的配置宏CONFIG_DEBUG_PAGEALLOC,伙伴充当警戒页。
          */
         if (page_is_guard(buddy)) {
               clear_page_guard(zone, buddy, order, migratetype);
         } else {/* 伙伴是空闲的,把伙伴从空闲链表中删除 */
               list_del(&buddy->lru);
               zone->free_area[order].nr_free--;
               rmv_page_order(buddy);
         }
         combined_pfn = buddy_pfn & pfn;
         page = page + (combined_pfn - pfn);
         pfn = combined_pfn;
         order++;
     }
     if (max_order < MAX_ORDER) {
         /*
          * 运行到这里,意味着阶数大于或等于分组阶数pageblock_order,
          * 阻止把隔离类型的页块和其他类型的页块合并
          */
         if (unlikely(has_isolate_pageblock(zone))) {
              int buddy_mt;
              buddy_pfn = __find_buddy_pfn(pfn, order);
              buddy = page + (buddy_pfn - pfn);

              buddy_mt = get_pageblock_migratetype(buddy);
              /*如果一个是隔离类型的页块,另一个是其他类型的页块,不能合并 */
              if (migratetype != buddy_mt
                         && (is_migrate_isolate(migratetype) ||
                             is_migrate_isolate(buddy_mt)))
                   goto done_merging;
         }
         /* 如果两个都是隔离类型的页块,或者都是其他类型的页块,那么继续合并 */
         max_order++;
         goto continue_merging;
     }
done_merging:
     set_page_order(page, order);
     /*
      * 最后合并成的页块阶数是order,如果order小于(MAX_ORDER-2),
      * 则检查(order+1)阶的伙伴是否空闲,如果空闲,那么order阶的伙伴可能正在释放,
      * 很快就可以合并成(order+2)阶的页块。为了防止当前页块很快被分配出去,
      * 把当前页块添加到空闲链表的尾部
      */
     if ((order < MAX_ORDER-2) && pfn_valid_within(buddy_pfn)) {
           struct page *higher_page, *higher_buddy;
           combined_pfn = buddy_pfn & pfn;
           higher_page = page + (combined_pfn - pfn);
           buddy_pfn = __find_buddy_pfn(combined_pfn, order + 1);
           higher_buddy = higher_page + (buddy_pfn - combined_pfn);
           if (pfn_valid_within(buddy_pfn) &&
                 page_is_buddy(higher_page, higher_buddy, order + 1)) {
                 list_add_tail(&page->lru,
                      &zone->free_area[order].free_list[migratetype]);
                 goto out;
           }
     }
     /* 添加到空闲链表的首部 */
     list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
out:
     zone->free_area[order].nr_free++;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值