前文回顾

在上篇文章 《深化了解 Linux 物理内存办理》中,笔者详细的为咱们介绍了 Linux 内核怎么对物理内存进行办理以及相关的一些内核数据结构

在介绍物理内存办理之前,笔者先从 CPU 的视点开端,介绍了三种 Linux 物理内存模型:FLATMEM 平坦内存模型,DISCONTIGMEM 非接连内存模型,SPARSEMEM 稀少内存模型。

深入理解 Linux 物理内存分配全链路实现

深入理解 Linux 物理内存分配全链路实现

深入理解 Linux 物理内存分配全链路实现

随后笔者又带咱们站在一个新的视角上,把物理内存看做成一个全体,从 CPU 拜访物理内存以及 CPU 与物理内存的相对方位变化的视点介绍了两种物理内存架构:一致性内存拜访 UMA 架构,非一致性内存拜访 NUMA 架构。

深入理解 Linux 物理内存分配全链路实现

深入理解 Linux 物理内存分配全链路实现

在 NUMA 架构下,只需 DISCONTIGMEM 非接连内存模型和 SPARSEMEM 稀少内存模型是可用的。而 UMA 架构下,前面介绍的三种内存模型可以装备运用。

无论是 NUMA 架构仍是 UMA 架构在内核中都是运用相同的数据结构来安排办理的,在内核的内存办理模块中会把 UMA 架构作为只需一个 NUMA 节点的伪 NUMA 架构。

深入理解 Linux 物理内存分配全链路实现

这样一来这两种架构形式就在内核中被统一办理起来,咱们依据这个现实,深化剖析了内核针对 NUMA 架构下用于物理内存办理的相关数据结构:struct pglist_data (NUMA 节点),struct zone(物理内存区域),struct page(物理页)。

深入理解 Linux 物理内存分配全链路实现

上图展示的是在 NUMA 架构下,NUMA 节点与物理内存区域 zone 以及物理内存页 page 之间的层次联系。

物理内存被区分成了一个一个的内存节点(NUMA 节点),在每个 NUMA 节点内部又将其所办理的物理内存依照功用不同区分成了不同的内存区域 zone ,每个内存区域 zone 办理一片用于详细功用的物理内存页 page,而内核会为每一个内存区域分配一个同伴体系用于办理该内存区域下物理内存页 page 的分配和开释。

物理内存在内核中办理的层级联系为:None -> Zone -> page

在上篇文章的最终,笔者又花了很多的篇幅来为咱们介绍了 struct page 结构,咱们了解了内核怎么通过 struct page 结构来描述物理内存页,这个结构是内核中最为杂乱的一个结构体,由于它是物理内存办理的最小单位,被频频应用在内核中的各种杂乱机制下。

通过以上内容的介绍,笔者觉得咱们现已在架构层面上对 Linux 物理内存办理有了一个较为深刻的认识,现在物理内存办理的架构咱们现已建立起来了,那么内核怎么依据这个架构层次来分配物理内存呢?

为了给咱们收拾清楚内核分配物理内存的进程及其涉及到的各个重要模块,所以就有了本文的内容~~

深入理解 Linux 物理内存分配全链路实现

1. 内核物理内存分配接口

深入理解 Linux 物理内存分配全链路实现

在为咱们介绍物理内存分配之前,笔者先来介绍下内核顶用于物理内存分配的几个中心接口,这几个物理内存分配接口悉数是依据同伴体系的,同伴体系有一个特点便是它所分配的物理内存页悉数都是物理上接连的,而且只能分配 2 的整数幂个页,这儿的整数幂在内核中称之为分配阶。

下面要介绍的这些物理内存分配接口均需求指定这个分配阶,意思便是从同伴体系恳求多少个物理内存页,假定咱们指定分配阶为 order,那么就会从同伴体系中恳求 2 的 order 次幂个物理内存页。

内核中供给了一个 alloc_pages 函数用于分配 2 的 order 次幂个物理内存页,参数中的 unsigned int order 标明向底层同伴体系指定的分配阶,参数 gfp_t gfp 是内核中界说的一个用于规范物理内存分配行为的修饰符,这儿咱们先不展开,后边的末节中笔者会详细为咱们介绍。

struct page *alloc_pages(gfp_t gfp, unsigned int order);

alloc_pages 函数用于向底层同伴体系恳求 2 的 order 次幂个物理内存页组成的内存块,该函数回来值是一个 struct page 类型的指针用于指向恳求的内存块中第一个物理内存页。

alloc_pages 函数用于分配多个接连的物理内存页,在内核的某些内存分配场景中有时分并不需求分配这么多的接连内存页,而是只需求分配一个物理内存页即可,所以内核又供给了 alloc_page 宏,用于这种单内存页分配的场景,咱们可以看到其底层仍是依靠了 alloc_pages 函数,只不过 order 指定为 0。

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

当体系中闲暇的物理内存无法满意内存分配时,就会导致内存分配失利,alloc_pages,alloc_page 就会回来空指针 NULL 。

vmalloc 分配机制底层便是用的 alloc_page

在物理内存分配成功的状况下, alloc_pages,alloc_page 函数回来的都是指向其恳求的物理内存块第一个物理内存页 struct page 指针。

咱们可以直接了解成回来的是一块物理内存,而 CPU 可以直接拜访的却是虚拟内存,所以内核又供给了一个函数 __get_free_pages ,该函数直接回来物理内存页的虚拟内存地址。用户可以直接运用。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

__get_free_pages 函数在运用方法上和 alloc_pages 是相同的,函数参数的意义也是相同,只不过一个是回来物理内存页的虚拟内存地址,一个是直接回来物理内存页。

现实上 __get_free_pages 函数的底层也是依据 alloc_pages 完成的,只不过多了一层虚拟地址转换的作业。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;
    // 不能在高端内存中分配物理页,由于无法直接映射获取虚拟内存地址
	page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
	if (!page)
		return 0;
    // 将直接映射区中的物理内存页转换为虚拟内存地址
	return (unsigned long) page_address(page);
}

page_address 函数用于将给定的物理内存页 page 转换为它的虚拟内存地址,不过这儿只适用于内核虚拟内存空间中的直接映射区,由于在直接映射区中虚拟内存地址到物理内存地址是直接映射的,虚拟内存地址减去一个固定的偏移就可以直接得到物理内存地址。

假如物理内存页处于高端内存中,则不能这样直接进行转换,在通过 alloc_pages 函数获取物理内存页 page 之后,需求调用 kmap 映射将 page 映射到内核虚拟地址空间中。

深入理解 Linux 物理内存分配全链路实现

忘掉这块内容的同学,可以在回看下笔者之前的文章 《深化了解虚拟内存办理》中的 “ 7.1.4 永久映射区 ” 末节。

同 alloc_page 函数相同,内核也供给了 __get_free_page 用于只分配单个物理内存页的场景,底层仍是依靠于 __get_free_pages 函数,参数 order 指定为 0 。

#define __get_free_page(gfp_mask) \
		__get_free_pages((gfp_mask), 0)

无论是 alloc_pages 也好仍是 __get_free_pages 也好,它们恳求到的内存页中包括的数据在一开端都不是空白的,而是内核随机产生的一些废物信息,但其实这些信息或许并不都是彻底随机的,很有或许随机的包括一些灵敏的信息。

这些灵敏的信息或许会被一些黑客所运用,并对核算机体系产生一些危害行为,所以从运用安全的视点考虑,内核又供给了一个函数 get_zeroed_page,顾名思义,这个函数会将从同伴体系中恳求到内存页悉数初始化填充为 0 ,这在分配物理内存页给用户空间运用的时分十分有用。

unsigned long get_zeroed_page(gfp_t gfp_mask)
{
	return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}

get_zeroed_page 函数底层也依靠于 __get_free_pages,指定的分配阶 order 也是 0,标明从同伴体系中只恳求一个物理内存页并初始化填充 0 。

除此之外,内核还供给了一个 __get_dma_pages 函数,专门用于从 DMA 内存区域分配适用于 DMA 的物理内存页。其底层也是依靠于 __get_free_pages 函数。

unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order);

这些底层依靠于 __get_free_pages 的物理内存分配函数,在遇到内存分配失利的状况下都会回来 0 。

以上介绍的物理内存分配函数,分配的均是在物理上接连的内存页。

当然了,有内存的分配就会有内存的开释,所以内核还供给了两个用于开释物理内存页的函数:

void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
  • __free_pages : 同 alloc_pages 函数对应,用于开释一个或许 2 的 order 次幂个内存页,开释的物理内存区域开端地址由该区域中的第一个 page 实例指针标明,也便是参数里的 struct page *page 指针。

  • free_pages:同 __get_free_pages 函数对应,与 __free_pages 函数的区别是在开释物理内存时,运用了虚拟内存地址而不是 page 指针。

在开释内存时需求十分谨慎小心,咱们只能开释属于你自己的内存页,传递了过错的 struct page 指针或许过错的虚拟内存地址,或许传递错了 order 值,都或许会导致体系的崩溃。在内核空间中,内核是彻底信赖自己的,这点和用户空间不同。

另外内核也供给了 __free_page 和 free_page 两个宏,专门用于开释单个物理内存页。

#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

到这儿,关于内核中关于物理内存分配和开释的接口,笔者就为咱们告知完了,可是咱们或许会有一个疑问,便是咱们在介绍 alloc_pages 和 __get_free_pages 函数的时分,它们的参数中都有 gfp_t gfp_mask,之前笔者简略的提过这个 gfp_mask 掩码:它是内核中界说的一个用于规范物理内存分配行为的掩码。

那么这个掩码究竟规范了哪些物理内存的分配行为 ?并对物理内存的分配有哪些影响呢 ?咱们跟着笔者的节奏持续往下看~~~

2.规范物理内存分配行为的掩码 gfp_mask

笔者在 《深化了解 Linux 物理内存办理》一文中的 “ 4.3 NUMA 节点物理内存区域的区分 ” 末节中从前为咱们详细的介绍了 NUMA 节点中物理内存区域 zone 的区分。

