Linux 内存管理(3) - Buddy system

  • 了解linux buddy system

1.描述

  在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担。Linux内核使用伙伴算法来管理和分配物理内存页面,该算法由Knowlton设计,后来Knuth又进行了更深刻的描述。

  伙伴系统是一个结合2的方幂个分配器和空闲缓冲区合并技术的内存分配方案, 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者。

2.常用内存分配函数
在这里插入图片描述
2.1.分配掩码gfp_mask

  分配掩码包括两部分:

  • 内存域修饰符(低4位)
  • 内存分配标志(从第5位开始)
    在这里插入图片描述

2.1.1.内存域修饰符

  内存域zone的几种类型:ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM、ZONE_MOVABLE。与类型不同,内存域的修饰符只有___GFP_DMA、___GFP_HIGHMEM、___GFP_DMA32、___GFP_MOVABLE 4种,没有ZONE_NORMAL对应的修饰符,因为ZONE_NORMAL是默认的内存申请类型。如下所示,为内存域修饰符的定义:

   19 #define ___GFP_DMA          0x01u
   20 #define ___GFP_HIGHMEM      0x02u
   21 #define ___GFP_DMA32        0x04u                                                                      
   22 #define ___GFP_MOVABLE      0x08u
   
   56 #define __GFP_DMA   ((__force gfp_t)___GFP_DMA)                                                        
   57 #define __GFP_HIGHMEM   ((__force gfp_t)___GFP_HIGHMEM)
   58 #define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
   59 #define __GFP_MOVABLE   ((__force gfp_t)___GFP_MOVABLE)  /* ZONE_MOVABLE allowed */
   60 #define GFP_ZONEMASK    (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

2.1.2.内存分配flag

  除了内存域修饰符之外,分配掩码中还包含了大量的分配标志,如下所示:
在这里插入图片描述
2.1.3.掩码分组

  由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志, 称之为类型标志。

#define GFP_ATOMIC      (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT      (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO        (__GFP_RECLAIM)
#define GFP_NOFS        (__GFP_RECLAIM | __GFP_IO)
#define GFP_TEMPORARY   (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \
                         __GFP_RECLAIMABLE)
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA         __GFP_DMA
#define GFP_DMA32       __GFP_DMA32
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE   ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
                         __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \
                         ~__GFP_RECLAIM)
/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3

在这里插入图片描述
  在编写的绝大多数代码中, 用么用到的是GFP_KERNEL, 要么是GFP_ATOMIC,当然各个类型标志也均有其应用场景。
在这里插入图片描述
掩码布局如下所示:
在这里插入图片描述
3.内存分配API统一到alloc_pages接口
在这里插入图片描述
3.1.alloc_pages

491 #ifdef CONFIG_NUMA 
492 extern struct page *alloc_pages_current(gfp_t gfp_mask, unsigned order);
493 
494 static inline struct page *
495 alloc_pages(gfp_t gfp_mask, unsigned int order)
496 {
497     return alloc_pages_current(gfp_mask, order);
498 }
499 extern struct page *alloc_pages_vma(gfp_t gfp_mask, int order,
500             struct vm_area_struct *vma, unsigned long addr,
501             int node, bool hugepage);
502 #define alloc_hugepage_vma(gfp_mask, vma, addr, order)  \
503     alloc_pages_vma(gfp_mask, order, vma, addr, numa_node_id(), true)

504 #else
505 #define alloc_pages(gfp_mask, order) \
506         alloc_pages_node(numa_node_id(), gfp_mask, order)
507 #define alloc_pages_vma(gfp_mask, order, vma, addr, node, false)\
508     alloc_pages(gfp_mask, order)
509 #define alloc_hugepage_vma(gfp_mask, vma, addr, order)  \
510     alloc_pages(gfp_mask, order)
511 #endif 
  • 如果定义CONFIG_NUMA,则调用alloc_pages_current分配内存;否则调用 alloc_pages_node;

alloc_pages_node
  ->__alloc_pages_node
     ->__alloc_pages
      ->__alloc_pages_nodemask

__alloc_pages_nodemask()的实现:

【file:/mm/page_alloc.c】
/*
 * 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)
{
	enum zone_type high_zoneidx = gfp_zone(gfp_mask);
	struct zone *preferred_zone;
	struct page *page = NULL;
	int migratetype = allocflags_to_migratetype(gfp_mask);
	unsigned int cpuset_mems_cookie;
	int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
	struct mem_cgroup *memcg = NULL;

	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;

	/*
	 * Will only have any effect when __GFP_KMEMCG is set.  This is
	 * verified in the (always inline) callee
	 */
	if (!memcg_kmem_newpage_charge(gfp_mask, &memcg, order))
		return NULL;

