背景
Read the fucking source code!
--By 鲁迅A picture is worth a thousand words.
--By 高尔基
说明:
- Kernel版本:4.14
- ARM64处理器,Contex-A53,双核
- 使用工具:Source Insight 3.5, Visio
1. 介绍
之前的系列内存管理文章基本上描述的是物理页面的初始化过程,以及虚拟页面到物理页面的映射建立过程,从这篇文章开始,真正要涉及到页面的分配了。接下来的文章会围绕着分区页框分配器(zoned page frame allocator)
来展开,其中会包含大家熟知的Buddy System
分析。
2. 数据结构
2.1 概述
先回顾一下(五)Linux内存管理zone_sizes_init的数据结构图:
上述的结构体,描述的是下面这张图:
Node ---> ZONE ---> Page
的组织关系,其中Buddy System
中,页面都是以2的次幂来组织成链表,比如free_area[0]
,对应的是1个page
链表,其中又根据不同的MIGRATE_xxxx
类型来组织,如下图:
ARM64
中MAX_ORDER
默认值为11,PAGE_SIZE=4K
,因此总共有0 ~ 10
11个链表数组,链表中的连续的页面为2^0 ~ 2^10
,对应大小为4K ~ 4M
。
可以通过cat /proc/pagetypeinfo
来查看下系统的页面信息,如下图:
可以通过cat /proc/zoneinfo
来查看Node
的ZONE
计数信息:
2.2 Migrate类型
从上边的图中可以看到MIGRATE_xxx
不同的迁移类型,表明页面的移动属性,并在可能的情况下通过将相同属性的页面分组在一起来抑制内存的连续碎片。
enum migratetype {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
/*
* MIGRATE_CMA migration type is designed to mimic the way
* ZONE_MOVABLE works. Only movable pages can be allocated
* from MIGRATE_CMA pageblocks and page allocator never
* implicitly change migration type of MIGRATE_CMA pageblock.
*
* The way to use it is to change migratetype of a range of
* pageblocks to MIGRATE_CMA which can be done by
* __free_pageblock_cma() function. What is important though
* is that a range of pageblocks must be aligned to
* MAX_ORDER_NR_PAGES should biggest page be bigger then
* a single pageblock.
*/
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
MIGRATE_UNMOVABLE
:无法移动和检索的类型,用于内核分配的页面,I/O缓冲区,内核堆栈等;MIGRATE_MOVABLE
:当需要大的连续内存时,通过移动当前使用的页面来尽可能防止碎片,用于分配用户内存;MIGRATE_RECLAIMABLE
:当没有可用内存时使用此类型;MIGRATE_HIGHATOMIC
:减少原子分配请求无法进行高阶页面分配的可能,内核会提前准备一个页面块;MIGRATE_CMA
:页面类型由CMA内存分配器单独管理;MIGRATE_ISOLATE
:内核会暂时更改为这种类型,以迁移使用中的系列活动页面;
2.3 __GFP_xxx请求标志(gfp_mask)
__GFP_xxx
为内部使用的标志,在include/linux/gfp.h
文件中,外部不应该使用这些Flag,这些标志在页面申请的时候使用,其中GFP
表示get free page
。
罗列部分如下:
__GFP_DMA
:请求在ZONE_DMA
区域中分配页面;__GFP_HIGHMEM
:请求在ZONE_HIGHMEM
区域中分配页面;__GFP_MOVABLE
:ZONE_MOVALBE
可用时在该区域分配页面,同时表示页面分配后可以在内存压缩时进行迁移,也能进行回收;__GFP_RECLAIMABLE
:请求分配到可恢复页面;__GFP_HIGH
:高优先级处理请求;__GFP_IO
:请求在分配期间进行I/O操作;__GFP_FS
:请求在分配期间进行文件系统调用;__GFP_ZERO
:请求将分配的区域初始化为0;__GFP_NOFAIL
:不允许请求失败,会无限重试;__GFP_NORETRY
:请求不重试内存分配请求;
2.4 ALLOC_xxxx分配标志(alloc_flags)
分配标志定义在mm/internal.h
文件中,在页面的分配函数中与gfp_mask
分开使用,这些标志时用于内部函数的分配。
ALLOC_WMARK_MIN
:仅在最小水位water mark
及以上限制页面分配;ALLOC_WMARK_LOW
:仅在低水位water mark
及以上限制页面分配;ALLOC_WMARK_HIGH
:仅在高水位water mark
及以上限制页面分配;ALLOC_HARDER
:努力分配,一般在gfp_mask
设置了__GFP_ATOMIC
时会使用;ALLOC_HIGH
:高优先级分配,一般在gfp_mask
设置了__GFP_HIGH
时使用;ALLOC_CPUSET
:检查是否为正确的cpuset;ALLOC_CMA
:允许从CMA区域进行分配;
2.5 struct alloc_context
在页面分配的过程中,有一个结构叫struct alloc_context
,这个结构用于存储各个函数之间传递的参数。这种思想在平时的coding中是可以去借鉴的,比如有些人写代码很喜欢用全局变量,改成这种context
的形式,在各个函数之间传递显得更为优雅。直接看代码吧:
/*
* Structure for holding the mostly immutable allocation parameters passed
* between functions involved in allocations, including the alloc_pages*
* family of functions.
*
* nodemask, migratetype and high_zoneidx are initialized only once in
* __alloc_pages_nodemask() and then never change.
*
* zonelist, preferred_zone and classzone_idx 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 strucure
* by a const pointer.
*/
struct alloc_context {
struct zonelist *zonelist;
nodemask_t *nodemask;
struct zoneref *preferred_zoneref;
int migratetype;
enum zone_type high_zoneidx;
bool spread_dirty_pages;
};
zonelist
:用于分配页面的区域列表;nodemask
:指定Node,如果没有指定,则在所有节点中进行分配;preferred_zone
:指定要在快速路径中首先分配的区域,在慢路径中指定了zonelist
中的第一个可用区域;migratetype
:要分配的迁移页面类型;high_zoneidx
:将分配限制为小于区域列表中指定的高区域;spread_dirty_pages
:脏区平衡相关;
3. build_all_zonelists
在上篇文章中描述到各个zone
,实际上各个zone
最终组织起来是在build_all_zonelists
函数中实现的:
整体完成的工作也比较简单,将所有Node
中可用的zone
全部添加到各个Node
中的zonelist
中,也就是对应的struct pglist_data
结构体中的struct zonelist node_zonelists
字段。
这一步之后,准备工作基本就绪,进行页面申请的工作就可以开始了。
4. alloc_pages
下面的流程开始真正的页面申请了,在内部的实现中通过__alloc_pages
来实现的:
在页面分配时,有两种路径可以选择,如果在快速路径中分配成功了,则直接返回分配的页面;快速路径分配失败则选择慢速路径来进行分配。
4.1 Fast Path
快速路径分配,是通过get_page_from_freelist
来完成的,具体的流程及分析如下图所示:
4.2 Slow Path
慢速路径分配,最终也会调用get_page_from_freelist
,流程分析如下:
1. 概述
本文将分析Buddy System
。Buddy System
伙伴系统,是操作系统内核中用于管理物理内存的一种算法。所谓伙伴指的是物理内存中地址连续的页块之间互为伙伴。若两个页面块之间可以互为伙伴,则两个页面块可以合并为更大的一个页面块。伙伴系统(buddy system)是通过将物理内存划分为页面来进行管理的系统,支持连续的物理页面分配和释放。此外,使用与碎片相关的算法来确保最大的连续页面。
为了便于页面的维护,将多个页面组成内存块,每个内存块都有2的方幂个页,方幂的指数被称为阶。在操作内存时,经常将这些内存块分成大小相等的两个块,分成的两个内存块被称为伙伴块,采用一位二进制数来表示它们的伙伴关系。当这个位为1,表示其中一块在使用;当这个位为0,表示两个页面块都空闲或者都在使用。系统根据该位为0或位为1来决定是否使用或者分配该页面块。系统每次分配和回收伙伴块时都要对它们的伙伴位跟1进行异或运算。所谓异或是指刚开始时,两个伙伴块都空闲,它们的伙伴位为0,如果其中一块被使用,异或后得1;如果另一块也被使用,异或后得0;如果前面一块回收了异或后得1;如果另一块也回收了异或后得0。
1. 内存碎片问题
在前面的章节中,我们知道在内存管理中,存在两种碎片情况
内部碎片:是指已经分配出去的内存空间,却不能被利用的内存空间。也就是说已经被分配出去的内存空间大约请求所需的内存空间,而导致有些内存不能有效使用,自己无法使用,其他的进程也无法使用。
例如进程需要使用3K bytes物理内存,于是向系统申请了大小等于3Kbytes的内存,但是由于Linux内核伙伴系统算法最小颗粒是4K bytes,所以分配的是4Kbytes内存,那么其中1K bytes未被使用的内存就是内存内碎片。
外部碎片:是指还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。内存被分割成很小很小的一些块,这些块虽然是空闲的,但是却小到无法使用。随着申请和释放次数的增加,内存将变得越来越不连续,最终导致整个内存将只剩下碎片。即使有足够的内存可以满足请求,但是要分配一个大块的连续内存却无法满足。
例如系统剩余内存为16K bytes,但是这16K bytes内存是由4个4K bytes的页面组成,即16K内存物理页帧号#1不连续。在系统剩余16K bytes内存的情况下,系统却无法成功分配大于4K的连续物理内存,该情况就是内存外碎片导致,本文中阐述的就是物理内存外碎片化。
对于外部碎片,有两种解决方法:
采用非连续的内存分配,如果内存需要使用物理内存连续,暂时无法做到
采用一种有效的方法来监控内存,保证在内核只要申请一小块内存的情况下,不会从大块的连续内存中分配,从而保证大块内存的连续性和完整性
因此Linux采用后者来解决外部碎片的问题,也就是著名的伙伴系统。
2. 伙伴系统的原理
伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求,其基本的设计思路如下
3. 分配实例
首先,假设我们有一个1M空间的内存
如果第一次需要一个100K的空间,那么对于1M的空间进行分块处理,第一次分成两个512K,而对于此时还是不满足2i-1 <100 < 2i,继续分片,对第一个512K进行分片,分成2个256K,而256K还是不满足,所有对第一个256K继续进行分片,分成2个128K,满足了条件,所以此时会将第一个A=128K分出去
此时第二个分配请求,分配240K的空间,那么直接就从B=256K分配出去
第三个分配请求,分配64K的空间,从空间查找发现有128K的空间可以分成2个64K的空间,直接分配出去C=64K
第四个分配请求,分配256K的空间,发现只有512K的空间满足要求,直接分配D=256K
第五个释放请求,释放B空间,发现不满足合并规则,那么内存中有3块内存独立的内存空间
第六个释放请求,释放A空间,发现A空间与后面的地址空间被C地址隔离,导致不满足合并规则
第七个分配请求,申请75K的空间,发现第一个128K满足条件,直接分配E=128K
第八个释放请求,释放C空间,发现C相邻的地址空间,满足合并请求,直接合并成128K,合并后与前后相邻的无法再合并
第九个释放请求,释放E空间,发现E释放后,与后面的128K空间合并成256K,又与后面的256K合并成512K,那么现在只剩下D空间
第十个释放请求,释放D空间,发现D释放后,与后面的256K空间合并成512K,而前面的512K又满足合并条件,可以合并成1M的空间
对于合并的其原理满足下图
通过以上的算法可以看出,伙伴系统虽然解决了外部碎片,但是会导致内部碎片,例如,针对上图,我们如果需要一个257K的内存空间,那么伙伴系统的算法会怎么处理呢?按照其算法,针对该问题会分配一个512K的内存空间,那么就会有255K的内存空间浪费。
先通过一个例子大体介绍一下原理吧:
空闲的物理页框按大小分组成0~MAX_ORDER
个链表,每个链表存放页框的大小为2的n次幂,其中n在0 ~ MAX_ORDER-1
中取值。
假设请求分配2^8 = 256
个页框块:
- 检查
n = 8
的链表,检查是否有空闲块,找到了则直接返回; - 没有找到满足需求的,则查找
n = 9
的链表,找到512大小
空闲块,拆分成两个256大小
块,将其中一个256大小
块返回,另一个256大小
块添加到n = 8
的链表中; - 在
n = 9
的链表中没有找到合适的块,则查找n = 10
的链表,找到1024大小空闲块,将其拆分成512 + 256 + 256
大小的块,返回需要获取的256大小
的块,将剩下的512大小
块插入n = 9
链表中,剩下的256大小
块插入n = 8
的链表中;
合并过程是上述流程的逆过程,试图将大小相等的Buddy块
进行合并成单独的块,并且会迭代合并下去,尝试合并成更大的块。合并需要满足要求:
- 两个
Buddy块
大小一致; - 它们的物理地址连续;
- 第一个
Buddy块
的起始地址为(2 x N x 4K)
的整数倍,其中4K
为页面大小,N
为Buddy块
的大小;
struct page
结构中,与Buddy System
相关的字段有:
_mapcount
: 用于标记page
是否处在Buddy System
中,设置成-1
或PAGE_BUDDY_MAPCOUNT_VALUE(-128)
;private
: 一个2^k
次幂的空闲块的第一个页描述符中,private
字段存放了块的order
值,也就是k
值;index
: 存放MIGRATE
类型;_refcount
: 用户使用计数值,没有用户使用为0,有使用的话则增加;
合并时如下图所示:
2. Buddy页面分配
Linux内核为了尽量减少空间的浪费,减少申请释放内存的消耗时间,采用基于伙伴算法的存储分配机制。伙伴系统算法把内存中的所有页框按照大小分成10组不同大小的页块,每块分别包含1,2,4,8,……,512个页框。每种不同的页块都通过一个free-area-struct结构体来管理。系统将10个free-area-struct结构体组成一个free-area[]数组。在free-area-struct包含指向空闲页块链表的指针。此外在每个free-area-struct中还包含一个系统空闲页块位图(bitmap),位图中的每一位都用来表示系统按照当前页块大小划分时每个页块的使用情况,同mem-map一样,系统在初始化时调用free-area-struct()函数来初始化每个free-area-struct中的位图结构。
Buddy页面分配的流程如下图所示:
从上图中可以看出,在页面进行分配的时候,有以下四个步骤:
- 如果申请的是
order = 0
的页面,直接选择从pcp
中进行分配,并直接退出; order > 0
时,如果分配标志中设置了ALLOC_HARDER
,则从free_list[MIGRATE_HIGHATOMIC]
的链表中进行页面分配,分配成功则返回;- 前两个条件都不满足,则在正常的
free_list[MIGRATE_*]
中进行分配,分配成功则直接则返回; - 如果3中分配失败了,则查找
后备类型fallbacks[MIGRATE_TYPES][4]
,并将查找到的页面移动到所需的MIGRATE
类型中,移动成功后,重新尝试分配;
如下图:
上述分配的过程,前3个步骤都会调用到__rmqueue_smallest
,第4步调用__rmqueue_fallback
,将从这两个函数来分析。
2.1 __rmqueue_smallest
__rmqueue_smallest
的源代码比较简单,贴上来看看吧:
static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
/* Find a page of the appropriate size in the preferred list */
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = list_first_entry_or_null(&area->free_list[migratetype],
struct page, lru);
if (!page)
continue;
list_del(&page->lru);
rmv_page_order(page);
area->nr_free--;
expand(zone, page, order, current_order, area, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}
从代码中可以看出:
- 从申请的
order
大小开始查找目标MIGRATE
类型链表中页表,如果没有找到,则从更大的order
中查找,直到MAX_ORDER
; - 查找到页表之后,从对应的链表中删除掉,并调用
expand
函数进行处理;
expand
函数的处理逻辑就跟本文概述中讲的例子一样,当在大的order
链表中申请到了内存后,剩余部分会插入到其他的order
链表中,来一张图就清晰了:
2.2 __rmqueue_fallback
当上述过程没有分配到内存时,便会开始从后备迁移类型中进行分配。
其中,定义了一个全局的二维fallbacks
的数组,并根据该数组进行查找,代码如下:
/*
* This array describes the order lists are fallen back to when
* the free lists for the desirable migrate type are depleted
*/
static int fallbacks[MIGRATE_TYPES][4] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
[MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
[MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */
#endif
};
__rmqueue_fallback
完成的主要工作就是从后备fallbacks
中找到一个迁移类型页面块,将其移动到目标类型中,并重新进行分配。
下图将示例整个流程:
3. Buddy页面释放
当向内存请求分配一定数目的页框时,若所请求的页框数目不是2的幂次方,则按稍大于此数目的2的幂次方在页块链表中查找空闲页块,如果对应的页块链表中没有空闲页块,则在更大的页块链表中查找。当分配的页块中有多余的页框时,伙伴系统将根据多余的页框大小插入到对应的空闲页块链表中。向伙伴系统释放页框时,伙伴系统会将页框插入到对应的页框链表中,并且检查新插入的页框能否和原有的页块组合构成一个更大的页块,如果两个块的大小相同且这两个块的物理地址连续,则合并成一个新页块加入到对应的页块链表中,并迭代此过程直到不能合并为止,这样可以极大限度地减少内存的外碎片。
页面释放是申请的逆过程,相对来说要简单不少,先看一下函数调用图吧:
当order = 0
时,会使用Per-CPU Page Frame
来释放,其中:
MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE
三个按原来的类型释放;MIGRATE_CMA, MIGRATE_HIGHATOMIC
类型释放到MIGRATE_UNMOVABLE
类型中;MIGRATE_ISOLATE
类型释放到Buddy系统中;
此外,在PCP释放的过程中,发生溢出时,会调用free_pcppages_bulk()
来返回给Buddy系统。来一张图就清晰了:
在整个释放过程中,核心函数为__free_one_page
,该函数的核心逻辑部分如下所示:
continue_merging:
while (order < max_order - 1) {
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
if (!pfn_valid_within(buddy_pfn))
goto done_merging;
if (!page_is_buddy(page, buddy, order))
goto done_merging;
/*
* Our buddy is free or it is CONFIG_DEBUG_PAGEALLOC guard page,
* merge with it and move up one order.
*/
if (page_is_guard(buddy)) {
clear_page_guard(zone, buddy, order, migratetype);
} else {
list_del(&buddy->lru);
zone->free_area[order].nr_free--;
rmv_page_order(buddy);
}
combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++;
}
__find_buddy_pfn
: 根据释放页面的pfn
计算对应的buddy_pfn
,比如pfn = 0x1000, order = 3
,则buddy_pfn = 0x1008
,pfn = 0x1008, order = 3
,则buddy_pfn = 0x1000
;page_is_buddy
:将page
和buddy
进行配对处理,判断是否能配对;- 进行combine之后,再将pfn指向合并后的开始位置,继续往上一阶进行合并处理;
按照惯例,再来张图片吧:
三、伙伴系统优缺点
1.优点
伙伴系统能够解决内存外部碎片问题,当需要分配若干个内存页面时,用于DMA的内存页面必须连续,伙伴算法很好的满足了这个要求。只要请求的块不超过1024个页面,内核就尽量分配连续的页面。
2.缺点
①合并的要求太过严格,只能是满足伙伴关系的块才能合并。
②. 碎片问题:一个连续的内存中仅仅一个页面被占用,导致整块内存区都不具备合并的条件
③. 浪费问题:伙伴算法只能分配2的幂次方内存区,当需要2的幂次时,好说,当需要35K时,那就需要分配64K的内存空间,但是实际只用到35K空间,多余的29K空间就被浪费掉。
4. 算法的效率问题: 伙伴算法涉及了比较多的计算还有链表和位图的操作,开销还是比较大的,如果每次2^n大小的伙伴块就会合并到2^(n+1)的链表队列中,那么2^n大小链表中的块就会因为合并操作而减少,但系统随后立即有可能又有对该大小块的需求,为此必须再从2^(n+1)大小的链表中拆分,这样的合并又立即拆分的过程是无效率的。
4. 优缺点
优点:
较好的解决了外部碎片问题
只要申请的块不超过1024个页面(4M),内核就尽量分配连续的页面
针对大内存分配设计
缺点:
合并的要求太过严格,只能是满足伙伴关系的块才能合并。一个很小的块往往会阻碍一个大块的合并,一个系统中,对内存块的分配,大小是随机的,一片内存中仅一个小的内存块没有释放,旁边两个大的就不能合并
浪费问题:伙伴系统是按2的幂次方大小进行分配内存块,如果所需内存大小不是2的幂次方,就会有部分页面浪费,有时候还很严重。
拆分和合并开销很大
5. 总结
inux针对大内存的物理地址分配,采用伙伴算法,如果是针对小于一个page的内存,频繁的分配和释放,有更加适宜的解决方案,如slab,后面章节会学习关于linux内核是如何进行伙伴系统的软件实现方式。
在内核初始化完成后,内存管理的责任由伙伴系统承担。前面一章主要学习了伙伴系统的软件算法实现原理伙伴系统原理,本章正式开始Linux下伙伴系统的学习,本章主要是原理性的梳理一些流程,其主要包括
linux对于伙伴系统的设计思路
内存碎片的问题和分配器如何处理碎片
伙伴系统的分配器API
1. 设计思路
前面章节中学习了伙伴系统原理,我们重新梳理伙伴系统的核心思路:内核将系统的空闲页面分成11个块链表,每个块链表分别管理着1,2,4,8,16,32,64,128,256,512和1024个物理页帧号,每个页面大小为4K bytes,那么对于伙伴系统管理的块大小范围从4K bytes到4M bytes,以2的倍数递增,其内存管理框图如下图所示
2. 伙伴系统的结构
系统内存中的每个物理内存页,都对应于一个struct page实例。每个内存域都关联一个struct zone的实例,其中保存了用于管理伙伴系统数据的主要结构组。
struct zone {
/* free areas of differents sizes */
struct free_area free_area[MAX_ORDER];
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
对于free_area数组总共有11个索引,每个索引管理着不同大小的块链表,对于其构成如下
free_area[0]管理的内存单元为1(2^0)个页面,即大小为4K byte内存
free_area[1]管理的内存单元为2(2^1)个页面,即大小为8K byte内存
以此类推,即可得到free_area[2],free_area[3] … free_area[11]
struct free_area 是一个伙伴系统的辅助数据结构:
字段 描述
free_list 用于连接空闲页的链表,页链表包含大小相同的连续内存区域
nr_free 指定了当前内存区中空闲页块的数目,而每种迁移类型都对应于一个空闲列表
伙伴系统的分配器维护着空闲页面组成的块,每一个块都是一个 2 的幂次方个页,指数为阶.比如两个页就是 21,4 个页就是 22,这其中的 1 和 2 就是阶,以此类推可以到达 MAX_ORDER。zone->free_area[MAX_ORDER] 数组中阶作为各个元素的索引,用来对应链表中的连续内存块包含的页面数量。我们来看看一个示意图,索引 0 指向的链表就是 20 阶链表,他携带的内存块都是 1 个页面,再比如 24 这个位置链表就是表示他下面挂的都是 64 个页大小的连续内存块,那么他的字节数为 256K。
3. 内存块是如何连接
从 struct zone 的 free_area 结构体数组内的 free_list 可以得知,这个数组保存的是一个链表的头,所以他其实指向的是一个完整的链表,根据这个数组的索引可以得知,这个链表下面挂载的都是 2x 方个数的连续页面,每一个 free_list 项表示的是一个连续的物理内存块,这样管理起来很简单而且开销不大。具体实现如图所示:
伙伴不必是彼此连续的,从图中可以看出,不同大小的连续页面块都是挂载在不同的链表上,其满足以下关系
当低阶连续的连续的页面不足时,一个内存区在分配期间会自动分解成两半,内核会自动将未用的一般加入到对应的链表中
如果未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴,如果是伙伴,那么就会被合并起来。
4. 避免碎片
在linux的内存管理方面,有一个长期存在的问题,在系统启动并长期运行后,物理内存中会产生很多的内存碎片问题,如下图所示
对于该空间,最大的连续空页只有一页,这对于用户空间的应用程序没有什么问题,其内存时通过页表映射的范式,无论空闲页在物理内存中如何的分布,应用程序看到的内存总是连续的。
对于内核,碎片确实一个大问题,物理内存一致映射到地址空间的内核部分,此时内核无法映射比一页更大的内存区。
物理内存的碎片化一直是linux的一大问题,内核对于该问题仿照文件系统的方式,通过碎片合并的方式解决该问题。但是由于许多的物理内存页时不能移动到任意未知的,阻碍了该方法的实施,所以内核采用的时反碎片化,即试图从最初开始尽可能的防止碎片问题。
对于内核,将已分配的页划分成下面3种不同类型
页面类型 概述 例子
不可移动页 在内存中有固定的位置,不能移动到其他地方 核心内核分配的大多数内存属于该类型
可回收页 不能直接移动,但可以删除,其内容可以从某些源重新生成 kswapd守护进程会根据可回收页访问的频繁程度,周期性的释放此类内存。另外在内存短缺的情况下,页可以发起页面回收机制。
可移动页 可以随意地移动,属于用户空间应用程序的页属性 他们是通过页表映射的。如果他们复制到新的位置,页表项页可以相应的更新,应用程序不会注意到任何事。
而对于内核,使用的反碎片化技术,即基于将具有相同可移动性的页分组思想。前面由于页无法移动,导致在原本空余的内存区中将无法进行连续内存分配。根据页的可移动性,将其分配到不同的列表中,即可防止这种情况。内核可以采用以下思想
内存将内存区域划分为分别用于可移动页和不可移动页的分配
free_area管理的内存还细分为各种类型,例如不可移动页面和可移动页面等,每种类型的页面类型对应一个free_list链表,该链表就链接着页面结构体。
enum {
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE,
#endif
MIGRATE_TYPES
};
宏 类型
MIGRATE_UNMOVABLE 不可移动页,用于内核分配的页面,I/O缓冲区,内核堆栈等
MIGRATE_MOVABLE 可移动页,当需要大的连续内存时,通过移动当前使用的页面来尽可能防止碎片,用于分配用户内存;
MIGRATE_RECLAIMABLE 可回收页,当没有可用内存时使用此类型
MIGRATE_PCPTYPES 是per_cpu_pageset, 即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目
MIGRATE_HIGHATOMIC 在罕见的情况下,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存
MIGRATE_CMA Linux内核最新的连续内存分配器(CMA), 用于避免预留大块内存
MIGRATE_ISOLATE 是一个特殊的虚拟区域, 用于跨越NUMA结点移动物理内存页。在大型系统上, 它有益于将物理内存页移动到接近于使用该页最频繁的CPU。
MIGRATE_TYPES 只是表示迁移类型的数目, 也不代表具体的区域
如果内核无法满足针对某一给定迁移类型的分配请求,会怎么办呢?内核提供一种备用列表fallbacks的方式,规定了在指定列表中无法满足分配请求时,接下来应使用哪种迁移类型
static int fallbacks[MIGRATE_TYPES][4] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
[MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
[MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */
#endif
};
以MIGRATE_RECLAIMABLE为例,如果我需要申请这种页框,当然会优先从这类页框的链表中获取,如果没有,我会依次尝试从MIGRATE_UNMOVABLE -> MIGRATE_MOVABLE 链表中进行分配。
5. 初始化伙伴系统
在初始化伙伴系统之前,所有的node和zone的描述符都已经初始化完毕,同时物理内存中所有的页描述符页相应的初始化为了MIGRATE_MOVABLE类型的页。初始化过程中首先将所有管理区的伙伴系统链表置空,首先回顾下free_area的相关域都被初始化
static void __meminit zone_init_free_lists(struct zone *zone)
{
unsigned int order, t;
for_each_migratetype_order(order, t) {
INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);
zone->free_area[order].nr_free = 0;
}
}
#define for_each_migratetype_order(order, type) \
for (order = 0; order < MAX_ORDER; order++) \
for (type = 0; type < MIGRATE_TYPES; type++)
在内存子系统初始化期间,memmap_init_zone负责处理内存域的page实列,所有的页最初都标记为可移动的
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
unsigned long start_pfn, enum memmap_context context)
{
........
/* 该区所有页都设置为MIGRATE_MOVABLE */
if ((z->zone_start_pfn <= pfn) && (pfn < zone_end_pfn(z)) && !(pfn & (pageblock_nr_pages - 1)))
set_pageblock_migratetype(page, MIGRATE_MOVABLE);
........
}
对于高端内存区和低端内存区在上章节已经梳理过,本章将不在重复梳理。到这里,高端内存和低端内存的初始化就已经完成了。所以未使用的页框都已经放入伙伴系统中供伙伴系统进行管理。
6. 分配器API
buddy分配器是按照页为单位分配和释放物理内存的,free_area就是通过buddy分配器来管理的,其职能分配2的整数幂的页。那么就决定了该接口不能像标准的C库提供的malloc或者bootmem分配器那样指定所需大小的内存,必须指定的是分配阶,伙伴系统将在内存中分配2^n页,内核中细颗粒的分配只能使用slab分配器(或者slub/slob分配器),内核提供多个接口供其他模块申请页框使用
函数接口 功能
struct page * alloc_pages (gfp_mask, order) 向伙伴系统请求连续的2的order次方个页框,返回第一个页描述符。
struct page * alloc_page (gfp_mask) 相当于struct page * alloc_pages(gfp_mask, 0)。
unsigned long get_zeroed_page(gfp_t gfp_mask) 分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的)
void * __get_free_pages (gfp_mask, order) 工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例
CPU的高速缓存,对于申请单个页框,系统会从每个CPU的高速缓存维护的单个页框链表中进行分配;而对于申请多个页框,系统则从伙伴系统中进行分配,可以说每个CPU的高速缓存算是伙伴系统的一部分,专门用于分配单个 页框,因为系统希望尽量让那些刚释放掉的单个页框分配出去,这样有效的提高缓存命中率,因为释放掉的页框可能还处于缓存中,而杠分配的页框一般都会马上使用,系统就不用对这些页框进行换入换出缓存了
有4个函数用于释放不在使用的页,其定义如下:
free_page(struct page *)和free_pages(struct page *, order)用于将一个或2^n页返回给内存管理子系统中,内存区的起始地址由指向该内存区的第一个page实例的指针表示
__ free_page(addr) 和 __free_pages(addr, order),其定于与前面两个类似,但在表示需要释放内存区域时,使用了虚拟地址而不是page实例
内存分配掩码(Get Free Page Mask, GFP_mask),是描述内核分配内存方法的32位或64位标志符,可分为两类:行为修饰符、区修饰符。行为描述符表示分配方式,区修饰符表示分配区。
区修饰符表示内存应该从哪个区分配,通常分配可以从任何区开始,不过,内核优先从ZONE_NORMAL开始,这样可确保其他区在需要时有足够的空闲页可用 [1] 。下表是区修饰符的列表。
标志 描述
__GFP_DMA 从ZONE_DMA分配
__GFP_DMA32 只在ZONE_DMA32分配
__GFP_HIGHMEM 从ZONE_HIGHMEM或ZONE_NORMAL分配
行为修饰符表示内核应当如何分配所需的内存,例如分配器分配内存中的睡眠行为、失败行为、启动各类设备文件行为,具体含义如下表。
行为修饰符 描述
__GFP_RECLAIMABLE
__GFP_MOVABLE 是页迁移机制所需的标志,它们分别将分配的内存标记为可回收的或可移动的。
__GFP_HIGH 分配器可以访问紧急事件缓冲池
__GFP_IO 在查找空闲内存期间,分配器可以进行磁盘I/O操作。
__GFP_FS 分配器可执行VFS操作,可启动文件系统I/O。
__GFP_REPEAT 分配器在分配失败后自动重试,重试也可能失败,但有上限次数。
__GFP_NOFAIL 分配器在分配失败后一直重试,直至成功
__GFP_NORETRY 分配器在分配失败后不重试,从而导致分配失败
__GFP_COMP 添加混合页元素, 在hugetlb的代码内部使用
__GFP_ZERO 分配器在分配成功时,将返回填充字节0的页
在源码中注释强调,一般不直接使用行为修饰符,而是采用类型标志组合行为修饰符和区修饰符,将各种可能用到的组合进行组合,用户使用时无需记住各类行为修饰符的意义,而是直接使用下述表格中的类型标志。
类型标志 描述
GFP_ATOMIC 用于原子分配,在任何情况下都不能中断,用在中断处理程序,下半部,持有自旋锁以及其他不能睡眠的地方
GFP_NOWAIT 与GFP_ATOMIC类似,不同之处在于,调用不会退给紧急内存池,这就增加了内存分配失败的可能性
GFP_KERNEL 这是一种常规的分配方式,可能会阻塞。这个标志在睡眠安全时用在进程的长下文代码中。为了获取调用者所需的内存,内核会尽力而为。这个标志应该是首选标志
GFP_NOIO 这种分配可以阻塞,但不会启动磁盘I/O,这个标志在不能引发更多的磁盘I/O时阻塞I/O代码,这可能导致令人不愉快的递归
GFP_NOFS 这种分配在必要时可以阻塞,但是也可能启动磁盘,但是不会启动文件系统操作,这个标志在你不能在启动另一个文件系统操作时,用在文件系统部分的代码中
GFP_USER 这是一种常规的分配方式,可能会阻塞。这个标志用于为用户空间进程分配内存时使用
GFP_DMA GFP_DMA32 用于分配适用于DMA的内存,当前是__GFP_DMA的同义词,GFP_DMA32也是__GFP_GMA32的同义词
GFP_HIGHUSER 是GFP_USER的一个扩展,也用于用户空间。 它允许分配无法直接映射的高端内存。使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的
GFP_HIGHUSER_MOVABLE 用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行
对于我们驱动中使用最多的场景是GFP_KERNEL和GFP_ATOMIC
GFP_KERNEL:进程上下文中使用,可以睡眠,也可以用在不可以睡眠的场景
GFP_ATMOIC:常用中断处理程序、软中断、tasklet,不能用于睡眠的使用场景
上一章学习了伙伴系统的设计思路和其申请和释放API的使用方式,本章主要是梳理下伙伴系统分配是如何分配出连续的物理页面的。内核中常用的分配物理内存页面的接口是alloc_pages,用于分配一个或者多个连续的物理页面,分配的页面个数只能是2^n。相对于多次离散的物理页面,分配连续的物理页面有利于提高系统内存的碎片化。
1. 页的分配
对于所有的内存分配接口,最后都会调用alloc_pages_node,这个是伙伴系统最重要的接口,其定义如下:
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);
}
这个函数只是执行了一个简单的检查,防止申请的过大的内存。如果指定节点不存在,内核自动使用当前执行CPU对应的节点ID,最终调用__alloc_pages_nodemask,这个函数使伙伴系统的核心函数,下面来看看这个函数处理流程,
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */
struct alloc_context ac = {
.high_zoneidx = gfp_zone(gfp_mask),
.zonelist = zonelist,
.nodemask = nodemask,
.migratetype = gfpflags_to_migratetype(gfp_mask),
};
...
}
struct alloc_context数据结构是伙伴系统分配函数中用于保存相关参数的数据结构,对于该结构gfp_zone()函数从分配掩码中计算出zone的zoneidx,并放到high_zoneidx成员中。
static inline enum zone_type gfp_zone(gfp_t flags)
{
enum zone_type z;
int bit = (__force int) (flags & GFP_ZONEMASK);
z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
((1 << GFP_ZONES_SHIFT) - 1);
VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
return z;
}
首先flags&GFP_ZONEMASK,是将与GFP_XXX无关的其他位清零, 内核通过标志判断从哪个zone分配内存。而对于该结构体中,通过gfpflags_to_migratetype函数将gpf_mask分配掩码转换成MIGRATE_TYPES类型,例如分配的掩码为GFP_KERNEL,那么其类型为MIGRATE_UNMOVABLE;如果分配的掩码为GFP_HIGHUSER_MOVABLE,那么类型就是MIGRATE_MOVABLE,其定义为
static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{
VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE);
BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE);
if (unlikely(page_group_by_mobility_disabled))
return MIGRATE_UNMOVABLE;
/* Group based on mobility */
return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}
然后进入到should_fail_alloc_page函数,检查内存分配是否可行,如果不可行就直接返回,即以失败告终,否则就继续执行内存分配,之后就是一些判断条件,然后进入到真正尝试分配物理页面,其处理流程如下:
* may get reset for allocations that ignore memory policies.
*/
ac.preferred_zoneref = first_zones_zonelist(ac.zonelist,
ac.high_zoneidx, ac.nodemask);
if (!ac.preferred_zoneref->zone) {
page = NULL;
/*
* This might be due to race with cpuset_current_mems_allowed
* update, so make sure we retry with original nodemask in the
* slow path.
*/
goto no_zone;
}
/* First allocation attempt */
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
goto out;
首先通过first_zones_zonelist()从给定的zoneidx开始查找,这个给定的zoneidx就是highidx,之前通过gfp_zone()函数转换得来的。
static inline struct zoneref *first_zones_zonelist(struct zonelist *zonelist,
enum zone_type highest_zoneidx,
nodemask_t *nodes)
{
return next_zones_zonelist(zonelist->_zonerefs,
highest_zoneidx, nodes);
}
first_zones_zonelist()函数会调用next_zones_zonelist()函数来计算zoneref,最后返回zone数据结构
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
*/
if (likely(nodes == NULL))
while (zonelist_zone_idx(z) > highest_zoneidx)
z++;
else
while (zonelist_zone_idx(z) > highest_zoneidx ||
(z->zone && !zref_in_nodemask(z, nodes)))
z++;
return z;
该函数提供3个参数,对于处理如下
highest_zoneidx是gfp_zone()函数计算分配掩码得来
z是通过node_zonelist,主要是node_zonelists,zone在系统处理时会初始化这个数组,具体函数在build_zonelists_node()中,分配物理页面时会优先考虑ZONE_HIGHMEM,因为ZONE_HIGHMEM在zonelist中排在ZONE_NORMAL前面;
nodes,一般为NULL
如果我们分配gfp_zone(GFP_KERNEL)函数返回0,那么highest_zoneidx为0,而这个节点在内存第按高到低排列,那么第一个zone是ZONE_HIGHMEM,其zone编号zone_index的值为1,因此最终next_zones_zonelist中,z++,那么返回的是ZONE_NORMAL;而如果分配的时gfp_zone(GFP_HIGHUSER_MOVABLE),那么这个highest_zoneidx返回的时2,所以zone_index的值小于highest_zoneidx,那么就直接返回ZONE_HIGHMEM。
/* First allocation attempt */
page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
if (likely(page))
goto out;
no_zone:
/*
* Runtime PM, block IO and its error handling path can deadlock
* because I/O on the device might not complete.
*/
alloc_mask = memalloc_noio_flags(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.
*/
if (unlikely(ac.nodemask != nodemask))
ac.nodemask = nodemask;
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
首先get_page_from_freelist()会去尝试分配物理页面,这里是快速分配,是以alloc_flags = ALLOC_WMARK_LOW为参数,以low为标准,遍历zonelist,尝试获取2^order个连续的页框,在遍历zone时,如果zone的当前空闲内存减去需要申请的内存之后,空闲内存是低于low阀值,那么此zone会进行快速内存回收
如果这里分配失败,就会调用到__alloc_pages_slowpath()函数,这里是慢速分配,并且同样分配时不允许进行IO操作 ,这个函数会尝试唤醒页框回收线程,后面会详细分析。
2. Alloc fast path
首先我们来看看快速分配get_page_from_freelist()接口函数,其主要流程如下
从流程中,当判断当前的zone空闲页面低于WMARK_LOW水位后,会调用node_reclaim函数进行页面回收;而当空闲页面充足时候,会调用buffered_rmqueue函数从伙伴系统中分配物理页面
static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
struct zone *zone, unsigned int order,
gfp_t gfp_flags, unsigned int alloc_flags,
int migratetype)
{
unsigned long flags;
struct page *page;
bool cold = ((gfp_flags & __GFP_COLD) != 0);
if (likely(order == 0)) { ------------------(1)
struct per_cpu_pages *pcp;
struct list_head *list;
local_irq_save(flags);//禁止本地CPU中断,禁止前先保存中断状态
do {
pcp = &this_cpu_ptr(zone->pageset)->pcp; //获取此zone的每CPU高速缓存
list = &pcp->lists[migratetype];//根据迁移类型,得到高速缓存区的freelist
if (list_empty(list)) {//高速缓存没有数据;这可能是上次获取的cpu高速缓存迁移
pcp->count += rmqueue_bulk(zone, 0,
pcp->batch, list,
migratetype, cold);//从伙伴系统中获取batch个页框加入到这个链表中
if (unlikely(list_empty(list)))
goto failed;
}
if (cold)//需要冷的高速缓存,则从每CPU高速缓存的双向链表的后面开始分配
page = list_last_entry(list, struct page, lru);
else//需要热的高速缓存,则从每CPU高速缓存的双向链表的前面开始分配,
page = list_first_entry(list, struct page, lru);
list_del(&page->lru);//从每CPU高速缓存链表中拿出来
pcp->count--;
} while (check_new_pcp(page));
} else { ------------------(2)
/*
* We most definitely don't want callers attempting to
* allocate greater than order-1 page units with __GFP_NOFAIL.
*/
//申请多个页框时是有可能会发生失败的情况的,而分配时又表明__GFP_NOFAIL不允许发生失败
WARN_ON_ONCE((gfp_flags & __GFP_NOFAIL) && (order > 1));
spin_lock_irqsave(&zone->lock, flags);
do {
page = NULL;
if (alloc_flags & ALLOC_HARDER) {//放宽限制,即如果当前阶无可用内存时,可以向分配更高阶的内存区
page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
if (page)
trace_mm_page_alloc_zone_locked(page, order, migratetype);
}
if (!page)//从伙伴系统中获取连续页框,返回第一个页的页描述符
page = __rmqueue(zone, order, migratetype);
} while (page && check_new_pages(page, order));
spin_unlock(&zone->lock);
if (!page)
goto failed;
__mod_zone_freepage_state(zone, -(1 << order),
get_pcppage_migratetype(page));//统计,减少zone的free_pages数量统计,因为里面使用加法,所以这里传进负数
}
__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order); ------------------(3)
zone_statistics(preferred_zone, zone, gfp_flags);
local_irq_restore(flags);
VM_BUG_ON_PAGE(bad_range(zone, page), page);
return page;
failed:
local_irq_restore(flags);
return NULL;
}
这里根据order数值分为两种情况,一种情况是order等于0的情况,也就是分配一个物理页面。直接从zone->per_cpu_pageset列表中分配;另外一种情况是order大于0的情况,就从伙伴系统中分配。
1.分配的页面数为1,那么就不需要从buddy系统中获取,因为per-cpu的页缓存提供了一种更快分配和释放的机制。在伙伴系统中每个CPU都对应高速缓存,里面保持着migratetype分类的单页框的双向链表,当申请内存只需要一个页框时,内核从CPU的高速缓存中相应类型的单页框链表中获取一个页框交给申请者,这样好处是,释放单个页框时会放入CPU高速缓存链表,这样的页框就称为热页。
2.需要多个页框,从伙伴系统中分配,如果分配标志位中设置了ALLOC_HARDER,则从free_list[MIGRATE_HIGHATOMIC]的链表中进行页面分配,分配成功则返回;前两个条件都不满足,则在正常的free_list[MIGRATE_*]中进行分配,分配成功则直接则返回
内核经常请求和释放单个页框,为了提升系统性能,每个内存管理区定义了一个"每CPUI"页框的高速缓存,所有“每CPU”高速缓存包含了一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。
为每个内存管理区和每CPU提供两个高速缓存,在内存管理区中,分配单页使用per-cpu机制,分配多页使用伙伴算法
热高速缓存,它存放页框中所包含的内容很可能就在CPU硬件高速缓存中
冷高速缓存
zone结构体中pageset成员指向内存域per-CPU管理结构
struct zone {
...
#ifdef CONFIG_NUMA //若定义了CONFIG_NUMA宏,pageset为二级指针,否则为数组
struct per_cpu_pageset *pageset[NR_CPUS];
#else
struct per_cpu_pageset pageset[NR_CPUS];
#endif
...
}
static struct page *__rmqueue(struct zone *zone, unsigned int order,
int migratetype)
{
struct page *page;
page = __rmqueue_smallest(zone, order, migratetype);//直接从migratetype类型的链表中获取了2的order次方个页框
if (unlikely(!page)) {// 如果page为空,没有在需要的migratetype类型中分配获得页框,说明当前需求类型(migratetype)的页框没有空闲
if (migratetype == MIGRATE_MOVABLE)
page = __rmqueue_cma_fallback(zone, order);//从CMA中获取内存
if (!page)
page = __rmqueue_fallback(zone, order, migratetype);//根据fallbacks数组从其他migratetype类型的链表中获取内存
}
trace_mm_page_alloc_zone_locked(page, order, migratetype);
return page;
}
根据传递的分配阶、用于获取页的内存域、迁移类型,__rmqueue_smallest扫描页的列表,直至找到适当的连续内存块。如果指定的迁移列表不能满足分配请求,就会看migratetype类型是MIGRATE_MOVABLE,就首先从CMA中分配;如果分配失败,则调用 _ _rmqueue_fallback尝试其他的迁移列表,作为应急措施。
__rmqueue_smallest本质上,由一个循环组成,按照递增顺序遍历内存域的各个特定迁移类型的空闲页列表,直至找到合适的一项为止。
static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
/// 循环遍历这层之后的空闲链表
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = list_first_entry_or_null(&area->free_list[migratetype],
struct page, lru);
if (!page)//获取空闲链表中第一个结点所代表的连续页框
continue;
list_del(&page->lru);//将页框从空闲链表中删除
rmv_page_order(page);//将首页框的private设置为0
area->nr_free--;
expand(zone, page, order, current_order, area, migratetype);//将current_order阶的页拆分成小块并重新放到对应的链表中去
set_pcppage_migratetype(page, migratetype);//设置页框的类型与migratetype一致
return page;
}
return NULL;
}
__rmqueue_smallest()函数中只会对migratetype类型的链表进行操作,并且会从需要的order值开始向上遍历,直到成功分配连续页框或者无法分配连续页框为止,比如order为8,页就是需要连续的256个页框,那么尝试从order为8的空闲页框链表中申请内存,如果失败,order就会变成9,从连续512个页框的空闲页框块链表中尝试分配,如果还是失败,就以此寻找和尝试分配…当分配到内存的order与最初的order不相等,比如最初传入的值是8,而成功分配是10,那么就会连续页框进行拆分,这时候就会拆分为256、256、512这三块连续页框,并把512放入order为9的free_list,把一个256放入order为8的free_list,剩余一个256用于分配。
如果需要分配的内存块长度小于所选择的连续页范围,即如果因为没有更小的适当内存块可用,而从较高的分配阶分配一块内存,那么该内存块必须按照伙伴系统的原理分裂成小的块,其过程主要是在expand中实现
static inline void expand(struct zone *zone, struct page *page,
int low, int high, struct free_area *area,
int migratetype)
{
unsigned long size = 1 << high;
//如果high大于 low说明在需要拆分高阶页块来满足本次内存分配
while (high > low) {//循环拆分大页块直到与low一样大
area--;
high--;
size >>= 1;
VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);
/*
* Mark as guard pages (or page), that will allow to
* merge back to allocator when buddy will be freed.
* Corresponding page table entries will not be touched,
* pages will stay not present in virtual address space
*/
if (set_page_guard(zone, &page[size], high, migratetype))
continue;
//将大块拆分成两块,将后半块重新放到伙伴系统中
list_add(&page[size].lru, &area->free_list[migratetype]);
area->nr_free++;//增加统计计数
set_page_order(&page[size], high);//设置页块阶数
}
}
当在大的order链表中申请到了内存后,剩余部分会插入到其他的order链表中,实例如下:
如果在特定的迁移类型列表上没有连续内存区可用,则__rmqueue_smallest返回NULL指针,说明zone的mirgratetype类型的连续页框不足以分配本次1 << order个连续页框。内核接下来根据备用次序,尝试使用其他迁移类型的列表满足分配请求,那么就会调用_ _rmqueue_fallback()进行分配,在__rmqueue_fallback()函数中,主要根据fallbacks表,尝试将其他migratetype类型的pageblock中的空闲页移动到目标类型的mirgratetype类型的空闲页框块链表中
static inline struct page *
__rmqueue_fallback(struct zone *zone, unsigned int order, int start_migratetype)
{
struct free_area *area;
unsigned int current_order;
struct page *page;
int fallback_mt;
bool can_steal;
//这是和指定迁移类型的遍历不一样,这里是从最大阶开始遍历,找到最大可能的内存块,就是为了防止内存碎片
for (current_order = MAX_ORDER-1;
current_order >= order && current_order <= MAX_ORDER-1;
--current_order) {
area = &(zone->free_area[current_order]);//得到高阶空闲数组元素
fallback_mt = find_suitable_fallback(area, current_order,
start_migratetype, false, &can_steal);//检查是否有合适的fallback空闲页框
if (fallback_mt == -1)//如果没有找到适合就查找下一个order
continue;
page = list_first_entry(&area->free_list[fallback_mt],
struct page, lru);
if (can_steal)//调用steal_suitable_fallback进行真正的page的迁移
steal_suitable_fallback(zone, page, start_migratetype);
/* Remove the page from the freelists */
area->nr_free--;
list_del(&page->lru);
rmv_page_order(page);//设置page->_mapcount = -1 并且 page->private = 0
expand(zone, page, order, current_order, area,
start_migratetype);//如果有多余的页框,则把多余的页框放回伙伴系统中
/*
* The pcppage_migratetype may differ from pageblock's
* migratetype depending on the decisions in
* find_suitable_fallback(). This is OK as long as it does not
* differ for MIGRATE_CMA pageblocks. Those can be used as
* fallback only via special __rmqueue_cma_fallback() function
*/
set_pcppage_migratetype(page, start_migratetype);
trace_mm_page_alloc_extfrag(page, order, current_order,
start_migratetype, fallback_mt);
return page;
}
return NULL;
}
内核定义了一个二维数组来描述迁移的规则,其定义如下:static int fallbacks[MIGRATE_TYPES][4] = {
[MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
[MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
[MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
[MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */
#endif
};
不可移动的备用迁移类型优先级顺序:MIGRATE_RECLAIMABLE > MIGRATE_MOVABLE
可回收的备用迁移类型优先级顺序: MIGRATE_UNMOVABLE > MIGRATE_MOVABLE
可移动的备份迁移类型优先级顺序: MIGRATE_RECLAIMABLE > MIGRATE_UNMOVABLE
int find_suitable_fallback(struct free_area *area, unsigned int order,
int migratetype, bool only_stealable, bool *can_steal)
{
int i;
int fallback_mt;
if (area->nr_free == 0)
return -1;
*can_steal = false;
for (i = 0;; i++) {
fallback_mt = fallbacks[migratetype][i]; ------------------(1)
if (fallback_mt == MIGRATE_TYPES)
break;
if (list_empty(&area->free_list[fallback_mt])) ------------------(2)
continue;
if (can_steal_fallback(order, migratetype)) ------------------(3)
*can_steal = true;
if (!only_stealable)
return fallback_mt;
if (*can_steal)
return fallback_mt;
}
return -1;
}
此函数主要的用途是找到合适的迁移类型,其主要完成以下工作1.根据当前的迁移类型获取到一个备份的迁移类型,如果迁移类型MIGRATE_TYPES,则break
2.如果当前的迁移类型的freelist的链表为空,说明备份的迁移类型没有可用的页,则去下一级获取页
3.can_steal_fallback来判断此迁移类型释放可以作为备用迁移类型,如果则返回true
static void steal_suitable_fallback(struct zone *zone, struct page *page,
int start_type)
{
unsigned int current_order = page_order(page);
int pages;
/* Take ownership for orders >= pageblock_order */
if (current_order >= pageblock_order) {//如果选定的页块大于pageblock_order,就改变整页块的迁移类型
change_pageblock_range(page, current_order, start_type);
return;
}
//统计页块在伙伴系统中的页和不在伙伴系统中并且类型为MOVABLE的页数量并且删除在伙伴系统中的页
pages = move_freepages_block(zone, page, start_type);
/* Claim the whole block if over half of it is free */
if (pages >= (1 << (pageblock_order-1)) ||
page_group_by_mobility_disabled)
set_pageblock_migratetype(page, start_type);//通过修改页块在zone-> pageblock_flags中对应bit来修改页块的迁移类型
}
到此,对于当申请一个page的时候,去对应order的freelist的迁移类型链表中找对应的page,如果没有找到对应的page,则就会去对应类型的备用类型的freelist去获取page,将此page挂载到之前需要申请的freelsit中,然后进行retry再通过__rmqueue_smallest申请一次即可。
3. Alloc slowpath
当前面快速分配内存没有成功,就会通过各种途径尝试分配所需的内存,对于慢速分配,里面涉及的流程太过于复杂,涉及到的内存压缩(同步和异步)、直接内存回收和kswapd线程唤醒,放到后面章节。
4. 总结
到这里,伙伴系统的分配流程已经完毕,本章只是对分配内存的流程进行了梳理,学习了alloc_pages,大致清楚了伙伴系统是如何分配出连续的页面的整个过程。
对于内存释放函数也可以归纳到一个主要的函数(__free_pages),只是用不同的参数调用而已,前者是通过page,后者是通过虚拟地址addr,其定义如下
extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
对于free_pages和__free_pages之间的关系,首先需要将虚拟地址转换成指向struct page的指针
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);
}
}
对于__free_pages是一个基础函数,用于实现内核API中所有涉及到内存释放的接口函数,其代码流程如下:
void __free_pages(struct page *page, unsigned int order)
{
if (put_page_testzero(page)) {//检查页框是否还有进程在使用,就是检查_count变量的值是否为0
if (order == 0) //如果是1个页框,则放回每CPU高速缓存中
free_hot_cold_page(page, false);
else //如果是多个页框,则放回伙伴系统
__free_pages_ok(page, order);
}
}
首先,调用put_page_testzero来查看该页是否还有其他引用(struct page结构中的_count),如果没有被引用,就走到对应的页面释放流程中,如果还被引用,就啥也不做
由申请页面的时候,会区分是申请的单页还是多页,那么释放的时候,就也做同样的处理。会判断所需释放的内存是单页还是较大的内存块。
如果释放的是单页,则不还给伙伴系统,还是放回per-cpu缓存中。
如果释放的时多页,就直接调用__free_pages_ok归还到伙伴系统中
1. free_hot_cold_page流程
void free_hot_cold_page(struct page *page, bool cold)
{
struct zone *zone = page_zone(page); //用于根据page得到所在zone
struct per_cpu_pages *pcp;
unsigned long flags;
unsigned long pfn = page_to_pfn(page); //根据给出页地址求出对应的页帧号
int migratetype;
//释放前pcp的准备工作,检查释放满足释放条件
if (!free_pcp_prepare(page))
return;
//获取页框所在pageblock的页框类型
migratetype = get_pfnblock_migratetype(page, pfn);
//设置页框类型为pageblock的页框类型,因为在页框使用过程中,这段pageblock可以移动到了其他类型
set_pcppage_migratetype(page, migratetype);
local_irq_save(flags);
__count_vm_event(PGFREE);
//如果不是高速缓存类型,就放回到伙伴系统中
if (migratetype >= MIGRATE_PCPTYPES) {
if (unlikely(is_migrate_isolate(migratetype))) {
free_one_page(zone, page, pfn, 0, migratetype);
goto out;
}
migratetype = MIGRATE_MOVABLE;
}
//获取当前cpu页列表的zone->pageset->pcp
pcp = &this_cpu_ptr(zone->pageset)->pcp;
if (!cold) //hot page加入list 头部,便于优先使用
list_add(&page->lru, &pcp->lists[migratetype]);
else //对于cold page,将其加入list尾部
list_add_tail(&page->lru, &pcp->lists[migratetype]);
pcp->count++;
//当前CPU高速缓存中页框数量高于最大值,将pcp->batch数量的页框放回伙伴系统
if (pcp->count >= pcp->high) {
unsigned long batch = READ_ONCE(pcp->batch);
free_pcppages_bulk(zone, batch, pcp);
pcp->count -= batch;
}
out:
local_irq_restore(flags);
}
对于冷页和热页,主要表现是当一个页被释放时,默认设置为热页的话,因为该页可能有些地址的数据还是处于映射到CPU cache,当该CPU上有进程申请单个页框时,优先把这些热页分配出去,这样能提高cache的命中率,提高效率,则软件上的实现方式也比较简单,热页,则直接加入到CPU页框高速缓存链表的链表头,冷页则直接加入到链表尾。
2. __free_pages_ok流程
再看看连续页框的释放,连续页框释放主要是__free_pages_ok()函数
static void __free_pages_ok(struct page *page, unsigned int order)
{
unsigned long flags;
int migratetype;
unsigned long pfn = page_to_pfn(page); //根据给出页地址求出对应的页帧号
if (!free_pages_prepare(page, order, true)) //释放前pcp的准备工作,检查释放满足释放条件
return;
migratetype = get_pfnblock_migratetype(page, pfn);//获取页框所在pageblock的页框类型
local_irq_save(flags);
__count_vm_events(PGFREE, 1 << order); //统计当前CPU一共释放的页框数
free_one_page(page_zone(page), page, pfn, order, migratetype); //释放函数
local_irq_restore(flags);
}
对于该接口无论是释放单页还是连续页,在释放时,会获取该页所载的pageblock的类型,然后把此页设置成pageblock一致的类型,因为有一种情况,比如一个pageblock为MIGRATE_MOVABLE类型,并且有部分页已经被使用(这些正在被使用的页都为MIGRATE_MOVABLE),然后MIGRATE_RECLAIMABLE类型的页不足,需要从MIGRATE_MOVABLE这里获取这个pageblock到MIGRATE_RECLAIMABLE类型中,这个pageblock的类型就被修改成了MIGRATE_RECLAIMABLE,这样就造成了正在使用的页的类型会与pageblock的类型不一致。最后调用free_one_page函数,其定义如下
static void free_one_page(struct zone *zone,
struct page *page, unsigned long pfn,
unsigned int order,
int migratetype)
{
unsigned long nr_scanned;
spin_lock(&zone->lock);
nr_scanned = node_page_state(zone->zone_pgdat, NR_PAGES_SCANNED);//数据更新
if (nr_scanned)
__mod_node_page_state(zone->zone_pgdat, NR_PAGES_SCANNED, -nr_scanned);
//内存隔离
if (unlikely(has_isolate_pageblock(zone) ||
is_migrate_isolate(migratetype))) {
migratetype = get_pfnblock_migratetype(page, pfn);
}
__free_one_page(page, pfn, zone, order, migratetype);//释放page开始的order次方个页框到伙伴系统,这些页框的类型时migratetype
spin_unlock(&zone->lock);
}
整个释放过程的核心函数使__free_one_page,依据申请的算法,那么释放就涉及到对页面能够进行合并的。相关的内存区被添加到伙伴系统中适当的free_area列表中,在释放时,该函数将其合并为一个连续的内存区,放置到高一阶的free_are列表中。如果还能合并一个进一步的伙伴对,那么也进行合并,转移到更高阶的列表中。该过程会一致重复下去,直至所有可能的伙伴对都已经合并,并将改变尽可能向上传播。
static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype)
{
unsigned long page_idx;
unsigned long combined_idx;
unsigned long uninitialized_var(buddy_idx);
struct page *buddy;
unsigned int max_order;
//则最大的order应该为MAX_ORDER与pageblock_order+1中最小那个
max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1);
//检查
VM_BUG_ON(!zone_is_initialized(zone));
VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);
//
VM_BUG_ON(migratetype == -1);
if (likely(!is_migrate_isolate(migratetype)))
__mod_zone_freepage_state(zone, 1 << order, migratetype);
//将释放的页面转换成page idx
page_idx = pfn & ((1 << MAX_ORDER) - 1);
//如果被释放的页不是所释放阶的第一个页,则说明参数有误
VM_BUG_ON_PAGE(page_idx & ((1 << order) - 1), page);
VM_BUG_ON_PAGE(bad_range(zone, page), page);//检查页面是否处于zone之中
continue_merging://释放页以后,当前页面可能与前后的空闲页组成更大的空闲页面,直到放到最大阶的伙伴系统中
while (order < max_order - 1) {
buddy_idx = __find_buddy_index(page_idx, order);//找到与当前页属于同一个阶的伙伴页面索引
buddy = page + (buddy_idx - page_idx);//计算伙伴页面的页地址
//检查buddy是否描述了大小为order的空闲页框块的第一个页
if (!page_is_buddy(page, buddy, order))
goto done_merging;
//页面调试功能,页面被释放时是整个的从内核地址空间中移除的。该选项显著地降低了速度,但它也能迅速指出特定类型的内存崩溃错误,需要配置CONFIG_DEBUG_PAGEALLOC
if (page_is_guard(buddy)) {
clear_page_guard(zone, buddy, order, migratetype);
} else {
list_del(&buddy->lru);//如果能够合并,则将伙伴页从伙伴系统中摘除
zone->free_area[order].nr_free--;//同时减少当前阶中的空闲页计数
rmv_page_order(buddy);//清除伙伴页的伙伴标志,因为该页会被合并
}
combined_idx = buddy_idx & page_idx;//将当前页与伙伴页合并后,新的页面起始地址
page = page + (combined_idx - page_idx);
page_idx = combined_idx;
order++;
}
if (max_order < MAX_ORDER) {
if (unlikely(has_isolate_pageblock(zone))) {
int buddy_mt;
buddy_idx = __find_buddy_index(page_idx, order);
buddy = page + (buddy_idx - page_idx);
buddy_mt = get_pageblock_migratetype(buddy);
if (migratetype != buddy_mt
&& (is_migrate_isolate(migratetype) ||
is_migrate_isolate(buddy_mt)))
goto done_merging;
}
max_order++;
goto continue_merging;
}
done_merging:
set_page_order(page, order);//设置伙伴页中第一个空闲页的阶
/**
* 如果当前合并后的页不是最大阶的,那么将当前空闲页放到伙伴链表的最后。
* 这样,它将不会被很快被分配,更有可能与更高阶页面进行合并。
*/
if ((order < MAX_ORDER-2) && pfn_valid_within(page_to_pfn(buddy))) {
struct page *higher_page, *higher_buddy;
combined_idx = buddy_idx & page_idx;//计算更高阶的页面索引及页面地址
higher_page = page + (combined_idx - page_idx);
buddy_idx = __find_buddy_index(combined_idx, order + 1);
higher_buddy = higher_page + (buddy_idx - combined_idx);
if (page_is_buddy(higher_page, higher_buddy, order + 1)) {//更高阶的页面是空闲的,属于伙伴系统
//将当前页面合并到空闲链表的最后,尽量避免将它分配出去
list_add_tail(&page->lru,
&zone->free_area[order].free_list[migratetype]);
goto out;
}
}
//更高阶的页面已经分配出去,那么将当前页面放到链表前面
list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
out:
zone->free_area[order].nr_free++;//将当前阶的空闲计数加
}
但内核如何知道一个伙伴对的两个部分都位于空闲页的列表中呢?为将内存块放回伙伴系统,内核必须计算潜在的伙伴地址,以及在有可能合并的情况下合并后内存块的索引。内核提供辅助函数用于计算
static inline unsigned long
__find_buddy_index(unsigned long page_idx, unsigned int order)
{
return page_idx ^ (1 << order);
}
对于__free_one_page试图释放一个order的一个内存块,有可能不只是当前内存块与能够与其合并的伙伴直接合并,而且高阶的伙伴也可以合并,因此内核需要找到可能的最大分配阶。假设释放一个0阶内存块,即一页,该页的索引值为10,假设页10是合并两个3阶伙伴最后形成一个4阶的内存块,计算如下图所示
ordrr page_idx buddy_index-page-index __find_combined_index
0 10 1 10
1 10 -2 8
2 8 4 8
3 8 -8 0
第一遍寻找到页10的伙伴页11,由于需要的不是伙伴的页号,而是指向对应page的实例指针,buddy_index-page_idx就派上用场了,该值表示当前页与伙伴系统的差值,page指针加上该值,即可得到伙伴page的实例。
然后通过page_is_buddy需要改指针来检查伙伴系统是否是空闲,如果恰好是空闲,那么久可以合并这两个伙伴。这时候就需要将页11从伙伴系统中移除,重新合并形成一个更大的内存块,而rmv_page_order负责清楚PG_buddy标志和private数据。然后下一遍循环工作类似,但这一次order=1,也就是说,内核试图合并两个2页的伙伴,得到一个4页的内存块,其合并图如下图所示
到此,伙伴系统的页释放流程也梳理完毕,其实现过程也比较简单。
非常重要的一个参考文章:一篇长文叙述Linux内核虚拟地址空间的基本概括 - 知乎