笔者在文章中说到,由于实际的核算机体系结构受到硬件方面的制约,直接约束了页框的运用方法。所以内核会依据不同的物理内存区域的功用不同,将 NUMA 节点内的物理内存区分为:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM 这几个物理内存区域。

ZONE_MOVABLE 区域是内核从逻辑上的区分,该区域中的物理内存页面来自于上述几个内存区域,意图是避免内存碎片和支撑内存热插拔

深入理解 Linux 物理内存分配全链路实现

当咱们调用上末节中介绍的那几个物理内存分配接口时,比方:alloc_pages 和 __get_free_pages。就会遇到一个问题,便是咱们恳求的这些物理内存到底来自于哪个物理内存区域 zone,假如咱们想要从指定的物理内存区域中恳求内存,咱们该怎么告知内核呢 ?

struct page *alloc_pages(gfp_t gfp, unsigned int order);
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

这时,这些物理内存分配接口中的 gfp_t 参数就派上用场了,前缀 gfp 是 get free page 的缩写,意思是在获取闲暇物理内存页的时分需求指定的分配掩码 gfp_mask。

gfp_mask 中的低 4 位用来标明应该从哪个物理内存区域 zone 中获取内存页 page。

深入理解 Linux 物理内存分配全链路实现

gfp_mask 掩码中这些区域修饰符 zone modifiers 界说在内核 /include/linux/gfp.h 文件中:

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

咱们这儿或许会感到好奇,为什么没有界说 ___GFP_NORMAL 的掩码呢?

这是由于内核对物理内存的分配首要是落在 ZONE_NORMAL 区域中,假如咱们不指定物理内存的分配区域,那么内核会默许从 ZONE_NORMAL 区域中分配内存,假如 ZONE_NORMAL 区域中的闲暇内存不行,内核则会降级到 ZONE_DMA 区域中分配。

关于物理内存分配的区域降级战略,笔者在前面的文章《深化了解 Linux 物理内存办理》的 “ 5.1 物理内存区域中的预留内存 ” 末节中现已详细地为咱们介绍过了,可是之前的介绍仅仅停留在理论层面,那么这个物理内存区域降级战略是在哪里完成的呢?接下来的内容笔者就为咱们揭晓~~~

内核在 /include/linux/gfp.h 文件中界说了一个叫做 gfp_zone 的函数,这个函数用于将咱们在物理内存分配接口中指定的 gfp_mask 掩码转换为物理内存区域,回来的这个物理内存区域是内存分配的第一流内存区域,假如这个第一流内存区域缺乏以满意内存分配的需求,则依照 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 的次序依次降级。

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;
}

上面的这个 gfp_zone 函数是在内核 5.19 版别中的完成,在高版别的完成顶用很多的移位操作替换了低版别中的完成,意图是为了提高程序的性能,可是带来的却是可读性的大幅下降。

笔者写到这儿觉得给咱们分析清楚每一步移位操作的完成对咱们了解这个函数的骨干逻辑并没有什么本质意义上的协助,而且和本文主题违背太远,所以咱们退回到低版别 2.6.24 中的完成,在这一版中直击 gfp_zone 函数原本的相貌。

static inline enum zone_type gfp_zone(gfp_t flags)
{
	int base = 0;
#ifdef CONFIG_NUMA
	if (flags & __GFP_THISNODE)
		base = MAX_NR_ZONES;
#endif
#ifdef CONFIG_ZONE_DMA
	if (flags & __GFP_DMA)
		return base + ZONE_DMA;
#endif
#ifdef CONFIG_ZONE_DMA32
	if (flags & __GFP_DMA32)
		return base + ZONE_DMA32;
#endif
	if ((flags & (__GFP_HIGHMEM | __GFP_MOVABLE)) ==
			(__GFP_HIGHMEM | __GFP_MOVABLE))
		return base + ZONE_MOVABLE;
#ifdef CONFIG_HIGHMEM
	if (flags & __GFP_HIGHMEM)
		return base + ZONE_HIGHMEM;
#endif
    // 默许从 normal 区域中分配内存
	return base + ZONE_NORMAL;
}

咱们看到在内核 2.6.24 版别中的 gfp_zone 函数完成逻辑就十分的明晰了,中心逻辑首要如下:

  • 只需掩码 flags 中设置了 __GFP_DMA,则不论 __GFP_HIGHMEM 有没有设置,内存分配都只会在 ZONE_DMA 区域中分配。

  • 假如掩码只设置了 ZONE_HIGHMEM,则在物理内存分配时,优先在 ZONE_HIGHMEM 区域中进行分配,假如容量不行则降级到 ZONE_NORMAL 中,假如仍是不行则进一步降级至 ZONE_DMA 中分配。

  • 假如掩码既没有设置 ZONE_HIGHMEM 也没有设置 __GFP_DMA,则走到最终的分支,默许优先从 ZONE_NORMAL 区域中进行内存分配,假如容量不行则降级至 ZONE_DMA 区域中分配。

  • 独自设置 __GFP_MOVABLE 其实并不会影响内核的分配战略,咱们假如想要让内核在 ZONE_MOVABLE 区域中分配内存需求一同指定 __GFP_MOVABLE 和 __GFP_HIGHMEM 。

ZONE_MOVABLE 仅仅内核界说的一个虚拟内存区域,意图是避免内存碎片和支撑内存热插拔。上述介绍的 ZONE_HIGHMEM,ZONE_NORMAL,ZONE_DMA 才是真实的物理内存区域,ZONE_MOVABLE 虚拟内存区域中的物理内存来自于上述三个物理内存区域。

在 32 位体系中 ZONE_MOVABLE 虚拟内存区域中的物理内存页来自于 ZONE_HIGHMEM。

在64 位体系中 ZONE_MOVABLE 虚拟内存区域中的物理内存页来自于 ZONE_NORMAL 或许 ZONE_DMA 区域。

下面是不同的 gfp_t 掩码设置方法与其对应的内存区域降级战略汇总列表:

gfp_t 掩码 内存区域降级战略
什么都没有设置 ZONE_NORMAL -> ZONE_DMA
__GFP_DMA ZONE_DMA
__GFP_DMA & __GFP_HIGHMEM ZONE_DMA
__GFP_HIGHMEM ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA

除了上述介绍 gfp_t 掩码中的这四个物理内存区域修饰符之外,内核还界说了一些规范内存分配行为的修饰符,这些行为修饰符并不会约束内核从哪个物理内存区域中分配内存,而是会约束物理内存分配的行为,那么具领会约束哪些内存分配的行为呢?让咱们接着往下看~~~

这些内存分配行为修饰符同样也是界说在 /include/linux/gfp.h 文件中:

#define ___GFP_RECLAIMABLE	0x10u
#define ___GFP_HIGH		0x20u
#define ___GFP_IO		0x40u
#define ___GFP_FS		0x80u
#define ___GFP_ZERO		0x100u
#define ___GFP_ATOMIC		0x200u
#define ___GFP_DIRECT_RECLAIM	0x400u
#define ___GFP_KSWAPD_RECLAIM	0x800u
#define ___GFP_NOWARN		0x2000u
#define ___GFP_RETRY_MAYFAIL	0x4000u
#define ___GFP_NOFAIL		0x8000u
#define ___GFP_NORETRY		0x10000u
#define ___GFP_HARDWALL		0x100000u
#define ___GFP_THISNODE		0x200000u
#define ___GFP_MEMALLOC		0x20000u
#define ___GFP_NOMEMALLOC	0x80000u
  • ___GFP_RECLAIMABLE 用于指定分配的页面是可以收回的,___GFP_MOVABLE 则是用于指定分配的页面是可以移动的,这两个标志会影响底层的同伴体系从哪个区域中去获取闲暇内存页,这块内容咱们会在后边讲解同伴体系的时分详细介绍。

  • ___GFP_HIGH 标明该内存分配恳求是高优先级的,内核急迫的需求内存,假如内存分配失利则会给体系带来十分严峻的结果,设置该标志一般内存是不答应分配失利的,假如闲暇内存缺乏,则会从紧迫预留内存中分配。

关于物理内存区域中的紧迫预留内存相关内容,笔者在之前文章 《深化了解 Linux 物理内存办理》一文中的 “ 5.1 物理内存区域中的预留内存 ” 末节中现已详细介绍过了。

  • ___GFP_IO 标明内核在分配物理内存的时分可以发起磁盘 IO 操作。什么意思呢?比方当内核在进行内存分配的时分,发现物理内存缺乏,这时需求将不常常运用的内存页置换到 SWAP 分区或许 SWAP 文件中,这时就涉及到了 IO 操作,假如设置了该标志,标明答应内核将不常用的内存页置换出去。

  • ___GFP_FS 答应内核履行底层文件体系操作,在与 VFS 虚拟文件体系层相相关的内核子体系中有必要禁用该标志,不然或许会引起文件体系操作的循环递归调用,由于在设置 ___GFP_FS 标志分配内存的状况下,或许会引起更多的文件体系操作,而这些文件体系的操作或许又会进一步产生内存分配行为,这样一向递归持续下去。

  • ___GFP_ZERO 在内核分配内存成功之后,将内存页初始化填充字节 0 。

  • ___GFP_ATOMIC 该标志的设置标明内存在分配物理内存的时分不答应睡觉有必要是原子性地进行内存分配。比方在中止处理程序中,就不能睡觉,由于中止程序不能被从头调度。一同也不能在持有自旋锁的进程上下文中睡觉,由于或许导致死锁。综上所述这个标志只能用在不能被从头安全调度的进程上下文中

  • ___GFP_DIRECT_RECLAIM 标明内核在进行内存分配的时分,可以进行直接内存收回。当剩下内存容量低于水位线 _watermark[WMARK_MIN] 时,阐明此刻的内存容量现已十分风险了,假如进程在这时恳求内存分配,内核就会进行直接内存收回,直到内存水位线康复到 _watermark[WMARK_HIGH] 之上。