retry_cpuset:
	cpuset_mems_cookie = get_mems_allowed();

	/* The preferred zone is used for statistics later */
	first_zones_zonelist(zonelist, high_zoneidx,
				nodemask ? : &cpuset_current_mems_allowed,
				&preferred_zone);
	if (!preferred_zone)
		goto out;

#ifdef CONFIG_CMA
	if (allocflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
		alloc_flags |= ALLOC_CMA;
#endif
retry:
	/* First allocation attempt */
	page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
			zonelist, high_zoneidx, alloc_flags,
			preferred_zone, migratetype);
	if (unlikely(!page)) {
		if (alloc_flags & ALLOC_FAIR) {
			reset_alloc_batches(zonelist, high_zoneidx,
					    preferred_zone);
			alloc_flags &= ~ALLOC_FAIR;
			goto retry;
		}
		/*
		 * Runtime PM, block IO and its error handling path
		 * can deadlock because I/O on the device might not
		 * complete.
		 */
		gfp_mask = memalloc_noio_flags(gfp_mask);
		page = __alloc_pages_slowpath(gfp_mask, order,
				zonelist, high_zoneidx, nodemask,
				preferred_zone, migratetype);
	}

	trace_mm_page_alloc(page, order, gfp_mask, migratetype);

out:
	/*
	 * When updating a task's mems_allowed, it is possible to race with
	 * parallel threads in such a way that an allocation can fail while
	 * the mask is being updated. If a page allocation is about to fail,
	 * check if the cpuset changed during allocation and if so, retry.
	 */
	if (unlikely(!put_mems_allowed(cpuset_mems_cookie) && !page))
		goto retry_cpuset;

	memcg_kmem_commit_charge(page, memcg, order);

	return page;
}

  这是伙伴管理算法的核心,__alloc_pages_nodemask()分配内存页面的关键函数是:get_page_from_freelist()和__alloc_pages_slowpath()

  • get_page_from_freelist():最先用于尝试页面分配,如果分配失败的情况下,则会进一步调用__alloc_pages_slowpath();
  • __alloc_pages_slowpath()是用于慢速页面分配,允许等待和内存回收。

3.2.get_page_from_freelist

  该函数主要是遍历各个内存管理区列表zonelist以尝试页面申请。其中for_each_zone_zonelist_nodemask()则是用于遍历zonelist的,每个内存管理区尝试申请前,都将检查内存管理区是否有可分配的内存空间、根据alloc_flags判断当前CPU是否允许在该内存管理区zone中申请以及做watermark水印检查以判断zone中的内存是否足够等。该函数调用 rmqueue函数,然后调用 __rmqueue_smallest。

3615 try_this_zone:
3616         page = rmqueue(ac->preferred_zoneref->zone, zone, order,                                        
3617                 gfp_mask, alloc_flags, ac->migratetype);

__rmqueue_smallest:

  该函数实现了分配算法的核心功能,首先for()循环其由指定的伙伴管理算法链表order阶开始,如果该阶的链表不为空,则直接通过list_del()从该链表中获取空闲页面以满足申请需要;如果该阶的链表为空,则往更高一阶的链表查找,直到找到链表不为空的一阶,至于若找到了最高阶仍为空链表,则申请失败;否则将在找到链表不为空的一阶后,将空闲页面块通过list_del()从链表中摘除出来,然后通过expand()将其对等拆分开,并将拆分出来的一半空闲部分挂接至低一阶的链表中,直到拆分至恰好满足申请需要的order阶,最后将得到的满足要求的页面返回回去。至此,页面已经分配到了。

3.3.伙伴管理算法页面释放

void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

3.3.1. free_pages

4270 void __free_pages(struct page *page, unsigned int order)
4271 {
4272     if (put_page_testzero(page)) {   /*判断页没有被使用*/
4273         if (order == 0)
4274             free_hot_cold_page(page, false);   /*单页则释放到每CPU页框高速缓存中*/                                                          
4275         else
4276             __free_pages_ok(page, order);     /*多页则释放到伙伴系统*/
4277     }
4278 }
4279 
4280 EXPORT_SYMBOL(__free_pages);
4281 
4282 void free_pages(unsigned long addr, unsigned int order)
4283 {
4284     if (addr != 0) {
4285         VM_BUG_ON(!virt_addr_valid((void *)addr));
4286         __free_pages(virt_to_page((void *)addr), order);
4287     }
4288 }  

代码分析:
  put_page_testzero():是对page结构的_count引用计数做原子减及测试,用于检查内存页面是否仍被使用,如果不再使用,则进行释放。其中order表示页面数量,如果释放的是单页,则会调用free_hot_cold_page()将页面释放至per-cpu page缓存中,而不是伙伴管理算法;真正的释放至伙伴管理算法的是__free_pages_ok(),同时也是用于多个页面释放的情况。

