伙伴分配器

伙伴分配器定义

基本的伙伴分配器

连续的物理页合在一起称为页块(page block); 阶(order)是伙伴分配器中的一个专业术语,是页的数量单位,2^n个连续的物理页成为n阶页块;
满足以下条件的两个n阶页块称为伙伴(buddy):
1、两个页块是相邻的,即物理地址是连续的;
2、页块的第一页的物理页号必须是2^n的整数倍;
3、如果合并成(n+1)阶页块,第一页的物理页号必须是2^(n+1)的整数倍。
这是伙伴分配器(buddy allocator)的名字的来源;
以单页(0阶页块)为例,0号页和1号页是伙伴,2号页和3号页是伙伴;1号页和2号页不是伙伴,因为1号页和2号页合并成1阶页块,第一个页即1号页不是2^(0+1)的整数倍;

伙伴分配器分配和释放物理页的数量单位为阶。分配n阶页块的过程如下:
1、查看是否有空闲的n阶页块,如果有直接分配;否则,继续执行下一步;
2、查看是否存在空闲的(n+1)阶页块,如果有,把(n+1)阶页块分裂为两个n阶
页块,一个插入空闲n阶页块链表,另一个分配出去;否则继续执行下一步。
3、查看是否存在空闲的(n+2)阶页块,如果有把(n+2)阶页块分裂为两个(n+1)
阶页块,一个插入空闲(n+1)阶页块链表,另一个分裂为两个n阶页块,一个插入空闲n阶页
块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。

释放n阶页块时,查看它的伙伴是否空闲;如果伙伴不空闲,则把n阶页块插入空闲的n阶页块链表;如果伙伴空闲,则合并为(n+1)阶页块,接下来释放(n+1)阶页块;

内核在基本的分区伙伴分配器的基础上做了一些扩展:

  1. 对于内存的管理是基于节点(node,即pglist_data)和区域(zone)的,成为分区的伙伴分配器(zoned buddy allocator);
  2. 为了预防内存碎片,把物理页根据可移动性分组;
  3. 针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域(zone)中增加一个每处理器页集合;

对于可移动性分组和每处理器页集合,本文不做介绍,可以查阅相关资料进行了解;

分区伙伴分配器

数据结构

分区的伙伴分配器基于某个内存节点(node)的某个区域(zone);struct zone中的成员free_area维护空闲页块,数组下标对那个页块的阶数(order);struct free_area中的成员free_list表示空闲页块的链表;free_list本身是一个数组,数组的成员是链表;free_list数组的下标对应可移动性类型(migrate
type),空闲页块根据migrate type分组;
struct zone中的成员managed_pages是伙伴分配器管理的物理页的数量;

struct zone {
    /* Read-mostly fields */

    /* zone watermarks, access with *_wmark_pages(zone) macros */
    unsigned long watermark[NR_WMARK];


    long lowmem_reserve[MAX_NR_ZONES];

    #ifdef CONFIG_NUMA
    int node;
    #endif
    struct pglist_data	*zone_pgdat;
    struct per_cpu_pageset __percpu *pageset;

    #ifndef CONFIG_SPARSEMEM
    /*
    * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
      * In SPARSEMEM, this map is stored in struct mem_section
      */
    unsigned long		*pageblock_flags;
    #endif /* CONFIG_SPARSEMEM */

    /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
    unsigned long		zone_start_pfn;

    unsigned long		managed_pages;
    unsigned long		spanned_pages;
    unsigned long		present_pages;

        /* free areas of different sizes */
        struct free_area	free_area[MAX_ORDER];

} ____cacheline_internodealigned_in_smp;

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

MAX_ORDER是最大阶数,伙伴分配器可分配的最大阶数是(MAX_ORDER - 1);

/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))

内存区域时如何连接的?内存区域中第一页内的链表元素,可用于将内存区域维持在链表中;
image.png

首选区域(zone),备用区域(zonelist)

每个内存节点的struct pglist_data中,有两个成员node_zones、node_zonelists;node_zones表示该node中的内存区域数组,node_zonelists表示备用区域数组;

typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES]; // 内存区域数组
	struct zonelist node_zonelists[MAX_ZONELISTS]; // 备用区域数组
	int nr_zones;
    ...
} pg_data_t;

/*
 * One allocation request operates on a zonelist. A zonelist
 * is a list of zones, the first one is the 'goal' of the
 * allocation, the other zones are fallback zones, in decreasing
 * priority.
 *
 * To speed the reading of the zonelist, the zonerefs contain the zone index
 * of the entry being read. Helper functions to access information given
 * a struct zoneref are
 *
 * zonelist_zone()	- Return the struct zone * for an entry in _zonerefs
 * zonelist_zone_idx()	- Return the index of the zone for an entry
 * zonelist_node_idx()	- Return the index of the node for an entry
 */