深入理解 Linux 物理内存分配全链路实现

  • ___GFP_KSWAPD_RECLAIM 标明内核在分配内存的时分,假如剩下内存容量在 _watermark[WMARK_MIN] 与 _watermark[WMARK_LOW] 之间时,内核就会唤醒 kswapd 进程开端异步内存收回,直到剩下内存高于 _watermark[WMARK_HIGH] 中止。

  • ___GFP_NOWARN 标明当内核分配内存失利时,抑制内核的分配失利过错报告。

  • ___GFP_RETRY_MAYFAIL 在内核分配内存失利的时分,答应重试,但重试依然或许失利,重试若干次后中止。与其对应的是 ___GFP_NORETRY 标志标明分配内存失利时不答应重试。

  • ___GFP_NOFAIL 在内核分配失利时一向重试直到成功中止。

  • ___GFP_HARDWALL 该标志约束了内核分配内存的行为只能在当时进程分配到的 CPU 所相关的 NUMA 节点上进行分配,当进程可以运转的 CPU 受限时,该标志才会有意义,假如进程答应在一切 CPU 上运转则该标志没有意义。

  • ___GFP_THISNODE 该标志约束了内核分配内存的行为只能在当时 NUMA 节点或许在指定 NUMA 节点中分配内存,假如内存分配失利不答应从其他备用 NUMA 节点中分配内存。

  • ___GFP_MEMALLOC 答应内核在分配内存时可以从一切内存区域中获取内存,包括从紧迫预留内存中获取。但运用该标明时需求保证进程在获得内存之后会很快的开释掉内存不会过长时刻的占用,尤其要警觉避免过多的耗费紧迫预留内存区域中的内存。

  • ___GFP_NOMEMALLOC 标志用于明晰制止内核从紧迫预留内存中获取内存。___GFP_NOMEMALLOC 标识的优先级要高于 ___GFP_MEMALLOC

好了到现在中止,咱们现已知道了 gfp_t 掩码中包括的内存区域修饰符以及内存分配行为修饰符,是不是感觉头有点大了,现实上确实很让人头大,由于内核在不同场景下会运用不同的组合,这么多的修饰符总是以组合的形式出现,假如咱们每次运用的时分都需求独自指定,那就会十分冗杂也很简略出错。

所以内核将各种规范情形下用到的 gfp_t 掩码组合,提早为咱们界说了一些规范的分组,便利咱们直接运用。

#define GFP_ATOMIC	(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL	(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_NOWAIT	(__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO	(__GFP_RECLAIM)
#define GFP_NOFS	(__GFP_RECLAIM | __GFP_IO)
#define GFP_USER	(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA		__GFP_DMA
#define GFP_DMA32	__GFP_DMA32
#define GFP_HIGHUSER	(GFP_USER | __GFP_HIGHMEM)
  • GFP_ATOMIC 是掩码 __GFP_HIGH,__GFP_ATOMIC,__GFP_KSWAPD_RECLAIM 的组合,标明内存分配行为有必要是原子的,是高优先级的。在任何状况下都不答应睡觉,假如闲暇内存不行,则会从紧迫预留内存中分配。该标志适用于中止程序,以及持有自旋锁的进程上下文中。

  • GFP_KERNEL 是内核中最常用的标志,该标志设置之后内核的分配内存行为或许会堵塞睡觉,可以答应内核置换出一些不活泼的内存页到磁盘中。适用于可以从头安全调度的进程上下文中。

  • GFP_NOIO 和 GFP_NOFS 别离制止内核在分配内存时进行磁盘 IO 和 文件体系 IO 操作。

  • GFP_USER 用于映射到用户空间的内存分配,一般这些内存可以被内核或许硬件直接拜访,比方硬件设备会将 Buffer 直接映射到用户空间中

  • GFP_DMA 和 GFP_DMA32 标明需求从 ZONE_DMA 和 ZONE_DMA32 内存区域中获取适用于 DMA 的内存页。

  • GFP_HIGHUSER 用于给用户空间分配高端内存,由于在用户虚拟内存空间中,都是通过页表来拜访非直接映射的高端内存区域,所以用户空间一般运用的是高端内存区域 ZONE_HIGHMEM。

现在咱们算是真实了解了,在本末节开端时,介绍的那几个内存分配接口函数中关于内存分配掩码 gfp_mask 的一切内容,其间包括用于约束内核从哪个内存区域中分配内存,内核在分配内存进程中的行为,以及内核在各种规范分配场景下预先界说的掩码组合。

这时咱们在回过头来看内核中关于物理内存分配的这些接口函数是不是感觉了如指掌了:

struct page *alloc_pages(gfp_t gfp, unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
unsigned long get_zeroed_page(gfp_t gfp_mask)
unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order)

好了,现在咱们现已清楚了这些内存分配接口的运用,那么这些接口又是怎么完成的呢 ?让咱们再一次深化到内核源码中去探究内核到底是怎么分配物理内存的~~

3. 物理内存分配内核源码完成

本文依据内核 5.19 版别评论

在介绍 Linux 内核关于内存分配的源码完成之前,咱们需求先找到内存分配的进口函数在哪里,在上末节中为咱们介绍的很多内存分配接口的依靠层级联系如下图所示:

深入理解 Linux 物理内存分配全链路实现

咱们看到内存分配的使命最终会落在 alloc_pages 这个接口函数中,在 alloc_pages 中会调用 alloc_pages_node 然后调用 __alloc_pages_node 函数,最终通过 __alloc_pages 函数正式进入内核内存分配的世界~~

__alloc_pages 函数为 Linux 内核内存分配的中心进口函数

static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    // 校验指定的 NUMA 节点 ID 是否合法,不要越界
    VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
    // 指定节点有必要是有用在线的
    VM_WARN_ON((gfp_mask & __GFP_THISNODE) && !node_online(nid));
    return __alloc_pages(gfp_mask, order, nid, NULL);
}

__alloc_pages_node 函数参数中的 nid 便是咱们在上篇文章 《深化了解 Linux 物理内存办理》的 “ 4.1 内核怎么统一安排 NUMA 节点 ” 末节介绍的 NUMA 节点 id。

深入理解 Linux 物理内存分配全链路实现

内核运用了一个巨细为 MAX_NUMNODES 的全局数组 node_data[] 来办理一切的 NUMA 节点,数组的下标即为 NUMA 节点 Id 。

#ifdef CONFIG_NUMA
extern struct pglist_data *node_data[];
#define NODE_DATA(nid)		(node_data[(nid)])

这儿指定 nid 是为了告知内核应该在哪个 NUMA 节点上分配内存,咱们看到在 alloc_pages 函数中通过 numa_node_id() 获取运转当时进程的 CPU 地点的 NUMA 节点。并通过 !node_online(nid) 保证指定的 NUMA 节点是有用在线的。

关于 NUMA 节点的状况信息,咱们可回看上篇文章的 《4.5 NUMA 节点的状况 node_states》末节。

深入理解 Linux 物理内存分配全链路实现

3.1 内存分配行为标识掩码 ALLOC_*

在咱们进入 __alloc_pages 函数之前,笔者先来为咱们介绍几个影响内核分配内存行为的标识,这些重要标识界说在内核文件 /mm/internal.h 中:

#define ALLOC_WMARK_MIN     WMARK_MIN
#define ALLOC_WMARK_LOW     WMARK_LOW
#define ALLOC_WMARK_HIGH    WMARK_HIGH
#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */
#define ALLOC_HARDER         0x10 /* try to alloc harder */
#define ALLOC_HIGH       0x20 /* __GFP_HIGH set */
#define ALLOC_CPUSET         0x40 /* check for correct cpuset */
#define ALLOC_KSWAPD        0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */

咱们先来看前四个标识内存水位线的常量意义,这四个内存水位线标识标明内核在分配内存时有必要考虑内存的水位线,在不同的水位线下内存的分配行为也会有所不同。

笔者在上篇文章 《深化了解 Linux 物理内存办理》的 “ 5.2 物理内存区域中的水位线 ” 末节中曾详细地介绍了各个水位线的意义以及在不同水位线下内存分配的不同体现。

上篇文章中咱们说到,内核会为 NUMA 节点中的每个物理内存区域 zone 定制三条用于指示内存容量的水位线,它们别离是:WMARK_MIN(页最小阈值), WMARK_LOW (页低阈值),WMARK_HIGH(页高阈值)。

这三个水位线界说在 /include/linux/mmzone.h 文件中:

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

三条水位线对应的 watermark 详细数值存储在每个物理内存区域 struct zone 结构中的 _watermark[NR_WMARK] 数组中。

struct zone {
    // 物理内存区域中的水位线
    unsigned long _watermark[NR_WMARK];
}

物理内存区域中不同水位线的意义以及内存分配在不同水位线下的行为如下图所示:

深入理解 Linux 物理内存分配全链路实现

  • 当该物理内存区域的剩下内存容量高于 _watermark[WMARK_HIGH] 时,阐明此刻该物理内存区域中的内存容量十分满意,内存分配彻底没有压力。

  • 当剩下内存容量在 _watermark[WMARK_LOW] 与_watermark[WMARK_HIGH] 之间时,阐明此刻内存有必定的耗费可是还可以承受,可以持续满意进程的内存分配需求。

  • 当剩下内存容量在 _watermark[WMARK_MIN] 与 _watermark[WMARK_LOW] 之间时,阐明此刻内存容量现已有点风险了,内存分配面临必定的压力,可是还可以满意进程此刻的内存分配要求,当给进程分配完内存之后,就会唤醒 kswapd 进程开端内存收回,直到剩下内存高于 _watermark[WMARK_HIGH] 中止。