分析free_hot_cold_page():释放单个页,如果cold是1,则释放的是冷页,是0,则释放的是热页。

void free_hot_cold_page(struct page *page, int cold)
{
	struct zone *zone = page_zone(page);
	struct per_cpu_pages *pcp;
	unsigned long flags;
	int migratetype;

	if (!free_pages_prepare(page, 0))  //释放页之前要检查一下,是否真的适合释放
		return;

	migratetype = get_pageblock_migratetype(page); //获得该页所在块的迁移类型。
	set_freepage_migratetype(page, migratetype);
	local_irq_save(flags);
	__count_vm_event(PGFREE);

	/*只有不可移动页,可回收页和可移动页才能放到每CPU页框高速缓存中,如果
	  迁移类型不属于这个范围,则要将该页释放回伙伴系统*/
	if (migratetype >= MIGRATE_PCPTYPES) {
		if (unlikely(is_migrate_isolate(migratetype))) {
			free_one_page(zone, page, 0, migratetype);
			goto out;
		}
		migratetype = MIGRATE_MOVABLE;
	}

    //获得per-CPU缓存链表
	pcp = &this_cpu_ptr(zone->pageset)->pcp;
	if (cold)   /*冷页插入表尾*/
		list_add_tail(&page->lru, &pcp->lists[migratetype]);
	else      /*热页插入表头*/
		list_add(&page->lru, &pcp->lists[migratetype]);
	pcp->count++;
	
	//如果per-CPU缓存中页数目超出pcp->high,则需要进行惰性合并,将数量为pcp->batch的页退还给伙伴系统
	if (pcp->count >= pcp->high) {
		unsigned long batch = ACCESS_ONCE(pcp->batch);
		free_pcppages_bulk(zone, batch, pcp);
		pcp->count -= batch;
	}

out:
	local_irq_restore(flags);
}
  • free_pages_prepare
    其中trace_mm_page_free()用于trace追踪机制;而kmemcheck_free_shadow()用于内存检测工具kmemcheck,如果未定义CONFIG_KMEMCHECK的情况下,它是一个空函数。接着后面的PageAnon()等都是用于检查页面状态的情况,以判断页面是否允许释放,避免错误释放页面。由此可知该函数主要作用是检查和调试。

  • get_pageblock_migratetype()和set_freepage_migratetype()分别是获取和设置页面的迁移类型,即设置到page->index;

  • 判断migratetype >= MIGRATE_PCPTYPES,MIGRATE_PCPTYPES用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目,如果某个页面类型大于MIGRATE_PCPTYPES则表示其可挂到可移动列表中;如果迁移类型是MIGRATE_ISOLATE则直接将该其释放到伙伴管理算法中。

    迁移类型如下所示:

enum {
	MIGRATE_UNMOVABLE,
	MIGRATE_RECLAIMABLE,
	MIGRATE_MOVABLE,
	MIGRATE_PCPTYPES,	/* the number of types on the pcp lists */
	MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
	MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
	MIGRATE_ISOLATE,	/* can't allocate from here */
#endif
	MIGRATE_TYPES
};

迁移类型的目标就是反内存碎片,Linux3.10内核有多达7中迁移类型,比较明显的是per-CPU类型的迁移类型的增加。
- MIGRATE_UNMOVABLE标识类型的内存在内存中有固定的地址范围且不能够被移动,多数的内核核心代码从此类型内存申请空间。
- MIGRATE_RECLAIMABLE,不能够直接移动,但是内存可以被回收,数据结构可以被重建。由文件映射的数据就是这一类型。
- MIGRATE_MOVABLE:可以被移动,属于用户空间的页就是这一类型的。
- MIGRATE_PCPTYPES:管理per-CPU变量的类型。
- MIGRATE_RESERVE:其和MIGRATE_PCPTYPES是一样的类型的,数据为紧急情况预留的类型。
- MIGRATE_ISOLATE:是一个特殊的虚拟内存域,用于NUMA情况下夸节点移动物理页,不能从其申请内存。
  • this_cpu_ptr:其中pcp表示内存管理区的每CPU管理结构,cold表示冷热页面,如果是冷页就将其挂接到对应迁移类型的链表尾,而若是热页则挂接到对应迁移类型的链表头。其中if (pcp->count >= pcp->high)判断值得注意,其用于如果释放的页面超过了每CPU缓存的最大页面数时,则将其批量释放至伙伴管理算法中,其中批量数为pcp->batch。

重点来了:free_pcppages_bulk

static void free_pcppages_bulk(struct zone *zone, int count,
					struct per_cpu_pages *pcp)
{
	int migratetype = 0;
	int batch_free = 0;
	int to_free = count;

