避免物理内存碎片化

以前的项目碰到了buddy内存分配失败的情况,虽然当前可用内存和可回收内存还有很多,但是仍然无法满足分配,经过分析是物理内存碎片化导致申请连续物理内存失败。

 当linux系统持续运行很长时间没有重新启动后,系统内持续的进行页面的分配和释放,系统空闲物理内存被使用的物理内存分割开,大块的连续物理内存为0,空闲页面只能满足小数量页面的分配,此时系统可能还有很多空闲页面,但是buddy系统无法满足连续page分配申请。

一般情况下,出错信息如下:


<4>[ 3308.564000] lowmem_reserve[]: 0 0 0

<4>[ 3308.564000] Normal: 743*4kB 3*8kB 0*16kB 0*32kB 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 2996kB


Linux buddyy系统是linux kernel比较稳定的一个模块,但是并不是说它没有缺陷,Linux内存管理系统自诞生之日,就一直存在物理内存碎片化的问题:在系统启动并且运行很长一段时间后,极端情况下,尽管总的可用物理page数目很高,但是空闲的连续物理内存可能并不大,这就造成申请大块连续物理内存分配时失败。尤其是当分配操作带有ATOMAIC标记时,系统连回收内存的机会都没有。


首先要明确以下一点:

物理内存碎片化是无法避免的,所以相关的bug在理论上是无法彻底解决的,只能进行规避,或者减少出错的几率,本文也是对规避和减少出错几率的方法做个总结。


避免碎片

很长时间以来,物理内存的碎片化一直是Linux操作系统的弱点之一,尽管已经有人提出了很多解决方法,但是没有哪个方法能够彻底的解决,memory buddy分配就是解决方法之一。 我们知道磁盘文件也有碎片化问题,但是磁盘文件的碎片化只会减慢系统的读写速度,并不会导致功能性错误,而且我们还可以在不影响磁盘功能的前提的下,进行磁盘碎片整理。而物理内存碎片则截然不同,物理内存和操作系统结合的太过于紧密,以至于我们很难在运行时,进行物理内存的搬移(这一点上,磁盘碎片要容易的多;实际上mel gorman已经提交了内存紧缩的patch,只是还没有被主线内核接收)。 因此解决的方向主要放在预防碎片上。

在2.6.24内核开发期间,防止碎片的内核功能加入了主线内核。在了解反碎片的基本原理前,先对内存页面做个归类:

1. 不可移动页面 unmoveable:在内存中位置必须固定,无法移动到其他地方,核心内核分配的大部分页面都属于这一类。

2.  可回收页面 reclaimable:不能直接移动,但是可以回收,因为还可以从某些源重建页面,比如映射文件的数据属于这种类别,kswapd会按照一定的规则,周期性的回收这类页面。

3. 可移动页面 movable:可以随意的移动。属于用户空间应用程序的页属于此类页面,它们是通过页表映射的,因此我们只需要更新页表项,并把数据复制到新位置就可以了,当然要注意,一个页面可能被多个进程共享,对应着多个页表项。


防止碎片的方法就是把这三类page放在不同的链表上,避免不同类型页面相互干扰。考虑这样的情形,一个不可移动的页面位于可移动页面中间,那么我们移动或者回收这些页面后,这个不可移动的页面阻碍着我们获得更大的连续物理空闲空间。

针对页面的分类,我们引入了movable zone,事实上movable zone是虚拟zone,是在运行时逐渐建立的。当然内核的确可以建立真实的内存zone。

我们知道大部分buddy分配失败,发生在申请unremovable页面时。这样分类还有一个潜在的好处,为unremovable保留的页面,被reclaimable和movable分配的优先级低(参见fallbacks),因此客观上减少了buddy分配unremovable页面的几率。


数据结构

kernel引入了一些宏来表示不同的迁移类型:

[html] view plain copy print ?
  1. <mmzone.h>  
  2. #define MIGRATE_UNMOVABLE 0  
  3. #define MIGRATE_RECLAIMABLE 0  
  4. #define MIGRATE_MOVALBE 0  
  5. #define MIGRATE_RESERVE 0  
  6. #define MIGRATE_ISOLATE 0  
  7. #define MIGRATE_TYPES 0  