在这种状况下,进程的内存分配会触发内存收回,但恳求进程本身不会被堵塞,由内核的 kswapd 进程异步收回内存。

  • 当剩下内存容量低于 _watermark[WMARK_MIN] 时,阐明此刻的内存容量现已十分风险了,假如进程在这时恳求内存分配,内核就会进行直接内存收回,这时内存收回的使命将会由恳求进程同步完成。

留意:上面说到的物理内存区域 zone 的剩下内存是需求刨去 lowmem_reserve 预留内存巨细(用于紧迫内存分配)。也便是说 zone 里被同伴体系所办理的内存并不包括 lowmem_reserve 预留内存。

好了,在咱们从头回顾了内存分配行为在这三条水位线:_watermark[WMARK_HIGH],_watermark[WMARK_LOW],watermark[WMARK_MIN] 下的不同体现之后,咱们在回过来看本末节开端处说到的那几个 ALLOC* 内存分配标识。

ALLOC_NO_WATERMARKS 标明在内存分配进程中彻底不会考虑上述三个水位线的影响。

ALLOC_WMARK_HIGH 标明在内存分配的时分,当时物理内存区域 zone 中剩下内存页的数量至少要到达 _watermark[WMARK_HIGH] 水位线,才能进行内存的分配。

ALLOC_WMARK_LOW 和 ALLOC_WMARK_MIN 要表达的内存分配语义也是相同,当时物理内存区域 zone 中剩下内存页的数量至少要到达水位线 _watermark[WMARK_LOW] 或许 _watermark[WMARK_MIN],才能进行内存的分配。

ALLOC_HARDER 标明在内存分配的时分,会放宽内存分配规矩的约束,所谓的放宽规矩便是降低 _watermark[WMARK_MIN] 水位线,努力使内存分配最大或许成功。

当咱们在 gfp_t 掩码中设置了 ___GFP_HIGH 时,ALLOC_HIGH 标识才起作用,该标识标明当时内存分配恳求是高优先级的,内核急迫的需求内存,假如内存分配失利则会给体系带来十分严峻的结果,设置该标志一般内存是不答应分配失利的,假如闲暇内存缺乏,则会从紧迫预留内存中分配。

ALLOC_CPUSET 标明内存只能在当时进程所答应运转的 CPU 所相关的 NUMA 节点中进行分配。比方运用 cgroup 约束进程只能在某些特定的 CPU 上运转,那么进程所发起的内存分配恳求,只能在这些特定 CPU 地点的 NUMA 节点中进行。

ALLOC_KSWAPD 标明答应唤醒 NUMA 节点中的 KSWAPD 进程,异步进行内存收回。

内核会为每个 NUMA 节点分配一个 kswapd 进程用于收回不常常运用的页面。

typedef struct pglist_data {
        .........
    // 页面收回进程
    struct task_struct *kswapd;
        ..........
} pg_data_t;

3.2 内存分配的心脏 __alloc_pages

好了,在为咱们介绍完这些影响内存分配行为的相关标识掩码:GFP_*ALLOC_* 之后,下面就该来介绍本文的主题——物理内存分配的中心函数 __alloc_pages ,从下面内核源码的注释中咱们可以看出,这个函数正是同伴体系的中心心脏,它是内核内存分配的中心进口函数,整个内存分配的完好进程悉数封装在这儿。

该函数的逻辑比较杂乱,由于在内存分配进程中需求涉及处理各种 GFP_*ALLOC_* 标识,然后依据上述各种标识的意义来决议内存分配该怎么进行。所以咱们需求多点耐心,一步一步跟着笔者的思路往下走~~~

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
                            nodemask_t *nodemask)
{
    // 用于指向分配成功的内存
    struct page *page;
    // 内存区域中的剩下内存需求在 WMARK_LOW 水位线之上才能进行内存分配,不然失利(初次测验快速内存分配)
    unsigned int alloc_flags = ALLOC_WMARK_LOW;
    // 之前末节中介绍的内存分配掩码集合
    gfp_t alloc_gfp; 
    // 用于在不同内存分配辅佐函数中传递参数
    struct alloc_context ac = { };
    // 查看用于向同伴体系恳求内存容量的分配阶 order 的合法性
    // 内核界说最大分配阶 MAX_ORDER -1 = 10,也便是说一次最多只能从同伴体系中恳求 1024 个内存页。
    if (WARN_ON_ONCE_GFP(order >= MAX_ORDER, gfp))
        return NULL;
    // 标明在内存分配期间进程可以休眠堵塞
    gfp &= gfp_allowed_mask;
    alloc_gfp = gfp;
    // 初始化 alloc_context,并为接下来的快速内存分配设置相关 gfp
    if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
            &alloc_gfp, &alloc_flags))
        // 提早判别本次内存分配是否可以成功,假如不能则尽早失利
        return NULL;
    // 避免内存碎片化的相关分配标识设置,可暂时疏忽
    alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);
    // 内存分配快速途径:第一次测验从底层同伴体系分配内存,留意此刻是在 WMARK_LOW 水位线之上分配内存
    page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
    if (likely(page))
        // 假如内存分配成功则直接回来
        goto out;
    // 流程走到这儿标明内存分配在快速途径下失利
    // 这儿需求康复开端的内存分配标识设置,后续会测验愈加急进的内存分配战略
    alloc_gfp = gfp;
    // 康复开端的 node mask 由于它或许在第一次内存分配的进程中被改动
    // 本函数中 nodemask 起初被设置为 null
    ac.nodemask = nodemask;
    // 在第一次快速内存分配失利之后,阐明内存现已缺乏了,内核需求做更多的作业
    // 比方通过 kswap 收回内存,或许直接内存收回等方法获取更多的闲暇内存以满意内存分配的需求
    // 所以下面的进程称之为慢速分配途径
    page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
out:
    // 内存分配成功,直接回来 page。不然回来 NULL
    return page;
}

__alloc_pages 函数中的内存分配全体逻辑如下:

  • 首要内核会测验在内存水位线 WMARK_LOW 之上快速的进行一次内存分配。这一点咱们从开端的 unsigned int alloc_flags = ALLOC_WMARK_LOW 语句中可以看得出来。

深入理解 Linux 物理内存分配全链路实现

  • 校验本次内存分配指定同伴体系的分配阶 order 的有用性,同伴体系在内核中的最大分配阶界说在 /include/linux/mmzone.h 文件中,最大分配阶 MAX_ORDER -1 = 10,也便是说一次最多只能从同伴体系中恳求 1024 个内存页,对应 4M 巨细的接连物理内存。
/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
  • 调用 prepare_alloc_pages 初始化 alloc_context ,用于在不同内存分配辅佐函数中传递内存分配参数。为接下来即将进行的快速内存分配做预备。
struct alloc_context {
    // 运转进程 CPU 地点 NUMA  节点以及其一切备用 NUMA 节点中答应内存分配的内存区域
    struct zonelist *zonelist;
    // NUMA  节点状况掩码
    nodemask_t *nodemask;
    // 内存分配优先级最高的内存区域 zone
    struct zoneref *preferred_zoneref;
    // 物理内存页的搬迁类型分为:不行搬迁,可收回,可搬迁类型,避免内存碎片
    int migratetype;
    // 内存分配最高优先级的内存区域 zone
    enum zone_type highest_zoneidx;
    // 是否答应当时 NUMA 节点中的脏页均衡分散搬迁至其他 NUMA 节点
    bool spread_dirty_pages;
};
  • 调用 get_page_from_freelist 方法首次测验在同伴体系中进行内存分配,这次内存分配比较快速,仅仅快速的扫描一下各个内存区域中是否有满意的闲暇内存可以满意本次内存分配,假如有则立马从同伴体系中恳求,假如没有当即回来, page 设置为 null,进行后续慢速内存分配处理。

这儿需求留意的是:首次测验的快速内存分配是在 WMARK_LOW 水位线之上进行的。

  • 当快速内存分配失利之后,状况就会变得十分杂乱,内核将不得不做更多的作业,比方敞开 kswapd 进程异步内存收回,更极点的状况则需求进行直接内存收回,或许直接内存收拾以获取更多的闲暇接连内存。这一切的杂乱逻辑悉数封装在 __alloc_pages_slowpath 函数中。

alloc_pages_slowpath 函数杂乱在于需求结合前边末节中介绍的 GFP*,ALLOC* 这些内存分配标识,依据不同的标识进入不同的内存分配逻辑分支,涉及到的状况比较冗杂。这儿咱们只需求简略了解,后边笔者会详细介绍~~~

以上介绍的 __alloc_pages 函数内存分配逻辑以及与对应的内存水位线之间的联系如下图所示:

深入理解 Linux 物理内存分配全链路实现

全体流程介绍完之后,咱们接着来看一下以上内存分配进程涉及到的三个重要内存分配辅佐函数:prepare_alloc_pages,__alloc_pages_slowpath,get_page_from_freelist 。

3.3 prepare_alloc_pages

prepare_alloc_pages 初始化 alloc_context ,用于在不同内存分配辅佐函数中传递内存分配参数,为接下来即将进行的快速内存分配做预备。

static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
        int preferred_nid, nodemask_t *nodemask,
        struct alloc_context *ac, gfp_t *alloc_gfp,
        unsigned int *alloc_flags)
{
    // 依据 gfp_mask 掩码中的内存区域修饰符获取内存分配最高优先级的内存区域 zone
    ac->highest_zoneidx = gfp_zone(gfp_mask);
    // 从 NUMA 节点的备用节点链表中一次性获取答应进行内存分配的一切内存区域
    ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
    ac->nodemask = nodemask;
    // 从 gfp_mask 掩码中获取页面搬迁特点,搬迁特点分为:不行搬迁,可收回,可搬迁。这儿只需求简略知道,后边在相关章节会细讲
    ac->migratetype = gfp_migratetype(gfp_mask);
   // 假如运用 cgroup 将进程绑定约束在了某些 CPU 上,那么内存分配只能在
   // 这些绑定的 CPU 相相关的 NUMA 节点中进行
    if (cpusets_enabled()) {
        *alloc_gfp |= __GFP_HARDWALL;
        if (in_task() && !ac->nodemask)
            ac->nodemask = &cpuset_current_mems_allowed;
        else
            *alloc_flags |= ALLOC_CPUSET;
    }
    // 假如设置了答应直接内存收回,那么内存分配进程则或许会导致休眠被从头调度 
    might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
    // 提早判别本次内存分配是否可以成功,假如不能则尽早失利
    if (should_fail_alloc_page(gfp_mask, order))
        return false;
    // 获取最高优先级的内存区域 zone
    // 后续内存分配则首要会在该内存区域中进行分配
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    return true;
}

