一、前言
在《深入理解Linux物理内存管理》《Linux 物理内存管理涉及的三大结构体之struct pglist》《Linux 物理内存管理涉及的三大结构体之struct zone》《Linux 物理内存管理涉及的三大结构体之struct page》中,给大家详细介绍了物理内存的三大模型:FLATMEM 平坦内存模型,DISCONTIGMEM 非连续内存模型和SPARSEMEM 稀疏内存模型。物理内存架构:一致性内存访问 UMA 架构和非一致性内存访问 NUMA 架构。Linux 分层管理物理内存的三大结构体:Node(struct pglist_data)->Zone(struct zone)->Page(struct page)。如下图,形象的展示了物理内存在内核中管理的层级关系。本文基于kernel-5.4/5.10代码分析。
通过上面内容,无论是从架构还是代码上面对Linux 物理内存管理有了一个深刻的认识。现在我们在此基础上出发,给大家介绍一些Linux kernel是如何根据这个架构来对物理内存进行分配/释放的?
二、物理内存分配/释放接口API
本节先介绍一下物理内存分配/释放的接口,这几个接口都是基于buddy system。就这些基于buddy system系统的接口而言,在UMA和NUMA物理内存架构下代码实现流程有稍微的差异,不过核心的函数都是一样,调用语法是相同的,搜索源码代码就能发现。
伙伴系统有一个特点就是,其分配的物理内存页全是物理上连续的,且只能分配2的整数次幂个页,以物理页为单位分配,整数幂在内核中称之为分配阶(order)。我们介绍的物理内存分配/释放接口均要指定order,用于表示从buddy system申请2^order个连续物理页。一个物理页(page)系统默认是4KB。
对于内核中细粒度的物理内存分配借助于slub allocator分配器,按字节为单位分配,但也是基于伙伴系统实现的,已有我写的文章《slub allocator工作原理》介绍,感兴趣的可以看下。对于Linux系统启动早期的bootmem allocator分配器,由于早期伙伴系统还没启动,早期的物理内存分配是靠这个分配器,而且该分配是指定所需内存大小分配,不是2^order个物理页,后面会出文章详细专门介绍,这里暂不细讲。
下面开始介绍基于伙伴系统,物理内存分配/释放的所有接口函数,如下列表所示。这里需要提一下:这些接口入参或者返回值是虚拟内存地址的,都是属于内核态虚拟内存空间,用户态虚拟内存空间的虚拟地址在malloc(),new/delete,mmap(),calloc(),realloc()里面,这里暂不涉及。
分配函数 | 描述 |
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) | 分配2^order个连续的物理页组成的内存块,返回值是一个struct page类型指针,指向该连续物理页中第一个物理页。因空闲内存无法满足而分配失败时,返回NULL |
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) | 分配一个物理页,返回值是一个struct page类型指针,指向该被分配的物理页。因空闲内存无法满足而分配失败时,返回NULL |
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) | 分配2^order个连续的物理页组成的内存块,返回值是物理内存块对应的虚拟地址。因空闲内存无法满足而分配失败时,返回0 |
#define __get_free_page(gfp_mask) \ __get_free_pages((gfp_mask), 0) | 分配一个物理页,返回值是该物理页对应的虚拟地址。因空闲内存无法满足而分配失败时,返回0 |
unsigned long get_zeroed_page(gfp_t gfp_mask) | 分配一个物理页且该物理页内容全部清0,返回值是该物理页对应的虚拟地址。因空闲内存无法满足而分配失败时,返回0 |
#define __get_dma_pages(gfp_mask, order) \ __get_free_pages((gfp_mask) | GFP_DMA, (order)) | 专门从ZONE_DMA物理内存区域,分配2^order个连续的物理页组成的内存块,用于执行DMA操作的设备,返回值是物理内存块对应的虚拟地址。因空闲内存无法满足而分配失败时,返回0 |
释放函数 | 描述 |
void __free_pages(struct page *page, unsigned int order) | 与alloc_pages对应,释放2^order个连续的物理页组成的内存块,第一个参数是指向该内存块中第一个物理页page实例的指针 |
#define __free_page(page) __free_pages((page), 0) | 释放一个物理页,第一个参数是指向该物理页page实例的指针 |
void free_pages(unsigned long addr, unsigned int order) | 与__get_free_pages对应,释放2^order个连续的物理页组成的内存块,第一个参数是指向该内存块对应的起始虚拟内存地址 |
#define free_page(addr) free_pages((addr), 0) | 释放一个物理页,第一个参数是指向该物理页对应的起始虚拟内存地址 |
注意:在内核编程中,通常我们除了直接使用这些函数外,有时我们我也常用kmalloc()/kfree(),vmalloc()/vfree(),kmem_cache_alloc()/kmem_cache_free()等来对物理内存操作。
2.1 alloc_pages/alloc_page函数
alloc_pages 函数用于向底层伙伴系统申请 2^order 个连续物理页组成的内存块,参数中的 unsigned int order 表示向底层伙伴系统指定的分配阶,参数 gfp_t gfp_mask 是内核中定义的一个用于规范物理内存分配行为的分配掩码,这里我们先不展开,后面的小节中会详细给大家介绍。该函数返回值是一个 struct page 类型的指针用于指向申请的内存块中第一个物理内存页。
从下面代码中,也可以看出UMA和NUMA架构下,alloc_pages函数的调用有所差异,其中,alloc_pages_current函数会根据当前NUMA节点的不同的内存分配策略,调用不同函数执行。但是它们最后均殊途同归的调用__alloc_pages来实现物理内存的分配。
//include/linux/gfp.h
#ifdef CONFIG_NUMA
extern struct page *alloc_pages_current(gfp_t gfp_mask, unsigned order);
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order);//alloc_pages_current函数会根据不同的NUMA节点的不同的内存分配策略,调用不同函数执行
}
#else
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
#endif
从实现中也可以看出,alloc_pages 函数用于分配多个连续的物理内存页,但在内核的某些内存分配场景中有时候并不需要分配这么多的连续内存页,而是只需要分配一个物理内存页即可,因而内核提供了已经封装好的 alloc_page 宏,用于这种单物理内存页分配的场景,我们可以看到其底层还是依赖了 alloc_pages 函数,只不过 order 默认指定为 0。
//include/linux/gfp.h
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
需要注意的,当系统中空闲物理内存无法满足内存分配而导致内存分配失败时,alloc_pages,alloc_page 返回的是空指针 NULL 。
插播一条:vmalloc 分配机制底层用的是 alloc_page。虽然vmalloc函数的实现需要用到底层的内存分配函数alloc_page,但它并不直接调用alloc_page函数,而是会调用vm_area_struct数据结构中的vm_ops->fault()函数。这个fault函数会完成内存页的初始化和内存映射等操作,并将一个虚拟地址地址返回给调用者。当用户程序需要使用到vmalloc函数分配的内存时,由于返回的是虚拟内存,因此需要使用缺页异常处理机制,执行vm_ops->fault()函数来完成内存映射等操作。在执行vm_ops->fault() 函数过程中,就会调用alloc_page函数进行物理页的分配操作。vmalloc函数返回的虚拟内存地址是内核态虚拟地址空间的,用户程序无法访问。而且根据vmalloc函数返回的虚拟地址,内核进程可以得到一段连续的虚拟地址空间,但是物理内存不一定是连续,由于该虚拟地址是内核态的,对内核进程是共享的。由于物理内存不是连续的,所以这个函数的运行效率通常比普通内存分配函数如 kmalloc() 低。
//mm/vmalloc.c
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
*
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*
* Return: pointer to the allocated memory or %NULL on error
*/
void *vmalloc(unsigned long size)
{
return __vmalloc_node(size, 1, GFP_KERNEL, NUMA_NO_NODE,
__builtin_return_address(0));
}
EXPORT_SYMBOL(vmalloc);
2.2 __get_free_pages/__get_free_page函数
从上面可知,在物理内存分配成功的情况下, alloc_pages,alloc_page 函数返回的都是指向其申请的物理内存块第一个物理页 struct page 类型指针,也可以直接理解成返回的是一块物理内存,但是 CPU 可以直接访问的却是虚拟内存,上述函数返回物理内存地址需要通过MMU转换后CPU才能访问,因而内核又提供了一个函数 __get_free_pages ,该函数直接返回物理页的虚拟内存地址。进程和CPU均可直接使用。
//mm/page_alloc.c
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
__get_free_pages 函数在使用方式上和 alloc_pages 是一样的,函数参数的含义也是一样,是分配2^order个连续的物理页组成的内存块。只不过一个是返回物理内存块的虚拟内存地址,一个是直接返回物理内存地址。需要注意的,__get_free_pages函数返回的虚拟内存地址页属于内核态虚拟内存空间,vmalloc虽然也是,但是__get_free_pages该虚拟地址对应的物理内存是连续,而vmalloc不一定是。
事实上,稍微看下 __get_free_pages 函数,就会发现其底层也是基于 alloc_pages 实现的,只不过多了一层虚拟地址转换的工作。
//mm/page_alloc.c
/*
* Common helper functions. Never use with __GFP_HIGHMEM because the returned
* address cannot represent highmem pages. Use alloc_pages and then kmap if
* you need to access high mem.
*/
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);
}
EXPORT_SYMBOL(__get_free_pages);
page_address 函数用于将给定的物理页 page 转换为它的虚拟内存地址,不过这里只适用于内核态虚拟内存空间中的直接映射区,因为在直接映射区中虚拟内存地址到物理内存地址是直接映射的,虚拟内存地址减去一个固定的偏移就可以直接得到物理内存地址。
如果物理内存页处于高端内存中,则不能这样直接进行转换,在通过 alloc_pages 函数获取物理内存页 page 之后,需要调用 kmap 映射将 page 映射到内核虚拟地址空间中,因而__get_free_pages函数不能用于分配高端内存ZONE_HIGHMEM的物理页,如果分配,用alloc_pages搭配kmap函数使用即可。不过高端内存只在32位系统中有,当前基本都是64位系统,很少遇到这种情况了。
那为什么不能用于高端内存,只是用于低端内存的直接映射区呢?
不同于vmalloc()是先获得虚拟地址空间,发生缺页异常再申请物理内存并建立映射关系,kmalloc()和__get_free_pages()都是先获取物理内存,再建立页表来映射关联的虚拟地址。由于ZONE_HIGHMEM中的映射是动态的,不能保证同虚拟地址的映射一定可以建立,而低端内存ZONE_DMA和ZONE_NORMAL属于直接映射区,是满足的,因而,kmalloc()和__get_free_pages()的gfp_mask都不可以是__GFP_HIGHMEM。
继续回来介绍 __get_free_page 函数,同 alloc_page 函数一样,用于分配单个物理页的场景,返回值是该物理页对应的虚拟地址,底层还是依赖于 __get_free_pages 函数,参数 order 默认指定为 0 。注意:__get_free_pages 和 __get_free_page分配内存失败,返回的是0。
//include/linux/gfp.h
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask), 0)
2.3 get_zeroed_page/__get_dma_pages函数
前面的 alloc_pages 和 __get_free_pages,它们申请到的物理内存块里面包含的数据一开始都是内核随机产生的一些垃圾信息,但是这些信息可能并不是完全是随机的,可能会包含一些敏感信息。
这些敏感的信息可能会被黑客所利用,并对计算机系统产生一些危害行为,因而从使用安全的角度考虑,内核提供了一个函数 get_zeroed_page,该函数会将从伙伴系统中申请的物理页全部初始化填充为 0 。从函数实现看,其也是依赖于__get_free_pages,order为0,分配一个物理页且该物理页内容全部清0,返回值是该物理页对应的虚拟地址。实现物理页内容全部清0操作,跟分配掩码__GFP_ZERO有关,下节会详细介绍GFP分配掩码。
//mm/page_alloc.c
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
EXPORT_SYMBOL(get_zeroed_page);
除此之外,内核还提供了一个 __get_dma_pages 函数,专门从 ZONE_DMA 物理内存区域分配适用于 DMA 的物理内存块,因为有些操作或者设备只能使用DMA的物理内存,那么用这个可以直接分配到属于ZONE_DMA的物理内存。其底层也是依赖于 __get_free_pages 函数,从ZONE_DMA 分配物理内存跟分配掩码GFP_DMA有关。该函数是分配2^order个连续的物理页组成的内存块,返回值是物理内存块对应的起始虚拟地址。
//include/linux/gfp.h
#define __get_dma_pages(gfp_mask, order) \
__get_free_pages((gfp_mask) | GFP_DMA, (order))
由于get_zeroed_page 和 __get_dma_pages 函数的底层均依赖于 __get_free_pages 函数,因此在遇到内存分配失败的情况下也是返回 0。
至此基于伙伴系统的物理内存分配接口就介绍完了,各个函数之间关系如下图所示。
2.4 __free_pages/__free_page函数
现在开始介绍伙伴系统的物理内存释放函数,对于__free_pages 其跟 alloc_pages 函数对应,用于释放2^order个连续的物理页组成的内存块,第一个参数是指向该内存块中第一个物理页page实例的指针。从实现看,其是循环调用free_the_page来实现物理内存的释放。
//mm/page_alloc.c
void __free_pages(struct page *page, unsigned int order)
{
trace_android_vh_free_pages(page, order);
if (put_page_testzero(page))
free_the_page(page, order);
else if (!PageHead(page))
while (order-- > 0)
free_the_page(page + (1 << order), order);
}
EXPORT_SYMBOL(__free_pages);
同时,内核页定义了__free_page宏,专门用于释放单个物理页,第一个参数是指向该物理页page实例的指针。
//include/linux/gfp.h
#define __free_page(page) __free_pages((page), 0)
2.5 free_pages/free_page函数
对于free_pages,其跟__get_free_pages 函数对应,与 __free_pages 函数的区别在于:释放物理内存时,使用了虚拟内存地址而不是 page 指针。释放2^order个连续的物理页组成的内存块,第一个参数是指向该内存块对应的起始虚拟内存地址。从代码实现看,首先是通过virt_to_page函数,将指向物理内存块里面第一个物理页的虚拟地址(即内存块起始虚拟地址)转换为指向第一个物理页page的物理地址,然后调用__free_pages完成实际的物理内存释放。
//mm/page_alloc.c
void free_pages(unsigned long addr, unsigned int order)
{
if (addr != 0) {
VM_BUG_ON(!virt_addr_valid((void *)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
EXPORT_SYMBOL(free_pages);
同时,内核页定义了free_page宏,专门用于释放单个物理页,第一个参数是指向该物理页对应的起始虚拟内存地址。
//include/linux/gfp.h
#define free_page(addr) free_pages((addr), 0)
注意:在释放内存时需要特别小心,只能释放属于自己的物理内存,因而需要特别注意传入的参数,传递了错误的 struct page 指针或者错误的虚拟内存地址,或者传递错了 order 值,都可能会导致系统的崩溃。在内核空间中,内核是完全信赖自己的,这点和用户空间不同。
至此基于伙伴系统的物理内存释放接口就介绍完了,各个函数之间关系如下图所示。
三、物理内存分配掩码gfp_mask
前面介绍了内核中对于物理内存分配和释放接口函数,大家也会发现,这些分配函数,其参数中均有gfp_t gfp_mask。这是gfp_mask就是物理内存分配掩码,是规范物理内存页分配行为的标志。前缀 gfp 是 get free page 的缩写。
那这个掩码究竟规范了哪些物理内存的分配行为 ?并对物理内存的分配有哪些影响呢 ?本节就给大家介绍。
gfp_mask对应的类型是gfp_t,根据如下定义,其是一个带有bitwise属性的unsigned int类型,长度是32位。
至于bitwise功能,这里大致描述一下,主要在编译时有所体现。第一、提供一种超强制的类型匹配检查,且每次使用bitwise属性总是创建一个新的数据类型;2、强制安全位运算,保证数据位不丢失或循环移动。不过这个功能起作用,那么必须使用Sparse,是由linux之父Linus开发的, 目的就是提供一个静态检查代码的工具, 从而减少linux内核的隐患。内核代码中有一个简略的关于 Sparse的说明文件: Documentation/sparse.txt。Sparse是一个独立于gcc的工具,默认一般不使能该工具。
//include/linux/types.h
//数据类型gfp_t 就是带有bitwise属性的unsigned int类型
typedef unsigned int __bitwise gfp_t;
关于gfp_mask的定义,在include/linux/gfp.h中。根据其用途,大致可分为如下几类:物理内存管理区域修饰符,移动修饰符,水位修饰符,页面回收修饰符和行为修饰符。下面开始逐一介绍。
3.1 物理内存管理区域修饰符
在《Linux 物理内存管理涉及的三大结构体之struct zone》中,我们知道实际的计算机体系结构受限硬件方面约束,物理页的使用也受到的一定的限制。因而kernel根据不同功能将系统中每个Node下面所管理的物理内存划分成不同的物理内存管理区域,主要有ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM,ZONE_MOVABLE和ZONE_DEVICE这几个物理内存区域。
ZONE_MOVABLE 区域是kernel从逻辑上的划分,是虚拟的,该区域中的物理内存页面来自于ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM这4个物理内存管理区域,目的是避免内存碎片和支持内存热插拔。ZONE_DEVICE区域是为支持热插拔设备而分配的非易失性内存,也可用于保留内核崩溃时的崩溃信息,便于调试,这个一般手机和普通电脑,甚至服务器上也没有,这里不做讨论。
当我们调用上小节介绍的那几个物理内存分配接口时,比如:alloc_pages 和 __get_free_pages。有时就要考虑我们申请的这些物理内存到底来自于哪个物理内存区域 zone,假如我们想要从指定的物理内存区域中申请内存,如何给内核指定?
//include/linux/gfp.h
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
//mm/page_alloc.c
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
内核在分配掩码gfp_mask解决了这个问题,里面的物理内存管理区域描述符,可以指定分配物理内存的物理内存管理区域。内核用 gfp_mask 中的低 4 位用来表示应该从哪个物理内存区域 zone 中获取内存页 page。
从下面代码可看出,内核定义了DMA,DMA32,HIGHMEM,MOVABLE的掩码,但是没有定义___GFP_NORMAL 的掩码。这是因为内核对物理内存的分配主要是落在 ZONE_NORMAL 区域中,如果我们不指定物理内存的分配区域,那么内核会默认从 ZONE_NORMAL 区域中分配内存,如果 ZONE_NORMAL 区域中的空闲内存不够,内核则会降级到 ZONE_DMA 区域中分配。
至于CMA,虽然不是内核定义的ZONE,但是如果使能了CONFIG_CMA,那么系统启动时,专门预留一块物理内存区域当做CMA区域,用于CMA(Contiguous Memory Allocator,连续内存分配器)进行内存的分配,主要用于多媒体相关的业务,因为伙伴系统最多只能分配4M的连续的物理内存,而且容易因内存碎片化,导致分配耗时长。__GFP_CMA表示分配物理内存时,从CMA区域分配。
这个CMA区域有个特点:第一、在 CMA 业务不使用时,允许其他业务有条件地使用 CMA 区域,条件是申请页面的属性必须是可迁移的。第二、当 CMA 业务使用时,把其他业务的页面迁移出 CMA 区域,以满足 CMA 业务的需求。
//include/linux/gfp.h
/* Plain integer GFP bitmasks. Do not use this directly. */
//这里可以看出内核用底4位用于指定物理内存区域zone
#define ___GFP_DMA 0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u
/* 至于CMA,如果使能了,那么系统启动时,专门预留一块物理内存区域当做CMA区域,用于CMA(Contiguous Memory Allocator,连续内存分配器)进行内存的分配,主要用于多媒体相关的业务,因为伙伴系统最多只能分配4M的连续的物理内存,而且容易因内存碎片化,导致分配耗时长
这个区域有个特点:在 CMA 业务不使用时,允许其他业务有条件地使用 CMA 区域,条件是申请页面的属性必须是可迁移的。当 CMA 业务使用时,把其他业务的页面迁移出 CMA 区域,以满足 CMA 业务的需求。
*/
#ifdef CONFIG_CMA
#define ___GFP_CMA 0x2000000u
#else
#define ___GFP_CMA 0
/*
* Physical address zone modifiers (see linux/mmzone.h - low four bits)
*
* Do not put any conditional on these. If necessary modify the definitions
* without the underscores and use them consistently. The definitions here may
* be used in bit comparisons.
*/
#define __GFP_DMA ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */
#define __GFP_CMA ((__force gfp_t)___GFP_CMA)
#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE) //物理内存区域的标志位集合,可用于判断物理内存使用具体哪个物理内存区域
回到正题,内核在mm/mmzone.c文件中定义了一个 gfp_zone 函数,这个函数用于将我们在物理内存分配接口中指定的 gfp_mask 掩码转换为物理内存区域,注意:返回的这个物理内存区域是内存分配的最高级内存区域,如果这个最高级内存区域不足以满足内存分配的需求,则按照的ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA顺序依次降级。
如下这个 gfp_zone 函数是在kernel-5.4实现的,在kernel-2.6.25版本中就启用了,当前版本使用了大量的移位操作替换了kernel低版本中原有的if-else语句,目的是为了提高程序的性能,但是带来的却是可读性的大幅下降。
//mm/mmzone.c
enum zone_type gfp_zone(gfp_t flags)
{
enum zone_type z;
gfp_t local_flags = flags;
int bit;
trace_android_rvh_set_gfp_zone_flags(&local_flags);
//通过跟GFP_ZONEMASK按位与,得到相应物理内存域的bit
bit = (__force int) ((local_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;
}
EXPORT_SYMBOL_GPL(gfp_zone);
为了让大家更好的理解,我们以kernel-2.6.25版本为例,介绍内核如何利用gfp_zone 函数将gfp_mask 掩码转换为物理内存区域。代码如下所示,实现逻辑非常清晰明了。
//include/linux/gfp.h 此时该函数是在gfp.h里面
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;
}
核心逻辑主要如下:
- 如果分配掩码flags中设置了__GFP_THISNODE,则base值增加MAX_NR_ZONES(即__MAX_NR_ZONES)。
-
只要掩码 flags 中设置了 __GFP_DMA,则不管 __GFP_HIGHMEM 有没有设置,内存分配都只会在 ZONE_DMA 区域中分配。
-
只要掩码 flags 中设置了 __GFP_DMA32,则不管 __GFP_HIGHMEM 有没有设置,内存分配优先在 ZONE_DMA32 区域中分配,若容量不足则降级到ZONE_DMA。
-
如果掩码只设置了 __GFP_HIGHMEM,则在物理内存分配时,优先在 ZONE_HIGHMEM 区域中进行分配,如果容量不够则降级到 ZONE_NORMAL 中,如果还是不够则进一步降级至 ZONE_DMA 中分配。
-
如果掩码既没有设置 __GFP_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 虚拟内存区域中的物理内存来自于上述三个物理内存区域。在64位系统中ZONE_MOVABLE 该内存区域取自ZONE_NORMAL和ZONE_DMA,32位系统中来自ZONE_HIGHMEM。
根据如上gfp_zone函数的将gfp_t 掩码转换为对应的物理内存区域逻辑,可得到如下不同的 gfp_t 掩码设置方式与其对应的物理内存区域降级策略汇总列表。
gfp_t 掩码 | 物理内存区域降级策略 |
---|---|
什么都没有设置 | ZONE_NORMAL -> ZONE_DMA32 -> ZONE_DMA |
__GFP_DMA | ZONE_DMA |
__GFP_DMA32 | ZONE_DMA32 -> ZONE_DMA |
__GFP_HIGHMEM | ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA |
__GFP_MOVABLE & __GFP_HIGHMEM | ZONE_MOVABLE,由于是虚拟的物理内存区域,只能在ZONE_MOVABLE,无降级策略 |
3.2 移动修饰符
移动修饰符主要用来指示分配出来的页面具有的移动属性。最早是在kernel-2.6.24中,为了解决外碎片化的问题,引入了迁移类型,因此在分配内存时需指定所分配的物理页具有哪些移动属性。
插播一下:内部碎片指:物理内存已经被分配出去(能明确指出属于哪个进程)却不能被利用的物理内存空间,相当于分配的物理内存大于进程的申请的物理内存,导致多出来的冗余内存其他进程页无法使用,当然进程共享物理内存的除外。外部碎片指:物理内存还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存的进程的内存空闲区域,ZONE_MOVABLE避免的内存碎片主要就是针对这种。
//include/linux/gfp.h
#define ___GFP_RECLAIMABLE 0x10u
#define ___GFP_WRITE 0x1000u
#define ___GFP_HARDWALL 0x100000u
#define ___GFP_THISNODE 0x200000u
#define ___GFP_ACCOUNT 0x400000u
/**
* DOC: Page mobility and placement hints
* Page mobility and placement hints
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* These flags provide hints about how mobile the page is. Pages with similar
* mobility are placed within the same pageblocks to minimise problems due
* to external fragmentation.
*/
#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)
#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE)
#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL)
#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE)
#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT)
__GFP_RECLAIMABLE用于指定分配的页面是可以回收的。如果在slub 分配器中,指定了SLAB_RECLAIM_ACCOUNT标志位,则在slab中使用的可回收物理页面是通过shrinkers来回收。这里同步提下__GFP_MOVABLE ,其是用于指定分配的页面是可以移动的,这两个标志会影响底层的伙伴系统从哪个区域中去获取空闲内存页,后面讲解伙伴系统的时候详细介绍。
__GFP_WRITE用于指定分配的页面是可写的。通常在需要写入数据的情况下使用这一标志,可告诉内核在分配内存时需要考虑写入操作对内存的影响,以免在写入数据时造成内存泄漏或其他问题,不过需要注意的是,虽然这个分配掩码代表可写,但是不保证一定能成功往这个页面写入数据,毕竟需要考虑其他因素,如内存访问权限等。
__GFP_HARDWALL,使能了cupset后才有的分配策略,用于限制当前进程只能从可运行的CPU相关联的NUMA节点上分配。既然是cpuset,那么显然意味着进程不能在所有CPU上运行,否则,这个标志没有任何意义。
__GFP_THISNODE用于从指定的NUMA节点或者当前NUMA节点分配物理页面,而且当内存分配失败时,不允许从其他备用 NUMA 节点中分配内存,即没有fallback机制。
__GFP_ACCOUNT表示分配的物理页面的过程会被kmemcg记录。kmemcg 是 Linux 内核中的一个资源控制子系统,用于控制进程/模块所使用的内存。它是基于 cgroup 技术实现的,可以将进程/模块的内存用量限制在一定范围内,以避免它们占用过多的内存导致系统崩溃。
3.3 水位修饰符
水位修饰符用于控制是否可以访问系统预留的紧急内存。这个紧急内存实际上就是Linux 物理内存管理涉及的三大结构体之struct zone中_watermark[WMARK_MIN]/nr_reserved_highatomic对应的内存。普通优先级的内存分配请求,是没有资格访问紧急内存,因为这部分内存是用于内存分配绝对不能失败的进程使用的,如用于中断程序,以及持有自旋锁的进程上下文中。这些进程会有高优先级的分配掩码,如__GFP_HIGH,__GFP_ATOMIC等。
//include/linux/gfp.h
#define ___GFP_HIGH 0x20u
#define ___GFP_ATOMIC 0x200u
#define ___GFP_MEMALLOC 0x20000u
#define ___GFP_NOMEMALLOC 0x80000u
/**
* DOC: Watermark modifiers
*
* Watermark modifiers -- controls access to emergency reserves
*/
#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC)
#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH)
#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC)
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC)
__GFP_ATOMIC表示在分配内存时申请内存的进程不能执行页面回收或者睡眠动作,必须原子性执行内存分配。比如在中断处理程序中,就不能睡眠,因为中断程序不能被重新调度。同时也不能在持有自旋锁的进程上下文中睡眠,因为可能导致死锁。综上所述这个标志只能用在不能被重新安全调度的进程上下文中。
__GFP_HIGH表示分配内存请求是高优先级的,设置该标志通常表示需求紧急,不允许失败。在物理空闲内存不足情况下,可以从紧急预留内存中分配。
__GFP_MEMALLOC表示分配过程中允许访问所有的内存,包括系统预留的紧急内存。使用该标示时需要保证进程在获得内存之后会很快的释放掉内存不会过长时间的占用,尤其要警惕避免过多的消耗紧急预留内存区域中的内存。
__GFP_NOMEMALLOC表示分配过程中明确禁止使用系统预留的紧急内存。该优先级是要高于___GFP_MEMALLOC。
3.4 页面回收修饰符
代码如下所示。
//include/linux/gfp.h
#define ___GFP_IO 0x40u
#define ___GFP_FS 0x80u
#define ___GFP_DIRECT_RECLAIM 0x400u
#define ___GFP_KSWAPD_RECLAIM 0x800u
#define ___GFP_RETRY_MAYFAIL 0x4000u
#define ___GFP_NOFAIL 0x8000u
#define ___GFP_NORETRY 0x10000u
/**
* DOC: Reclaim modifiers
*
* Reclaim modifiers
* ~~~~~~~~~~~~~~~~~
* Please note that all the following flags are only applicable to sleepable
* allocations (e.g. %GFP_NOWAIT and %GFP_ATOMIC will ignore them).
*/
#define __GFP_IO ((__force gfp_t)___GFP_IO)
#define __GFP_FS ((__force gfp_t)___GFP_FS)
#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */
#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))
#define __GFP_RETRY_MAYFAIL ((__force gfp_t)___GFP_RETRY_MAYFAIL)
#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL)
#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY)
__GFP_IO表示在分配物理内存时,允许执行磁盘的I/O操作。当内核进行内存分配时,发现物理空闲内存不足,这时可以将不经常使用的内存页置换到 SWAP 分区(SWAP disk或者 SWAP file)中,这时就涉及到了 IO 操作,如果设置了该标志,表示允许内核将不常用的内存页置换出去。
__GFP_FS表示允许内核执行底层文件系统操作,在与 VFS 虚拟文件系统层相关联的内核子系统中必须禁用该标志,否则可能会引起文件系统操作的循环递归调用,从而产生死锁,因为在设置 ___GFP_FS 标志分配内存的情况下,可能会引起更多的文件系统操作,而这些文件系统的操作可能又会进一步产生内存分配行为,这样一直递归持续下去。
__GFP_DIRECT_RECLAIM表示在分配物理内存时,进程有条件可同步进行直接内存回收。当剩余内存容量低于水位线 _watermark[WMARK_MIN] 时,说明此时的内存容量已经非常危险了,如果进程在这时请求内存分配,就会先进行直接内存回收,再尝试分配内存。注意:直接内存回收后,不保证一定能满足分配需求,在buddy system的慢速分配路径__alloc_pages_slowpath函数实现中可以发现,直接内存回收和内存规整会重复执行多次,直到满足特定没法回收的条件会进入OOM。这部分内容后面5.3节会细讲。
__GFP_KSWAPD_RECLAIM表示在分配内存时,如果剩余内存容量在 _watermark[WMARK_MIN] 与 _watermark[WMARK_LOW] 之间时,内核会唤醒 kswapd 进程开始异步内存回收,直到剩余内存高于 _watermark[WMARK_HIGH] 为止。
__GFP_RECLAIM从上面代码的定义可知,其是__GFP_DIRECT_RECLAIM和__GFP_KSWAPD_RECLAIM的并集。表示在分配物理内存时,若剩余物理内存容量达到一定条件时,可以进行直接内存回收和使用kswapd线程异步回收,以满足分配需求。更多关于水线,直接内存回收和kswapd内容,在Linux 物理内存管理涉及的三大结构体之struct pglist_data中2.11-2.17和Linux 物理内存管理涉及的三大结构体之struct zone中2.1中细讲,感兴趣的可以看下。
__GFP_RETRY_MAYFAIL表示若在分配物理内存失败时,允许重试,但重试仍然可能失败,重试若干次后停止。与其对应的是 ___GFP_NORETRY 标志表示分配内存失败时不允许重试。
__GFP_NOFAIL表示在分配物理内存失败时,一直尝试分配,直到成功为止。当进行希望分配内存不失败时,应该使用这个标志位或者__GFP_HIGH,而不是自己写一个while循环来不断调用页面分配接口函数。
__GFP_NORETRY表示在分配物理内存失败后,包括判断可否和尝试直接内存回收和内存规整后,从5.3节代码可知其是做了这两个动作的,最后不再重试。
3.5 行为修饰符
行为修饰符代码定义如下。
//include/linux/gfp.h
#define ___GFP_NOWARN 0x2000u
#define ___GFP_COMP 0x40000u
#define ___GFP_ZERO 0x100u
#define ___GFP_ZEROTAGS 0x800000u
#define ___GFP_SKIP_KASAN_POISON 0x1000000u
/**
* DOC: Action modifiers
*
* Action modifiers
* ~~~~~~~~~~~~~~~~
*
* %__GFP_NOWARN suppresses allocation failure reports.
*
* %__GFP_COMP address compound page metadata.
*
* %__GFP_ZERO returns a zeroed page on success.
*
* %__GFP_ZEROTAGS returns a page with zeroed memory tags on success, if
* __GFP_ZERO is set.
*
* %__GFP_SKIP_KASAN_POISON returns a page which does not need to be poisoned
* on deallocation. Typically used for userspace pages. Currently only has an
* effect in HW tags mode.
*/
#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN)
#define __GFP_COMP ((__force gfp_t)___GFP_COMP)
#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO)
#define __GFP_ZEROTAGS ((__force gfp_t)___GFP_ZEROTAGS)
#define __GFP_SKIP_KASAN_POISON ((__force gfp_t)___GFP_SKIP_KASAN_POISON)
__GFP_NOWARN表示在分配物理内存时,不打印分配过程中任何警告信息。避免在kernel log中打印过多日志.
__GFP_COMP表示在分配物理内存时,从复合页中分配。
__GFP_ZERO表示在分配物理内存时,分配的物理页内容全部填充为0。get_zeroed_page 函数就用了这个分配掩码。
__GFP_ZEROTAGS表示在分配物理内存时,若成功分配的物理页面全部填充为0,那么使用该掩码可同步给对应的物理页面的内存标签(TAG)清0,以提升部分性能,省去另外单独清0标签。
__GFP_SKIP_KASAN_POISON表示在分配物理内存时,跳过KASAN(Kernel Address Sanitizer)毒化内存页的步骤。从而提高部分性能。一般只有使能CONFIG_KASAN_HW_TAGS,再使用这个分配掩码才意义。
3.6 常用gfp_mask分配掩码组合
由于前面描述的修饰符种类繁多,而且内核在使用时一般都是组合使用,因而内核定义了一些常用的分配掩码组合,供大家使用。常用gfp_mask分配掩码组合代码定义如下。
//include/linux/gfp.h
/**
* DOC: Useful GFP flag combinations
*
* Useful GFP flag combinations
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#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)
#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE | \
__GFP_SKIP_KASAN_POISON)
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
#define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)
/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
GFP_ATOMIC是掩码 __GFP_HIGH,__GFP_ATOMIC,__GFP_KSWAPD_RECLAIM 的组合。表示分配物理内存的行为是原子的(即任何情况下,不允许睡眠或进程本身下场做直接内存回收),且是高优先级的。若剩余物理空闲内存不足,允许从系统预留的紧急内存中分配。该标志适用于中断程序,以及持有自旋锁的进程上下文中。
GFP_KERNEL是内核最常用的物理内存分配掩码。表示分配物理内存的行为可能被阻塞,进入睡眠,而且分配内存时,若空闲内存无法满足时,可执行直接内存或者使用kswapd线程异步回收,也可将不常用的匿名内存页置换到磁盘中。适用于可以重新安全调度的进程上下文中,避免死锁问题或其他系统异常。跟GFP_ATOMIC刚好相反。
GFP_KERNEL_ACCOUNT和GFP_KERNEL作用一样,但是分配的过程会被kmemcg记录。
GFP_NOWAIT看代码定义,跟__GFP_KSWAPD_RECLAIM一样,表示在分配物理内存时,进程不允许睡眠等待,若剩余空闲内存不足,只能异步回收,不能同步回收,阻塞申请内存的进程。
GFP_NOIO看代码定义,跟__GFP_RECLAIM一样,表示在分配物理内存时,可以进行直接内存回收和使用kswapd线程异步回收,以满足分配需求,但禁止任何I/O操作(相当于禁止了SWAP机制)。当然分配过程时可以被阻塞的,因为这里没有限制这条。
GFP_NOFS表示在分配物理内存时,可以进行直接内存回收和使用kswapd线程异步回收,以满足分配需求,但禁止任何文件系统操作。
GFP_USER表示分配的物理内存是给用户空间进程和程序用的。跟用户态虚拟地址空间建立映射。通常这些内存可以被内核或者硬件直接访问,比如硬件设备会将 Buffer 直接映射到用户空间中,跟DMA相关。
GFP_DMA表示从ZONE_DMA物理内存区域分配物理内存。
GFP_DMA32表示从ZONE_DMA32物理内存区域分配物理内存。
GFP_HIGHUSER表示从ZONE_HIGHMEM分配物理内存给用户空间进程和程序使用。
GFP_HIGHUSER_MOVABLE表示从ZONE_HIGHMEM物理内存区域分配可迁移的页面给用户空间进程和程序使用。
GFP_TRANSHUGE_LIGHT用于透明大页(THP)的分配,表示从ZONE_HIGHMEM物理内存区域的复合页中分配可迁移的物理页,分配过程中不打印任何警告信息,且当剩余物理内存不满足时,禁止使用直接内存回收,使用kswapd线程异步回收和系统预留的紧急内存。
GFP_TRANSHUGE也用于透明大页(THP)的分配,相比GFP_TRANSHUGE_LIGHT,在剩余物理内存不满足时,可使用直接内存回收。
GFP_MOVABLE_MASK表示在分配物理内存时,分配的物理页是可移动的和可回收的。该分配掩码可用于判断页面是否是可回收和可移动的。
四、伙伴系统物理内存分配核心__alloc_pages_nodemask
前面已经介绍物理内存分配的接口和相应的分配掩码gfp_mask,同时分析接口函数的调用,物理内存的分配最终是落到了__alloc_pages_nodemask函数里面。
之所以不是__alloc_pages,是因为如果物理内存架构是NUMA话,根据NUMA节点不同的内存分配策略,会调用不同函数执行,但最终是走到__alloc_pages_nodemask。而对于UMA物理内存结构,最终也是走到__alloc_pages_nodemask。因而无论哪种物理内存架构,最终在__alloc_pages_nodemask合为一处。关于NUMA节点的物理内存分配策略和节点状态node_states(对应__alloc_pages_nodemask的入参nodemask)在深入理解Linux物理内存管理中的3.2.1和4.4节有细讲,感兴趣的可以看下。
因而__alloc_pages_nodemask才是基于伙伴系统,进行物理内存分配的核心,无论是UMA还是NUMA物理内存架构。当然不同kernel版本代码在这里是有差异的,其中kernel-5.4,5.10还是如上,但在kernel-5.15开始,内核进一步简化,少了很多封装,此时__alloc_pages就是基于伙伴系统,物理内存分配的核心。
//include/linux/gfp.h
#ifdef CONFIG_NUMA
extern struct page *alloc_pages_current(gfp_t gfp_mask, unsigned order);
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order);//alloc_pages_current函数会根据不同的NUMA节点的不同的内存分配策略,调用不同函数执行,最终也是走到__alloc_pages_nodemask
}
#else
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_node(numa_node_id(), gfp_mask, order); //如下面的代码调用关系所示,最后是走到了__alloc_pages_nodemask
}
#endif
//UMA物理内存架构
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
if (nid == NUMA_NO_NODE)
nid = numa_mem_id();
return __alloc_pages_node(nid, gfp_mask, order);
}
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
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);
}
static inline struct page *
__alloc_pages(gfp_t gfp_mask, unsigned int order, int preferred_nid)
{
return __alloc_pages_nodemask(gfp_mask, order, preferred_nid, NULL);
}
//NUMA物理内存架构
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
struct mempolicy *pol = &default_policy;//开始是默认的物理内存分配策略
struct page *page;
if (!in_interrupt() && !(gfp & __GFP_THISNODE))
pol = get_task_policy(current); //若当前进程没有处于中断上下文且没有限制在当前NUMA或指定NUMA节点,则在这里获取该进程的NUMA节点的物理内存分配策略,否则就是上面的默认策略
/*
* No reference counting needed for current->mempolicy
* nor system default_policy
*/
//根据不同物理内存分配策略,调用不同函数分配物理内存
//MPOL_INTERLEAVE表示本地节点和远程节点均可允许分配内存
if (pol->mode == MPOL_INTERLEAVE)
page = alloc_page_interleave(gfp, order, interleave_nodes(pol));//alloc_page_interleave函数走__alloc_pages方式获取内存,最后调用__alloc_pages_nodemask获取物理内存
else
/* 非MPOL_INTERLEAVE,统一进入这里直接__alloc_pages_nodemask函数
* policy_node函数根据物理内存策略pol再进行一些判断,返回期望从那个NUMA节点分配内存
* policy_nodemask 根据物理内存策略pol再进行一些判断,返回对应NUMA节点的状态
*/
page = __alloc_pages_nodemask(gfp, order,
policy_node(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));//直接调用__alloc_pages_nodemask获取物理内存
return page;
}
EXPORT_SYMBOL(alloc_pages_current);
如下是__alloc_pages_nodemask的入参,comment里面也说了This is the 'heart' of the zoned buddy allocator。其中gfp_mask是分配掩码,order是前面提及的分配阶,perferred_nid是期望从哪个NUMA/UMA节点分配物理内存,nodemask即为对应节点状态node_states。
//mm/page_alloc.c
/*
* This is the 'heart' of the zoned buddy allocator.
*/
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
4.1 内存分配行为标识掩码 alloc_flags
进入__alloc_pages_nodemask后,第二个局部变量就是alloc_flags,这也是影响内存分配行为的掩码,以ALLOC_开头,是在伙伴系统内部申请内存时使用的。相比gfp_mask使用范围,alloc_flags范围小。这里我们同步介绍一下这些掩码。
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;//这里
gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
struct alloc_context ac = { };
......
}
ALLOC掩码定义如下。不过在内核中,alloc_flags也可以从gfp_mask经过gfp_to_alloc_flags函数计算转换得到的。
///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 /* don't check watermarks at all */
/* Mask to get the watermark bits */
#define ALLOC_WMARK_MASK (ALLOC_NO_WATERMARKS-1)
/*
* Only MMU archs have async oom victim reclaim - aka oom_reaper so we
* cannot assume a reduced access to memory reserves is sufficient for
* !MMU
*/
#ifdef CONFIG_MMU
#define ALLOC_OOM 0x08
#else
#define ALLOC_OOM ALLOC_NO_WATERMARKS
#endif
#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_CMA 0x80 /* allow allocations from CMA areas */
#ifdef CONFIG_ZONE_DMA32
#define ALLOC_NOFRAGMENT 0x100 /* avoid mixing pageblock types */
#else
#define ALLOC_NOFRAGMENT 0x0
#endif
#define ALLOC_KSWAPD 0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */
首先前面三个ALLOC_WMARK_MIN,ALLOC_WMARK_LOW,ALLOC_WMARK_HIGH跟物理内存区域zone的水线有关。我们知道伙伴系统管理的物理内存页来源就是struct zone里面的free_area[MAX_ORDER],其是buddy system的核心数据结构。因而,这三个掩码表示了伙伴系统在分配物理内存时,需要考虑的水线,不同水线会有不同的内存分配行为。关于水线和free_area的内容,在Linux 物理内存管理涉及的三大结构体之struct zone中的2.1和2.19节有详细介绍,感兴趣的可以看下。
-
当该物理内存区域的剩余内存容量高于 _watermark[WMARK_HIGH] 时,说明此时该物理内存区域中的内存充足,内存分配没有压力。
-
当剩余内存容量在_watermark[WMARK_HIGH] 与_watermark[WMARK_LOW]之间时,说明此时内存有一定的消耗但还可以接受,依然能够满足进程的内存分配需求,不需要启动内存回收。
-
当剩余内存容量在 _watermark[WMARK_LOW] 与 _watermark[WMARK_MIN]之间时,说明此时内存容量已经吃紧了,内存分配面临一定的压力,但是还可以满足进程此时的内存分配要求,不过就是一边给进程分配内存,同时内核会唤醒 kswapd 进程开始内存异步回收,直到剩余内存高于 _watermark[WMARK_HIGH] 为止,kswapd进程才会进入睡眠状态。
-
当剩余内存容量低于 _watermark[WMARK_MIN] 时,说明此时的内存容量已经非常危险,剩余内存不够用了,如果进程在这时请求内存分配,那么进程就会被阻塞,亲自下场进行直接内存回收。
注意:前面说的每个物理内存管理区域的剩余内存(即被伙伴系统所管理的内存)是要剔除掉lowmem_reserve[MAX_NR_ZONES]里面预留的内存大小。
在大致概述了三条水位线的意义后,以及其物理内存分配的不同行为后,回到正题继续介绍alloc_flags。
ALLOC_WMARK_MIN表示伙伴系统在分配物理内存时,当前zone中剩余空闲物理页数量至少要达到_watermark[WMARK_MIN],才能进行分配。
ALLOC_WMARK_LOW表示在分配物理内存时,当前zone中剩余空闲页数量至少要达到_watermark[WMARK_LOW],才能进行分配。
ALLOC_WMARK_HIGH 表示在分配物理内存时,当前zone中剩余空闲页数量至少要达到 _watermark[WMARK_HIGH],才能进行分配。
ALLOC_NO_WATERMARKS 表示在分配物理内存时,完全不考虑上述三个水线的影响。
ALLOC_WMARK_MASK跟ALLOC_NO_WATERMARKS相反,表示在分配物理内存时,需要考虑上述三个水线的影响。
ALLOC_OOM表示在分配物理内存时,在经历过多轮的直接内存回收和内存规整后,允许动用OOM,将OOM优先级高的进程kill,以释放内存,来满足当前进程的内存分配需求。这部分在慢速分配路径__alloc_pages_slowpath有实现。本文后面细讲。注意:该掩码需在使能了MMU后,才有效,不过kernel一般默认使能MMU。
ALLOC_HARDER表示在分配物理内存时,会降低内存分配的下限,就是降低_watermark[WMARK_MIN]的水线值,以让内存分配成功。同时若有这个alloc_flags,则页迁移类型是MIGRATE_HIGHATOMIC保留的内存(即nr_reserved_highatomic)也可以使用。关于ALLOC_HARDER的使用,一般在分配物理内存,检查watermark时会使用到(__zone_watermark_ok函数),如在Linux 物理内存管理涉及的三大结构体之struct zone中的2.4.1节有详细介绍,感兴趣的可以看下。
ALLOC_HIGH跟__GFP_HIGH一样,一般在gfp_mask中设置了__GFP_HIGH,ALLOC_HIGH才有意义。表示分配内存请求是高优先级的,设置该标志通常表示需求紧急,不允许失败。在物理空闲内存不足情况下,可以从紧急预留内存中分配。该标志比ALLOC_HARDER更紧急。
ALLOC_CPUSET跟__GFP_HARDWALL一样,使能了cupset后才有的分配策略,用于限制当前进程只能从可运行的CPU相关联的NUMA节点上分配。既然是cpuset,那么显然意味着进程不能在所有CPU上运行,否则,这个标志没有任何意义。
ALLOC_CMA跟__GFP_CMA一样,表示从CMA中申请内存。关于CMA的部分,前面已经3.1节已经讲了。
ALLOC_NOFRAGMENT表示在分配物理内存时,只能从当前或者指定NUMA节点对应的zone下面分配内存,禁止fallback策略,不能跨节点申请内存。用于优化内存碎片,且在使能了CONFIG_ZONE_DMA32才有意义。
ALLOC_KSWAPD表示在分配物理内存时,若出现剩余空闲内存低于_watermark[WMARK_LOW],允许唤醒kswapd进程,进行内存的异步回收。
4.2 物理内存分配上下文参数容器alloc_context
沿着__alloc_pages_nodemask函数实现,继续往下看会发现一个新的结构体struct alloc_context。这是一个保存伙伴系统物理内存分配过程中一些重要参数的结构体,做到跟踪内存分配状态和信息的作用。
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
struct alloc_context ac = { };//这里
......
}
结构体struct alloc_context定义如下,该结构体主要在prepare_alloc_pages进行初始化,4.3节介绍该函数,后续根据一些动态条件在快速路径分配get_page_from_freelist和慢速路径分配__alloc_pages_slowpath中对该结构体实时更新。
//mm/internal.h
/*
* Structure for holding the mostly immutable allocation parameters passed
* between functions involved in allocations, including the alloc_pages*
* family of functions.
*
* nodemask, migratetype and highest_zoneidx are initialized only once in
* __alloc_pages_nodemask() and then never change.
*
* zonelist, preferred_zone and highest_zoneidx are set first in
* __alloc_pages_nodemask() for the fast path, and might be later changed
* in __alloc_pages_slowpath(). All other functions pass the whole structure
* by a const pointer.
*/
struct alloc_context {
//NUMA节点的物理内存区域zone及其所有备用NUMA节点的zone按照优先级顺序排列组成的链表
struct zonelist *zonelist;
//NUMA节点的状态码node_states
nodemask_t *nodemask;
//指定最希望从哪个物理内存区域zone分配
struct zoneref *preferred_zoneref;
//物理内存页的迁移类型,根据gfp_mask得到
int migratetype;
/*
* highest_zoneidx represents highest usable zone index of
* the allocation request. Due to the nature of the zone,
* memory on lower zone than the highest_zoneidx will be
* protected by lowmem_reserve[highest_zoneidx].
*
* highest_zoneidx is also used by reclaim/compaction to limit
* the target zone since higher zone than this index cannot be
* usable for this allocation request.
*/
//表示内存分配最高优先级的内存区域 zone,利用前面介绍的gfp_zone(gfp_mask)函数得到
enum zone_type highest_zoneidx;
//表示是否考虑脏页平衡,若考虑(true),那么在某个zone的脏页超过预设的最大值,则本次分配不在该zone上进行
bool spread_dirty_pages;
};
4.3 物理内存分配核心__alloc_pages_nodemask
前面做了很多铺垫,这里正是开始介绍物理内存分配heart:__alloc_pages_nodemask的总体流程。暂时先不对__alloc_pages_nodemask函数里面的每个子函数进行细节的分析,因为内核整个伙伴系统物理内存分配的完整过程全部封装在这里,非常的复杂。在本文后面以及下篇文章Buddy System原理与实现里面会逐一挨个介绍分析。
如下是对__alloc_pages_nodemask函数沿着顺序,每部分要做的事情大致介绍了一下。大家可以过一遍,以对流程有个熟悉。
//关于gfp_allowed_mask的介绍
//include/linux/gfp.h
/*
* gfp_allowed_mask is set to GFP_BOOT_MASK during early boot to restrict what
* GFP flags are used before interrupts are enabled. Once interrupts are
* enabled, it is set to __GFP_BITS_MASK while the system is running. During
* hibernation, it is used by PM to avoid I/O during memory allocation while
* devices are suspended.
*/
extern gfp_t gfp_allowed_mask;
//mm/internal.h
/* The GFP flags allowed during early boot */
#define GFP_BOOT_MASK (__GFP_BITS_MASK & ~(__GFP_RECLAIM|__GFP_IO|__GFP_FS))
//mm/page_alloc.c
gfp_t gfp_allowed_mask __read_mostly = GFP_BOOT_MASK;
//主角
/*
* This is the 'heart' of the zoned buddy allocator.
*/
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask) //nodemask一般是NULL
{
//指向成功分配物理页或物理内存块的第一个物理页
struct page *page;
//内存分配行为标识掩码alloc_flags初始化为ALLOC_WMARK_LOW
unsigned int alloc_flags = ALLOC_WMARK_LOW;
//实际用于内存分配的分配掩码
gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
//保存物理内存分配过程中的上下文参数
struct alloc_context ac = { };
/*
* There are several places where we assume that the order value is sane
* so bail out early if the request is out of bound.
*/
//如果分配阶order大于等于MAX_ORDER,MAX_ORDER为11,则直接分配失败,打印警告信息,返回NULL
//order在伙伴系统中最大值为:MAX_ORDER-1 = 10
if (unlikely(order >= MAX_ORDER)) {
WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
return NULL;
}
//gfp_allowed_mask表示允许的gfp标志位,具体定义和comment如上所示
//在启动阶段和kernel_init_freeable之后的阶段,gfp_allowed_mask的内容是有可能不一样
//其中启动阶段,不允许分配__GFP_RECLAIM|__GFP_IO|__GFP_FS这三种类型的内存
//最后将更新后的gfp_mask赋值给alloc_mask,得到实际用于分配内存的分配掩码
gfp_mask &= gfp_allowed_mask;
alloc_mask = gfp_mask;
//1、初始化 alloc_context,并为接下来的内存分配更新合适的alloc_flags,正常返回true
if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
return NULL;
/*
* Forbid the first pass from falling back to types that fragment
* memory until all local zones are considered.
*/
//首先,若gfp_mask设置了__GFP_KSWAPD_RECLAIM,则alloc_flags要添加上ALLOC_KSWAPD;其次如果有ZONE_DMA32区域,则alloc_flags要添加上ALLOC_NOFRAGMENT,其跟优化内存碎片化相关
alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);
/* First allocation attempt */
//2、物理内存分配快速路径:尝试直接从struct zone里面的pcp或者free_area上分配内存,如果失败则要走下面的慢速路径
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
//如果快速路径分配成功,直接跳转到out,准备返回
goto out;
/*
* Apply scoped allocation constraints. This is mainly about GFP_NOFS
* resp. GFP_NOIO which has to be inherited for all allocation requests
* from a particular context which has been marked by
* memalloc_no{fs,io}_{save,restore}.
*/
//执行到这里,说明快速路径分配失败,alloc_mask和spread_dirty_pages状态值要复原,以防在快速路径中被污染,影响慢速分配
//如果当前进程的task struct->flags有PF_MEMALLOC_NOIO或者PF_MEMALLOC_NOFS属性,则需要根据前面更新过的gfp_mask来调整alloc_mask
//PF_MEMALLOC_NOIO:表示分配内存时,禁用IO操作和文件系统操作,提高性能和避免死锁
//PF_MEMALLOC_NOFS:表示分配内存时,仅禁用文件系统操作,避免死锁
//看代码实现实际上,不允许分配__GFP_IO|__GFP_FS这两种类型的内存
alloc_mask = current_gfp_context(gfp_mask);
ac.spread_dirty_pages = false;
/*
* Restore the original nodemask if it was potentially replaced with
* &cpuset_current_mems_allowed to optimize the fast-path attempt.
*/
//同上,也是nodemask状态值复原,看物理内存分配调用路径可知,这个nodemask一般是NULL
ac.nodemask = nodemask;
//3、物理内存慢速路径分配:在快速分配无法满足时,在慢速分配这里内核需要做更多内容,如kswapd异步回收,直接内存回收,内存规整和OOM,
//来满足物理内存分配需求,这一系列的行为组成了慢速路径分配
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge_page(page, gfp_mask, order) != 0)) {
//若使能了kmemecg,有分配掩码__GFP_ACCOUNT且分配到了物理页(物理内存块),
//但是却无法将这个物理页面登记进该需要内存的进程,其对应的内存控制组memcg,则返回非0,否则返回0
//看struct page里面的参数mem_cgroup就是为了实现这个功能
//若为非0,则该page不符合要求,直接就释放,将page置为NULL,分配内存失败
__free_pages(page, order);
page = NULL;
}
//跟踪并记录与内存页面分配相关的事件,用于定位内存分配问题
trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);
//内存分配成功,直接返回 page,否则返回 NULL
return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);
从上面函数实现物理内存分配的逻辑来看,重点就是初始化记录物理内存分配上下文信息alloc_context的prepare_alloc_pages函数,物理内存快速路径分配get_page_from_freelist函数和物理内存慢速路径分配__alloc_pages_slowpath函数。
- 首先从开始alloc_flags置为 ALLOC_WMARK_LOW,且在快速路径分配之前内核没有更改过这个水线,可以看出第一次物理内存分配默认是在_watermark[WMARK_LOW]之上进行的。
- 在执行快速路径分配之前,第一,会校验分配阶order值,其范围必须是[0,10],否则报错,返回NULL。内核最大分配阶是10,意味着伙伴系统一次物理内存分配可达1024个物理页,假设1 page = 4KB,那么相当于一次分配4096KB=4MB的内存。不过内核中有个costly_order值,一般不超过3,相当于不建议order超过3,但没有强制限制。第二,获取实际用于内存分配的分配掩码alloc_mask。第三,调用prepare_alloc_pages初始化保存内存分配上下文信息的alloc_context。
-
调用 get_page_from_freelist 执行快速路径分配,为了体现“快速”二字,这次内存分配,是从最期望zone开始,快速扫描一下包括本NUMA节点和其余备用NUMA节点在内的所有zone中是否有能够满足本次内存分配的zone,若有,则直接从该zone中分配内存。若没有立即返回,page置为NULL,进入后续的慢速路径分配。注意:这个分配的内存可能是从pcplist或者free_area中获取的。而且满足本次分配是指:分配完内存还能满足该zone的某个水位线的要求,一般都是_watermark[WMARK_LOW],具体满足哪个水线根据gfp_flags设置,cpuset、脏页平衡等因素有关。
-
当快速内存分配失败之后,情况就会变得非常复杂,内核将不得不做所谓的慢速路径工作,比如kswapd异步回收,直接内存回收,内存规整和OOM。通过这一系列耗时行为,来让伙伴系统获得足够的空闲内存以满足物理内存分配需求,最后再调用get_page_from_freelist分配内存。而这一切的复杂逻辑全部封装在慢速分配路径 __alloc_pages_slowpath 函数中。
结合水线图和__alloc_pages_nodemask函数逻辑,我们可以得到如下的图:
总体流程介绍完了后,本章继续介绍一下prepare_alloc_pages函数,以及其余配置函数:alloc_flags_nofragment和current_gfp_context。至于__alloc_pages_slowpath在第四章会详细介绍。同时相对而言,get_page_from_freelist涉及更为直接具体分配动作,包括__alloc_pages_slowpath也是调用它完成分配,需要结合Buddy System原理讲解更为合理,因而考虑篇幅的原因,放到下篇文章Buddy System原理与实现详细介绍。
4.4 初始化内存分配上下文信息prepare_alloc_pages
前面我们已经知道了prepare_alloc_pages用于初始化alloc_context,该结构体记录了伙伴系统物理内存分配一些重要参数。话不多说,直接上代码。
先熟悉下前面介绍的alloc_context结构体。
struct alloc_context {
//NUMA节点的物理内存区域zone及其所有备用NUMA节点的zone按照优先级顺序排列组成的链表
struct zonelist *zonelist;
//NUMA节点的状态码node_states
nodemask_t *nodemask;
//指定最希望从哪个物理内存区域zone分配
struct zoneref *preferred_zoneref;
//物理内存页的迁移类型,根据gfp_mask得到
int migratetype;
/*
* highest_zoneidx represents highest usable zone index of
* the allocation request. Due to the nature of the zone,
* memory on lower zone than the highest_zoneidx will be
* protected by lowmem_reserve[highest_zoneidx].
*
* highest_zoneidx is also used by reclaim/compaction to limit
* the target zone since higher zone than this index cannot be
* usable for this allocation request.
*/
//表示内存分配最高优先级的内存区域 zone,利用前面介绍的gfp_zone(gfp_mask)函数得到
enum zone_type highest_zoneidx;
//表示是否考虑脏页平衡,若考虑(true),那么在某个zone的脏页超过预设的最大值,则本次分配不在该zone上进行
bool spread_dirty_pages;
};
如下是prepare_alloc_pages函数概览。
//mm/page_alloc.c
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_mask,
unsigned int *alloc_flags)
{
//上来简单高效,直接就初始化了alloc_context结构体里面的6个参数的4个
//从gfp分配掩码得到此次内存分配最高优先级的物理内存区域zone
ac->highest_zoneidx = gfp_zone(gfp_mask);
//根据期望的node和分配掩码,选定相应的zonelist,得到当前node以及备用node对应的zone按照优先级顺序构成的zonelist
ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
//获取当前node的节点状态node_states,从物理分配接口,向下逐级调用来看,nodemask一般是NULL
ac->nodemask = nodemask;
//根据分配掩码得到此次分配的物理内存是需要何种迁移类型的
ac->migratetype = gfp_migratetype(gfp_mask);
//如果使能了cpuset策略,即使用 cgroup 将当前进程绑定在指定CPU上运行,那么内存分配只能在
//这些绑定的 CPU 相关联的 NUMA 节点中进行
if (cpusets_enabled()) {
*alloc_mask |= __GFP_HARDWALL;//因而gfp分配掩码需要加上__GFP_HARDWALL
/*
* When we are in the interrupt context, it is irrelevant
* to the current task context. It means that any node ok.
*/
//如果当前进程没有处于中断上下文且nodemaks为NULL,那么就会使用cpuset配置的cpuset_current_mems_allowed
if (!in_interrupt() && !ac->nodemask)
ac->nodemask = &cpuset_current_mems_allowed;
else
*alloc_flags |= ALLOC_CPUSET;//否则,使用alloc分配掩码ALLOC_CPUSET(跟__GFP_HARDWALL作用一样)
}
//如下两个函数跟死锁检测模块Lockdep功能相关,主要是用于检测文件系统系统递归调用是否出现死锁问题
fs_reclaim_acquire(gfp_mask);//用于申请一个的锁定映射,并将该锁定映射与一个锁相关联。锁定映射是一种机制,用于跟踪和管理锁的状态,确保对共享资源的并发访问安全。
fs_reclaim_release(gfp_mask);//在死锁检测完毕后,用于释放上面申请的锁定映射的相关资源
//表示如果gpf分配掩码有__GFP_DIRECT_RECLAIM,可以直接内存回收,那么内存分配的进程有可能被阻塞,亲自去直接内存回收
might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
//内核提供的故障注入(fault injection)技术,用于提前判断本次内存分配是否能够成功,如果不能,则should_fail_alloc_page返回true,尽早分配失败
//使能了CONFIG_FAIL_PAGE_ALLOC,该函数才有意义,并在should_fail函数中做主要的模拟故障注入试验,检测当前分配掩码和order是否会导致内存分配失败
if (should_fail_alloc_page(gfp_mask, order))
return false;
//根据当前进程的pflags和gfp_mask,更新alloc分配掩码。主要判断是否添加ALLOC_CMA
*alloc_flags = current_alloc_flags(gfp_mask, *alloc_flags);
/* Dirty zone balancing only done in the fast path */
ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);
/*
* The preferred zone is used for statistics but crucially it is
* also used as the starting point for the zonelist iterator. It
* may get reset for allocations that ignore memory policies.
*/
//返回从选定的zonelist中选定一个用于物理内存分配的首选zone
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
//初始化成功,返回true
return true;
}
首先上来就是初始化了alloc_context结构体6个参数中的4个:highest_zoneidx,zonelist,nodemask,migratetype。对于highest_zoneidx的初始化,调用了本文3.1节介绍物理内存管理区域修饰符时提到的gfp_zone函数。因为在gfp分配掩码中,会指定此次物理内存分配的物理内存管理区域zone,同时,根据向低位zone fallback机制,这个zone就是此次物理分配当前NUMA节点的highest_zoneidx。从gfp_zone函数看到,通过一系列移位操作得到最高优先级物理内存区域zone。
//mm/mmzone.c
enum zone_type gfp_zone(gfp_t flags)
{
enum zone_type z;
gfp_t local_flags = flags;
int bit;
trace_android_rvh_set_gfp_zone_flags(&local_flags);
//通过跟GFP_ZONEMASK按位与,得到相应物理内存域的bit
bit = (__force int) ((local_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;
}
EXPORT_SYMBOL_GPL(gfp_zone);
对于zonelist的初始化,node_zonelist函数利用给定的node(nid),得到该node对应的node_zonlists,不过这是个数组头指针,从Linux 物理内存管理涉及的三大结构体之struct pglist_data中的2.2节可知,我们node_zonlists数组长度最小为1,最大为2,需要给个数组索引,从而确定选用哪个zonelist。因而,node_zonelist会再次调用gfp_zonelist函数。在gfp_zonelist函数中,如果Linux系统是NUMA物理内存架构且gfp分配掩码有__GFP_THISNODE,则返回ZONELIST_NOFALLBACK,相当于只能从本地节点管理的zone构成的zonelist中分配内存。否则,返回ZONELIST_FALLBACK,相当于可以从本地节点和系统其它节点管理的zone中分配内存,这些zone按照优先节顺序排列组成zonelist。更多关于node_zonelist的内容,感兴趣可以看上面我关于struct palist_data文章。
//include/linux/gfp.h
static inline int gfp_zonelist(gfp_t flags)
{
//如果使能了CONFIG_NUMA,是NUMA物理内存架构,做进一步判断
//如果gfp分配掩码有__GFP_THISNODE,则相当于禁止跨节点分配内存,则返回ZONELIST_NOFALLBACK,
//相当于只能从本地节点管理的zone中分配内存
#ifdef CONFIG_NUMA
if (unlikely(flags & __GFP_THISNODE))
return ZONELIST_NOFALLBACK;
#endif
//如果未使能NUMA或者分配掩码没有限定本地节点,则返回ZONELIST_FALLBACK
//相当于可以从本地节点和系统其它备用节点的zone中分配内存,这些zone按照优先节顺序排列组成zonelist
return ZONELIST_FALLBACK;
}
/*
* We get the zone list from the current node and the gfp_mask.
* This zone list contains a maximum of MAXNODES*MAX_NR_ZONES zones.
* There are two zonelists per node, one for all zones with memory and
* one containing just zones from the node the zonelist belongs to.
*
* For the normal case of non-DISCONTIGMEM systems the NODE_DATA() gets
* optimized to &contig_page_data at compile-time.
*/
static inline struct zonelist *node_zonelist(int nid, gfp_t flags)
{
//首先利用nid找到该nid对应的node,得到该node对应的node_zonelists数组
//其次,根据gfp_zonelist确定node_zonelists数组的索引,从而确定选用哪个zonelist
return NODE_DATA(nid)->node_zonelists + gfp_zonelist(flags);
}
对于nodemask的初始化,非常简单,直接将入参的nodemask赋值给alloc_context的nodemask即可。而且从根据前面的介绍物理分配接口,向下逐级调用来看,nodemask一般是NULL。
对于migratetype的初始化,在gfp_migratetype函数里面实现,先是进行一些gfp分配掩码的异常处理判断。然后判断page_group_by_mobility_disabled参数,若为1,表示整个系统的物理内存是不可移动的,则直接返回迁移类型MIGRATE_UNMOVABLE。这个参数表示系统物理内存是否是完全不可以移动的。其在系统启动早期,物理内存初始化的时候设置,设置后就不可以更改,除非增大物理内存大小,重启系统,系统可重新更新设置。最后,如果page_group_by_mobility_disabled为0,则将gfp分配掩码通过跟迁移属性标志码GFP_MOVABLE_MASK按位与,再左移3位,得到enum migratetype枚举体的索引,从而得到迁移类型。
//mm/page_alloc.c
//定义在page_alloc.c 是__read_mostly属性
//意味着该变量存放在.data.read_mostly段,这样Linux内核被加载时,该数据将自动被存放到core cache中,以提高整个系统的执行效率
int page_group_by_mobility_disabled __read_mostly;
//初始化page_group_by_mobility_disabled地方,在系统启动早期,物理内存初始化的时候设置,设置后就不可更改
void __ref build_all_zonelists(pg_data_t *pgdat)
{
unsigned long vm_total_pages;
......
/* Get the number of free pages beyond high watermark in all zones. */
vm_total_pages = nr_free_zone_pages(gfp_zone(GFP_HIGHUSER_MOVABLE));
/*
* Disable grouping by mobility if the number of pages in the
* system is too low to allow the mechanism to work. It would be
* more accurate, but expensive to check per-zone. This check is
* made on memory-hotadd so a system can start with mobility
* disabled and enable it later
*/
//如上comment,如内存大小不足以分配到各种类型时,支持相应机制时,就不适合启用可移动性,代价太大
//此时就将page_group_by_mobility_disabled置为1
if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
page_group_by_mobility_disabled = 1;
else
page_group_by_mobility_disabled = 0;
......
}
//include/linux/gfp.h
/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3
//正主
static inline int gfp_migratetype(const gfp_t gfp_flags)
{
//VM_WARN_ON 是一种调试宏,这里是判断gfp分配掩码是否有GFP_MOVABLE_MASK标志位,
//如果有该标志位,则打印log信息在kernel log中,便于后续定位内存分配问题
VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
//BUILD_BUG_ON 是一个编译期间的断言宏,检测代码编译时的错误,当条件表达式为true时,编译错误,终止编译
//如下相当于检测gfp参数是否正确
BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE);
BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE);
//如果page_group_by_mobility_disabled为true,表示整个系统的物理内存是不可移动的,则直接返回迁移类型MIGRATE_UNMOVABLE,
//不用继续后续的判断了
if (unlikely(page_group_by_mobility_disabled))
return MIGRATE_UNMOVABLE;
//经过前面的异常判断处理后,这里进行最终的迁移属性确认,
//通过跟迁移属性标志码GFP_MOVABLE_MASK按位与,再左移3位,得到enum migratetype枚举体的索引
/* Group based on mobility */
return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}
继续往下看,跟cpuset相关的处理,是否更新alloc_mask,nodemask和alloc_flags。如果cpusets_enabled返回true,意味着系统使能了cpuset策略,那么gfp分配掩码必须加上__GFP_HARDWALL。同时,若当前进程没有处于中断上下文且nodemaks为NULL,那么节点状态码nodemask就会使用cpuset配置的cpuset_current_mems_allowed,由于这里是我们是使能了cpusets_enabled,因而cpuset_current_mems_allowed使用了当前需要内存分配进程的task_struct里面的mems_allowed的节点状态码。否则,nodemask这里不更新,而是alloc_flags分配掩码加上了ALLOC_CPUSET(跟__GFP_HARDWALL作用一样)。
//include/linux/cpuset.h
#ifdef CONFIG_CPUSETS
//使用了当前进程task_struct里面的mems_allowed的节点状态码
#define cpuset_current_mems_allowed (current->mems_allowed)
......
#else /* !CONFIG_CPUSETS */
......
//若未使能,则当前直接使用node_states[N_MEMORY]状态码,表示该节点有 ZONE_NORMAL,ZONE_HIGHMEM,ZONE_MOVABLE 内存区域
#define cpuset_current_mems_allowed (node_states[N_MEMORY])
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_mask,
unsigned int *alloc_flags)
{
......
//如果使能了cpuset策略,即使用 cgroup 将当前进程绑定在指定CPU上运行,那么内存分配只能在
//这些绑定的 CPU 相关联的 NUMA 节点中进行
if (cpusets_enabled()) {
*alloc_mask |= __GFP_HARDWALL;//因而gfp分配掩码需要加上__GFP_HARDWALL
/*
* When we are in the interrupt context, it is irrelevant
* to the current task context. It means that any node ok.
*/
//如果当前进程没有处于中断上下文且nodemaks为NULL,那么就会使用cpuset配置的cpuset_current_mems_allowed
if (!in_interrupt() && !ac->nodemask)
ac->nodemask = &cpuset_current_mems_allowed;
else
*alloc_flags |= ALLOC_CPUSET;//否则,使用alloc分配掩码ALLOC_CPUSET(跟__GFP_HARDWALL作用一样)
}
......
}
fs_reclaim_acquire和fs_reclaim_release函数跟死锁检测模块Lockdep功能相关,这里就是用于检测文件系统递归调用是否出现死锁问题。
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_mask,
unsigned int *alloc_flags)
{
......
//如下两个函数跟死锁检测模块Lockdep功能相关,主要是用于检测文件系统系统递归调用是否出现死锁问题
fs_reclaim_acquire(gfp_mask);//用于申请一个的锁定映射,并将该锁定映射与一个锁相关联。锁定映射是一种机制,用于跟踪和管理锁的状态,确保对共享资源的并发访问安全。
fs_reclaim_release(gfp_mask);//在死锁检测完毕后,用于释放上面申请的锁定映射的相关资源
......
}
might_sleep_if表示如果gpf分配掩码有__GFP_DIRECT_RECLAIM,可以直接内存回收,那么内存分配的进程有可能被阻塞,亲自去直接内存回收。
//include/linux/kernel.h
#define might_sleep_if(cond) do { if (cond) might_sleep(); } while (0)
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_mask,
unsigned int *alloc_flags)
{
......
//表示如果gpf分配掩码有__GFP_DIRECT_RECLAIM,可以直接内存回收,那么内存分配的进程有可能被阻塞,亲自去直接内存回收
might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
......
}
should_fail_alloc_page函数利用了内核提供的故障注入(fault injection)技术,用于提前判断本次内存分配是否能够成功,如果不能,则should_fail_alloc_page返回true,分配失败。
//include/linux/kernel.h
#define might_sleep_if(cond) do { if (cond) might_sleep(); } while (0)
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_mask,
unsigned int *alloc_flags)
{
......
//内核提供的故障注入(fault injection)技术,用于提前判断本次内存分配是否能够成功,如果不能,则should_fail_alloc_page返回true,尽早分配失败
//使能了CONFIG_FAIL_PAGE_ALLOC,该函数才有意义,并在should_fail函数中做主要的模拟故障注入试验,检测当前分配掩码和order是否会导致内存分配失败
if (should_fail_alloc_page(gfp_mask, order))
return false;
......
}
current_alloc_flags函数是根据当前进程的pflags和gfp_mask,更新alloc分配掩码。主要判断是否添加ALLOC_CMA。
//mm/page_alloc.c
static inline unsigned int current_alloc_flags(gfp_t gfp_mask,
unsigned int alloc_flags)
{
#ifdef CONFIG_CMA
unsigned int pflags = current->flags;
//如果pflags没有禁用CMA,迁移类型是MIGRATE_MOVABLE且gfp_mask是__GFP_CMA,则alloc_flags更新上ALLOC_CMA
if (!(pflags & PF_MEMALLOC_NOCMA) &&
gfp_migratetype(gfp_mask) == MIGRATE_MOVABLE &&
gfp_mask & __GFP_CMA)
alloc_flags |= ALLOC_CMA;
#endif
return alloc_flags;
}
//include/linux/kernel.h
#define might_sleep_if(cond) do { if (cond) might_sleep(); } while (0)
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_mask,
unsigned int *alloc_flags)
{
......
//根据当前进程的pflags和gfp_mask,更新alloc分配掩码。主要判断是否添加ALLOC_CMA
*alloc_flags = current_alloc_flags(gfp_mask, *alloc_flags);
......
}
对于spread_dirty_pages的初始化,如果gfp分配掩码有__GFP_WRITE,即分配的内存用于写入数据的,则将spread_dirty_pages初始化为true,在快速路径分配中需要考虑脏页平衡。在慢速路径中暂时先不考虑,因而在__alloc_pages_nodemask函数中,调用慢速路径接口前将spread_dirty_pages置为了false。
//include/linux/kernel.h
#define might_sleep_if(cond) do { if (cond) might_sleep(); } while (0)
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_mask,
unsigned int *alloc_flags)
{
......
//判断是否启用脏页平衡,注意这个只在快速路径分配里面启用
/* Dirty zone balancing only done in the fast path */
ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);
......
}
对于preferred_zoneref的初始化,first_zones_zonelist从zonelist中选定一个用于物理内存分配的首选zone,用于后面的物理内存分配,这个zonelist在前面zonelist的初始化已经确定了。在node的状态码node_states是NULL的情况或者UMA物理内存架构下,返回的是zonelist中小于等于给定highest_zoneidx的第一个zone;在node的状态码node_states不是NULL且是NUMA架构下,首先遍历整个zonelist,找到小于等于highest_zoneidx的zone,然后若该zone对应的node的状态码node_states跟给定的nodemask匹配,则返回该zone,否则继续找,若一直没找到,返回NULL。
//include/linux/nodemask.h
/* No static inline type checking - see Subtlety (1) above. */
#define node_isset(node, nodemask) test_bit((node), (nodemask).bits)
//mm/mmzone.c
static inline int zref_in_nodemask(struct zoneref *zref, nodemask_t *nodes)
{
#ifdef CONFIG_NUMA
//如果是NUMA架构,则要判断选定的zone对应的node的状态码node_states是否跟跟给定的nodemask匹配,若匹配则返回1,否则返回0
return node_isset(zonelist_node_idx(zref), *nodes);
#else
//不是NUMA架构,直接返回1
return 1;
#endif /* CONFIG_NUMA */
}
/* Returns the next zone at or below highest_zoneidx in a zonelist */
struct zoneref *__next_zones_zonelist(struct zoneref *z,
enum zone_type highest_zoneidx,
nodemask_t *nodes)
{
/*
* Find the next suitable zone to use for the allocation.
* Only filter based on nodemask if it's set
*/
//根据节点的状态码node_states是否为NULL,更新while循环条件
if (unlikely(nodes == NULL))
//若nodemask是NULL,遍历整个zonelist,找到小于等于highest_zoneidx的第一个zone,然后返回zone
while (zonelist_zone_idx(z) > highest_zoneidx)
z++;
else
//若nodemask不为NULL,在非NUMA物理内存架构下,依旧是遍历整个zonelist,找到小于等于highest_zoneidx的第一个zone;
//在NUMA架构下,首先遍历整个zonelist,找到小于等于highest_zoneidx的zone,然后,若该zone对应的node的状态码node_states跟给定的nodemask匹配,则返回该zone
//注意:这里之所以需要这个zref_in_nodemask,是因为存在跨节点选择zone的情况
while (zonelist_zone_idx(z) > highest_zoneidx ||
(z->zone && !zref_in_nodemask(z, nodes)))
z++;
return z;
}
EXPORT_SYMBOL_GPL(__next_zones_zonelist);
//include/linux/mmzone.h
//获取该zone在zonelist->_zonerefs中索引,从而返回相应的zone
static inline int zonelist_zone_idx(struct zoneref *zoneref)
{
return zoneref->zone_idx;
}
//获取该zone对应的node
static inline int zonelist_node_idx(struct zoneref *zoneref)
{
return zone_to_nid(zoneref->zone);
}
static __always_inline struct zoneref *next_zones_zonelist(struct zoneref *z,
enum zone_type highest_zoneidx,
nodemask_t *nodes)
{
//若节点的状态码node_states为NULL,且zonelist中的第一个zone就小于等于highest_zoneidx,则直接返回该zone
if (likely(!nodes && zonelist_zone_idx(z) <= highest_zoneidx))
return z;
//否则,继续进行判断
return __next_zones_zonelist(z, highest_zoneidx, nodes);
}
//正主
static inline struct zoneref *first_zones_zonelist(struct zonelist *zonelist,
enum zone_type highest_zoneidx,
nodemask_t *nodes)
{
//提取到zonelist对应保存各个zone信息的_zonerefs,然后调用next_zones_zonelist
return next_zones_zonelist(zonelist->_zonerefs,
highest_zoneidx, nodes);
}
至此,关于初始化alloc_context结构体的函数prepare_alloc_pages介绍完毕。
4.5 alloc_flags_nofragment和current_gfp_context函数
alloc_flags_nofragment函数更新alloc_flags分配掩码,current_gfp_context则在快速路径分配失败,复原并更新gfp分配掩码gfp_mask。
//include/linux/mmzone.h
/*
* zone_idx() returns 0 for the ZONE_DMA zone, 1 for the ZONE_NORMAL zone, etc.
*/
#define zone_idx(zone) ((zone) - (zone)->zone_pgdat->node_zones)
//mm/page_alloc.c
static inline unsigned int
alloc_flags_nofragment(struct zone *zone, gfp_t gfp_mask)
{
unsigned int alloc_flags;
/*
* __GFP_KSWAPD_RECLAIM is assumed to be the same as ALLOC_KSWAPD
* to save a branch.
*/
//若gfp_mask设置了__GFP_KSWAPD_RECLAIM,则alloc_flags要添加上ALLOC_KSWAPD
alloc_flags = (__force int) (gfp_mask & __GFP_KSWAPD_RECLAIM);
//如果系统存在ZONE_DMA32区域
#ifdef CONFIG_ZONE_DMA32
if (!zone)
//如果该zone不存在,为NULL,则直接返回
return alloc_flags;
if (zone_idx(zone) != ZONE_NORMAL)
//如果zone不是ZONE_NORMAL,直接返回
return alloc_flags;
/*
* If ZONE_DMA32 exists, assume it is the one after ZONE_NORMAL and
* the pointer is within zone->zone_pgdat->node_zones[]. Also assume
* on UMA that if Normal is populated then so is DMA32.
*/
//既然系统存在ZONE_DMA32区域,那么相减应道为1,否则直接在编译阶段报错,终止编译
BUILD_BUG_ON(ZONE_NORMAL - ZONE_DMA32 != 1);
//根据前面if判断,此时zone必须是ZONE_NORMAL才能走到这里。
//若nr_online_nodes大于1,这意味着必须有多个NUMA节点,但若该ZONE_DMA32的剔除内存空洞的管理物理内存数为0,则直接返回
if (nr_online_nodes > 1 && !populated_zone(--zone))
return alloc_flags;
//经过前面异常判断处理,这里给alloc_flags要添加上ALLOC_NOFRAGMENT,其跟优化内存碎片化相关
alloc_flags |= ALLOC_NOFRAGMENT;
#endif /* CONFIG_ZONE_DMA32 */
return alloc_flags;
}
//include/linux/sched/mm.h
/*
* Applies per-task gfp context to the given allocation flags.
* PF_MEMALLOC_NOIO implies GFP_NOIO
* PF_MEMALLOC_NOFS implies GFP_NOFS
*/
static inline gfp_t current_gfp_context(gfp_t flags)
{
//从当前物理内存分配进程,获取task struct->flags给pflags
unsigned int pflags = READ_ONCE(current->flags);
//果当前进程的task struct->flags有PF_MEMALLOC_NOIO或PF_MEMALLOC_NOFS属性
if (unlikely(pflags & (PF_MEMALLOC_NOIO | PF_MEMALLOC_NOFS))) {
/*
* NOIO implies both NOIO and NOFS and it is a weaker context
* so always make sure it makes precedence
*/
//如果是PF_MEMALLOC_NOIO标志,则表示分配内存时,禁用IO操作和文件系统操作,提高性能和避免死锁
if (pflags & PF_MEMALLOC_NOIO)
flags &= ~(__GFP_IO | __GFP_FS);
//如果是PF_MEMALLOC_NOFS标志,则表示分配内存时,仅禁用文件系统操作,避免死锁
else if (pflags & PF_MEMALLOC_NOFS)
flags &= ~__GFP_FS;
}
//返回更新后的gfp分配掩码
return flags;
}
五、内存分配慢速路径__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_flages,设置更加激进的内存分配策略,包括将水线减低到_watermark[WMARK_MIN].........
..........更新preferred_zoneref,重新计算首选zone.........
..........唤醒所有相关node对应的kswapd进程,执行物理内存异步回收.........
..........根据alloc_flags和preferred_zoneref的更新,调用get_page_from_freelist尝试一次.........
..........若尝试失败,根据条件,触发内存规整并再尝试一次.........
..........若再次尝试失败,根据条件,更新内存规整状态.........
retry:
..........确保唤醒所有相关node对应的kswapd进程处于唤醒,执行物理内存异步回收.........
..........更新alloc_flages,设置更加激进的内存分配策略,包括不考虑水线或者OOM.........
..........更新preferred_zoneref,重新计算首选zone.........
..........根据alloc_flags和preferred_zoneref的更新,调用get_page_from_freelist尝试一次.........
..........若尝试失败,根据条件,分别触发直接内存回收,内存规整以尝试分配.........
..........若失败,根据条件,判断是否重试retry分支和retry_cpuset分支.........
..........若重试失败或不允许重试,根据条件,触发OOM以尝试分配.........
nopage:
..........前面若失败,根据条件,判断是否重试retry_cpuset分支.........
..........若重试失败或不允许重试,根据__GFP_NOFAIL条件,可以进行整个慢速路径分配最后一次尝试,并允许等待下次调度直至成功.........
fail:
..........打印慢速路径分配失败的error信息.........
got_pg:
..........若分配成功,则返回指向分配内存块首个物理页的指针,失败,则是NULL.........
}
从流程图中,可看出__alloc_pages_slowpath可以分成6部分,这里按照各个部分逐一介绍。
5.1 慢速路径分配相关参数的定义和部分初始化
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
//根据gfp_mask分配掩码,确认能否直接内存回收,若有__GFP_DIRECT_RECLAIM,则能直接内存回收
bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
//costly_order表示:内核一般认为order<=3的分配需求时比较容易满足的,大于3相对而言就costly
const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
//下面定义了一系列的局部变量
struct page *page = NULL;
unsigned int alloc_flags;//alloc分配掩码,专门用于伙伴系统内部的
unsigned long did_some_progress;//记录直接内存回收收了多少内存页
enum compact_priority compact_priority;//内存规整的优先级
enum compact_result compact_result;//内存规整的结果
int compaction_retries;//内存规整尝试次数
int no_progress_loops;//记录重试的次数,正常超过一定的次数(MAX_RECLAIM_RETRIES,16次)则内存分配失败
unsigned int cpuset_mems_cookie;//跟cpuset内存使用策略相关
int reserve_flags;//临时保存调整后的内存分配策略
unsigned long vh_record;//Android特有的,vendor hook,添加打点钩子函数,用于debug,kernel原生版本没有
//Android通过添加打点记录慢速路径分配,vendor hook,这是开始,__alloc_pages_slowpath最后还要个end,用于结束打点记录
trace_android_vh_alloc_pages_slowpath_begin(gfp_mask, order, &vh_record);
/*
* We also sanity check to catch abuse of atomic reserves being used by
* callers that are not in atomic context.
*/
//因为这里已经进入慢速路径分配了,而在慢速路径中涉及直接回收和OOM操作,这些是会阻塞进程
//因而需要对非上下文进程禁用掉原子操作__GFP_ATOMIC分配掩码,避免滥用原子操作。
//这里肯定可以确定的是,如果是中断处理程序和持有自旋锁的进程上下文,那么分配掩码肯定是不会有__GFP_DIRECT_RECLAIM,它们是不会走入这里的,如果有,那么这个程序设计就是个BUG
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:
trace_android_vh_alloc_pages_slowpath_end(gfp_mask, order, vh_record);
return page;
}
上面已对参数进行了一一介绍,这里对重点参数补充一下。
参数定义和初始化部分,首先是can_direct_reclaim,这个参数为后面是否启动直接内存回收的准入判断条件之一,因而这里从gfp分配掩码中,判断是否有__GFP_DIRECT_RECLAIM,如有,则can_direct_reclaim为true,则从gfp层面是允许使用直接内存回收的。
costly_order表示对伙伴系统物理页的分配是否有一定压力。其中,PAGE_ALLOC_COSTLY_ORDER定义为3。内核一般认为分配阶order<=3(2^3=8个物理页)的分配需求时比较容易满足的,大于3相对而言就costly,对伙伴系统有压力。若需要分配的内存order大于3或连续物理页个数大于8,costly_order就为true。
//include/linux/mmzone.h
#define PAGE_ALLOC_COSTLY_ORDER 3
no_progress_loops用于记录慢速路径中内存分配重试的次数,如果内存分配重试的次数超过最大限制 MAX_RECLAIM_RETRIES,则停止重试,开启 OOM。
//mm/internal.h
#define MAX_RECLAIM_RETRIES 16
对于gfp_mask分配掩码的更新,因为已进入慢速分配路径,而在慢速路径中涉及直接回收和OOM操作,这些是会阻塞进程,因而需要对非上下文进程禁用掉原子操作__GFP_ATOMIC分配掩码,避免滥用原子操作。当然对于中断处理程序和持有自旋锁的进程上下文不受影响,因为这些进程的分配掩码肯定是不会有__GFP_DIRECT_RECLAIM,它们是不会走入这里的,如果有,那么申请内存分配的申请者在使用分配掩码方面出现问题。
5.2 retry_cpuset
在介绍完相关参数和部分参数初始化后,我们来到retry_cpuset。
retry_cpuset:
compaction_retries = 0;
no_progress_loops = 0;
//对于内存规整的优先级,默认轻同步优先级,即轻同步模式,该模式下,允许绝大多数阻塞,但是不允许将脏页写回到存储设备上,因为等待时间比较长
compact_priority = DEF_COMPACT_PRIORITY;
//后面检查cpuset是否允许当前进程从哪些内存节点申请页,需要读当前进程的成员mems_allowed_seq,使用顺序锁保护
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.
*/
//将gfp_mask分配掩码转换为alloc分配掩码
//在之前的快速内存分配路径下设置的相关分配策略比较保守,在_watermark[WMARK_LOW]水线之上进行快速内存分配
//但走到这里表示快速内存分配失败,所以在慢速内存分配路径下需要重新设置相对激进的内存分配策略,采用更大的代价来分配内存
alloc_flags = gfp_to_alloc_flags(gfp_mask);
//在调用__alloc_pages_slowpath前,复原的nodemask值,可能会跟快速路径的不一样,
//或者进程的cpuset发生了变化,为了避免在不符合要求的zone反复重试,这里需要重新计算用于物理内存分配的首选zone
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
//如果上面没有适合的zone,则跳转到 nopage , 内存分配失败
if (!ac->preferred_zoneref->zone)
goto nopage;
//如果alloc分配掩码设置了ALLOC_KSWAPD,则唤醒所有相关node对应的kswapd进程,进行物理内存的异步回收
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
/*
* The adjusted alloc_flags might result in immediate success, so try
* that first
*/
//根据前面调整过的alloc_flags和alloc_context,且开启kswapd进程后,重新调用快速分配接口函数get_page_from_freelist,再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;
//在可以直接内存回收条件下,can_direct_reclaim为true,
//1、如果是分配大内存costly_order = true (超过 8 个内存页),需要首先进行内存规整,这样内核可以避免直接内存回收从而获取更多的连续空闲内存页
//2、对于虽然order<3,但需要分配不可移动内存的情况,也需要先进行内存规整,防止永久内存碎片
//gfp_pfmemalloc_allowedi检查gfp分配掩码和当前的进程的pflags,判断是否可以不考虑水线影响或者启用OOM,若可以(返回true),则暂时不考虑使用内存规整,否则可以启用内存规整
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, //INIT_COMPACT_PRIORITY = COMPACT_PRIO_ASYNC 异步模式,不允许阻塞
&compact_result);
if (page)
goto got_pg;
/*
* Checks for costly allocations with __GFP_NORETRY, which
* includes some THP page fault allocations
*/
//流程走到这里表示经过内存规整之后依然没有足够的内存供分配
//分配大内存costly_order = true (超过 8 个内存页)且gfp分配掩码禁止分配失败的重试
if (costly_order && (gfp_mask & __GFP_NORETRY)) {
//如果compact_result是COMPACT_SKIPPED(跳过此zone,可能此zone不适合)或者COMPACT_DEFERRED(内存规整不能从此zone开始,由于此zone最近失败过)
//则跳转到nopage
if (compact_result == COMPACT_SKIPPED ||
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_SKIPPED和COMPACT_DEFERRED,那么更改内存规整的优先级,从前面默认的DEF_COMPACT_PRIORITY改为INIT_COMPACT_PRIORITY
//内存规整更激进了
compact_priority = INIT_COMPACT_PRIORITY;
}
}
上来是对两个重试次数compaction_retries和no_progress_loops先初始化为0,然后将内存规整优先级compact_priority初始化为轻同步模式,该模式下,允许绝大多数阻塞,但不允许将脏页写回到存储设备上。最后就是调用read_mems_allowed_begin函数更新cpuset_mems_cookie,用于确定该进程所允许运行的CPU相关联的NUMA节点。
接下来,就是调用前面提过的gfp_to_alloc_flags函数,将gfp分配掩码转换为伙伴系统内部使用的alloc分配掩码。因为前面快速路径分配失败,从而内核知道当前系统剩余内存可能有压力了,为提高内存分配成功率,在这里将前面快速路径使用的水线值从_watermark[WMARK_LOW]减低到_watermark[WMARK_MIN],即alloc_flags从ALLOC_WMARK_LOW变为ALLOC_WMARK_MIN。同时根据进程的gfp分配掩码是否有__GFP_HIGH,__GFP_KSWAPD_RECLAIM,__GFP_ATOMIC不断的降低放宽内存分配限制,包括降低_watermark[WMARK_MIN]水线值和使用系统紧急预留内存。关于函数里面调用的子函数current_alloc_flags,前面4.4节已经介绍,这里不做介绍,具体代码如下。
//mm/page_alloc.c
static inline unsigned int
gfp_to_alloc_flags(gfp_t gfp_mask)
{
//在前面快速路径是_watermark[WMARK_LOW]水线分配失败情况下,这里上来讲水线标准减低到_watermark[WMARK_MIN],以提高成功概率
unsigned int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;
/*
* __GFP_HIGH is assumed to be the same as ALLOC_HIGH
* and __GFP_KSWAPD_RECLAIM is assumed to be the same as ALLOC_KSWAPD
* to save two branches.
*/
//BUILD_BUG_ON 是一个编译期间的断言宏,检测代码编译时的错误,当条件表达式为true时,编译错误,终止编译
//如下相当于检测分配掩码参数是否正确
BUILD_BUG_ON(__GFP_HIGH != (__force gfp_t) ALLOC_HIGH);
BUILD_BUG_ON(__GFP_KSWAPD_RECLAIM != (__force gfp_t) ALLOC_KSWAPD);
/*
* The caller may dip into page reserves a bit more if the caller
* cannot run direct reclaim, or if the caller has realtime scheduling
* policy or is asking for __GFP_HIGH memory. GFP_ATOMIC requests will
* set both ALLOC_HARDER (__GFP_ATOMIC) and ALLOC_HIGH (__GFP_HIGH).
*/
//如果gfp分配掩码设置了_GFP_HIGH和__GFP_KSWAPD_RECLAIM,那么alloc就要设置ALLOC_HIGH和ALLOC_KSWAPD,上面了编译器检查就是为了这一步
//如对于实时进程,我们必须保证其内存分配成功才能继续运行,设置了__GFP_HIGH,意味着进程可以使用_watermark[WMARK_MIN]水线下的系统紧急预留内存
alloc_flags |= (__force int)
(gfp_mask & (__GFP_HIGH | __GFP_KSWAPD_RECLAIM));
//经过__alloc_pages_slowpath第一步的剔除,若gfp分配掩码依旧有__GFP_ATOMIC,说明这次分配的进程是中断处理程序,或持有自旋锁的进程上下文,不允许阻塞和睡眠
if (gfp_mask & __GFP_ATOMIC) {
//对于这种__GFP_ATOMIC情况,一般情况比较紧急
/*
* Not worth trying to allocate harder for __GFP_NOMEMALLOC even
* if it can't schedule.
*/
//___GFP_NOMEMALLOC 标志用于明确禁止内核从紧急预留内存中获取内存
//___GFP_NOMEMALLOC 标识的优先级要高于 ___GFP_MEMALLOC
if (!(gfp_mask & __GFP_NOMEMALLOC))
//如果允许从紧急预留内存中获取内存,则会进一步放宽内存分配的限制,开始降低_watermark[WMARK_MIN]水线值,alloc分配器掩码加上ALLOC_HARDER
alloc_flags |= ALLOC_HARDER;
/*
* Ignore cpuset mems for GFP_ATOMIC rather than fail, see the
* comment for __cpuset_node_allowed().
*/
//由于在__GFP_ATOMIC条件下,相对紧急,且进程不能阻塞,为了尽快让其分配到内存,去掉cpuset限制,让进程可以从所有NUMA节点上分配内存
alloc_flags &= ~ALLOC_CPUSET;
} else if (unlikely(rt_task(current)) && !in_interrupt())
//如果当前进程是实时进程(rt_task返回1)且不是处于中断上下文中,对于这类对延迟敏感的实时进程,为了避免给该类进程分配内存过于耗时,alloc分配器掩码加上ALLOC_HARDER
//进一步降低_watermark[WMARK_MIN]水线值
alloc_flags |= ALLOC_HARDER;
//当前进程的pflags和gfp_mask,更新alloc分配掩码。主要判断是否添加ALLOC_CMA。
alloc_flags = current_alloc_flags(gfp_mask, alloc_flags);
return alloc_flags;
}
在调用__alloc_pages_slowpath前,复原了nodemask值,可能会跟快速路径的不一样,或者进程的cpuset发生了变化,为了避免在不符合要求的zone反复重试,因而在慢速路径分配这里需要使用first_zones_zonelist重新计算用于物理内存分配的首选zone。
/*
* 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.
*/
//在调用__alloc_pages_slowpath前,复原的nodemask值,可能会跟快速路径的不一样,
//或者进程的cpuset发生了变化,为了避免在不符合要求的zone反复重试,这里需要重新计算用于物理内存分配的首选zone
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
继续往下走,如果alloc分配掩码设置了ALLOC_KSWAPD,则调用wake_all_kswapds唤醒所有相关node对应的kswapd进程,进行物理内存的异步回收。通过遍历对应zonelist下面的所有小于等于highest_zoneidx的zone,找到它们对应的node(zone->zone_pgdat),将该node对应的kswapd进程唤醒,同时借助last_pgdat避免重复唤醒。
//如果alloc分配掩码设置了ALLOC_KSWAPD,则唤醒所有相关node对应的kswapd进程,进行物理内存的异步回收
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
//mm/page_alloc.c
static void wake_all_kswapds(unsigned int order, gfp_t gfp_mask,
const struct alloc_context *ac)
{
struct zoneref *z;
struct zone *zone;
pg_data_t *last_pgdat = NULL;
enum zone_type highest_zoneidx = ac->highest_zoneidx;
//for_each_zone_zonelist_nodemask实际上调用了first_zones_zonelist所使用的的方式来完成遍历
//按照这个方式,基本上可以遍历该zonelist下面的所有小于等于highest_zoneidx的zone
for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, highest_zoneidx,
ac->nodemask) {
//找到符合要求的zone,判断这个zone的节点是否跟上一个一样,如果不一样,则唤醒该节点的kswapd进程,否则不用重复唤醒
//内核每个node都有一个kswapd进程和kcompact进程
//之所以通过一个last_pgdat就可以,是因为zonelist里面的zone是按照优先级顺序排列的,同一个node的所有zone在zonelist里面一定连在一起的
if (last_pgdat != zone->zone_pgdat)
wakeup_kswapd(zone, gfp_mask, order, highest_zoneidx);
//更新last_pgdat
last_pgdat = zone->zone_pgdat;
}
}
到目前为止,内核已经在慢速分配路径下通过 gfp_to_alloc_flags 调整为更激进的内存分配策略,并将水位线降低到 _watermark[WMARK_MIN],同时也唤醒了 kswapd 进程来异步回收内存。此时在新的内存分配策略下进行内存分配很可能会成功,所以内核会首先尝试进行一次内存分配。如果分配成功,直接跳转到got_pg,返回指向已分配物理内存块的第一个物理页的指针page。
/*
* The adjusted alloc_flags might result in immediate success, so try
* that first
*/
//根据前面调整过的alloc_flags和alloc_context,且开启kswapd进程后,重新调用快速分配接口函数get_page_from_freelist,再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)
goto got_pg;
如果前面尝试内存分配失败,则进入如下操作,在可直接内存回收(can_direct_reclaim为true)前提下,进一步判断是否可以内存规整,因为前面尝试失败了是有可能是内存碎片导致的,先尝试内存规整,看能否满足内存分配需求,分配成功。如果不允许内存规整,则进入retry部分了。若可以内存规整,如果规整后分配成功,则直接跳转到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.
*/
//在可以直接内存回收条件下,
//1、如果是分配大内存costly_order = true (超过 8 个内存页),需要首先进行内存规整,这样内核可以避免直接内存回收从而获取更多的连续空闲内存页
//2、对于虽然order<3,但需要分配不可移动内存的情况,也需要先进行内存规整,防止永久内存碎片
//gfp_pfmemalloc_allowedi检查gfp分配掩码和当前的进程的pflags,判断是否可以不考虑水线或者启用OOM,若可以(返回true),则暂时不考虑使用内存规整,否则可以启用内存规整
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,//INIT_COMPACT_PRIORITY = COMPACT_PRIO_ASYNC 异步模式,不允许阻塞
&compact_result);
if (page)
goto got_pg;
更新内存规整的状态的前提是:分配大内存costly_order = true (超过 8 个内存页)且gfp分配掩码禁止分配失败的重试。第一,就是根据上面__alloc_pages_direct_compact返回的compact_result,判断是否要跳转到nopage部分。第二,更新内存规整的优先级,从前面默认的DEF_COMPACT_PRIORITY改为INIT_COMPACT_PRIORITY,从同步阻塞内存规整变成异步,更加激进了,下面retry部分会使用到。如上就是完整的retry_cpuset部分代码流程。
/*
* Checks for costly allocations with __GFP_NORETRY, which
* includes some THP page fault allocations
*/
//流程走到这里表示经过内存规整之后依然没有足够的内存供分配
//分配大内存costly_order = true (超过 8 个内存页)且gfp分配掩码禁止分配失败的重试
if (costly_order && (gfp_mask & __GFP_NORETRY)) {
//如果compact_result是COMPACT_SKIPPED(跳过此zone,可能此zone不适合)或者COMPACT_DEFERRED(内存规整不能从此zone开始,由于此zone最近失败过)
//则跳转到nopage
if (compact_result == COMPACT_SKIPPED ||
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_SKIPPED和COMPACT_DEFERRED,那么更改内存规整的优先级,从前面默认的DEF_COMPACT_PRIORITY改为INIT_COMPACT_PRIORITY
//内存规整更激进了
compact_priority = INIT_COMPACT_PRIORITY;
结合内存状态,我们得到的retry_cpuset部分流程如下所示。
5.3 retry
内存分配流程如果来到 retry 分支,说明情况已经变得非常紧急的,内存处于紧缺状态,而不单单压力大的问题。在经过 retry_cpuset 分支的处理,内核将内存水位线下调至 _watermark[WMARK_MIN],并开启相关联node的 kswapd 进程进行异步内存回收,甚至在条件满足情况下,会触发内存规整 direct_compact,而在采取了这些措施之后,依然无法满足内存分配的需求。那么内核就会走入retry分支,尝试更加激进的方式获取所申请的物理内存。其代码流程如下所示。
retry:
/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
//这里再次确认和确保,如果alloc分配掩码设置了ALLOC_KSWAPD,
//则所有相关node对应的kswapd进程是唤醒,以确保物理内存的异步回收在进行
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
//流程走到这里,说明在_watermark[WMARK_MIN]水线之上也分配内存失败了
//并且有可能经过内存规整之后,内存分配仍然失败,说明当前剩余内存容量已经严重不足
//接下来就需要使用更加激进的手段来尝试内存分配:“忽略掉内存水位线或者开启OOM”,
//根据gfp分配掩码和当前的进程的pflags 修改 alloc_flags 保存在 reserve_flags 中
//__gfp_pfmemalloc_flags函数实际上在gfp_pfmemalloc_allowed中有调用
reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
//如果reserve_flags不为0,则意味着要尝试更加激进的手段。据此再更新alloc分配掩码,大概率是ALLOC_NO_WATERMARKS
//current_alloc_flags主要判断是否添加ALLOC_CMA,并将reserve_flags更新到alloc_flags
if (reserve_flags)
alloc_flags = current_alloc_flags(gfp_mask, reserve_flags);
/*
* Reset the nodemask and zonelist iterators if memory policies can be
* ignored. These allocations are high priority and system rather than
* user oriented.
*/
//如果alloc_flags没有设置CPUSET,意味着有内存需求的进程可以在所有CPU上运行,那么也就意味着内存分配是可以跨Node节点的
//或者reserve_flags非0,即忽略掉内存水位线或者开启OOM
//那么物理内存分配的上下文信息alloc_context,需要更新。
//这里重置了节点状态码nodemask和重新计算用于物理内存分配的首选zone,更新preferred_zoneref
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
}
/* Attempt with potentially adjusted zonelist and alloc_flags */
//在更新alloc_flags(大概率是ALLOC_NO_WATERMARKS)和alloc_context之后,且确保kswapd进程开启后,重新调用快速分配接口函数get_page_from_freelist,再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Caller is not willing to reclaim, we can't balance anything */
//如果can_direct_reclaim为false,即无法直接内存回收,直接跳转到nopage
if (!can_direct_reclaim)
goto nopage;
/* Avoid recursion of direct reclaim */
//如果申请内存分配的进程的flags为PF_MEMALLOC,
//首先走到这里说明能直接内存回收,那么就要避免direct reclaim的递归
//因为当为PF_MEMALLOC,进程是忽略内存水线的,而且该进程属于紧急进程,可以使用紧急内存
//由于直接内存回收是在_watermark[WMARK_MIN]水线下触发的,而该水线下的空闲内存就是紧急内存,
//但是PF_MEMALLOC属性的进程可以无限制的使用,这就很有可能直接内存回收的紧急内存被它给使用了,
//导致直接内存回收一直在执行...,因而这里会跳转到nopage
if (current->flags & PF_MEMALLOC)
goto nopage;
/* Try direct reclaim and then allocating */
//如果前面更新了alloc_flags(大概率是ALLOC_NO_WATERMARKS)和alloc_context之后,还是分配失败
//这里开始启用直接内存回收,利用did_some_progress记录直接内存回收了多少page
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Try direct compaction and then allocating */
//如果直接内存回收失败,这里开始启用内存规整
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Do not loop if specifically requested */
//如果gfp分配掩码设置了__GFP_NORETRY,即内存分配失败后,不在重试,那么跳转到nopage,不loop
if (gfp_mask & __GFP_NORETRY)
goto nopage;
/*
* Do not retry costly high order allocations unless they are
* __GFP_RETRY_MAYFAIL
*/
//如果没有__GFP_NORETRY,这里做进一步判断
// 由于后面会触发 OOM,这里需要判断本次内存分配是否需要分配大量的内存页(大于 8 ) costly_order = true
// 如果是的,则内核认为即使执行 OOM 也未必会满足这么多的内存页分配需求.
// 所以还是直接失败比较好,即跳转nopage,不再执行 OOM,除非设置 __GFP_RETRY_MAYFAIL,即允许重试后失败
if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
goto nopage;
//判断是否重新尝试回收内存,如果值得重试,返回true,跳转到retry进行重试,no_progress_loops会记录重试次数
//流程走到这里说明前面已经经历过,kswapd异步内存回收和直接内存回收,内存规整,但是依然无法满足内存分配要求
//should_reclaim_retry里面会判断是否重试,其中,如果重试次数超过16次或者已经回收了LRU链表中所有可回收的内存,但依然无法满足内存分配请求,那么返回false
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)
*/
//如果内核判断不应进行直接内存回收的重试,这里还需要判断下是否应该进行内存规整的重试。
//did_some_progress 表示上次直接内存回收,回收了多少内存页,如果 did_some_progress = 0 则没有必要在进行内存规整重试了,因为内存规整的实现依赖于足够的空闲内存量
//should_compact_retry函数,对于order为0,即分配一个页请求是不用内存规整的,直接返回false;其余order会进行一系列的判断,包括降低重试的上限和提升内存规整的优先级,以判断是否重试
//compaction_retries记录了重试次数
if (did_some_progress > 0 &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;
//根据nodemask的内存分配策略和进程的内存分配策略mems_allowed
//判断是否应该在进程所允许运行的所有 CPU 关联的 NUMA 节点上重试,这里考虑到了在更新时的并行线程竞争问题,
//导致跟内存分配策略相关的mems_allowed 或 mempolicy nodemaske更新失败,即数据不一致问题,从而导致内存分配失败,因而这里会判断,如果有此情况,则返回true,跳转到retry_cpuset
/* Deal with possible cpuset update races before we start OOM killing */
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
/* Reclaim has failed us, start killing things */
//如果上面的直接内存回收和内存规整的重试都不允许,也不允许retry_cpuset,那么只能通过OOM,查杀低优先级的进程(/proc/<pid>/oom_adj,值的范围是-17到+15,取值越高,越容易被杀掉)来尝试满足当前进程的内存分配需求
//__alloc_pages_may_oom函数里面首先会继续调用get_page_from_freelist函数再做一次挣扎,如果不成功,
//然后,只能进行out_of_memory函数来kill进程,最后调用__alloc_pages_cpuset_fallback进程回收后尝试
//通过did_some_progress记录OOM回收了多少内存页,如果page不为NULL,则直接跳转got_pg返回。否则继续往下走
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 */
//为了避免过度循环执行retry
//如果发现自己曾经被OOM选中,即被OOM kill过,然后该进程自己alloc分配掩码是ALLOC_OOM或者gfp分配掩码是__GFP_NOMEMALLOC,则直接跳转到nopage,退出循环
if (tsk_is_oom_victim(current) &&
(alloc_flags & ALLOC_OOM ||
(gfp_mask & __GFP_NOMEMALLOC)))
goto nopage;
/* Retry as long as the OOM killer is making progress */
//如果did_some_progress不为0,说明通过OOM回收了一些内存页,可以继续尝试一下,因而这里会将内存回收次数no_progress_loops归零,继续retry
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}
从代码流程里面,可以看到,既然已经走到retry,说明剩余空闲物理内存已经紧缺,那么首先就要确保相关node的kswapd进程处于唤醒工作状态,因而通过wake_all_kswapds进行保证。
/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
//这里再次确认和确保,如果alloc分配掩码设置了ALLOC_KSWAPD,
//则所有相关node对应的kswapd进程是唤醒,以确保物理内存的异步回收在进行
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
在确保kswapd进处于唤醒后,因为前面retry_cpuset在_watermark[WMARK_MIN]水线之上分配内存失败了,__gfp_pfmemalloc_flags这里需要根据gfp分配掩码和当前的进程的pflags 修改alloc_flags,看是否要忽略水线或者开启OOM,以提高内存分配成功概率,正常情况,大概率是将alloc_flags更新上忽略水线标志,因为很多进程都是默认允许开启OOM的,最后保存在 reserve_flags 中。如果reserve_flags非0,在current_alloc_flags中,更新给alloc_flags,顺带判断一下是否加上ALLOC_CMA,如果有,则从CMA中分配内存。current_alloc_flags前面已经扩展介绍了,这里主要展开讲一下__gfp_pfmemalloc_flags。
//流程走到这里,说明在_watermark[WMARK_MIN]水线之上也分配内存失败了,并且有可能经过内存规整之后,内存分配仍然失败,说明当前剩余内存容量已经严重不足
//接下来就需要使用更加激进的手段来尝试内存分配:“忽略掉内存水位线或者开启OOM”,
//根据gfp分配掩码和当前的进程的pflags 修改 alloc_flags 保存在 reserve_flags 中
//__gfp_pfmemalloc_flags函数实际上在gfp_pfmemalloc_allowed中有调用
reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
//如果reserve_flags不为0,则意味着要尝试更加激进的手段。据此更新alloc分配掩码,大概率是ALLOC_NO_WATERMARKS
//current_alloc_flags主要判断是否添加ALLOC_CMA,并将reserve_flags更新到alloc_flags
if (reserve_flags)
alloc_flags = current_alloc_flags(gfp_mask, reserve_flags);
/*
* Distinguish requests which really need access to full memory
* reserves from oom victims which can live with a portion of it
*/
static inline int __gfp_pfmemalloc_flags(gfp_t gfp_mask)
{
//如果禁止从紧急内存分配,即_watermark[WMARK_MIN]水线之下,则直接返回0,说明内存分配有限制,无法做到忽略水线
if (unlikely(gfp_mask & __GFP_NOMEMALLOC))
return 0;
//如果gfp设置了可允许访问所有内存,包括紧急内存,那么返回ALLOC_NO_WATERMARKS
if (gfp_mask & __GFP_MEMALLOC)
return ALLOC_NO_WATERMARKS;
//如果进程在softriq中断上下文,且进程的pflags为PF_MEMALLOC,即忽略水线,那么返回ALLOC_NO_WATERMARKS,表示内存分配忽略水线
if (in_serving_softirq() && (current->flags & PF_MEMALLOC))
return ALLOC_NO_WATERMARKS;
//如果进程不处于中断上下文中
if (!in_interrupt()) {
//如果程的pflags为PF_MEMALLOC,即忽略水线,那么返回ALLOC_NO_WATERMARKS
if (current->flags & PF_MEMALLOC)
return ALLOC_NO_WATERMARKS;
//否则如果允许OOM,则返回ALLOC_OOM,表示当前进程允许使用OOM回收内存来满足其内存分配要求
else if (oom_reserves_allowed(current))
return ALLOC_OOM;
}
return 0;
}
static bool oom_reserves_allowed(struct task_struct *tsk)
{
//判断当前进程是否是OOM内存回收的受害者,如果是,tsk_is_oom_victim返回true,表示不建议开启OOM。
//如果不是,返回false,可没理解这里!false,然后if语句成立,return false意思还是不让开启OOM....
if (!tsk_is_oom_victim(tsk))
return false;
/*
* !MMU doesn't have oom reaper so give access to memory reserves
* only to the thread with TIF_MEMDIE set
*/
//如果没有使能CONFIG_MMU,且TIF_MEMDIE没有设置,则不允许OMM
if (!IS_ENABLED(CONFIG_MMU) && !test_thread_flag(TIF_MEMDIE))
return false;
//最后这里返回true,表示允许OOM
return true;
}
如果alloc_flags没有设置ALLOC_CPUSET或者reserve_flags有更新,那么,需要调用first_zones_zonelist重新更新物理内存分配的首选zone,first_zones_zonelist前面已经讲了,这里不展开了。因为前面retry_cpuset,是在cpuset关联的node分配内存,手机和电脑,一般默认配置CONFIG_CPUSETS=y。如果这里alloc_flags没有设置ALLOC_CPUSET,相当于可选的node范围变大了,需要更新首选zone。至于reserve_flags有更新,也一样,更新了alloc_flags,那么物理内存分配的上下文信息alloc_context也需要更新。
/*
* Reset the nodemask and zonelist iterators if memory policies can be
* ignored. These allocations are high priority and system rather than
* user oriented.
*/
//如果alloc_flags没有设置CPUSET,意味着有内存需求的进程可以在所有CPU上运行,那么也就意味着内存分配是可以跨Node节点的
//或者reserve_flags非0,即忽略掉内存水位线或者开启OOM
//那么物理内存分配的上下文信息alloc_context,需要更新。
//这里重置了节点状态码nodemask和重新计算用于物理内存分配的首选zone,更新preferred_zoneref
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
}
在更新了物理内存分配的上下文信息alloc_context信息后,会首先调用get_page_from_freelist尝试一次内存分配,如果成功,则不用继续往下走,跳转got_pg。
/* Attempt with potentially adjusted zonelist and alloc_flags */
//在更新alloc_flags(大概率是ALLOC_NO_WATERMARKS)和alloc_context之后,且确保kswapd进程开启后,重新调用快速分配接口函数get_page_from_freelist,再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
否则,要开始启动retry里面的直接内存回收和内存规整,但之前需先做一下判断。首先如果can_direct_reclaim为false,即无法直接内存回收,直接跳转到nopage。其次,如果申请内存分配的进程的flags为PF_MEMALLOC,而走到if (current->flags & PF_MEMALLOC)这里说明能直接内存回收,那么就要避免direct reclaim的递归,直接跳转nopage。这是因为当flags为PF_MEMALLOC,进程是忽略内存水线的,而且该进程属于紧急进程,可以使用紧急内存,由于直接内存回收是在_watermark[WMARK_MIN]水线下触发的,而该水线下的空闲内存就是紧急内存,但是PF_MEMALLOC属性的进程可以无限制的使用,这就很有可能存在直接内存回收的紧急内存被它给使用了,用于执行直接内存回收行为,导致空闲内存一直没有提升,直接内存回收一直在执行...,因而这里会跳转到nopage。
/* Caller is not willing to reclaim, we can't balance anything */
//如果can_direct_reclaim为false,即无法直接内存回收,直接跳转到nopage
if (!can_direct_reclaim)
goto nopage;
/* Avoid recursion of direct reclaim */
//如果申请内存分配的进程的flags为PF_MEMALLOC,
//首先走到这里说明能直接内存回收,那么就要避免direct reclaim的递归
if (current->flags & PF_MEMALLOC)
goto nopage;
做完前面的判断后,下面开始正式的直接内存回收和内存规整,直接内存回收会用did_some_progress记录直接内存回收的物理页数,内存规整用compact_result记录内存规整的结果。如果这两个方式操作后,尝试内存分配成功了,均会跳转到got_pg,结束内存分配行为。否则,继续往下走,判断是否要重试和OOM。__alloc_pages_direct_reclaim和__alloc_pages_direct_compact函数后面会出文章会展开细讲。
/* Try direct reclaim and then allocating */
//如果前面更新了alloc_flags(大概率是ALLOC_NO_WATERMARKS)和alloc_context之后,还是分配失败
//这里开始启用直接内存回收,利用did_some_progress记录直接内存回收了多少page
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Try direct compaction and then allocating */
//如果直接内存回收失败,这里开始启用内存规整
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
在前面直接内存回收和内存规整后都未能分配到内存后,这里首先判断能否重试,如果gfp分配掩码是__GFP_NORETRY,即表示禁止重试,那就跳转nopage,不循环重试了。然后对于order>3且gfp没有设置__GFP_RETRY_MAYFAIL的,也是跳转nopage,这是因为内核认为order>3的连续物理页分配失败概率大,而且也未设置重试分配允许失败标志,这是不允许的,因而不让重试。
/* Do not loop if specifically requested */
//如果gfp分配掩码设置了__GFP_NORETRY,即内存分配失败后,不再重试,那么跳转到nopage,不loop
if (gfp_mask & __GFP_NORETRY)
goto nopage;
/*
* Do not retry costly high order allocations unless they are
* __GFP_RETRY_MAYFAIL
*/
//如果没有__GFP_NORETRY,这里做进一步判断
// 由于后面会触发 OOM,这里需要判断本次内存分配是否需要分配大量的内存页(大于 8 ) costly_order = true
// 如果是的,则内核认为即使执行 OOM 也未必会满足这么多的内存页分配需求.
// 所以还是直接失败比较好,即跳转nopage,不再执行 OOM,除非设置 __GFP_RETRY_MAYFAIL,即允许尝试多次后失败
if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
goto nopage;
如果上面没有跳转到nopage,那么下面通过三个if语句依次判断是否重试内存回收,内存规整和重试retry_cpuset。首先,通过should_reclaim_retry函数判断是否应当重试内存回收,如果允许,返回true,则跳转到retry,开始重试。其中no_progress_loops会记录重试次数,超过16次,则禁止重试,或者should_reclaim_retry检测到已经回收了LRU链表中所有可回收的内存,但依然无法满足内存分配请求,也会禁止重试。
如果不允许重试内存回收,则判断是否可重试内存规整,这是should_compact_retry做的事情。当然前提是前面直接内存回收,回收了内存,即did_some_progress > 0,否则,没必要内存规整,因为内存规整实现内存分配依赖于剩余内存。至于should_compact_retry函数,对于order为0,即分配一个页请求是不用内存规整的,直接返回false;其余order会进行一系列的判断,包括降低重试的上限和提升内存规整的优先级,以判断是否重试,compaction_retries记录了重试次数。若最后可以重试内存规整,则跳转到retry,开始重试。
如果上面两个重试都不允许,那么会判断是否重试retry_cpuset,这是考虑到前面内存分配策略因为线程并行竞争导致更新失败,数据不一致问题,导致的前面一系列内存分配失败,因而这里会调用check_retry_cpuset进行检查,如果有,则跳转retry_cpuset,重新整个慢速分配流程,里面就涉及了重新更新内存分配策略。
//判断是否重新尝试回收内存,如果值得重试,返回true,跳转到retry进行重试,no_progress_loops会记录重试次数
//流程走到这里说明前面已经经历过,kswapd异步内存回收和直接内存回收,内存规整,但是依然无法满足内存分配要求
//should_reclaim_retry里面会判断是否重试,其中,如果重试次数超过16次或者已经回收了LRU链表中所有可回收的内存,但依然无法满足内存分配请求,那么返回false
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)
*/
//如果内核判断不应进行内存回收的重试,这里还需要判断下是否应该进行内存规整的重试。
//did_some_progress 表示上次直接内存回收,回收了多少内存页,如果 did_some_progress = 0 则没有必要再进行内存规整重试了,因为内存规整的实现依赖于足够的空闲内存页
//should_compact_retry函数,对于order为0,即分配一个页的请求是不用内存规整的,直接返回false;其余order会进行一系列的判断,包括降低重试的上限和提升内存规整的优先级,以判断是否重试
//compaction_retries记录了重试次数
if (did_some_progress > 0 &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;
//根据nodemask的内存分配策略和进程的内存分配策略mems_allowed
//判断是否应该在进程所允许运行的所有 CPU 关联的 NUMA 节点上重试,这里考虑到了在更新时的并行线程竞争问题,
//导致跟内存分配策略相关的mems_allowed 或 mempolicy nodemaske更新失败,从而导致内存分配失败,因而这里会判断,如果有此情况,则返回true,跳转到retry_cpuset
/* Deal with possible cpuset update races before we start OOM killing */
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
如果前面的三个重试都不允许,则只能最后的杀招,OOM,通过Kill低优先级进程来满足当前进程的内存分配需求,一般进程下面有个oom_adj值,范围[-17,15],该值越高,越容易被杀掉,低优先级的进程该值会比较大,我们可通过cat /proc/<pid>/oom_adj 来看对应pid进程的oom_adj 值。__alloc_pages_may_oom函数里面首先会继续调用get_page_from_freelist函数再做一次挣扎,如果不成功,然后,只能进行out_of_memory函数来kill进程,最后调用__alloc_pages_cpuset_fallback进行尝试。通过did_some_progress记录OOM回收了多少内存页。
/* Reclaim has failed us, start killing things */
//如果上面的直接内存回收和内存规整的重试都不允许,也不允许retry_cpuset,那么只能通过OOM,查杀低优先级的进程来尝试满足当前进程的内存分配需求
//__alloc_pages_may_oom函数里面首先会继续调用get_page_from_freelist函数再做一次挣扎,如果不成功,
//然后,只能进行out_of_memory函数来kill进程,最后调用__alloc_pages_cpuset_fallback尝试
//通过did_some_progress记录OOM回收了多少内存页,如果page不为NULL,则直接跳转got_pg返回。否则继续往下走
page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
if (page)
goto got_pg;
如果OOM还是没有让内存分配成功,则走到如下判断。第一个是为了避免过度的循环重试,若发现当前进程曾被OOM kill过,然后该进程自己alloc分配掩码是ALLOC_OOM或者gfp分配掩码是__GFP_NOMEMALLOC,则直接跳转到nopage,退出循环。第二是更新no_progress_loops参数,内核在慢速路径里面规定是,只要OOM回收了物理内存页,即did_some_progress不为0,那么就将前面记录重试的参数no_progress_loops归零,然后跳转到retry,继续重试。否则结束retry,开始进入nopage。
/* Avoid allocations with no watermarks from looping endlessly */
//为了避免过度循环执行retry
//如果发现自己曾经被OOM选中,即被OOM kill过,然后该进程自己alloc分配掩码是ALLOC_OOM或者gfp分配掩码是__GFP_NOMEMALLOC,则直接跳转到nopage,退出循环
if (tsk_is_oom_victim(current) &&
(alloc_flags & ALLOC_OOM ||
(gfp_mask & __GFP_NOMEMALLOC)))
goto nopage;
/* Retry as long as the OOM killer is making progress */
//如果did_some_progress不为0,说明通过OOM回收了一些内存页,可以继续尝试一下,因而这里会将内存回收次数no_progress_loops归零,继续retry
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}
至此,retry整个流程已经介绍完毕,从流程中可知,retry部分是整个慢速路径分配的核心部分,物理内存分配会在这里重试很多次,尝试通过直接内存回收,内存规整和OOM来实现物理内存的成功分配。若最终还是不成功,就会落入nopage部分,在nopage部分,内核还会做最后一次的尝试。如下是结合物理内存情况,梳理的retry流程图。
5.4 nopage
到nopage为止,内核尝试了包括直接内存回收,内存规整和OOM在内的所有回收内存的措施,且重试了多次,但是仍然没有足够的内存来满足分配要求,虽然看上去此次内存分配就要宣告失败了,但是这里还有一定的回旋余地。如果gfp内存分配策略中配置了 __GFP_NOFAIL,即表示此次内存分配不允许失败,那么内核会在这里不停的重试直到分配成功为止。如下是代码流程。
nopage:
/* Deal with possible cpuset update races before we fail */
//作用跟前面一样
//根据nodemask的内存分配策略和进程的内存分配策略mems_allowed
//判断是否应该在进程所允许运行的所有 CPU 关联的 NUMA 节点上重试,这里考虑到了在更新时的并行线程竞争问题,
//导致跟内存分配策略相关的mems_allowed 或 mempolicy nodemaske更新失败,从而导致内存分配失败,因而这里会判断,如果有此情况,则返回true,跳转到retry_cpuset
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
/*
* Make sure that __GFP_NOFAIL request doesn't leak out and make sure
* we always retry
*/
//再次确认是否是分配不能失败的分配掩码__GFP_NOFAIL,
//如果是,下面会利用WARN_ON_ONCE在kernellog中打印相应的警告信息,然后继续往下走,再做一次尝试
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
*/
//如果can_direct_reclaim是false,表示禁止直接内存回收,那么会打印一次警告信息,然后跳转到fail
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
*/
//如果当前task的flags是PF_MEMALLOC标志,则打印一次警告信息
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.
*/
//如果order大于3,也打印一次警告信息,相当于告诉使用者,一次性分配超过8物理页的连续内存太多了,会失败,建议不要一次申请这么多,不过这个wran只在此次分配失败才会打印
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
*/
//这里再做整个慢速分配路径的最后一次尝试,将alloc_flags改成ALLOC_HARDER,而不是ALLOC_NO_WATERMARKS,进行尝试
//__alloc_pages_cpuset_fallback函数里面会进行考虑cpuset和不考虑cupset两轮尝试内存分配
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
if (page)//如果这最后一次成功,则跳转got_pg
goto got_pg;
//调用cond_resched函数,此次内存分配的进程将cpu让出去,等待下次重新调度,然后再尝试分配
cond_resched();
//如果得到重新调度,跳转到retry,重新尝试分配
goto retry;
}
首先,第一个是跟retry里面作用一样的check_retry_cpuset,考虑前面内存分配策略因为线程并行竞争导致更新失败,导致的前面一系列内存分配失败,因而这里会调用check_retry_cpuset进行检查,如果有,则跳转retry_cpuset,重新整个慢速分配流程,里面就涉及了重新更新内存分配策略。
/* Deal with possible cpuset update races before we fail */
//作用跟前面一样
//根据nodemask的内存分配策略和进程的内存分配策略mems_allowed
//判断是否应该在进程所允许运行的所有 CPU 关联的 NUMA 节点上重试,这里考虑到了在更新时的并行线程竞争问题,
//导致跟内存分配策略相关的mems_allowed 或 mempolicy nodemaske更新失败,从而导致内存分配失败,因而这里会判断,如果有此情况,则返回true,跳转到retry_cpuset
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
然后就是nopage关键部分,如果gfp分配掩码设置了 __GFP_NOFAIL,那么还有戏,可以临终一搏。第一,判断能否直接内存回收can_direct_reclaim,如果为false,那么打印警告信息,跳转fail,直接宣告失败,拜拜了。第二,如果可以直接内存回收,那么闯过一关,开始根据flags和order打印一些警告信息,相当于告诉使用者,注意一下flags和order的使用规范。第三,有惊无险的过了第二关,到了最终关,这里会调用__alloc_pages_cpuset_fallback再做整个慢速分配路径的最后一次尝试,将alloc_flags改成ALLOC_HARDER,而不是ALLOC_NO_WATERMARKS,进行尝试,在__alloc_pages_cpuset_fallback函数里面会进行考虑cpuset和不考虑cupset两轮尝试内存分配。不考虑cpuset,相当于会跨node节点去访问其他内存节点,虽然访问速度会慢很对,但是为了得到内存也是穷尽一切办法。
若最后得到page,那皆大欢喜,否则调用cond_resched函数,此次内存分配的进程会将cpu让出去,等待下次重新调度,然后再尝试分配。如果得到重新调度,直接跳转到retry,重新尝试分配。需要注意的是,虽然将CPU让出去了,但是我们发现各个Node节点的kswapd进程并没有主动进入睡眠,说明前面wakeup后,就算此次内存分配的进程已经调度出去了,但是kswapd依然有可能在后台异步回收内存,这样,当重新调度时,增大了重试内存分配的成功概率。
/*
* Make sure that __GFP_NOFAIL request doesn't leak out and make sure
* we always retry
*/
//再次确认是否是分配不能失败的分配掩码__GFP_NOFAIL,
//如果是,下面会利用WARN_ON_ONCE在kernellog中打印相应的警告信息,然后继续往下走,再做一次尝试
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
*/
//如果can_direct_reclaim是false,表示禁止直接内存回收,那么会打印一次警告信息,然后跳转到fail
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
*/
//如果当前task的flags是PF_MEMALLOC标志,则打印一次警告信息
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.
*/
//如果order大于3,也打印一次警告信息,相当于告诉使用者,一次性分配超过8物理页的连续内存太多了,会失败,建议不要一次申请这么多,不过这个wran只在此次分配失败才会打印
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
*/
//这里再做整个慢速分配路径的最后一次尝试,将alloc_flags改成ALLOC_HARDER,而不是ALLOC_NO_WATERMARKS,进行尝试
//__alloc_pages_cpuset_fallback函数里面会进行考虑cpuset和不考虑cupset两轮尝试内存分配
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
if (page)//如果这最后一次成功,则跳转got_pg
goto got_pg;
//调用cond_resched函数,此次内存分配的进程将cpu让出去,等待下次重新调度,然后再尝试分配
cond_resched();
//如果得到重新调度,跳转到retry,重新尝试分配
goto retry;
}
nopage就介绍完毕了,关键就是判断gfp分配掩码是否设置了 __GFP_NOFAIL,然后进行最后一次分配尝试,否则nopage流程很快就走完。如下是结合物理内存情况,梳理的retry流程图。
5.5 fail和got_pg
如果nopage里面if (WARN_ON_ONCE(!can_direct_reclaim))的判断是成立的,就会跳转到fail这里,打印慢速分配路径失败的error信息,当然如果没有跳转,顺序执行也会走到这里。
在got_pa,对于Android手机,还会结束记录慢速路径分配信息Android trace,跟函数开头的trace配对。最后返回指针page,如果内存分配成功,则返回指向分配内存首个物理页的指针,否则失败,则是NULL。
fail:
//打印慢速路径分配失败的error信息
warn_alloc(gfp_mask, ac->nodemask,
"page allocation failure: order:%u", order);
got_pg:
//android trace,记录慢速路径分配信息,便于调试,跟函数开头的trace配对
trace_android_vh_alloc_pages_slowpath_end(gfp_mask, order, vh_record);
//返回page,成功,则返回指向分配内存首个物理页的指针,失败,则是NULL
return page;
六、__alloc_pages_nodemask物理内存分配过程总览
通过前面四个章节,我们详细介绍物理内存分配和释放的API以及分配掩码gfp_mask。最后,结合代码,完整的梳理内核基于伙伴系统的物理内存分配流程。至于物理内存释放流程,我们后面文章会讲。如下是整体的流程框图和带注释的代码。
6.1 整体流程框图
6.2 __alloc_pages_nodemask函数
/*
* This is the 'heart' of the zoned buddy allocator.
*/
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
nodemask_t *nodemask) //nodemask一般是NULL
{
//指向成功分配物理页或物理内存块的第一个物理页
struct page *page;
//内存分配行为标识掩码alloc_flags初始化为ALLOC_WMARK_LOW
unsigned int alloc_flags = ALLOC_WMARK_LOW;
//实际用于内存分配的分配掩码
gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
//保存物理内存分配过程中的上下文参数
struct alloc_context ac = { };
/*
* There are several places where we assume that the order value is sane
* so bail out early if the request is out of bound.
*/
//如果分配阶order大于等于MAX_ORDER,MAX_ORDER为11,则直接分配失败,打印警告信息,返回NULL
//order在伙伴系统中最大值为:MAX_ORDER-1 = 10
if (unlikely(order >= MAX_ORDER)) {
WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
return NULL;
}
//gfp_allowed_mask表示允许的gfp标志位,具体定义和comment如上所示
//在启动阶段和kernel_init_freeable之后的阶段,gfp_allowed_mask的内容是有可能不一样
//其中启动阶段,不允许分配__GFP_RECLAIM|__GFP_IO|__GFP_FS这三种类型的内存
//最后将更新后的gfp_mask赋值给alloc_mask,得到实际用于分配内存的分配掩码
gfp_mask &= gfp_allowed_mask;
alloc_mask = gfp_mask;
//1、初始化 alloc_context,并为接下来的内存分配更新合适的alloc_flags,正常返回true
if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
return NULL;
/*
* Forbid the first pass from falling back to types that fragment
* memory until all local zones are considered.
*/
//首先,若gfp_mask设置了__GFP_KSWAPD_RECLAIM,则alloc_flags要添加上ALLOC_KSWAPD;其次如果有ZONE_DMA32区域,则alloc_flags要添加上ALLOC_NOFRAGMENT,其跟优化内存碎片化相关
alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask);
/* First allocation attempt */
//2、物理内存分配快速路径:尝试直接从struct zone里面的pcp或者free_area上分配内存,如果失败则要走下面的慢速路径
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
//如果快速路径分配成功,直接跳转到out,准备返回
goto out;
/*
* Apply scoped allocation constraints. This is mainly about GFP_NOFS
* resp. GFP_NOIO which has to be inherited for all allocation requests
* from a particular context which has been marked by
* memalloc_no{fs,io}_{save,restore}.
*/
//执行到这里,说明快速路径分配失败,alloc_mask和spread_dirty_pages状态值要复原,以防在快速路径中被污染,影响慢速分配
//如果当前进程的task struct->flags有PF_MEMALLOC_NOIO或者PF_MEMALLOC_NOFS属性,则需要根据前面更新过的gfp_mask来调整alloc_mask
//PF_MEMALLOC_NOIO:表示分配内存时,禁用IO操作和文件系统操作,提高性能和避免死锁
//PF_MEMALLOC_NOFS:表示分配内存时,仅禁用文件系统操作,避免死锁
//看代码实现实际上,不允许分配__GFP_IO|__GFP_FS这两种类型的内存
alloc_mask = current_gfp_context(gfp_mask);
ac.spread_dirty_pages = false;
/*
* Restore the original nodemask if it was potentially replaced with
* &cpuset_current_mems_allowed to optimize the fast-path attempt.
*/
//同上,也是nodemask状态值复原,看物理内存分配调用路径可知,这个nodemask一般是NULL
ac.nodemask = nodemask;
//3、物理内存慢速路径分配:在快速分配无法满足时,在慢速分配这里内核需要做更多内容,如kswapd异步回收,直接内存回收,内存规整和OOM,
//来满足物理内存分配需求,这一系列的行为组成了慢速路径分配
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
out:
if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge_page(page, gfp_mask, order) != 0)) {
//若使能了kmemecg,有分配掩码__GFP_ACCOUNT且分配到了物理页(物理内存块),
//但是却无法将这个物理页面登记进该需要内存的进程,其对应的内存控制组memcg,则返回非0,否则返回0
//看struct page里面的参数mem_cgroup就是为了实现这个功能
//若为非0,则该page不符合要求,直接就释放,将page置为NULL,分配内存失败
__free_pages(page, order);
page = NULL;
}
//跟踪并记录与内存页面分配相关的事件,用于定位内存分配问题
trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);
//内存分配成功,直接返回 page,否则返回 NULL
return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);
6.3 __alloc_pages_slowpath函数
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
struct alloc_context *ac)
{
//根据gfp_mask分配掩码,确认能否直接内存回收,若有__GFP_DIRECT_RECLAIM,则能直接内存回收
bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
//costly_order表示:内核一般认为order<=3的分配需求时比较容易满足的,大于3相对而言就costly
const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
//下面定义了一系列的局部变量
struct page *page = NULL;
unsigned int alloc_flags;//alloc分配掩码,专门用于伙伴系统内部的
unsigned long did_some_progress;//记录直接内存回收或者OOM回收了多少内存页
enum compact_priority compact_priority;//内存规整的优先级
enum compact_result compact_result;//内存规整的结果
int compaction_retries;//内存规整尝试次数,最多允许尝试16次(MAX_COMPACT_RETRIES)
int no_progress_loops;//记录重试的次数,正常超过一定的次数(MAX_RECLAIM_RETRIES,16次)则内存分配失败
unsigned int cpuset_mems_cookie;//跟cpuset 内存使用策略相关
int reserve_flags;//临时保存调整后的内存分配策略
unsigned long vh_record;//Android特有的,添加打点,用于debug,kernel原生版本没有
//Android通过添加打点记录慢速路径分配,这是开始,__alloc_pages_slowpath最后还要个end,用于结束打点记录
trace_android_vh_alloc_pages_slowpath_begin(gfp_mask, order, &vh_record);
/*
* We also sanity check to catch abuse of atomic reserves being used by
* callers that are not in atomic context.
*/
//因为这里已经进入慢速分配路径了,而在慢速路径中涉及直接回收和OOM操作,这些是会阻塞进程
//因而需要对非上下文进程禁用掉原子操作__GFP_ATOMIC分配掩码,避免滥用原子操作。
//这里肯定可以确定的是,如果是中断处理程序和持有自旋锁的进程上下文,那么分配掩码肯定是不会有__GFP_DIRECT_RECLAIM,它们是不会走入这里的,如果有,那么这个程序设计就是个BUG
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_seq,使用顺序锁保护
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.
*/
//将gfp_mask分配掩码转换为alloc分配掩码
//在之前的快速内存分配路径下设置的相关分配策略比较保守,在_watermark[WMARK_LOW]水线之上进行快速内存分配
//但走到这里表示快速内存分配失败,所以在慢速内存分配路径下需要重新设置相对激进的内存分配策略,采用更大的代价来分配内存
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.
*/
//在调用__alloc_pages_slowpath前,复原的nodemask值,可能会跟快速路径的不一样,
//或者进程的cpuset发生了变化,为了避免在不符合要求的zone反复重试,这里需要重新计算用于物理内存分配的首选zone
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
//如果上面没有适合的zone,则跳转到 nopage , 内存分配失败
if (!ac->preferred_zoneref->zone)
goto nopage;
//如果alloc分配掩码设置了ALLOC_KSWAPD,则唤醒所有相关node对应的kswapd进程,进行物理内存的异步回收
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
/*
* The adjusted alloc_flags might result in immediate success, so try
* that first
*/
//根据前面调整过的alloc_flags和alloc_context,且开启kswapd进程后,重新调用快速分配接口函数get_page_from_freelist,再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
//在可以直接内存回收条件下,
//1、如果是分配大内存costly_order = true (超过 8 个内存页),需要首先进行内存规整,这样内核可以避免直接内存回收从而获取更多的连续空闲内存页
//2、对于虽然order<3,但需要分配不可移动内存的情况,也需要先进行内存规整,防止永久内存碎片
//gfp_pfmemalloc_allowedi检查gfp分配掩码和当前的进程的pflags,判断是否可以不考虑水线影响或者启用OOM,若可以(返回true),则暂时不考虑使用内存规整,否则可以启用内存规整
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, //INIT_COMPACT_PRIORITY = COMPACT_PRIO_ASYNC 异步模式,不允许阻塞
&compact_result);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/*
* Checks for costly allocations with __GFP_NORETRY, which
* includes some THP page fault allocations
*/
//流程走到这里表示经过内存规整之后依然没有足够的内存供分配
//分配大内存costly_order = true (超过 8 个内存页)且gfp分配掩码禁止分配失败的重试
if (costly_order && (gfp_mask & __GFP_NORETRY)) {
//如果compact_result是COMPACT_SKIPPED(跳过此zone,可能此zone不适合)或者COMPACT_DEFERRED(内存规整不能从此zone开始,由于此zone最近失败过)
//则跳转到nopage
if (compact_result == COMPACT_SKIPPED ||
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_SKIPPED和COMPACT_DEFERRED,那么更改内存规整的优先级,从前面默认的DEF_COMPACT_PRIORITY改为INIT_COMPACT_PRIORITY
//内存规整更激进了
compact_priority = INIT_COMPACT_PRIORITY;
}
}
retry:
/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
//这里再次确认和确保,如果alloc分配掩码设置了ALLOC_KSWAPD,
//则所有相关node对应的kswapd进程是唤醒,以确保物理内存的异步回收在进行
if (alloc_flags & ALLOC_KSWAPD)
wake_all_kswapds(order, gfp_mask, ac);
//流程走到这里,说明在_watermark[WMARK_MIN]水线之上也分配内存失败了
//并且有可能经过内存规整之后,内存分配仍然失败,说明当前剩余内存容量已经严重不足
//接下来就需要使用更加激进的手段来尝试内存分配:“忽略掉内存水位线或者开启OOM”,
//根据gfp分配掩码和当前的进程的pflags 修改 alloc_flags 保存在 reserve_flags 中
//__gfp_pfmemalloc_flags函数实际上在gfp_pfmemalloc_allowed中有调用
reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
//如果reserve_flags不为0,则意味着要尝试更加激进的手段。据此再次根据更新alloc分配掩码,大概率是ALLOC_NO_WATERMARKS
//current_alloc_flags主要判断是否添加ALLOC_CMA,并将reserve_flags更新到alloc_flags
if (reserve_flags)
alloc_flags = current_alloc_flags(gfp_mask, reserve_flags);
/*
* Reset the nodemask and zonelist iterators if memory policies can be
* ignored. These allocations are high priority and system rather than
* user oriented.
*/
//如果alloc_flags没有设置CPUSET,意味着有内存需求的进程可以在所有CPU上运行,那么也就意味着内存分配是可以跨Node节点的
//或者reserve_flags非0,即忽略掉内存水位线或者开启OOM
//那么物理内存分配的上下文信息alloc_context,需要更新。
//这里重置了节点状态码nodemask和重新计算用于物理内存分配的首选zone,更新preferred_zoneref
if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
ac->nodemask = NULL;
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
}
/* Attempt with potentially adjusted zonelist and alloc_flags */
//在更新alloc_flags(大概率是ALLOC_NO_WATERMARKS)和alloc_context之后,且确保kswapd进程开启后,重新调用快速分配接口函数get_page_from_freelist,再次尝试分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Caller is not willing to reclaim, we can't balance anything */
//如果can_direct_reclaim为false,即无法直接内存回收,直接跳转到nopage
if (!can_direct_reclaim)
goto nopage;
/* Avoid recursion of direct reclaim */
//如果申请内存分配的进程的flags为PF_MEMALLOC,
//首先走到这里说明能直接内存回收,那么就要避免direct reclaim的递归
//因为当为PF_MEMALLOC,进程是忽略内存水线的,而且该进程属于紧急进程,可以使用紧急内存
//由于直接内存回收是在_watermark[WMARK_MIN]水线下触发的,而该水线下的空闲内存就是紧急内存,
//但是PF_MEMALLOC属性的进程可以无限制的使用,这就很有可能直接内存回收的紧急内存被它给使用了,
//导致直接内存回收一直在执行...,因而这里会跳转到nopage
if (current->flags & PF_MEMALLOC)
goto nopage;
/* Try direct reclaim and then allocating */
//如果前面更新了alloc_flags(大概率是ALLOC_NO_WATERMARKS)和alloc_context之后,还是分配失败
//这里开始启用直接内存回收,利用did_some_progress记录直接内存回收了多少page
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
&did_some_progress);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Try direct compaction and then allocating */
//如果直接内存回收失败,这里开始启用内存规整
page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
compact_priority, &compact_result);
if (page)//如果分配成功,跳转到got_pg,返回page指针
goto got_pg;
/* Do not loop if specifically requested */
//如果gfp分配掩码设置了__GFP_NORETRY,即内存分配失败后,不在重试,那么跳转到nopage,不loop
if (gfp_mask & __GFP_NORETRY)
goto nopage;
/*
* Do not retry costly high order allocations unless they are
* __GFP_RETRY_MAYFAIL
*/
//如果没有__GFP_NORETRY,这里做进一步判断
// 由于后面会触发 OOM,这里需要判断本次内存分配是否需要分配大量的内存页(大于 8 ) costly_order = true
// 如果是的,则内核认为即使执行 OOM 也未必会满足这么多的内存页分配需求.
// 所以还是直接失败比较好,即跳转nopage,不再执行 OOM,除非设置 __GFP_RETRY_MAYFAIL,即允许尝试多次后失败
if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
goto nopage;
//判断是否重新尝试回收内存,如果值得重试,返回true,跳转到retry进行重试,no_progress_loops会记录重试次数
//流程走到这里说明前面已经经历过,kswapd异步内存回收和直接内存回收,内存规整,但是依然无法满足内存分配要求
//should_reclaim_retry里面会判断是否重试,其中,如果重试次数超过16次或者已经回收了LRU链表中所有回收的内存,但依然无法满足内存分配请求,那么返回false
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 则没有必要在进行内存规整重试了,因为内存规整的实现依赖于足够的空闲内存量
//should_compact_retry函数,对于order为0,即分配一个页请求是不用内存规整的,直接返回false;其余order会进行一系列的判断,包括降低重试的上限和提升内存规整的优先级,以判断是否重试
//compaction_retries记录了重试次数
if (did_some_progress > 0 &&
should_compact_retry(ac, order, alloc_flags,
compact_result, &compact_priority,
&compaction_retries))
goto retry;
//根据nodemask的内存分配策略和进程的内存分配策略mems_allowed
//判断是否应该在进程所允许运行的所有 CPU 关联的 NUMA 节点上重试,这里考虑到了在更新时的并行线程竞争问题,
//导致跟内存分配策略相关的mems_allowed 或 mempolicy nodemaske更新失败,从而导致内存分配失败,因而这里会判断,如果有此情况,则返回true,跳转到retry_cpuset
/* Deal with possible cpuset update races before we start OOM killing */
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
/* Reclaim has failed us, start killing things */
//如果上面的直接内存回收和内存规整的重试都不允许,也不允许retry_cpuset,那么只能通过OOM,查杀低优先级的进程(/proc/<pid>/oom_adj,值的范围是-17到+15,取值越高,越容易被杀掉)来尝试满足当前进程的内存分配需求
//__alloc_pages_may_oom函数里面首先会继续调用get_page_from_freelist函数再做一次挣扎,如果不成功,
//然后,只能进行out_of_memory函数来kill进程,最后调用__alloc_pages_cpuset_fallback进程回收后尝试
//通过did_some_progress记录OOM回收了多少内存页,如果page不为NULL,则直接跳转got_pg返回。否则继续往下走
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 */
//为了避免过度循环执行retry
//如果发现自己曾经被OOM选中,即被OOM kill过,然后该进程自己alloc分配掩码是ALLOC_OOM或者gfp分配掩码是__GFP_NOMEMALLOC,则直接跳转到nopage,退出循环
if (tsk_is_oom_victim(current) &&
(alloc_flags & ALLOC_OOM ||
(gfp_mask & __GFP_NOMEMALLOC)))
goto nopage;
/* Retry as long as the OOM killer is making progress */
//如果did_some_progress不为0,说明通过OOM回收了一些内存页,可以继续尝试一下,因而这里会将内存回收次数no_progress_loops归零,继续retry
if (did_some_progress) {
no_progress_loops = 0;
goto retry;
}
nopage:
/* Deal with possible cpuset update races before we fail */
//作用跟前面一样
//根据nodemask的内存分配策略和进程的内存分配策略mems_allowed
//判断是否应该在进程所允许运行的所有 CPU 关联的 NUMA 节点上重试,这里考虑到了在更新时的并行线程竞争问题,
//导致跟内存分配策略相关的mems_allowed 或 mempolicy nodemaske更新失败,从而导致内存分配失败,因而这里会判断,如果有此情况,则返回true,跳转到retry_cpuset
if (check_retry_cpuset(cpuset_mems_cookie, ac))
goto retry_cpuset;
/*
* Make sure that __GFP_NOFAIL request doesn't leak out and make sure
* we always retry
*/
//再次确认是否是分配不能失败的分配掩码__GFP_NOFAIL,
//如果是,下面会利用WARN_ON_ONCE在kernellog中打印相应的警告信息,然后继续往下走,再做一次尝试
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
*/
//如果can_direct_reclaim是false,表示禁止直接内存回收,那么会打印一次警告信息,然后跳转到fail
if (WARN_ON_ONCE(!can_direct_reclaim))
goto fail;
//如果当前task的flags是PF_MEMALLOC标志,则打印一次警告信息
WARN_ON_ONCE(current->flags & PF_MEMALLOC);,
//如果order大于3,也打印一次警告信息,相当于告诉使用者,一次性分配超过8物理页的连续内存太多了,会失败,建议不要一次申请这么多,不过这个wran只在此次分配失败才会打印
WARN_ON_ONCE(order > PAGE_ALLOC_COSTLY_ORDER);
//这里再做整个慢速分配路径的最后一次尝试,将alloc_flags改成ALLOC_HARDER,而不是ALLOC_NO_WATERMARKS,进行尝试
//__alloc_pages_cpuset_fallback函数里面会进行考虑cpuset和不考虑cupset两轮尝试内存分配
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
if (page)//如果这最后一次成功,则跳转got_pg
goto got_pg;
//调用cond_resched函数,此次内存分配的进程将cpu让出去,等待下次重新调度,然后再尝试分配
cond_resched();
//如果得到重新调度,跳转到retry,重新尝试分配
goto retry;
}
fail:
//打印慢速路径分配失败的error信息
warn_alloc(gfp_mask, ac->nodemask,
"page allocation failure: order:%u", order);
got_pg:
//android trace,记录慢速路径分配信息,便于调试,跟函数开头的trace配对
trace_android_vh_alloc_pages_slowpath_end(gfp_mask, order, vh_record);
//返回page,成功,则返回指向分配内存首个物理页的指针,失败,则是NULL
return page;
}
参考资料
The Linux Kernel documentation