相关链接:
Linux伙伴系统(一)--伙伴系统的概述
http://www.zhishiwu.com/os/201206/134234.html;
Linux伙伴系统(二)--伙伴系统的初始化
http://www.zhishiwu.com/os/201206/134241.html;
Linux伙伴系统(三)--分配
http://www.zhishiwu.com/os/201206/134245.html;
Linux伙伴系统(四)--释放页
http://www.zhishiwu.com/os/201206/134247.html
linux在伙伴管理系统中引入迁移类型(migrate type)这么一个概念,用于避免系统在长期运行过程中产生碎片。关于迁移类型的一些概念在介绍伙伴系统的数据结构的时候有提到过(见<<Linux伙伴系统(一)>>),不过考虑到它较为复杂,因此在解析分配页的过程中没有介绍迁移类型是如何工作的,在这里将这部分拿出来单独作为一个部分进行分析。
在分析具体的代码前,再来说一下为什么要引入迁移类型。我们都知道伙伴系统是针对于解决外碎片的问题而提出的,那么为什么还要引入这样一个概念来避免碎片呢?我们注意到,碎片一般是指散布在内存中的小块内存,由于它们已经被分配并且插入在大块内存中,而导致无法分配大块的连续内存。而伙伴系统把内存分配出去后,要再回收回来并且重新组成大块内存,这样一个过程必须建立两个伙伴块必须都是空闲的这样一个基础之上,如果其中有一个伙伴不是空闲的,甚至是其中的一个页不是空闲的,都会导致无法分配一块连续的大块内存。我们再引用之前上过的一个例子来看这个问题: www.zhishiwu.com
图中,如果15对应的页是空闲的,那么伙伴系统可以分配出连续的16个页框,而由于15这一个页框被分配出去了,导致最多只能分配出8个连续的页框。假如这个页还会被回收回伙伴系统,那么至少在这段时间内产生了碎片,而如果更糟的,如果这个页用来存储一些内核永久性的数据而不会被回收回来,那么碎片将永远无法消除,这意味着15这个页所处的最大内存块永远无法被连续的分配出去了。假如上图中被分配出去的页都是不可移动的页,那么就可以拿出一段内存,专门用于分配不可移动页,虽然在这段内存中有碎片,但是避免了碎片散布到其他类型的内存中。在系统中所有的内存都被标识为可移动的!也就是说一开始其他类型都没有属于自己的内存,而当要分配这些类型的内存时,就要从可移动类型的内存中夺取一部分过来,这样可以根据实际情况来分配其他类型的内存。现在我们就来看看伙伴系统分配页时,当指定的迁移类型的内存不足时,系统是如何做的
[cpp]
<span style="font-size:12px;">/* Remove an element from the buddy allocator from the fallback list */
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;
/* Find the largest possible block of pages in the other list */
/*从最大的内存块链表开始遍历阶数,内核倾向于尽量找到大的内存块来满足分配*/
for (current_order = MAX_ORDER-1; current_order >= order;
--current_order) { www.zhishiwu.com
for (i = 0; i < MIGRATE_TYPES - 1; i++) {/*根据fallbacks中定义的顺序遍历后面的迁移类型*/
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--;
/*
* If breaking a large block of pages, move all free
* pages to the preferred allocation list. If falling
* back for a reclaimable kernel allocation, be more
* agressive about taking ownership of free pages
*/
/*pageblock_order定义着内核认为的大块内存究竟是多大*/
/*如果 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; www.zhishiwu.com
/*试图将当前页所处的最大内存块移到之前指定的迁移类型对应的链表中,
只有空闲页才会移动,所以真正可移动的页数pages可能小于2^pageblock_order*/
pages = move_freepages_block(zone, page,
start_migratetype);
/* Claim the whole block if over half of it is free */
/*移动的页面数大于大内存块的一半,则修改整个块的迁移类型*/
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);
/*拆分,这里注意的是如果之前改变了migratetype,则会将拆分的伙伴块添加到新的迁移类型链表中*/ www.zhishiwu.com
expand(zone, page, order, current_order, area, migratetype);
trace_mm_page_alloc_extfrag(page, order, current_order,
start_migratetype, migratetype);
return page;
}
}
return NULL;
}
</span>
首先我们看到的不寻常的一点就是,for循环优先遍历大的内存块,也就是说优先分配大内存块,这似乎和伙伴系统优先分配小内存块的原则相违背,但是仔细想想这样做其实就是为了避免在新的迁移类型中引入碎片。如何说?现在假如A类型内存不足,向B类型求援,假设只从B中分配一块最适合的小块,OK,那么过会儿又请求分配A类型内存,又得向B类型求援,这样来来回回从B类型中一点一点的分配内存将会导致B类型的内存四处都散布碎片,如果这些内存一直得不到释放……内核已经不敢想象了……B类型可能会因为A类型而引入的碎片导致其再也分配不出大的内存块了。出于这种原因,内核选择直接分配一块最大的内存块给A,你们爱怎么打怎么闹随便你们,反正都是在A类型中进行,只要不拖累我B类型就可以了
当请求分配的内存比较大时或者最初的请求类型为可回收类型时,会表现得更加积极,也就是我前面所说的将对应的最大内存块搬到最初的请求类型对应的链表中。我的理解是,这里判断一个比较大的内存类型的条件是用来和前面优先遍历大内存块的for循环相呼应的,也就是说假如得到的内存块是一个比较小的内存块,那就说明该内存类型自己也没有大块可分配的连续内存了,因此就不执行搬迁工作。而对于可回收类型的内存要执行搬迁是因为在一些时候,内核可能会非常频繁地申请小块可回收类型的内存。
当搬迁的内存的大于大块内存的一半时,将彻底将这块内存化为己有,也就是说将这片大块内存区对应的页类型标识位图区域标识成最初申请的内存类型。
用<<深入Linux内核架构>>上的一段话作为总结,“实际上,在启动期间分配可移动内存区的情况较少,那么分配器有很高的几率分配长度最大的内存区,并将其从可移动列表转换到不可移动列表。由于分配的内存区长度是最大的,因此不会向可移动内存中引入碎片。总而言之,这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处,从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要目标之一”
作者 vanbreaker