prepare_alloc_pages 首要的使命便是在快速内存分配开端之前,做一些预备初始化的作业,其间最中心的便是从指定 NUMA 节点中,依据 gfp_mask 掩码中的内存区域修饰符获取可以进行内存分配的一切内存区域 zone (包括其他备用 NUMA 节点中包括的内存区域)。

之前笔者现已在 《深化了解 Linux 物理内存办理》一文中的 “ 4.3 NUMA 节点物理内存区域的区分 ” 末节为咱们现已详细介绍了 NUMA 节点的数据结构 struct pglist_data。

struct pglist_data 结构中不仅包括了本 NUMA 节点中的一切内存区域,还包括了其他备用 NUMA 节点中的物理内存区域,当本节点中内存缺乏的状况下,内核会从备用 NUMA 节点中的内存区域进行跨节点内存分配。

typedef struct pglist_data {
    // NUMA 节点中的物理内存区域个数
    int nr_zones; 
    // NUMA 节点中的物理内存区域
    struct zone node_zones[MAX_NR_ZONES];
    // NUMA 节点的备用列表,其间包括了一切 NUMA 节点中的一切物理内存区域 zone,依照拜访间隔由近到远次序依次摆放
    struct zonelist node_zonelists[MAX_ZONELISTS];
} pg_data_t;

咱们可以依据 nid 和 gfp_mask 掩码中的物理内存区域描述符运用 node_zonelist 函数一次性获取答应进行内存分配的一切内存区域(一切 NUMA 节点)。

static inline struct zonelist *node_zonelist(int nid, gfp_t flags)
{
	return NODE_DATA(nid)->node_zonelists + gfp_zonelist(flags);
}

4. 内存慢速分配进口 alloc_pages_slowpath

正如前边末节咱们说到的那样,alloc_pages_slowpath 函数十分的杂乱,其间包括了内存分配的各种异常状况的处理,而且会依据前边介绍的 GFP_,ALLOC_ 等各种内存分配战略掩码进行不同分支的处理,这样就变得十分的庞大而冗杂。

alloc_pages_slowpath 函数包括了整个内存分配的中心流程,本身十分的冗杂庞大,为了可以给咱们明晰的收拾清楚这些杂乱的内存分配流程,所以笔者决议仍是以 总 - 分 - 总 的结构来给咱们呈现。

下面这段伪代码是笔者提取出来的 alloc_pages_slowpath 函数的骨干结构,其间包括的一些中心分支以及中心步骤笔者都通过注释的形式为咱们标注出来了,这儿我先从全体上大约浏览下 alloc_pages_slowpath 首要分为哪几个逻辑处理模块,它们别离处理了哪些事情。

仍是那句话,这儿咱们只需求全体把握,不需求把握每个细节,关于细节的部分,笔者后边会带咱们逐一击破!!!

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存分配途径下的相关参数 .......
retry_cpuset:
        ......... 调整内存分配战略 alloc_flags 选用愈加急进方法获取内存 ......
        ......... 此刻内存分配首要是在进程所答应运转的 CPU 相相关的 NUMA 节点上 ......
        ......... 内存水位线下调至 WMARK_MIN ...........
        ......... 唤醒一切 kswapd 进程进行异步内存收回  ...........
        ......... 触发直接内存收拾 direct_compact 来获取更多的接连闲暇内存 ......
retry:
        ......... 进一步调整内存分配战略 alloc_flags 运用愈加急进的十分手法进行内存分配 ...........
        ......... 在内存分配时疏忽内存水位线 ...........
        ......... 触发直接内存收回 direct_reclaim ...........
        ......... 再次触发直接内存收拾 direct_compact ...........
        ......... 最终的杀手锏触发 OOM 机制  ...........
nopage:
        ......... 通过以上急进的内存分配手法依然无法满意内存分配就会来到这儿 ......
        ......... 假如设置了 __GFP_NOFAIL 不答应内存分配失利,则不断重试上述内存分配进程 ......
fail:
        ......... 内存分配失利,输出告警信息 ........
      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
        ......... 内存分配成功,回来新恳求的内存块 ........
      return page;
}

4.1 初始化内存分配慢速途径下的相关参数

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
    // 在慢速内存分配途径中或许会导致内核进行直接内存收回
    // 这儿设置 __GFP_DIRECT_RECLAIM 标明答应内核进行直接内存收回
    bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
    // 本次内存分配是否是针对很多内存页的分配,内核界说 PAGE_ALLOC_COSTLY_ORDER = 3
    // 也便是说内存恳求内存页的数量大于 2 ^ 3 = 8 个内存页时,costly_order = true,后续会影响是否进行 OOM
    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;
    // 记载重试的次数,超越必定的次数(16次)则内存分配失利
    int no_progress_loops;
    // 暂时保存调整后的内存分配战略
    int reserve_flags;
    // 流程现在来到了慢速内存分配这儿,阐明快速分配途径现已失利了
    // 内核需求对 gfp_mask 分配行为掩码做一些修正,修正为一些更或许导致内存分配成功的标识
    // 由于接下来的直接内存收回十分耗时或许会导致进程堵塞睡觉,不适用原子 __GFP_ATOMIC 内存分配的上下文。
    if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
                (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
        gfp_mask &= ~__GFP_ATOMIC;
retry_cpuset:
retry:
nopage:
fail:
got_pg:
}

在内核进入慢速内存分配途径之前,首要会在这儿初始化后续内存分配需求的参数,由于笔者现已在各个字段上标注了丰富的注释,所以这儿笔者只对那些难以了解的中心参数为咱们进行相关细节的铺垫,这儿咱们对这些参数有个大约形象即可,后续在运用到的时分,笔者还会再次提起~~~

首要咱们看 costly_order 参数,order 标明底层同伴体系的分配阶,内核只能向同伴体系恳求 2 的 order 次幂个内存页,costly 从字面意思上来说标明有必定价值和耗费的,costly_order 连起来就标明在内核中 order 分配阶到达多少,在内核看来便是价值比较大的内存分配行为。

这个临界值便是 PAGE_ALLOC_COSTLY_ORDER 界说在 /include/linux/mmzone.h 文件中:

#define PAGE_ALLOC_COSTLY_ORDER 3

也便是说在内核看来,当恳求内存页的数量大于 2 ^ 3 = 8 个内存页时,costly_order = true,内核就以为本次内存分配是一次本钱比较大的行为。后续会依据这个参数 costly_order 来决议是否触发 OOM 。

    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;

当内存严峻缺乏的时分,内核会敞开直接内存收回 direct_reclaim ,参数 did_some_progress 标明通过一次直接内存收回之后,内核收回了多少个内存页。这个参数后续会影响是否需求进行内存分配重试。

no_progress_loops 用于记载内存分配重试的次数,假如内存分配重试的次数超越最大约束 MAX_RECLAIM_RETRIES,则中止重试,敞开 OOM。

MAX_RECLAIM_RETRIES 界说在 /mm/internal.h 文件中:

#define MAX_RECLAIM_RETRIES 16

compact_* 相关的参数用于直接内存收拾 direct_compact,内核一般会在直接内存收回 direct_reclaim 之前进行一次 direct_compact,假如通过 direct_compact 收拾之后有了满意多的空间内存就不需求进行 direct_reclaim 了。

那么这个 direct_compact 到底是干什么的呢?它在慢速内存分配进程起了什么作用?

随着体系的长时刻运转一般会伴随着不同巨细的物理内存页的分配和开释,这种不规矩的分配开释,随着体系的长时刻运转就会导致内存碎片,内存碎片会使得体系在明明有满意内存的状况下,依然无法为进程分配适宜的内存。

深入理解 Linux 物理内存分配全链路实现

如上图所示,假如现在体系一共有 16 个物理内存页,当时体系仅仅分配了 3 个物理页,那么在当时体系中还剩下 13 个物理内存页的状况下,假如内核想要分配 8 个接连的物理页由于内存碎片的存在则会分配失利。(只能分配最多 4 个接连的物理页)

内核中恳求分配的物理页面数只能是 2 的次幂!!

为了解决内存碎片化的问题,内核将内存页面分为了:可移动的,可收回的,不行移动的三种类型。

可移动的页面集合在一同,可收回的的页面集合在一同,不行移动的的页面集合也在一同。然后作为去碎片化的基础, 然后进行成块收回。

在收回时把可收回的一同收回,把可移动的一同移动,然后能空出很多接连物理页面。direct_compact 会扫描内存区域 zone 里的页面,把已分配的页记载下来,然后把一切已分配的页移动到 zone 的一端,这样就会把一个现已充满碎片的 zone 收拾成一段彻底未分配的区间和一段现已分配的区间,然后腾出大块接连的物理页面供内核分配。

深入理解 Linux 物理内存分配全链路实现

4.2 retry_cpuset

