蜗窝科技

linux kernel内存碎片防治技术

itrocker · · 内存管理 · 阅读 36829 · 2015年11月存档

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

1 低内存时整合碎片

从buddy申请内存页,如果找不到合适的页,则会进行两步调整内存的工作,compact和reclaim。前者是为了整合碎片,以得到更大的连续内存;后者是回收不一定必须占用内存的缓冲内存。这里重点了解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或者movablecore,kernelcore用来指定不可移动的内存数量,movablecore指定可移动的内存大小,如果两个都指定,取不可移动内存数量较大的一个。如果都不指定,则不启动。

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

find_zone_movable_pfns_for_nodes用来计算每个node中ZONE_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内存域申请内存。

标签:内存碎片

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(20 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • logv

    请教一下,“首先要满足order大于0,并且gfp_mask携带__GFP_FS和__GFP_IO”,这的order,以及这两个mask,实际意义是什么呢?谢谢
    • itrocker

      @logv:(1)order大于0。buddy system是以page为单位组织的,所谓“碎片整合”就是把分散在各个地方的不连续页调整在一起,形成连续的多个页。如果申请的order=0,不要求多个连续的页,也就没必要整合了; (2)要求__GFP_FS和__GFP_IO。是因为在碎片整合过程中,有可能进行“页迁移”,这个操作需要进行页面回写,操作VFS并访问磁盘IO,因此需要携带这两个flag。
      • logv

        @itrocker:多谢指点
      • rainys

        @itrocker:不带着两个标记,有可能造成申请流程的嵌套死锁,这个是实现层面的意义
  • zhuochong

    大神,如何避免出现内核碎片的问题?可以从哪几个方面下手?
    • itrocker

      @zhuochong:这个问题好大:) 碎片问题难就难在无法完全避免,即便现在已经有了上述几个技术,问题还是存在,我们能做的就是尽量降低概率、延迟时间。 对于开发者而言,就要尽量减少在buddy system中持续地申请释放page。 可以考虑在上层实现内存池;复用slab技术;优化malloc库;甚至在用户不感知的情况下重启系统服务等等
  • jixing

    linuxer: 膜拜 能加个好友吗?我对linux内核有比较强的兴趣, 但是接触时间较短, 想您指导一下。。。
    • 郭健

      @jixing:可以的,我没有QQ,不过有微信,如果你足够仔细,应该可以搜索到我,不过其实这个网站也很合适交流的。
  • komex

    您好,请教个问题,在实际的驱动程序中,有会用到MIGRATE_UNMOVABLE这个类型的吗?看了下code,好像没看到给page标志MIGRATE_UNMOVABLE这个类型的code. 谢谢!
    • wowo

      @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

        @wowo:这段是初始化的时候的代码吧,一般page_group_by_mobility_disabled 都等于 0 吧,那都初始化好之后,后面申请的都没有用到 MIGRATE_UNMOVABLE 这个type吗? 谢谢!
        • wowo

          @komex:看代码应该是这样。如果系统的page比较少,少到不足以支撑相应的机制的时候,就使用MIGRATE_UNMOVABLE,也就变相的禁止了内存整理的功能。
          • komex

            @wowo:在分配时,有一个gfpflags_to_migratetype(const gfp_t gfp_flags)函数 实现从 gfp_flags 转成 migrate_type。但分配的时候,一般我们给的都是GFP_KERNEL,照这个code看的话,用到的migrate_type都是同一个类型,所以就有点疑惑,不知道具体其他几种type的使用情况,求指点! 谢谢! code贴不上来,提示说黑客攻击,不知道是不是code问题,删了试下~ 囧~~~~
            • wowo

              @komex:code贴不上的问题已经解决了。话说内存这一块的东西实在是太多了,还是等linuxer同学的分析文章吧,我相信他会把大家的问题讲明白的:-)
              • komex

                @wowo:好的,谢谢!期待后续的分析文章~~ 谢谢!
            • 看客

              @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
  • Bob

    请问个问题,我们有个程序要申请大块内存的DMA,总的free内存是够得,但就是申请速度特别慢,分钟级。 把系统 extfrag_threshold 改成 1000后,申请速度快了很多。 按照你文章的意思,是慢在做内存 compact 上,改成 1000 后优先 reclaim 了,所以变快了。 但我想不通的是,不 compact 怎么有连续内存可用呢? 或者是说 extfrag_threshold 改成 1000 还有个隐含意思是,系统会去释放 buffer/cache 内存?
    • 凌云行者

      @Bob:把extfrag_threshold设置为1000只是说明kernel不会在分配内存时因为内存碎片程序进行compact了,不代表kernel不会compact,还有kcompactd线程负责compact
  • davidlamb

    碎片整合对系统的性能有没什么影响?
    • 凌云行者

      @davidlamb:肯定是有的,所以选择在系统内存使用率低的时候进行碎片整合