类型MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE和MIGRATE_MOVALBE就是我们上面介绍的三种页面类型。如果向特定类型页面分配请求失败,紧急情况下可以从MIGRATE_RESERVE分配内存。


当制定类型的空闲列表无法满足分配时,可以按照一定规则从其他类型空闲链表分配,这个次序用下面数据描述

[cpp] view plain copy print ?
  1. static int fallbacks[MIRGRATE_TYPES][MIGRATE_TYPES-1] = {  
  2.     [MIGRATE_UNMOVABLE] = {MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE},  
  3.     [MIGRATE_RECLAIMABLE] = {MIRGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE},  
  4.     [MIGRATE_MOVABLE] = {MIRGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE},  
  5.     [MIGRATE_RESERVE] = {MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE},  
  6. }   

和zone_list功能类似,当内核想要分配不可移动页面,如果该链表为空,则优先选择从RECLAIMABLE链表分配,然后是MOVABLE,最后使用RESERVE链表。

实际上,这种方法并不能解决我们的问题,因为用户空间页面映射以及内核申请RECLAIMABLE页面的需求可能是无止境的,当MOVABLE和RECLAIMABLE链表无法满足分配时,根据fallbacks会占用MIGRATE_UNMOVABLE链表,这就导致后面UNMOVABLE分配可能失败。

So,我们可以修改fallbacks如下

[html] view plain copy print ?
  1. static int fallbacks[MIRGRATE_TYPES][MIGRATE_TYPES-1] = {  
  2.     [MIGRATE_UNMOVABLE] = {MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE},  
  3.     [MIGRATE_RECLAIMABLE] = {MIRGRATE_MOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE},  
  4.     [MIGRATE_MOVABLE] = {MIRGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE},  
  5.     [MIGRATE_RESERVE] = {MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE},  
  6. }   

禁止MOVABLE或者RECLAIMABLE失败后尝试从UNMOVABLE链表分配页面,这样可以保持UNMOVABLE不受非关键页面分配的干扰。

注意:即便可移动分组特性已经编译到内核中,但是只有当系统中有足够内存可以分配给多个类型的链表时,该特性才有意义。这个足够的含义由pageblock_order和pageblock_nr_pages来定义。当可用内存过少的时候,引入页面迁移没有任何好处,相反会增加系统负担。系统会在 build_all_zonelist中进行检查,如果没有足够内存,则关闭该特性。

在内存子系统初始化期间,memmap_init_zone负责处理内存域的page实列,所有的页最初都标记为可移动的!