struct zonelist {
	struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

分配内存页块时,首先会尝试从首选内存区域分配,在首选的内存区域无法满足内存分配请求时,首先尝试同一节点的另一个内存区域,接下来再尝试另一个节点,直至满足请求;后面的内存区域叫做备用区域;
所有内存区域和节点的伙伴系统都通过备用区域列表连接起来;
image.png

如果首选的内存区域不能满足页分配请求,可以从备用的内存区域借用物理页;借用的规则涉及到区域类型;

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,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES

};

内存区域借用的规则如下:

  1. 一个内存节点的某个区域类型可以从另一个内存节点的相同区域类型借用物理页;例如节点0的普通区域可以从节点1的普通区域借用物理页;
  2. 高区域类型可以从低区域类型借用物理页,例如普通区域可以从DMA区域借用物理页;
  3. 低区域类型不能从高区域类型借用物理页,例如DMA区域不能从普通区域借用物理页;
根据分配标志得到首选区域类型

申请页时,最低的4个标志位用来指定首选的内存区域类型;通过函数gfp_zone确定首选内存区域类型;

#define ___GFP_DMA		0x01u
#define ___GFP_HIGHMEM		0x02u
#define ___GFP_DMA32		0x04u
#define ___GFP_MOVABLE		0x08u


#define GFP_ZONE_TABLE ( \
	(ZONE_NORMAL << 0 * GFP_ZONES_SHIFT)				       \
	| (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT)		       \
	| (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT)	       \
	| (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT)		       \
	| (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT)		       \
	| (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT)    \
	| (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\
	| (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT)\
)

static inline enum zone_type gfp_zone(gfp_t flags)
{
	enum zone_type z;
	int bit = (__force int) (flags & GFP_ZONEMASK);

	z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
					 ((1 << GFP_ZONES_SHIFT) - 1);
	VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
	return z;
}
区域水线

首选的内存区域什么情况下需要从备用区域借用物理页呢?即首选内存区域不能满足页分配的请求;这个问题涉及到区域水线;
每个内存区域有3个水线:

  1. 高水线(high):如果内存区域的空闲页数大于高水线,说明该内存区域的内存充足;
  2. 低水线(low):如果内存区域的空闲页数小于低水线,说明该内存区域的内存轻微不足;
  3. 最低水线(min):如果内存区域的空闲页数小于最低水线,说明该内存区域的内存严重不足;

enum zone_watermarks {
	WMARK_MIN,
	WMARK_LOW,
	WMARK_HIGH,
	NR_WMARK
};

#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])

struct zone {
	/* Read-mostly fields */

	/* zone watermarks, access with *_wmark_pages(zone) macros */
	unsigned long watermark[NR_WMARK];

    ...
	/*
	 * spanned_pages is the total pages spanned by the zone, including
	 * holes, which is calculated as:
	 * 	spanned_pages = zone_end_pfn - zone_start_pfn;
	 *
	 * present_pages is physical pages existing within the zone, which
	 * is calculated as:
	 *	present_pages = spanned_pages - absent_pages(pages in holes);
	 *
	 * managed_pages is present pages managed by the buddy system, which
	 * is calculated as (reserved_pages includes pages allocated by the
	 * bootmem allocator):
	 *	managed_pages = present_pages - reserved_pages;
	 */
    unsigned long		managed_pages;
    unsigned long		spanned_pages;
    unsigned long		present_pages;

	...
};

struct zone中的watermark数组,表示high,low,min三个水线;min水线以下的内存称为紧急保留内存;
申请页时,第一次尝试使用低水线,如果首选内存区域的空闲页数小于低水线,就从备用内存区域借用物理页;如果第一次分配失败,那么唤醒所有目标内存节点的页回收内核线程hswapd以异步回收页,然后尝试使用最低水线;如果首选内存区域的空闲页数小于最低水线,就从备用内存区域借用物理页;

内存区域的水线可以查询:

Node 0, zone      DMA
  pages free     3721
        min      33
        low      41
        high     49
        spanned  4095
        present  3743
        managed  3721

Node 0, zone   Normal
  pages free     819743
        min      7968
        low      9960
        high     11952
        spanned  919040
        present  919040
        managed  884306
    
Node 0, zone   Normal
  pages free     819743
        min      7968
        low      9960
        high     11952
        spanned  919040
        present  919040
        managed  884306

spanned,present,managed含义如下:

spanned_pages = zone_end_pfn - zone_start_pfn; //区域结束的物理页减去起始页=当前区域跨越的总页数(包括空洞) 
present_pages = spanned_pages - absent_pages(pages in holes) //当前区域跨越的总页数-空洞页数=当前区域可用物理页数 
managed_pages = present_pages - reserved_pages //当前区域可用物理页数-预留的页数=伙伴分配器管理物理页数