	spin_lock(&zone->lock);
	zone->pages_scanned = 0;

	while (to_free) {
		struct page *page;
		struct list_head *list;

		/*
		 * Remove pages from lists in a round-robin fashion. A
		 * batch_free count is maintained that is incremented when an
		 * empty list is encountered.  This is so more pages are freed
		 * off fuller lists instead of spinning excessively around empty
		 * lists
		 */
		do {
			batch_free++;
			if (++migratetype == MIGRATE_PCPTYPES)
				migratetype = 0;
			list = &pcp->lists[migratetype];
		} while (list_empty(list));

		/* This is the only non-empty list. Free them all. */
		if (batch_free == MIGRATE_PCPTYPES)
			batch_free = to_free;

		do {
			int mt;	/* migratetype of the to-be-freed page */

			page = list_entry(list->prev, struct page, lru);
			/* must delete as __free_one_page list manipulates */
			list_del(&page->lru);
			mt = get_freepage_migratetype(page);
			/* MIGRATE_MOVABLE list may include MIGRATE_RESERVEs */
			__free_one_page(page, zone, 0, mt);
			trace_mm_page_pcpu_drain(page, 0, mt);
			if (likely(!is_migrate_isolate_page(page))) {
				__mod_zone_page_state(zone, NR_FREE_PAGES, 1);
				if (is_migrate_cma(mt))
					__mod_zone_page_state(zone, NR_FREE_CMA_PAGES, 1);
			}
		} while (--to_free && --batch_free && !list_empty(list));
	}
	spin_unlock(&zone->lock);
}

__free_one_page:

  while (order < MAX_ORDER-1)前面主要是对释放的页面进行检查校验操作。而while循环内,通过__find_buddy_index()获取与当前释放的页面处于同一阶的伙伴页面索引值,同时藉此索引值计算出伙伴页面地址,并做伙伴页面检查以确定其是否可以合并,若否则退出;接着if (page_is_guard(buddy))用于对页面的debug_flags成员做检查,由于未配置CONFIG_DEBUG_PAGEALLOC,page_is_guard()固定返回false;则剩下的操作主要就是将页面从分配链中摘除,同时将页面合并并将其处于的阶提升一级。

  退出while循环后,通过set_page_order()设置页面最终可合并成为的管理阶。最后判断当前合并的页面是否为最大阶,否则将页面放至伙伴管理链表的末尾,避免其过早被分配,得以机会进一步与高阶页面进行合并。末了,将最后的挂入的阶的空闲计数加1。至此伙伴管理算法的页面释放完毕。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
假设系统的可利用空间容量为2m个字,则系统开始运行时,整个内存区是一个大小为2m的空闲分区。在系统运行过程中,由于不断的划分,可能会形成若干个不连续的空闲分区,将这些空闲分区根据分区的大小进行分类,对于每一类具有相同大小的所有空闲分区,单独设立一个空闲分区双向链表。这样,不同大小的空闲分区形成了k(0≤k≤m)个空闲分区链表。 当需要为进程分配一个长度为n的存储空间时,首先计算一个i值,使2i-1<n≤2i,然后在空闲分区大小为2i的空闲分区链表中查找。若找到,即把该空闲分区分配给进程。否则,表明长度为2i的空闲分区已经耗尽,则在分区大小为2i+1的空闲分区链表中寻找。若存在2i+1的一个空闲分区,则把该空闲分区分为相等的连个分区,这两个分区称为一对伙伴,其中的一个分区用于分配,而把另一个加入分区大小为2i的空闲分区链表中。若大小为2i+1的空闲分区不存在,则需要查找大小为2i+2的空闲分区,若找到则对其进行两次分割:第一次,将其分割为大小为2i+1的两个分区,一个用于分配,一个加入到大小为2i+1空闲分区链表中;第二次,将第一次用于分配的空闲分区分割为2i的两个分区,一个用于分配,一个加入到大小为2i空闲分区链表中。若仍然找不到,则继续查找大小为2i+3的空闲分区,以此类推。由此可见,在最坏的情况下,可能需要对2k的空闲分区进行k次分割才能得到所需分区。 与一次分配可能要进行多次分割一样,一次回收也可能要进行多次合并,如回收大小为2i的空闲分区时,若事先已存在2i的空闲分区时,则应将其与伙伴分区合并为大小为2i+1的空闲分区,若事先已存在2i+1的空闲分区时,又应继续与其伙伴分区合并为大小为2i+2的空闲分区,依此类推。 2.2 伙伴系统的需求 根据伙伴系统算法的思想,我们组对本系统的功能划分为3种: ⑴ 根据伙伴系统算法分配内存 ⑵ 根据伙伴系统算法回收内存 ⑶ 实时查看内存使用的情况

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值