[cpp] view plain copy print ?
  1. mm/page_alloc.c  
  2. void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, unsigned long start_pfn, enum memmap_context context)  
  3. {  
  4.     strcut page *page;  
  5.     unsigned long end_pfn = start_pfn+size;  
  6.     unsigned long pfn;  
  7.   
  8.     for (pfn = start_pfn; pfn < end_pfn; pfn++) {  
  9. ...  
  10.         if ((pfn & (pageblock_nr_pages - 1)))  
  11.             set_pageblock_migratetype(page, MIGRATE_MOVABLE);  
  12. ...  
  13. }  

在进行内存分配时,如果没有预定迁移类型的内存区。那么会尝试从MOVABLE链表上获取尽可能大的内存区,并转换到相应的列表,由于获取的内存区长度是最大的,因此不会向可移动内存区引入碎片。这种做法使得不同类型的页面从不同的页面范围内分配,从而使得不同类型的内存分配比避免干扰。

内存分配器如何知道分配申请是哪种迁移类型呢,这需要所有内存申请提供相应的分配标记,如果需要分配可移动的内存页,那么使用__GFP_MOVABLE,如果申请可回收的则使用__GFP_RECLAIMABLE。如果这些标记都没有设置,则认为是UNMOVABLE的。



Virtual Movable Zone

这种方法是提前分配一个称为ZONE_MOVABLE的内存zone,ZONE_MOVABLE内核特性必须由管理员显示激活。其基本思想是:把物理内存划分为两个内存zone,一个用于可移动分配,另外一个用于不可移动分配。这样不可移动内存域不会干扰可移动内存域引入碎片,而可移动内存域因此很容易满足分配。

当然,如何划分这两个内存域,对系统管理员是个挑战。

和系统中的其他内存域不同,ZONE_MOVABLE是一个虚拟的内存域,它的内存取自高端内存域或者普通内存域。ZONE_MOVABLE内存有两种提取方式:

1. 用于可移动分配的内存平均分布到所有的内存节点上。

2. 只使用来自最高内存域的内存,在内存较多的32位系统上,这通常是ZONE_HIGHMEM。注意这也和ZONE_MOVABLE的使用者吻合,因为只有用户页面映射才算是MOVA LE的内存页,而用户页面映射优先使用的也是HIGHMEM。

因此对于ZONE_MOVABLE我们可以使用如下策略:

1. 使能高端内存

2. ZONE_MOVABLE从HIGHMEM提取内存

3. 系统管理员估算ZONE_MOVABLE的大小,较小的ZONE_MOVABLE使得非movable ZONE有更多地物理内存。


ZONE_MOVABLE 使用者

因为ZONE_MOVABLE的使用者是带有GFP_MOVABLE和GFP_HIGHMEM标记的内核分配(主要是应用程序的页面映射)。

[cpp] view plain copy print ?
  1. void *vmalloc(unsigned long size)  
  2. {  
  3.     return __vmalloc_node_flags(size, -1, GFP_KERNEL | __GFP_HIGHMEM);  
  4. }  

从vmalloc的实现来看,vmalloc并没有调用GFP_MOVABLE和GFP_RECLAIMABLE标记,因此,vmalloc并不会从ZONE_MOVABLE分配内存


结论

ZONE_MOVABLE和页面分类方法相比,好处是很明显的:固定UNMOVABLE zone的大小(页面分类链表是动态生成的),UNMOVABLE zone供内核关键分配函数使用。系统频繁申请的MOVABLE分配,不会导致unmovable zone的碎片化。但是缺点仍然很明显,即RECLAIMABLE分配还是使用unmovable zone,频繁的分配回收仍然使得unmovable zone碎片化。

所以看起来,ZONE_MOVABLE方法只是缓解了物理内存碎片化,但是并没有完全解决。


对于某些特定的驱动,我们可以通过以下方式减少分配失败的可能性。

1. 减少分配所需的连续页面数目。

2. 如果内存申请操作对系统来说是关键操作(比如framebuffer,网络传输buffer),不允许分配失败,但是又无法做到1,那么可以考虑使用预分配的策略。


对于某些特定的项目,可以通过如下方法减少DMA内存分配失败的可能性。

1. 禁止带有GFP_HIGHMEM标记的内存分配在HIGHMEM zone 分配失败后,进入DMA zone寻找合适的页面。

2. 禁止对Normal zone分配失败,进入DMA zone寻找合适页面

HIGHMEM的使用者主要是应用程序的页面映射和内核vmalloc分配,这两种操作都不需要连续物理页面,HIGHMEM zone并不关心物理内存碎片化,而且这两种操作映射的页面本身就是reclaimable,实在没有必要再去占用Normal和DMA zone的物理页面。

尤其在Android系统上,Android退出应用操作只是把应用退到后台,并没有释放内存,当运行时间较长,启动多个应用后,这些应用占满HIGHMEM zone后,就会去占用Normal zone和Highmem zone的内存。在这里我们切断zone_list,就是防止贪得无厌的Android应用和Vmalloc占据Normal和DMA zone。

此外,linux kernel本身对待cache(此cache不是物理cache,而是指buffer cache, inode cache, dentry cache)也是有求必应的,Normal zone分配完,就使用DMA zone,直到把DMA zone占完为止。因此我们实在是有必要禁止cache这种贪得无厌的东西进入DMA zone。

这种截断的做法虽然背离了linux尽量使用系统内存的做法,但是却保证了三个内存区 DMA zone, Normal zone, Highmem zone互不干扰。


lowmem_reserve

上面提到了切断HIGHMEM zone分配失败回退到Normal zone和DMA zone,以及切断Normal zone失败回退到DMA zone。具体做法是配置lowmem_reserve

通过配置lowmem_reserve的为1,使得本内存zone针对高端分配保留尽可能多的空间,来减少fallback分配,这里用减少而不是禁止是因为lowmem_reserve算法在某些内存配置下,无法完全禁止fallback。

你可能感兴趣的:(内存管理)