1、伙伴算法引入

内核频繁请求和释放不同大小的一组连续页框,必然会导致在已经分配的块内分散了许多小块的空闲页框,带来的问题是,及时有足够的空闲页框可以满足请求,但是要分配一大块连续页框就无法满足,所以内核应该为分配一组连续的页框而建立一种健壮高效的分配策略。伙伴算法就是基于此。

2、伙伴算法的思想

linux把所有空闲页框分组为11个块链表,每个链表上的页框块是固定的,第i条链表中,每个页框块都包含2i个连续页,其中i称为分配阶,

假设要申请28个页,先从28即256个页框中查找空闲块,没有就去512个页框的链表中找,找到了则将页框分为2个256个页框的块,一个分配给应用,一个移到256个页框的链表中去,如果512个页框仍没有空闲块,继续向1024个页框链表查找,如果存在空闲块,则将其中256页框作为请求返回 ,剩余的768分成256和512分别插到相应的链表中,如果仍然没有,则返回错误。

3、伙伴算法在内核中的数据结构

	struct zone{
		struct  free_area freearea[MAX_ORDER];
	};
	//其中,define内核中为
	#define MAX_ORDER 11  //即上文中提到的11个块链表

free_area[]数组是一个struct free_area的结构体,其在内核中的定义为:

struct free_area
{
	struct list_head	free_list[MIGRATE_TYPES];/*说明了页的属性*/
	unsigned long		nr_free;/*来说明每个介中有多少个自由的页*/
};

在这个函数中有两个域,第一个是free_list,是个链表,它表示的就是当前分配阶所对应的页框块的链表。第二个nr_free指的是当前链表中空闲页框块的数目,比如free_area[2]中nr_free的值为5,表示有5个大小为4的页框块,那么总的空闲页为20.
对于free_list中的MIGRATE_TYPES表示的是迁移页的类型,在内核中的定义为:

#define MIGRATE_UNMOVABLE     0   //不可移动页:在内存中有固定位置,不能移动到其他地方  
#define MIGRATE_RECLAIMABLE   1   //可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成  
#define MIGRATE_MOVABLE       2   //可移动页:可以随意的移动  
#define MIGRATE_RESERVE       3   //如果向具有特定可移动性地列表请求分配内存失败,这种紧急情况下可以从MIGRATE_RESERVE分配内存  
#define MIGRATE_ISOLATE       4  //是一个特殊的虚拟区域,用于跨域NUMA结点移动物理内存页,在大型系统上,它有益于将物理内存页移动到接近于是用该页最频繁的CPU  
#define MIGRATE_TYPES         5  //只是表示迁移类型的数目,不代表具体的区域

4.伙伴算法在内核分配页框时的实现

内核中分配页框的函数入口是:alloc_pages函数:

#define alloc_pages(gfp_mask, order) \
		alloc_pages_node(numa_node_id(), gfp_mask, order)

lloc_pages_node()函数有三个参数:numa_node_id这个参数下文讲,gfp_mask这个参数指的是分配器的标志,也就是说分配的内存块所具有的属性(如果还是不懂,那就看看Linux内核设计与实现这本书,这里姑且认为是分配的内存块的属性),order指的是伙伴中的阶数。
介绍下numa_node_id
从硬件角度看,存在两种机器,uma和numa
在这里插入图片描述
uma多个cpu共享一个内存,numa,每个cpu有自己本地内存,然后处理器通过总线连接起来,进而可以访问其他cpu的本地内存。

(在多核系统中,如果物理内存对所有CPU来说没有区别,每个CPU访问内存的方式也一样,则这种体系结构被称为Uniform Memory Access(UMA)。如果物理内存是分布式的,由多个cell组成(比如每个核有自己的本地内存),那么CPU在访问靠近它的本地内存的时候就比较快,访问其他CPU的内存或者全局内存的时候就比较慢,这种体系结构被称为Non-Uniform Memory Access(NUMA)。)

Linux引入了一个概念称为node,一个node对应一个内存块,对于UMA系统,只有一个Node.其对应的数据结构为struct pglist_data.对于NUMA系统来讲,整个系统的内存由一个名为Node_data的struct pglist_data(page_data_t)指针数组来管理。

typedef struct pglist_data {
	int nr_zones;
	struct zone node_zones[MAX_NR_ZONES];
	struct zonelist node_zonelists[MAX_ZONELIST];
	unsigned long node_size;
	struct page *node_mem_map;
	int node_id;
	unsigned long node_start_paddr;
	struct pglist_data *node_next;
	spinlock_t lru_lock;
	...
} pg_data_t;

node_zones:当前节点中内存管理区的描述符数组,node_list;指的是zonelist结构的数组。
由以上结构体可以推断每个node中又被分成多个zone(区),每个zone对应一片内存区域。NUMA系统的内存划分如图:在这里插入图片描述

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_DMA:可用作DMA的内存区域。该类型的内存区域在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。

  • ZONE_NORMAL:直接被内核直接映射到自己的虚拟地址空间的地址。

  • ZONE_HIGHMEM:不能被直接映射到内核的虚拟地址空间的地址。

  • ZONE_MOVABLE:伪zone,在防止物理内存碎片机制中使用

  • MAX_NR_ZONES:结束标记

    很显然,当我们在分配内存时,是按区进行分配的,一般我们会从zone_normal这个区上申请。