在介绍完了内存分配在慢速途径下所需求的相关参数之后,下面就正式来到了 alloc_pages_slowpath 的内存分配逻辑:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存分配途径下的相关参数 .......
retry_cpuset:
    // 在之前的快速内存分配途径下设置的相关分配战略比较保存,不是很急进,用于在 WMARK_LOW 水位线之上进行快速内存分配
    // 走到这儿标明快速内存分配失利,此刻闲暇内存严峻缺乏了
    // 所以在慢速内存分配途径下需求从头设置愈加急进的内存分配战略,选用更大的价值来分配内存
    alloc_flags = gfp_to_alloc_flags(gfp_mask);
    // 从头依照新的设置依照内存区域优先级核算 zonelist 的迭代起点(最高优先级的 zone)
    // fast path 和 slow path 的设置不同所以这儿需求从头核算
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    // 假如没有适宜的内存分配区域,则跳转到 nopage , 内存分配失利
    if (!ac->preferred_zoneref->zone)
        goto nopage;
    // 唤醒一切的 kswapd 进程异步收回内存
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);
    // 此刻一切的 kswapd 进程现已被唤醒,正在异步进行内存收回
    // 之前咱们现已在 gfp_to_alloc_flags 方法中从头调整了 alloc_flags
    // 换成了一套愈加急进的内存分配战略,留意此刻是在 WMARK_MIN 水位线之上进行内存分配
    // 调整后的 alloc_flags 很或许会当即成功,因而这儿先测验一下
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        // 内存分配成功,跳转到 got_pg 直接回来 page
        goto got_pg;
    // 关于分配大内存来说 costly_order = true (超越 8 个内存页),需求首要进行内存收拾,这样内核可以避免直接内存收回然后获取更多的接连闲暇内存页
    // 关于需求分配不行移动的高阶内存的状况,也需求先进行内存收拾,避免永久内存碎片
    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;
        if (costly_order && (gfp_mask & __GFP_NORETRY)) {
            // 流程走到这儿标明通过内存收拾之后依然没有满意的内存供分配
            // 可是设置了 NORETRY 标识不答应重试,那么就直接失利,跳转到 nopage
            if (compact_result == COMPACT_SKIPPED ||
                compact_result == COMPACT_DEFERRED)
                goto nopage;
            // 同步内存收拾开支太大,后续敞开异步内存收拾
            compact_priority = INIT_COMPACT_PRIORITY;
        }
    }
retry:
nopage:
fail:
got_pg:
    return page;
}

流程走到这儿,阐明内核在 《3.2 内存分配的心脏 __alloc_pages》末节中介绍的快速途径下测验的内存分配现已失利了,所以才会走到慢速分配途径这儿来。

之前咱们介绍到快速分配途径是在 WMARK_LOW 水位线之上进行内存分配,与其相配套的内存分配战略比较保存,意图是快速的在各个内存区域 zone 之间查找可供分配的闲暇内存。

深入理解 Linux 物理内存分配全链路实现

快速分配途径下的失利意味着此刻体系中的闲暇内存现已缺乏了,所以在慢速分配途径下内核需求改动内存分配战略,选用愈加急进的方法来进行内存分配,首要会把内存分配水位线降低到 WMARK_MIN 之上,然后将内存分配战略调整为愈加简略促进内存分配成功的战略。

而内存分配战略相关的调整逻辑,内核界说在 gfp_to_alloc_flags 函数中:

static inline unsigned int gfp_to_alloc_flags(gfp_t gfp_mask)
{
    // 在慢速内存分配途径中,会进一步放宽对内存分配的约束,将内存分配水位线调低到 WMARK_MIN
    // 也便是说内存区域中的剩下内存需求在 WMARK_MIN 水位线之上才可以进行内存分配
    unsigned int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;
    // 假如内存分配恳求无法运转直接内存收回,或许分配恳求设置了 __GFP_HIGH 
    // 那么意味着内存分配会更多的运用紧迫预留内存
    alloc_flags |= (__force int)
        (gfp_mask & (__GFP_HIGH | __GFP_KSWAPD_RECLAIM));
    if (gfp_mask & __GFP_ATOMIC) {
        //  ___GFP_NOMEMALLOC 标志用于明晰制止内核从紧迫预留内存中获取内存。
        // ___GFP_NOMEMALLOC 标识的优先级要高于 ___GFP_MEMALLOC
        if (!(gfp_mask & __GFP_NOMEMALLOC))
           // 假如答应从紧迫预留内存中分配,则需求进一步放宽内存分配约束
           // 后续依据 ALLOC_HARDER 标识会降低 WMARK_LOW 水位线
            alloc_flags |= ALLOC_HARDER;
        // 在这个分支中标明内存分配恳求现已设置了  __GFP_ATOMIC (十分重要,不答应失利)
        // 这种状况下为了内存分配的成功,会去除去 CPUSET 的约束,可以在一切 NUMA 节点上分配内存
        alloc_flags &= ~ALLOC_CPUSET;
    } else if (unlikely(rt_task(current)) && in_task())
         // 假如当时进程不是 real time task 或许不在 task 上下文中
         // 设置 HARDER 标识
        alloc_flags |= ALLOC_HARDER;
    return alloc_flags;
}

在调整好的新的内存分配战略 alloc_flags 之后,就需求依据新的战略来从头获取可供分配的内存区域 zone。

  ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);

从上图中咱们可以看出,当剩下内存处于 WMARK_MIN 与 WMARK_LOW 之间时,内核会唤醒一切 kswapd 进程来异步收回内存,直到剩下内存从头回到水位线 WMARK_HIGH 之上。

    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

到现在中止,内核现已在慢速分配途径下通过 gfp_to_alloc_flags 调整为愈加急进的内存分配战略,并将水位线降低到 WMARK_MIN,一同也唤醒了 kswapd 进程来异步收回内存。

此刻在新的内存分配战略下进行内存分配很或许会一次性成功,所以内核会首要测验进行一次内存分配。

page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

假如首次测验分配内存失利之后,内核就需求进行直接内存收拾 direct_compact 来获取更多的可供分配的接连内存页。

假如通过 direct_compact 之后依然没有满意的内存可供分配,那么就会进入 retry 分支选用愈加急进的方法来分配内存。假如内存分配战略设置了 __GFP_NORETRY 标明不答应重试,那么就会直接失利,流程跳转到 nopage 分支进行处理。

深入理解 Linux 物理内存分配全链路实现

4.3 retry

内存分配流程来到 retry 分支这儿阐明状况现已变得十分危急了,在通过 retry_cpuset 分支的处理,内核将内存水位线下调至 WMARK_MIN,并敞开了 kswapd 进程进行异步内存收回,触发直接内存收拾 direct_compact,在采取了这些办法之后,依然无法满意内存分配的需求。

所以在接下来的分配逻辑中,内核会近一步采取愈加急进的十分手法来获取接连的闲暇内存,下面咱们来一同看下这部分急进的内容:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存分配途径下的相关参数 .......
retry_cpuset:
        ......... 调整内存分配战略 alloc_flags 选用愈加急进方法获取内存 ......
        ......... 此刻内存分配首要是在进程所答应运转的 CPU 相相关的 NUMA 节点上 ......
        ......... 内存水位线下调至 WMARK_MIN ...........
        ......... 唤醒一切 kswapd 进程进行异步内存收回  ...........
        ......... 触发直接内存收拾 direct_compact 来获取更多的接连闲暇内存 ......
retry:
    // 保证一切 kswapd 进程不要意外进入睡觉状况
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);
    // 流程走到这儿,阐明在 WMARK_MIN 水位线之上也分配内存失利了
    // 而且通过内存收拾之后,内存分配依然失利,阐明当时内存容量现已严峻缺乏
    // 接下来就需求运用愈加急进的十分手法来测验内存分配(疏忽掉内存水位线),持续修正 alloc_flags 保存在 reserve_flags 中
    reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
    if (reserve_flags)
        alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags);
    // 假如内存分配可以恣意跨节点分配(疏忽内存分配战略),这儿需求重置 nodemask 以及 zonelist。
    if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
        // 这儿的内存分配是高优先级体系级别的内存分配,不是面向用户的
        ac->nodemask = NULL;
        ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    }
    // 这儿运用从头调整的 zonelist 和 alloc_flags 在测验进行一次内存分配
    // 留意此次的内存分配是疏忽内存水位线的 ALLOC_NO_WATERMARKS
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        goto got_pg;
    // 在疏忽内存水位线的状况下依然分配失利,现在内核就需求进行直接内存收回了
    if (!can_direct_reclaim)
        // 假如进程不答应进行直接内存收回,则只能分配失利
        goto nopage;
    // 开端直接内存收回
    page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);
    if (page)
        goto got_pg;
    // 直接内存收回之后依然无法满意分配需求,则再次进行直接内存收拾
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;
    // 在内存直接收回和收拾悉数失利之后,假如不答应重试,则只能失利
    if (gfp_mask & __GFP_NORETRY)
        goto nopage;
    // 后续会触发 OOM 来开释更多的内存,这儿需求判别本次内存分配是否需求分配很多的内存页(大于 8 ) costly_order = true
    // 假如是的话则内核以为即便履行 OOM 也未必会满意这么多的内存页分配需求.
    // 所以仍是直接失利比较好,不再履行 OOM,除非设置 __GFP_RETRY_MAYFAIL
    if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
        goto nopage;
    // 流程走到这儿阐明咱们现已测验了一切办法内存依然分配失利了,此刻内存现已十分危急了。
    // 走到这儿阐明进程答应内核进行重试流程,但在开端重试之前,内核需求判别是否应该进行重试,重试规范:
    // 1 假如内核现已重试了 MAX_RECLAIM_RETRIES (16) 次依然失利,则抛弃重试履行后续 OOM。
    // 2 假如内核将一切可选内存区域中的一切可收回页面悉数收回之后,依然无法满意内存的分配,那么抛弃重试履行后续 OOM
    if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                 did_some_progress > 0, &no_progress_loops))
        goto retry;
    // 假如内核判别不该进行直接内存收回的重试,这儿还需求判别下是否应该进行内存收拾的重试。
    // did_some_progress 标明上次直接内存收回,详细收回了多少内存页
    // 假如 did_some_progress = 0 则没有必要在进行内存收拾重试了,由于内存收拾的完成依靠于满意的闲暇内存量
    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;
    // 依据 nodemask 中的内存分配战略判别是否应该在进程所答应运转的一切 CPU 相关的 NUMA 节点上重试
    if (check_retry_cpuset(cpuset_mems_cookie, ac))
        goto retry_cpuset;
    // 最终的杀手锏,进行 OOM,选择一个得分最高的进程,开释其占用的内存 
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;
    // 只需 oom 产生了作用并开释了内存 did_some_progress > 0 就不断的进行重试
    if (did_some_progress) {
        no_progress_loops = 0;
        goto retry;
    }