备用区域列表

内存节点的pg_data_t中定义了备用区域列表;

typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES]; // 内存区域数组
	struct zonelist node_zonelists[MAX_ZONELISTS]; // 备用区域数组
	int nr_zones;
    ...
} pg_data_t;

enum {
	ZONELIST_FALLBACK,	/* zonelist with fallback */
#ifdef CONFIG_NUMA
	/*
	 * The NUMA zonelists are doubled because we need zonelists that
	 * restrict the allocations to a single node for __GFP_THISNODE.
	 */
	ZONELIST_NOFALLBACK,	/* zonelist without fallback (__GFP_THISNODE) */
#endif
	MAX_ZONELISTS
};

/* Maximum number of zones on a zonelist */
#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
struct zonelist {
	struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

/*
 * This struct contains information about a zone in a zonelist. It is stored
 * here to avoid dereferences into large structures and lookups of tables
 */
struct zoneref {
	struct zone *zone;	/* Pointer to actual zone */
	int zone_idx;		/* zone_idx(zoneref->zone) */
};

UMA系统只有一个备用区域列表,按区域类型从高到低排序;假设UMA系统包含普通区域和DMA区域,那么备用区域列表是:{普通区域,DMA区域};
NUMA系统的每个内存节点有两个备用区域列表:一个包含所有内存节点的区域,另一个只包含当前内存节点的区域;如果申请页时指定flag中包含__GFP_THISNODE,从当前内存节点分配物理页,就需要从第二个备用区域列表中分配;

包含所有内存节点的备用区域列表有两种排序方法:

  1. 节点优先顺序:先根据节点距离从小到大排序,然后在每个节点里面根据区域类型从高到低排序;
  2. 区域优先顺序:先根据区域类型从高到低排序,然后再每个区域类型里面根据节点距离从小到大排序;

节点优先顺序有点是优先选择距离近的内存,缺点是在高区域耗尽以前就使用低区域;例如DMA区域一般比较小,节点优先顺序会增大DMA区域耗尽的概率;
区域优先顺序的有点是减小区域耗尽的概率,缺点是不能保证优先选择距离近的内存;
可以使用内核参数"numa_zonelist_order"指定排序方法;

Node

可移动性分组

后续调查补充

每处理器页集合

后续调查补充

核心函数的实现

在Linux内核中,所有分配页的函数最终都会调用到__alloc_pages_nodemask,此函数被称为分区的伙伴分配器的心脏。

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
			struct zonelist *zonelist, nodemask_t *nodemask)
{
	struct page *page;
	unsigned int alloc_flags = ALLOC_WMARK_LOW;
	gfp_t alloc_mask = gfp_mask; /* The gfp_t that was actually used for allocation */
	struct alloc_context ac = { };

	gfp_mask &= gfp_allowed_mask;
	if (!prepare_alloc_pages(gfp_mask, order, zonelist, nodemask, &ac, &alloc_mask, &alloc_flags))
		return NULL;

	finalise_ac(gfp_mask, order, &ac);

	/* First allocation attempt */
	page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
	if (likely(page))
		goto out;

	/*
	 * Apply scoped allocation constraints. This is mainly about GFP_NOFS
	 * resp. GFP_NOIO which has to be inherited for all allocation requests
	 * from a particular context which has been marked by
	 * memalloc_no{fs,io}_{save,restore}.
	 */
	alloc_mask = current_gfp_context(gfp_mask);
	ac.spread_dirty_pages = false;

	/*
	 * Restore the original nodemask if it was potentially replaced with
	 * &cpuset_current_mems_allowed to optimize the fast-path attempt.
	 */
	if (unlikely(ac.nodemask != nodemask))
		ac.nodemask = nodemask;

	page = __alloc_pages_slowpath(alloc_mask, order, &ac);

out:
	if (memcg_kmem_enabled() && (gfp_mask & __GFP_ACCOUNT) && page &&
	    unlikely(memcg_kmem_charge(page, gfp_mask, order) != 0)) {
		__free_pages(page, order);
		page = NULL;
	}

	if (kmemcheck_enabled && page)
		kmemcheck_pagealloc_alloc(page, order, gfp_mask);

	trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);

	return page;
}
EXPORT_SYMBOL(__alloc_pages_nodemask);

__alloc_pages_nodemask函数的算法流程:
1、根据分配标志位得到首选区域类型和迁移类型;
2、执行快速路径,使用低水线尝试第一次分配;
3、如果快速路径分配失败,执行慢速路径。

参考文献

  1. 《linux内核深度解析》,余华兵著
  2. 《professional linux kernel architecture》,Wolfgang Mauerer 著,郭旭 译
  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值