linux kernel内存碎片防治技术

作者:itrocker 发布于:2015-11-2 10:24 分类:内存管理

Linux kernel组织管理物理内存的方式是buddy system(伙伴系统),而物理内存碎片正式buddy system的弱点之一,为了预防以及解决碎片问题,kernel采取了一些实用技术,这里将对这些技术进行总结归纳。

1 低内存时整合碎片

buddy申请内存页,如果找不到合适的页,则会进行两步调整内存的工作,compactreclaim。前者是为了整合碎片,以得到更大的连续内存;后者是回收不一定必须占用内存的缓冲内存。这里重点了解comact,整个流程大致如下:

__alloc_pages_nodemask

    -> __alloc_pages_slowpath

        -> __alloc_pages_direct_compact

            -> try_to_compact_pages

                -> compact_zone_order

                    -> compact_zone

                        -> isolate_migratepages

                        -> migrate_pages

                        -> release_freepages

并不是所有申请不到内存的场景都会compact,首先要满足order大于0,并且gfp_mask携带__GFP_FS__GFP_IO;另外,需要zone的剩余内存情况满足一定条件,kernel称之为“碎片指数”(fragmentation index),这个值在0~1000之间,默认碎片指数大于500时才能进行compact,可以通过proc文件extfrag_threshold来调整这个默认值。fragmentation index通过fragmentation_index函数来计算:


        /*
	 * Index is between 0 and 1000
	 *
	 * 0 => allocation would fail due to lack of memory
	 * 1000 => allocation would fail due to fragmentation
	 */
	return 1000 - div_u64( (1000+(div_u64(info->free_pages * 1000ULL, requested))), info->free_blocks_total)

在整合内存碎片的过程中,碎片页只会在本zone的内部移动,将位于zone低地址的页尽量移到zone的末端。申请新的页面位置通过compaction_alloc函数实现。

移动过程又分为同步和异步,内存申请失败后第一次compact将会使用异步,后续reclaim之后将会使用同步。同步过程只移动当面未被使用的页,异步过程将遍历并等待所有MOVABLE的页使用完成后进行移动。

2 按可移动性组织页

按照可移动性将内存页分为以下三个类型:

UNMOVABLE:在内存中位置固定,不能随意移动。kernel分配的内存基本属于这个类型;

RECLAIMABLE:不能移动,但可以删除回收。例如文件映射内存;

MOVABLE:可以随意移动,用户空间的内存基本属于这个类型。

申请内存时,根据可移动性,首先在指定类型的空闲页中申请内存,每个zone的空闲内存组织方式如下:

struct zone {
......
struct free_area    free_area[MAX_ORDER];
......
}

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

当在指定类型的free_area申请不到内存时,可以从备用类型挪用,挪用之后的内存就会释放到新指定的类型列表中,kernel把这个过程称为“盗用”。

备用类型优先级列表如下定义:

static int fallbacks[MIGRATE_TYPES][4] = {
	[MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,     MIGRATE_RESERVE },
	[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,     MIGRATE_RESERVE },
#ifdef CONFIG_CMA
	[MIGRATE_MOVABLE]     = { MIGRATE_CMA,         MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
	[MIGRATE_CMA]         = { MIGRATE_RESERVE }, /* Never used */
#else
	[MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE,   MIGRATE_RESERVE },
#endif
	[MIGRATE_RESERVE]     = { MIGRATE_RESERVE }, /* Never used */
#ifdef CONFIG_MEMORY_ISOLATION
	[MIGRATE_ISOLATE]     = { MIGRATE_RESERVE }, /* Never used */
#endif
};

值得注意的是并不是所有场景都适合按可移动性组织页,当内存大小不足以分配到各种类型时,就不适合启用可移动性。有个全局变量来表示是否启用,在内存初始化时设置:

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

如果page_group_by_mobility_disabled,则所有内存都是不可移动的。

其中有个参数决定了每个内存区域至少拥有的页,pageblock_nr_pages,它的定义如下:


#define pageblock_order HUGETLB_PAGE_ORDER
#else /* CONFIG_HUGETLB_PAGE */
/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
#define pageblock_order		(MAX_ORDER-1)
#endif /* CONFIG_HUGETLB_PAGE */
#define pageblock_nr_pages	(1UL << pageblock_order)

在系统初始化期间,所有页都被标记为MOVABLE

void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
		unsigned long start_pfn, enum memmap_context context)
{
......
		if ((z->zone_start_pfn <= pfn)
		    && (pfn < zone_end_pfn(z))
		    && !(pfn & (pageblock_nr_pages - 1)))
			set_pageblock_migratetype(page, MIGRATE_MOVABLE);
......
}

其它可移动性类型的页都是后来产生的,也就是前面说的“盗取”。在这种情况发生时,通常会“盗取”fallback中更高优先级、更大块连续的页,从而避免小碎片的产生。

