伙伴算法buddy实现过程分析

                                    linux伙伴算法

Linux物理内存分配有三种方式:

        a. per-CPU机制,分配单个页面,per-CPU的页缓存提供了更快的分配和释放机制;

        b. 伙伴算法,适用于分配大块连续页面(至少一个页面),解决了外碎片问题;

        c. slab机制,分配小内存,访问频率较高的内存,解决了内碎片问题。


1.伙伴算法原理及伙伴关系

    为了便于页面的维护,将多个页面组成内存块,每个内存块都有 2 的方幂个页,方幂的指数被称为阶 order。order相同的内存块被组织到一个空闲链表中。伙伴系统基于2的方幂来申请释放内存页。
当申请内存页时,伙伴系统首先检查与申请大小相同的内存块链表中,检看是否有空闲页,如果有就将其分配出去,并将其从链表中删除,否则就检查上一级,即大小为申请大小的2倍的内存块空闲链表,如果该链表有空闲内存,就将其分配出去,同时将剩余的一部分(即未分配出去的一半)加入到下一级空闲链表中;如果这一级仍没有空闲内存;就检查它的上一级,依次类推,直到分配成功或者彻底失败,在成功时还要按照伙伴系统的要求,将未分配的内存块进行划分并加入到相应的空闲内存块链表
在释放内存页时,会检查其伙伴是否也是空闲的,如果是就将它和它的伙伴合并为更大的空闲内存块,该检查会递归进行,直到发现伙伴正在被使用或者已经合并成了最大的内存块。


        满足以下条件的两个块成为伙伴:

                a. 两个块大小相同;

                b. 两个块的物理地址连续。

        伙伴算法把满足以上条件的两个块合并为一个块,为迭代算法,如果合并后的块还可以跟相邻的块进行合并,那么该算法就继续合并。


2.相关结构体

#define  MAX_ORDER  11     //空闲页面分为11个块链表,每个链表中都是固定大小的2^order页块

数据结构:

struct  free_area {
 struct  list_head  free_list[MIGRATE_TYPES];	/*双向循环链表的头,集中了大小为2^order页的空闲块对应的页描述符*/
 unsigned long	nr_free;	/*表示大小为2^order页的空闲块的个数*/
};
struct zone{
    ...
    struct free_area  free_area[MAX_ORDER];
    ...
};

伙伴算法的关系图如下:

wKioL1Y-w37S2vGRAAHzZMOdDVM844.png

3.分配页框


a.伙伴算法的入口函数是buffer_rmqueue:

        首先判断要分配的页面数是否为 1,如果为1 的情况下,那么并不需要从buddy系统获取,因为per-CPU的页缓存提供了更快的分配和释放机制。per-CPU cache提供了两个链表,一个是cold page链表,另外一个是hot page链表。从hot-cold 链表获取page时要考虑迁移类型。

 如果per-CPU页缓存无法满足分配,那么调用rmqueue_bulk从buddy进行分配。

static inline struct page *buffered_rmqueue(struct zone *preferred_zone,
   struct zone *zone, int order, gfp_t gfp_flags,
   int migratetype)
{  ...
     if (likely(order == 0)) {
     ...
     list = &pcp->lists[migratetype];
     local_irq_save(flags);
         if (list_empty(list)) {
             pcp->count += rmqueue_bulk(zone, 0,pcp->batch, list,migratetype, cold);
         ...
	 }
    } else {
     ...
     spin_lock_irqsave(&zone->lock, flags);
     page = __rmqueue(zone, order, migratetype);
     __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
     spin_unlock(&zone->lock);
     ...
     }
 }


b.__rmqueue()函数

调用__rmqueue_smallest函数在指定的迁移类型上分配,分配失败并且migratetype不是MIGRATE_RESERVE类型就使用__rmqueue_fallback函数在其他备用迁移列表进行内存分配

函数参数:

    zone表示伙伴算法将从该内存管理区中分配页框;

    order即分配阶;

    migratetype表示迁移类型。


static struct page *__rmqueue(struct zone *zone, unsigned int order,
      int migratetype)
{
    struct page *page;
    retry_reserve:
    page = __rmqueue_smallest(zone, order, migratetype);
    if (unlikely(!page) && migratetype != MIGRATE_RESERVE) {
      page = __rmqueue_fallback(zone, order, migratetype);
      if (!page) {
      migratetype = MIGRATE_RESERVE;
      goto retry_reserve;
      }
    }
    trace_mm_page_alloc_zone_locked(page, order, migratetype);
    return page;
}


c.__rmqueue_smallest函数(分配页框的主要函数)

static inline struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
              int migratetype)
{
	unsigned int current_order;
	struct free_area * area;
	struct page *page;

	/* Find a page of the appropriate size in the preferred list */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]); /*将数组下标为current_order的元素地址赋给area*/
		if (list_empty(&area->free_list[migratetype]))  /*current_order的free_list链表为NULL,结束本次循环,继续遍历更高阶*/
			continue;

		page = list_entry(area->free_list[migratetype].next, /*free_list不为空,即就是该阶有空闲块,则分配*/
							struct page, lru);
		list_del(&page->lru); /*删除块的第一个页框描述符*/
		rmv_page_order(page);
		area->nr_free--;  /*空闲块个数减一*/
		/*需要的order中没找到空闲页,在更大阶中找到,需要分割大块,分配需要的块,将剩余的页块链接到合适阶的链表中,具体介绍请看步骤e*/
		expand(zone, page, order, current_order, area, migratetype); 
		return page;
	}

	return NULL;
}


d.__rmqueue_fallback()函数

  在介绍这个函数前,先看看内核为什么要引入迁移类型?

   我们知道buddy主要是为了避免内核外碎片问题的,buddy的碎片防止机制寄托于内存使用者会及时释放掉内存的情况,如果使用者长期不释放内存,或者说在使用者还没有释放内存的这一段时间期间,碎片将是存在的,并且可能还会导致很大的问题;

       随着用户程序的执行和结束,需要不断的为其分配和释放物理页面,但是频繁的请求和释放不同大小的一组连续的页面,必然导致在已分配的内存块中分散着许多小块的页面,即就是外碎片,由此带来的问题就是,这些小块空闲页面加起来虽已满足所请求的页面大小,但是要分配一个大块的连续页面可能就根本无法满足,只能等待其释放了。究其根源,就是buddy仅仅是在页面释放后的合并操作时防止碎片。

     因而仍然存在这种问题:

        连续块中已被使用的页中存放的是一些内核永久性的数据而不会被回收回来,那么这些碎片将永远无法消除,这也就意味着这个最大内存块永远无法被连续的分配出去;对于这些不可移动的页,拿出一段内存,专门用于分配不可移动页,虽然这段内存中有碎片,但是避免了碎片散步到其他来行的内存中去,这就是迁移类型的引入,在分配时就可以一定的约束,防止碎片。

       一开始其他类型都没有属于自己的内存,系统初始化时所有内存都被标识为可移动内存,只有在要分配这些类型的内存时,才从可移动内存中分出一些设置为该类型。


具体函数实现如下:

static inline struct page * __rmqueue_fallback(struct zone *zone, int order, 
                    int start_migratetype)
{
	struct free_area * area;
	int current_order;
	struct page *page;
	int migratetype, i;

	/*从最大的内核块链表开始遍历阶数,内核倾向于尽量找到大的内存块来满足分配*/
	for (current_order = MAX_ORDER-1; current_order >= order;
						--current_order) {
		/*根据fallbacks中定义的顺序遍历后面的迁移类型*/
		for (i = 0; i < MIGRATE_TYPES - 1; i++) {
			migratetype = fallbacks[start_migratetype][i];

			/* MIGRATE_RESERVE handled later if necessary */
			if (migratetype == MIGRATE_RESERVE)   /*不允许占用保留内存*/
				continue;

			area = &(zone->free_area[current_order]);
			if (list_empty(&area->free_list[migratetype]))
				continue;

			/*取块首的页*/
			page = list_entry(area->free_list[migratetype].next,
					struct page, lru);
			area->nr_free--;
			 
	             /*pageblock_order定义着内核认为的大块内存究竟是多大,此时宏值为MAX_ORDER-1*/  
                     /*如果 1.当前得到的块是一个比较大的块,即阶数大于pageblock_order/2=5
                      *或者 2.之前指定的迁移类型为可回收页 或者 3.没有启用迁移分组机制*/
			if (unlikely(current_order >= (pageblock_order >> 1)) ||
					start_migratetype == MIGRATE_RECLAIMABLE ||
					page_group_by_mobility_disabled) {
				unsigned long pages;

				/*将当前页所处的最大空闲内存块移到之前指定的迁移类型对应的链表中,*/
				pages = move_freepages_block(zone, page,
								start_migratetype);
				/*移动的空闲页面数大于大内存块的一半,则修改整个块的迁移类型*/
				if (pages >= (1 << (pageblock_order-1)) ||
						page_group_by_mobility_disabled)
					set_pageblock_migratetype(page,
								start_migratetype);

				migratetype = start_migratetype;
			}

			/* Remove the page from the freelists */
			list_del(&page->lru);
			rmv_page_order(page);

			/* Take ownership for orders >= pageblock_order */
			/*如果current_order大于等于10,则将超出部分的迁移类型设为start_migratetype*/
			if (current_order >= pageblock_order)
				change_pageblock_range(page, current_order,
							start_migratetype);
			/*当前order中没找到空闲页,在更大阶中找到,需要分隔大块,分配需要的块,将剩余的页面链接到合适阶的链表中*/
			expand(zone, page, order, current_order, area, migratetype);

			trace_mm_page_alloc_extfrag(page, order, current_order,
				start_migratetype, migratetype);

			return page;
		}
	}
	return NULL;
}


e.expand()函数

static inline void expand(struct zone *zone, struct page *page,
 int low, int high, struct free_area *area, int migratetype) 
{
 unsigned long size = 1 << high; /*将1左移high位*/
 /*current_order>order,即就是当前order阶上没有足够空闲块,在更高阶上分配空闲块,
  *此时页面数大于要申请的,需要按照buddy算法将大块页框块分割*/
 while (high > low) {
  area--;
  high--;
  size >>= 1;
  VM_BUG_ON(bad_range(zone, &page[size]));
  /*插入伙伴作为链表中第一个元素*/
  list_add(&page[size].lru, &area->free_list[migratetype]);
  area->nr_free++;  /*空闲块个数加一*/
  set_page_order(&page[size], high); /*设置private值,即就是buddy的order*/
 }
}


例如:

high=5,low=3时,1左移5位为32,第一次循环area数组和high都减一(area指向第4阶,high=4),size右移1位为16,list_add函数将大的16页框添加到area[4]指向的链表中,然后大小为2^4页的空闲块个数nr_free加1,并设置order;此时,high=4仍然大于3,进行第二次循环,area指向第三阶,high=3,size右移1位为8,list_add函数将后8个页框加入到area[3]指向的链表中,nr_free加1,设置order;第三次循环不成立,结束。


4.释放页块

    分配页块的过程是将大的页块分成小页块,使得内存更为零散;而页块释放则是与分配过程相反,只要合适就把小页合并成大页块。这也是伙伴算法防止外碎片的一个机制。主要由以下几个函数实现(依次调用):

free_pages

        >__free_pages

                >__free_pages_ok

                       >free_one_page

                              >__free_one_page

前边几个函数主要是做一些安全性检查,buddy释放页块主要的函数是__free_one_page。还有前边讲过分配单个页框会先使用per-CPU机制,同样的释放单个页框也是使用per-CPU机制来释放(在__free_pages函数中实现)。

static inline void __free_one_page(struct page *page,
		struct zone *zone, unsigned int order,
		int migratetype)
{
	unsigned long page_idx;  

	if (unlikely(PageCompound(page)))
		if (unlikely(destroy_compound_page(page, order)))
			return;

	VM_BUG_ON(migratetype == -1);
	
	page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1); //块中第一个页框的索引

	VM_BUG_ON(page_idx & ((1 << order) - 1));
	VM_BUG_ON(bad_range(zone, page));

	/*只要结束小于MAX_ORDER-1就可能被合并*/
	while (order < MAX_ORDER-1) {
		unsigned long combined_idx;
		struct page *buddy;
		/*寻找page页的伙伴块,通过异或得到伙伴页的索引,从而返回伙伴块的页描述符*/
		buddy = __page_find_buddy(page, page_idx, order); 
		if (!page_is_buddy(page, buddy, order)) //没有伙伴块跳出循环
			break;

		/* Our buddy is free, merge with it and move up one order. */
		list_del(&buddy->lru);   //将伙伴块从块链表中删除
		zone->free_area[order].nr_free--; //当前阶数链表的空闲块数减1
		rmv_page_order(buddy);   //清除伙伴页的伙伴标志
		/*计算出合并块后的索引*/
		combined_idx = __find_combined_index(page_idx, order);
		page = page + (combined_idx - page_idx);  //得到合并页的页起始描述符
		page_idx = combined_idx;  //更新索引
		order++;
	}
	set_page_order(page, order);  //设置合并后的阶数
	list_add(&page->lru,
		&zone->free_area[order].free_list[migratetype]);
	zone->free_area[order].nr_free++;
}


你可能感兴趣的:(buddy伙伴算法,__rmqueue,MIGRATE_TYPE)