11.伙伴系统(避免碎片及内存的迁移类型)
反碎片的工作原理如何?为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。
不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别。
可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。例如,映射自文件
的数据属于该类别。kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存。这是一个复杂的过程,本身就需要详细论述目前,了解到内核会在可回收页占据了太多内存时进行回收,就足够了。
另外,在内存短缺(即分配失败)时也可以发起页面回收。有关内核发起页面回收的时机,更具体的信息请参考下文。
可移动页可以随意地移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。
页的可移动性,依赖该页属于3种类别的哪一种。内核使用的反碎片技术,即基于将具有相同可移动性的页分组的思想。为什么这种方法有助于减少碎片?回想图3-25中,由于页无法移动,导致在
原本几乎全空的内存区中无法进行连续分配。根据页的可移动性,将其分配到不同的列表中,即可防止这种情形。例如,不可移动的页不能位于可移动内存区的中间,否则就无法从该内存区分配较大的
连续内存块。
想一下,图3-25中大多数空闲页都属于可回收的类别,而分配的页则是不可移动的。如果这些页聚集到两个不同的列表中,如图3-26所示。在不可移动页中仍然难以找到较大的连续空闲空间,但对
可回收的页,就容易多了。
数据结构
尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些宏来表示不同的迁移类型:
<mmzone.h> #define MIGRATE_UNMOVABLE 0 #define MIGRATE_RECLAIMABLE 1 #define MIGRATE_MOVABLE 2 #define MIGRATE_RESERVE 3 #define MIGRATE_ISOLATE 4 /* 不能从这里分配 */ #define MIGRATE_TYPES 5
如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_RESERVE分配内存。
MIGRATE_ISOLATE是一个特殊的虚拟区域,用于跨越NUMA结点移动物理内存页。在大型系统上,它有益于将物理内存页移动到接近于使用该页最频繁的CPU。MIGRATE_TYPES只是表示迁移类型的数
目,也不代表具体的区域。
对伙伴系统数据结构的主要调整,是将空闲列表分解为MIGRATE_TYPE个列表:
<mmzone.h> struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; };
如果内核无法满足针对某一给定迁移类型的分配请求,会怎么样?此前已经出现过一个类似的问题,即特定的NUMA内存域无法满足分配请求时。内核在这种情况下的做法是类似的,提供了一个备用列表,规定了在指定列表中无法满足分配请求时,接下来应使用哪一种迁移类型:
mm/page_alloc.c
/*
* 该数组描述了指定迁移类型的空闲列表耗尽时,其他空闲列表在备用列表中的次序。
*/
static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE }, [MIGRATE_RESERVE] = { MIGRATE_RESERVE, MIGRATE_RESERVE, MIGRATE_RESERVE }, /* 从来不用 */ }
内核如何知道给定的分配内存属于何种迁移类型?
有关各个内存分配的细节都通过分配掩码指定。内核提供了两个标志,分别用于表示分配的内存是可移动的(__GFP_MOVABLE)或可回收的(__GFP_RECLAIMABLE)。如果这些标志都没有设置,则分配的内存假定为不可移动的。下列辅助函数可用于转换分配标志及对应的迁移类型:
<gfp.h> static inline int allocflags_to_migratetype(gfp_t gfp_flags) { if (unlikely(page_group_by_mobility_disabled)) return MIGRATE_UNMOVABLE; /* 根据可移动性分组 */ return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) | ((gfp_flags & __GFP_RECLAIMABLE) != 0); }
如果停用了页面迁移特性,则所有的页都是不可移动的。否则,该函数的返回值可以直接用作free_area.free_list的数组索引。
set_pageblock_migratetype负责设置以page为首的一个内存区的迁移类型:
mm/page_alloc.c void set_pageblock_migratetype(struct page *page, int migratetype);
migratetype参数可以通过上文介绍的allocflags_to_migratetype辅助函数构建。请注意很重要的一点,页的迁移类型是预先分配好的,对应的比特位总是可用,与页是否由伙伴系统管理无关。
在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从get_pageblock_migratetype获得所需的信息。
初始化基于可移动性的分组
在内存子系统初始化期间,memmap_init_zone负责处理内存域的page实例。该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的!
在分配内存时,如果必须“盗取”不同于预定迁移类型的内存区,内核在策略上倾向于“盗取”更大的内存区。由于所有页最初都是可移动的,那么在内核分配不可移动的内存区时,则必须“盗取”。
实际上,在启动期间分配可移动内存区的情况较少,那么分配器有很高的几率分配长度最大的内存区,并将其从可移动列表转换到不可移动列表。由于分配的内存区长度是最大的,因此不会向可移动内存中引入碎片。
总而言之,这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处,从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一。
浙公网安备 33010602011771号