4.1 __alloc_pages()函数

追踪内核代码,进入alloc_pages()函数后,发现调用的是__alloc_pages()函数

static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
						unsigned int order)
{
	/* Unknown node is current node */
	if (nid < 0)
		nid = numa_node_id();

	return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}

我们发现其调用的是__alloc_pages()函数,在这个函数中调用了node_zonelist()函数,这个函数会根据节点号找到对应的zone,正好验证了我们上边说的:每个node又分成了好多个区,我们的内存其实是从具体的区上拿的。

4.2 __alloc_pages_nodemask()函数

在__alloc_pages函数中没有做任何的事情,就进入了__alloc_pages_nodemask()函数:

struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
			struct zonelist *zonelist, nodemask_t *nodemask)
{
	enum zone_type high_zoneidx = gfp_zone(gfp_mask);
	struct zone *preferred_zone;
	struct page *page;
	int migratetype = allocflags_to_migratetype(gfp_mask);

	gfp_mask &= gfp_allowed_mask;

	lockdep_trace_alloc(gfp_mask);

	might_sleep_if(gfp_mask & __GFP_WAIT);

	if (should_fail_alloc_page(gfp_mask, order))
		return NULL;

	/*
	 * Check the zones suitable for the gfp_mask contain at least one
	 * valid zone. It's possible to have an empty zonelist as a result
	 * of GFP_THISNODE and a memoryless node
	 */
	if (unlikely(!zonelist->_zonerefs->zone))
		return NULL;

	/* The preferred zone is used for statistics later */
	first_zones_zonelist(zonelist, high_zoneidx, nodemask, &preferred_zone);
	if (!preferred_zone)
		return NULL;

	/* First allocation attempt核心函数 */
	page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
			zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET,
			preferred_zone, migratetype);
	if (unlikely(!page))
		page = __alloc_pages_slowpath(gfp_mask, order,
				zonelist, high_zoneidx, nodemask,
				preferred_zone, migratetype);

	trace_mm_page_alloc(page, order, gfp_mask, migratetype);
	return page;
}

在这个函数中,首先,gfp_zone()会根据gfp_mask选取适当类型的zone.下面接着是几项参数的检测以及安全性的检测。然后通过zonelist->_zonerefs->zone判断zone是否为空,由unlike知道,一般情况下不会是空。接着是first_zones_zonelist()函数来确定在该zone中优先分配内存的内存管理区。可以发现核心的分配的函数是在get_page_from_freelist()这个函数中。

4.3 get_page_from_freelist

这个函数可以看作是计入伙伴算法的前置函数,它通过传递进来的分配标志和分配阶,寻找适合的区(zone),如果找到了满足条件的区,则进行伙伴算法,进行分配。我们来看下这个函数的主要功能:
首先是通过函数for_each_zone_zonelist_nodemask()函数来遍历zonelist

	for_each_zone_zonelist_nodemask(zone, z, zonelist,
						high_zoneidx, nodemask) {
		if (NUMA_BUILD && zlc_active &&
			!zlc_zone_worth_trying(zonelist, z, allowednodes))/*检查zlc(zone list cache),当前zone是否有freepage*/
				continue;
		if ((alloc_flags & ALLOC_CPUSET) &&  /*当前分配标志不允许在该管理区中分配页面*/
			!cpuset_zone_allowed_softwall(zone, gfp_mask))
				goto try_next_zone;

		BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
		if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
			unsigned long mark;
			int ret;

			mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
			if (zone_watermark_ok(zone, order, mark,/*如果freepage够,开始伙伴算法*/
				    classzone_idx, alloc_flags))
				goto try_this_zone;

首先是对zlc(zone list cache)的检测,检测当前zone是否有足够的freepage,如果没有,直接continue,进入下一个zone .
接着是对分配标志位的一个检测,检查当前分配标志是否允许在该管理区中分配页面。接着是对当前zone的一个水位线的获取,这个水位线是什么呢?来看下内核的定义:

enum zone_watermarks {
	WMARK_MIN, //说明当前可以用的内存已经达到最低限度了
	WMARK_LOW, //可用内存很少了,但是还没有最底线,不是很紧急的情况下,就不要占用内存了
	WMARK_HIGH, //剩余的内存还有很多,大家放心使用吧
	NR_WMARK
};

可以知道,水位线其实就是个标志,标志这个区域的内存的剩余量情况。那么zone_watermark_ok函数的作用就很明显了,就是检查这个区域的水位线是否有足够的free_page.如果够的话,就直接try_this_zone,进入我们的伙伴算法。
继续跟着我们的代码走:

4.4 buffered_rmqueue伙伴算法入口函数

4.5 __rmqueue_smallest函数

4.6 expand()函数

总结

Mon 06 Mon 13 已完成 进行中 计划中 现有任务 Adding GANTT diagram functionality to mermaid

https://www.cnblogs.com/wangzahngjun/p/4943518.html

Logo

更多推荐