nopage:
fail:  
      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
      return page;
}

retry 分支包括的是愈加急进的内存分配逻辑,所以在一开端需求调用 __gfp_pfmemalloc_flags 函数来从头调整内存分配战略,调整后的战略为:后续内存分配会疏忽水位线的影响,而且答应内核从紧迫预留内存中获取内存。

static inline int __gfp_pfmemalloc_flags(gfp_t gfp_mask)
{
    // 假如不答应从紧迫预留内存中分配,则不改动 alloc_flags
    if (unlikely(gfp_mask & __GFP_NOMEMALLOC))
        return 0;
    // 假如答应从紧迫预留内存中分配,则后边的内存分配会疏忽内存水位线的约束
    if (gfp_mask & __GFP_MEMALLOC)
        return ALLOC_NO_WATERMARKS;
    // 当时进程处于软中止上下文而且进程设置了 PF_MEMALLOC 标识
    // 则疏忽内存水位线
    if (in_serving_softirq() && (current->flags & PF_MEMALLOC))
        return ALLOC_NO_WATERMARKS;
    // 当时进程不在任何中止上下文中
    if (!in_interrupt()) {
        if (current->flags & PF_MEMALLOC)
            // 疏忽内存水位线
            return ALLOC_NO_WATERMARKS;
        else if (oom_reserves_allowed(current))
            // 当时进程答应进行 OOM
            return ALLOC_OOM;
    }
    // alloc_flags 不做任何修正
    return 0;
}

在调整好愈加急进的内存分配战略 alloc_flags 之后,内核会首要测验从同伴体系中进行一次内存分配,这时会有很大约率促进内存分配成功。

留意:此次测验进行的内存分配会疏忽内存水位线:ALLOC_NO_WATERMARKS

   page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

假如在疏忽内存水位线的状况下,内存依然分配失利,则进行直接内存收回 direct_reclaim 。

   page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);

通过 direct_reclaim 之后,依然没有满意的内存可供分配的话,那么内核会再次进行直接内存收拾 direct_compact 。

    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);

假如 direct_compact 之后仍是没有满意的内存,那么现在内核现已处于绝地了,是时分运用杀手锏:触发 OOM 机制杀死得分最高的进程以获取更多的闲暇内存。

可是在进行 OOM 之前,内核仍是需求通过一系列的判别,这时就用到了咱们在 《4.1 初始化内存分配慢速途径下的相关参数》末节中介绍的 costly_order 参数了,它会影响内核是否触发 OOM 。

假如 costly_order = true,标明此次内存分配的内存页大于 8 个页,内核会以为这是一次价值比较大的分配行为,况且此刻内存现已十分危急,严峻缺乏。在这种状况下内核以为即便触发了 OOM,也无法获取这么多的内存,依然无法满意内存分配。

所以当 costly_order = true 时,内核不会触发 OOM,直接跳转到 nopage 分支,除非设置了 __GFP_RETRY_MAYFAIL 内存分配战略:

    if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
        goto nopage;

下面内核也不会直接开端 OOM,而是进入到重试流程,在重试流程开端之前内核需求调用 should_reclaim_retry 判别是否应该进行重试,重试规范:

  1. 假如内核现已重试了 MAX_RECLAIM_RETRIES (16) 次依然失利,则抛弃重试履行后续 OOM。

  2. 假如内核将一切可选内存区域中的一切可收回页面悉数收回之后,依然无法满意内存的分配,那么抛弃重试履行后续 OOM。

假如 should_reclaim_retry = false,后边会进一步判别是否应该进行 direct_compact 的重试。

    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;

did_some_progress 标明上次直接内存收回详细收回了多少内存页,假如 did_some_progress = 0 则没有必要在进行内存收拾重试了,由于内存收拾的完成依靠于满意的闲暇内存量。

当这些一切的重试恳求都被回绝时,杀手锏 OOM 就开端上台了:

   page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;

假如 OOM 之后并没有开释内存,那么就来到 nopage 分支处理。

可是假如 did_some_progress > 0 标明 OOM 产生了作用,至少开释了一些内存那么就再次进行重试。

深入理解 Linux 物理内存分配全链路实现

4.4 nopage

到现在中止,内核现已测验了包括 OOM 在内的一切收回内存的办法,可是依然没有满意的内存来满意分配要求,看上去此次内存分配就要宣告失利了。

可是这儿还有必定的回旋余地,假如内存分配战略中装备了 __GFP_NOFAIL,则标明此次内存分配十分的重要,不答应失利。内核会在这儿不断的重试直到分配成功中止。

咱们在 《深化了解 Linux 物理内存办理》一文中的 “ 3.2 非一致性内存拜访 NUMA 架构 ” 末节,介绍 NUMA 内存架构的时分从前说到:当 CPU 自己地点的本地 NUMA 节点内存缺乏时,CPU 就需求跨 NUMA 节点去拜访其他内存节点,这种跨 NUMA 节点分配内存的行为就产生在这儿,这种状况下 CPU 拜访内存就会慢很多

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存分配途径下的相关参数 .......
retry_cpuset:
        ......... 调整内存分配战略 alloc_flags 选用愈加急进方法获取内存 ......
        ......... 此刻内存分配首要是在进程所答应运转的 CPU 相相关的 NUMA 节点上 ......
        ......... 内存水位线下调至 WMARK_MIN ...........
        ......... 唤醒一切 kswapd 进程进行异步内存收回  ...........
        ......... 触发直接内存收拾 direct_compact 来获取更多的接连闲暇内存 ......
retry:
        ......... 进一步调整内存分配战略 alloc_flags 运用愈加急进的十分手法尽心内存分配 ...........
        ......... 在内存分配时疏忽内存水位线 ...........
        ......... 触发直接内存收回 direct_reclaim ...........
        ......... 再次触发直接内存收拾 direct_compact ...........
        ......... 最终的杀手锏触发 OOM 机制  ...........
nopage:
    // 流程走到这儿标明内核现已测验了包括 OOM 在内的一切收回内存的动作。
    // 可是这些办法依然无法满意内存分配的需求,看上去内存分配到这儿就应该失利了。
    // 可是假如设置了 __GFP_NOFAIL 标明不答应内存分配失利,那么接下来就会进入 if 分支进行处理
    if (gfp_mask & __GFP_NOFAIL) {
        // 假如不答应进行直接内存收回,则跳转至 fail 分支宣告失利
        if (WARN_ON_ONCE_GFP(!can_direct_reclaim, gfp_mask))
            goto fail;
        // 此刻内核现已无法通过收回内存来获取可供分配的闲暇内存了
        // 关于 PF_MEMALLOC 类型的内存分配恳求,内核现在力不从心,只能不断的进行 retry 重试。
        WARN_ON_ONCE_GFP(current->flags & PF_MEMALLOC, gfp_mask);
        // 关于需求分配 8 个内存页以上的大内存分配,而且设置了不行失利标识 __GFP_NOFAIL
        // 内核现在也力不从心,究竟现实是现已没有闲暇内存了,仅仅给出一些告警信息
        WARN_ON_ONCE_GFP(order > PAGE_ALLOC_COSTLY_ORDER, gfp_mask);
       // 在 __GFP_NOFAIL 状况下,测验进行跨 NUMA 节点内存分配
        page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
        if (page)
            goto got_pg;
        // 在进行内存分配重试流程之前,需求让 CPU 从头调度到其他进程上
        // 运转一会其他进程,由于究竟此刻内存现已严峻缺乏
        // 立马重试的话只能糟蹋过多时刻在查找闲暇内存上,导致其他进程处于饥饿状况。
        cond_resched();
        // 跳转到 retry 分支,重试内存分配流程
        goto retry;
    }
fail:
      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
      return page;
}

这儿笔者需求着重强调的一点便是,在 nopage 分支中决议开端重试之前,内核不能当即进行重试流程,由于之前现现已历过那么多严厉急进的内存收回战略依然没有满意的内存,内存现状十分紧迫。

所以咱们有理由信任,假如内核当即开端重试的话,依然没有什么作用,反而会糟蹋过多时刻在查找闲暇内存上,导致其他进程处于饥饿状况。

所以在开端重试之前,内核会调用 cond_resched() 让 CPU 从头调度到其他进程上,让其他进程也运转一会,与此一同 kswapd 进程一向在后台异步收回着内存。

当 CPU 从头调度回当时进程时,说不定 kswapd 进程现已收回了满意多的内存,重试成功的概率会大大增加一同又避免了资源的无谓耗费。

5. __alloc_pages 内存分配流程总览

到这儿中止,笔者就为咱们完好地介绍完内核分配内存的整个流程,现在笔者再把内存分配的完好流程图放出来,咱们在结合完好的内存分配相关源码,全体在领会一下:

深入理解 Linux 物理内存分配全链路实现

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
    // 在慢速内存分配途径中或许会导致内核进行直接内存收回
    // 这儿设置 __GFP_DIRECT_RECLAIM 标明答应内核进行直接内存收回
    bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
    // 本次内存分配是否是针对很多内存页的分配,内核界说 PAGE_ALLOC_COSTLY_ORDER = 3
    // 也便是说内存恳求内存页的数量大于 2 ^ 3 = 8 个内存页时,costly_order = true,后续会影响是否进行 OOM
    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 int cpuset_mems_cookie;
    int reserve_flags;
    // 流程现在来到了慢速内存分配这儿,阐明快速分配途径现已失利了
    // 内核需求对 gfp_mask 分配行为掩码做一些修正,修正为一些更或许导致内存分配成功的标识
    // 由于接下来的直接内存收回十分耗时或许会导致进程堵塞睡觉,不适用原子 __GFP_ATOMIC 内存分配的上下文。
    if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
                (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
        gfp_mask &= ~__GFP_ATOMIC;
retry_cpuset:
    // 在之前的快速内存分配途径下设置的相关分配战略比较保存,不是很急进,用于在 WMARK_LOW 水位线之上进行快速内存分配
    // 走到这儿标明快速内存分配失利,此刻闲暇内存严峻缺乏了
    // 所以在慢速内存分配途径下需求从头设置愈加急进的内存分配战略,选用更大的价值来分配内存
    alloc_flags = gfp_to_alloc_flags(gfp_mask);
    // 从头依照新的设置依照内存区域优先级核算 zonelist 的迭代起点(最高优先级的 zone)
    // fast path 和 slow path 的设置不同所以这儿需求从头核算
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    // 假如没有适宜的内存分配区域,则跳转到 nopage , 内存分配失利
    if (!ac->preferred_zoneref->zone)
        goto nopage;
    // 唤醒一切的 kswapd 进程异步收回内存
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);
    // 此刻一切的 kswapd 进程现已被唤醒,正在异步进行内存收回
    // 之前咱们现已在 gfp_to_alloc_flags 方法中从头调整了 alloc_flags
    // 换成了一套愈加急进的内存分配战略,留意此刻是在 WMARK_MIN 水位线之上进行内存分配
    // 调整后的 alloc_flags 很或许会当即成功,因而这儿先测验一下
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        // 内存分配成功,跳转到 got_pg 直接回来 page
        goto got_pg;
    // 关于分配大内存来说 costly_order = true (超越 8 个内存页),需求首要进行内存收拾,这样内核可以避免直接内存收回然后获取更多的接连闲暇内存页
    // 关于需求分配不行移动的高阶内存的状况,也需求先进行内存收拾,避免永久内存碎片
    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;
        if (costly_order && (gfp_mask & __GFP_NORETRY)) {
            // 流程走到这儿标明通过内存收拾之后依然没有满意的内存供分配
            // 可是设置了 NORETRY 标识不答应重试,那么就直接失利,跳转到 nopage
            if (compact_result == COMPACT_SKIPPED ||
                compact_result == COMPACT_DEFERRED)
                goto nopage;
            // 同步内存收拾开支太大,后续敞开异步内存收拾
            compact_priority = INIT_COMPACT_PRIORITY;
        }
    }
retry:
    // 保证一切 kswapd 进程不要意外进入睡觉状况
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);
    // 流程走到这儿,阐明在 WMARK_MIN 水位线之上也分配内存失利了
    // 而且通过内存收拾之后,内存分配依然失利,阐明当时内存容量现已严峻缺乏
    // 接下来就需求运用愈加急进的十分手法来测验内存分配(疏忽掉内存水位线),持续修正 alloc_flags 保存在 reserve_flags 中
    reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
    if (reserve_flags)
        alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags);
    // 假如内存分配可以恣意跨节点分配(疏忽内存分配战略),这儿需求重置 nodemask 以及 zonelist。
    if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
        // 这儿的内存分配是高优先级体系级别的内存分配,不是面向用户的
        ac->nodemask = NULL;
        ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    }
    // 这儿运用从头调整的 zonelist 和 alloc_flags 在测验进行一次内存分配
    // 留意此次的内存分配是疏忽内存水位线的 ALLOC_NO_WATERMARKS
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        goto got_pg;
    // 在疏忽内存水位线的状况下依然分配失利,现在内核就需求进行直接内存收回了
    if (!can_direct_reclaim)
        // 假如进程不答应进行直接内存收回,则只能分配失利
        goto nopage;
    // 开端直接内存收回
    page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);
    if (page)
        goto got_pg;
    // 直接内存收回之后依然无法满意分配需求,则再次进行直接内存收拾
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;
    // 在内存直接收回和收拾悉数失利之后,假如不答应重试,则只能失利
    if (gfp_mask & __GFP_NORETRY)
        goto nopage;
    // 后续会触发 OOM 来开释更多的内存,这儿需求判别本次内存分配是否需求分配很多的内存页(大于 8 ) costly_order = true
    // 假如是的话则内核以为即便履行 OOM 也未必会满意这么多的内存页分配需求.
    // 所以仍是直接失利比较好,不再履行 OOM,除非设置 __GFP_RETRY_MAYFAIL
    if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
        goto nopage;
    // 流程走到这儿阐明咱们现已测验了一切办法内存依然分配失利了,此刻内存现已十分危急了。
    // 走到这儿阐明进程答应内核进行重试流程,但在开端重试之前,内核需求判别是否应该进行重试,重试规范:
    // 1 假如内核现已重试了 MAX_RECLAIM_RETRIES (16) 次依然失利,则抛弃重试履行后续 OOM。
    // 2 假如内核将一切可选内存区域中的一切可收回页面悉数收回之后,依然无法满意内存的分配,那么抛弃重试履行后续 OOM
    if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                 did_some_progress > 0, &no_progress_loops))
        goto retry;
    // 假如内核判别不该进行直接内存收回的重试,这儿还需求判别下是否应该进行内存收拾的重试。
    // did_some_progress 标明上次直接内存收回详细收回了多少内存页
    // 假如 did_some_progress = 0 则没有必要在进行内存收拾重试了,由于内存收拾的完成依靠于满意的闲暇内存量
    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;
    // 依据 nodemask 中的内存分配战略判别是否应该在进程所答应运转的一切 CPU 相关的 NUMA 节点上重试
    if (check_retry_cpuset(cpuset_mems_cookie, ac))
        goto retry_cpuset;
    // 最终的杀手锏,进行 OOM,选择一个得分最高的进程,开释其占用的内存 
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;
    // 只需 oom 产生了作用并开释了内存 did_some_progress > 0 就不断的进行重试
    if (did_some_progress) {
        no_progress_loops = 0;
        goto retry;
    }
nopage:
    // 流程走到这儿标明内核现已测验了包括 OOM 在内的一切收回内存的动作。
    // 可是这些办法依然无法满意内存分配的需求,看上去内存分配到这儿就应该失利了。
    // 可是假如设置了 __GFP_NOFAIL 标明不答应内存分配失利,那么接下来就会进入 if 分支进行处理
    if (gfp_mask & __GFP_NOFAIL) {
        // 假如不答应进行直接内存收回,则跳转至 fail 分支宣告失利
        if (WARN_ON_ONCE_GFP(!can_direct_reclaim, gfp_mask))
            goto fail;
        // 此刻内核现已无法通过收回内存来获取可供分配的闲暇内存了
        // 关于 PF_MEMALLOC 类型的内存分配恳求,内核现在力不从心,只能不断的进行 retry 重试。
        WARN_ON_ONCE_GFP(current->flags & PF_MEMALLOC, gfp_mask);
        // 关于需求分配 8 个内存页以上的大内存分配,而且设置了不行失利标识 __GFP_NOFAIL
        // 内核现在也力不从心,究竟现实是现已没有闲暇内存了,仅仅给出一些告警信息
        WARN_ON_ONCE_GFP(order > PAGE_ALLOC_COSTLY_ORDER, gfp_mask);
       // 在 __GFP_NOFAIL 状况下,测验进行跨 NUMA 节点内存分配
        page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
        if (page)
            goto got_pg;
        // 在进行内存分配重试流程之前,需求让 CPU 从头调度到其他进程上
        // 运转一会其他进程,由于究竟此刻内存现已严峻缺乏
        // 立马重试的话只能糟蹋过多时刻在查找闲暇内存上,导致其他进程处于饥饿状况。
        cond_resched();
        // 跳转到 retry 分支,重试内存分配流程
        goto retry;
    }
fail:
    warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
    return page;
}

现在内存分配流程中涉及到的三个重要辅佐函数:prepare_alloc_pages,__alloc_pages_slowpath,get_page_from_freelist 。笔者现已为咱们介绍了两个了。prepare_alloc_pages,__alloc_pages_slowpath 函数首要是依据不同的闲暇内存剩下容量调整内存的分配战略,尽量使内存分配行为尽最大或许成功。

了解了以上两个辅佐函数的逻辑,咱们就相当于收拾清楚了整个内存分配的链路流程。但现在咱们还没有涉及到详细内存分配的真实逻辑,而内核中履行详细内存分配动作是在 get_page_from_freelist 函数中,这也是把握内存分配的最终一道关卡。

由于 get_page_from_freelist 函数履行的是详细的内存分配动作,所以它和内核中的同伴体系有着千丝万缕的联系,而本文的主题愈加偏重描述整个物理内存分配的链路流程,考虑到文章篇幅的联系,笔者把同伴体系这部分的内容放在下篇文章为咱们讲解。

总结

本文首要从 Linux 内核中常见的几个物理内存分配接口开端,介绍了这些内存分配接口的各自的运用场景,以及接口函数中参数的意义。

深入理解 Linux 物理内存分配全链路实现

并以此为起点,结合 Linux 内核 5.19 版别源码详细评论了物理内存分配在内核中的整个链路完成。在整个链路中,内存的分配全体分为了两个途径:

  1. 快速途径 fast path:该途径的下,内存分配的逻辑比较简略,首要是在 WMARK_LOW 水位线之上快速的扫描一下各个内存区域中是否有满意的闲暇内存可以满意本次内存分配,假如有则立马从同伴体系中恳求,假如没有当即回来。

  2. 慢速途径 slow path:慢速途径下的内存分配逻辑就变的十分杂乱了,其间包括了内存分配的各种异常状况的处理,而且会依据文中介绍的 GFP_,ALLOC_ 等各种内存分配战略掩码进行不同分支的处理,整个链路十分庞大且冗杂。

本文铺垫了很多的内存分配细节,可是整个内存分配链路流程的精髓,笔者绘制在了下面这副流程图中,便利咱们忘掉的时分回顾。

深入理解 Linux 物理内存分配全链路实现