/* Remove an element from the buddy allocator from the fallback list */
static inline struct page *
__rmqueue_fallback(struct zone *zone, int order, int start_migratetype)
{
......
	/* Find the largest possible block of pages in the other list */
	for (current_order = MAX_ORDER-1; current_order >= order;
						--current_order) {
		for (i = 0;; i++) {
			migratetype = fallbacks[start_migratetype][i];
......
}

可以通过/proc/pageteypeinfo查看当前系统各种类型的页分布。

3 虚拟可移动内存域

在依据可移动性组织页的技术之前,还有一个方法已经合入kernel,那就是虚拟内存域:ZONE_MOVABLE。基本思想很简单:把内存分为两部分,可移动的和不可移动的。

enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
	__MAX_NR_ZONES
};


ZONE_MOVABLE的启用需要指定kernel参数kernelcore或者movablecorekernelcore用来指定不可移动的内存数量,movablecore指定可移动的内存大小,如果两个都指定,取不可移动内存数量较大的一个。如果都不指定,则不启动。

与其它内存域不同的是ZONE_MOVABLE不关联任何物理内存范围,该域的内存取自高端内存域或者普通内存域。

find_zone_movable_pfns_for_nodes用来计算每个nodeZONE_MOVABLE的内存数量,采用的内存区域通常是每个node的最高内存域,在函数find_usable_zone_for_movable中体现。

在对每个node分配ZONE_MOVABLE内存时,kernelcore会被平均分配到各个Node

kernelcore_node = required_kernelcore / usable_nodes;

kernel alloc page时,如果gfp_flag同时指定了__GFP_HIGHMEM__GFP_MOVABLE,则会从ZONE_MOVABLE内存域申请内存。


标签: 内存碎片

评论:

davidlamb
2019-08-08 15:50
碎片整合对系统的性能有没什么影响?
Bob
2018-03-07 17:46
请问个问题,我们有个程序要申请大块内存的DMA,总的free内存是够得,但就是申请速度特别慢,分钟级。

把系统 extfrag_threshold 改成 1000后,申请速度快了很多。

按照你文章的意思,是慢在做内存 compact 上,改成 1000 后优先 reclaim 了,所以变快了。

但我想不通的是,不 compact 怎么有连续内存可用呢?

或者是说 extfrag_threshold 改成 1000 还有个隐含意思是,系统会去释放 buffer/cache 内存?
komex
2016-10-25 10:43
您好,请教个问题,在实际的驱动程序中,有会用到MIGRATE_UNMOVABLE这个类型的吗?看了下code,好像没看到给page标志MIGRATE_UNMOVABLE这个类型的code. 谢谢!
wowo
2016-10-25 12:21
@komex:你可以参考kernel中的这段代码:
        /*                                                                      
         * 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                                        
         */                                                                    
        if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))              
                page_group_by_mobility_disabled = 1;                            
        else                                                                    
                page_group_by_mobility_disabled = 0;
komex
2016-10-25 14:28
@wowo:这段是初始化的时候的代码吧,一般page_group_by_mobility_disabled 都等于 0 吧,那都初始化好之后,后面申请的都没有用到 MIGRATE_UNMOVABLE 这个type吗?
谢谢!
wowo
2016-10-25 14:35
@komex:看代码应该是这样。如果系统的page比较少,少到不足以支撑相应的机制的时候,就使用MIGRATE_UNMOVABLE,也就变相的禁止了内存整理的功能。
komex
2016-10-25 14:38
@wowo:在分配时,有一个gfpflags_to_migratetype(const gfp_t gfp_flags)函数

实现从 gfp_flags 转成 migrate_type。但分配的时候,一般我们给的都是GFP_KERNEL,照这个code看的话,用到的migrate_type都是同一个类型,所以就有点疑惑,不知道具体其他几种type的使用情况,求指点!
谢谢!

code贴不上来,提示说黑客攻击,不知道是不是code问题,删了试下~ 囧~~~~
wowo
2016-10-26 21:48
@komex:code贴不上的问题已经解决了。话说内存这一块的东西实在是太多了,还是等linuxer同学的分析文章吧,我相信他会把大家的问题讲明白的:-)
komex
2016-10-27 11:04
@wowo:好的,谢谢!期待后续的分析文章~~
谢谢!
看客
2019-11-29 08:22
@komex:事实上gfpflags_to_migratetype()的代码已经把这个事情说清楚了。当kernel想要分配内存的时候,其传入的GFP_KERNEL:
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))

也就是说如果你没有显式传入__GFP_RECLAIMABLE或者__GFP_MOVABLE,那么gfp_flags中关于migratetype的值就是0,0不就是MIGRATE_UNMOVABLE的值吗。

#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3
jixing
2016-01-19 09:04
linuxer:
    膜拜
能加个好友吗?我对linux内核有比较强的兴趣, 但是接触时间较短, 想您指导一下。。。
郭健
2016-01-19 12:19
@jixing:可以的,我没有QQ,不过有微信,如果你足够仔细,应该可以搜索到我,不过其实这个网站也很合适交流的。
zhuochong
2015-11-11 17:24
大神,如何避免出现内核碎片的问题?可以从哪几个方面下手?
itrocker
2015-11-12 19:06
@zhuochong:这个问题好大:)  
碎片问题难就难在无法完全避免,即便现在已经有了上述几个技术,问题还是存在,我们能做的就是尽量降低概率、延迟时间。
对于开发者而言,就要尽量减少在buddy system中持续地申请释放page。
可以考虑在上层实现内存池;复用slab技术;优化malloc库;甚至在用户不感知的情况下重启系统服务等等
logv
2015-11-05 09:01
请教一下,“首先要满足order大于0,并且gfp_mask携带__GFP_FS和__GFP_IO”,这的order,以及这两个mask,实际意义是什么呢?谢谢
itrocker
2015-11-05 14:25
@logv:(1)order大于0。buddy system是以page为单位组织的,所谓“碎片整合”就是把分散在各个地方的不连续页调整在一起,形成连续的多个页。如果申请的order=0,不要求多个连续的页,也就没必要整合了;
(2)要求__GFP_FS和__GFP_IO。是因为在碎片整合过程中,有可能进行“页迁移”,这个操作需要进行页面回写,操作VFS并访问磁盘IO,因此需要携带这两个flag。
logv
2015-11-05 16:15
@itrocker:多谢指点
rainys
2016-01-04 16:57
@itrocker:不带着两个标记,有可能造成申请流程的嵌套死锁,这个是实现层面的意义

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog