记得我们在上篇文章mem_init()分析中提到__free_page()函数吗?我们现在来完善它。它的调用过程是这样的:mem_init()->free_all_bootmem_node():我们知道这个函数是统计一共释放了多少空闲页。->free_all_bootmem_core()就是在这个函数里面先后多次调用__free_pages()函数。目的就是为了释放每个内存node里面无用的内存孔洞页。我们现在就来好好探讨这个函数。
fastcall void __free_pages(struct page *page, unsigned int order)
{
if (!PageReserved(page) && put_page_testzero(page)) {
if (order == 0)
free_hot_page(page);//我们先来看看这个函数。
else
__free_pages_ok(page, order);//我们最后才来讨论这个函数。
}
}
void fastcall free_hot_page(struct page *page)
{
free_hot_cold_page(page, 0);
}
static void fastcall free_hot_cold_page(struct page *page, int cold)//如果cold是0的话,表示要把page所指向内存页释放到
//其所在内存页区中当前处理器的“热区”内存中,如果为1的话,就释放到这页所在页区
//中当前处理器的“冷区”高速缓存内存中。在这里肯定有不少人不太了解linux在启动后是
//如何分配和释放内存的。我在这里讲个大概,在以后的文章中会结合代码细讲的。
//一个核心概念要记住,多页时通过伙伴机制,单页时使用“热冷区”。我们这个函数先涉及后者机制。我们先来说说这个机制。
/首先,大家都很了解struct zoon这个结构体,里面涉及了一个与“冷热区”相关的重要成员:struct per_cpu_pageset pageset
//[NR_CPUS]里面的成员我不打算全讲,NR_CPUS表示这个系统中cpu对应的号。struct per_cpu_pages pcp[2]这个是我要讲的成员
//我们来看看struct per_cpu_pages 这个结构体的全貌:
struct per_cpu_pages {
int count;
int low;
int high;
int batch;
struct list_head list;
};
//这个结构体大家都是用“高速缓存内存”来称呼它的,这样说它也是表示一段内存啦!没错,我们对于一页的内存释放和分配的时候就会
//先去这两个内存看看。为什么说是两个内存?因为我们每个zone都会有冷区和热区,zone中的struct per_cpu_pageset[]对应着的
//其中一个cpu的冷区和热区,冷区我们会用pageset[n]->pcp[1],热区我们会用pageset[n]->pcp[0]来表示。我们会想,这个
//per_cpu_pages是怎么表示一段内存呢?我们会发现这里有个链表list,它是指向一段内存的开头。count就是表示现在有多少内存页。
//当我们要释放一页内存的时候,我们先看看找到这页所属的内存区zone当前cpu的热区或是冷区。如果此时发现count>high时,我们先
//把list的前batch数量的内存页释放到这个页区的free_area中(这个和伙伴机制有关,我们等下再讲),然后将要释放的页添加到list中
//当我们要分配时,我们先考虑热区或是冷区的count是否大于low,如果不是的话,我们先要从该页区一页一页释放batch数量内存页到
//list链表中,然后我们才从list中申请一页内存。好了,到这里我们来看看上述过程是如何实现的,但是我们这里只是讨论释放这个过程。
{
struct zone *zone = page_zone(page);//我们通过page_zone宏定义可以求得该页内存所属页区,并使zone指向它。
struct per_cpu_pages *pcp;//为等下访问冷热区做准备,pcp是一个中间变量。
unsigned long flags;
arch_free_page(page, 0);//只有在um处理器系统中定义了这个函数,其他的都为空。
kernel_map_pages(page, 1, 0);//同上
inc_page_state(pgfree);//当前处理器的per_cpu_page_state结构的pgfree成员加一,pgfree是空闲页计数器。
if (PageAnon(page))//说明page->mapping的第0位如果为1时,说明该页是匿名页
page->mapping = NULL;//这样就使page->mapping指向null
free_pages_check(__FUNCTION__, page);//这个函数就是检测page的flag和mapping,_count,_mapcount这几个成员,如果
//有误,我们就会进行相应的处理,我们还用一个全局变量taintd来记录错误。
pcp = &zone->pageset[get_cpu()].pcp[cold];//获得本页区的当前cpu,然后找到对应的热区的高速缓存内存,用pcp指针指向它。
//get_cpu()有preempt_disable()是cpu抢占禁止。
local_irq_save(flags);//禁止irq中断,保存cpsr到flags中。
if (pcp->count >= pcp->high)//如果count>high时,说明我们先要把batch页内存先释放到伙伴系统中。
pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0);//这个我们会在接下来的细讲的。
list_add(&page->lru, &pcp->list);//把page的lru挂在pcp->list上,挂在pcp->list前面。
pcp->count++;//更新
local_irq_restore(flags);//将flags中的值复制到当前cpsr中,同时使能irq中断。
put_cpu();//preempt_enable()是抢占使能。
}
我们现在来分析一下__free_pages_bulk()函数。
static intfree_pages_bulk(struct zone *zone, int count,struct list_head *list, unsigned int order)
{
unsigned long flags;
struct free_area *area;
struct page *base, *page = NULL;
int ret = 0;
base = zone->zone_mem_map;//该页区所有页对应的struct page变量起始地址保存在zone_mem_map,这是base指向
area = zone->free_area + order;//order=0时,area=free_area[0],其实我们就用这个区域来表示一页内存大小的区域。
//free_area[n]数组中成员的数据类型是struct free_area,这个数据类型有两个成员,free_list
//和map。如果释放到伙伴系统中的内存,我们就会用链表free_list来连接。就是相同order值的区
//域的首页struct page的lru成员会挂接在free_list链表前面。对于上面说的,每个zoon里面都有
//free_area[MAX_ORDER]数组,free_area[0]表示order=0的区域,也就是2的0次方,就是1页
//接下来介绍map这个成员,其实是该区域伙伴位码表基址,表中的每一位代表相邻两个相同order值的
//内存区域的状态,0表示状态一致,同空闲或者是都被使用。1表示状态相反。到这里,我不得不介绍伙
//伴机制了。
//伙伴机制buddy system是基于内存页区内进行内存分配和释放的。我们先要介绍一个概念--内存区域。一个内存区域=2的order次方页
//上面说的相邻order值相同的内存区域我们会用一位来表示两者的状态的,我们这时可以称这两个内存区域为伙伴区域。
spin_lock_irqsave(&zone->lock, flags);
zone->all_unreclaimable = 0;//不能收回标志,所有的页都被拴住不能收回。
zone->pages_scanned = 0;//已扫描页计数器。
while (!list_empty(list) && count--) {//一直循环到pcp->batch=0和list的链表清空。这个过程就是把list的头batch页释放到
//free_area[0]中去。
page = list_entry(list->prev, struct page, lru);//通过pcp->list找到第一个lru,再通过lru成员找到对应的struct page
list_del(&page->lru);//lru取出后,pcp->list链表就不再指向它了,也就是从链表里面移除了。
__free_pages_bulk(page, base, zone, area, order);//这个就是释放函数的主体,我们等下展开来看看。
ret++;//这个等while结束后返回,表示我们释放了多少页内存。
}
spin_unlock_irqrestore(&zone->lock, flags);
return ret;
}
我们现在来看看__free_pages_bulk()函数。
static inline void __free_pages_bulk (struct page *page, struct page *base,struct zone *zone, struct free_area
*area, unsigned int order)//page是要释放内存页的struct page指针,base是本内存zone所有页的struct page的起始地址,
//order=0,area=free_area[0]。
{
unsigned long page_idx, index, mask;
if (order)
destroy_compound_page(page, order);
mask = (~0UL) << order;
page_idx = page - base;//计算page相对于起始页的struct page的页偏移量。
if (page_idx & ~mask)//偏移量要是1<<order或是0,系统就不会崩溃了。
BUG();
index = page_idx >> (1 + order);//这个偏移量很和页数有点相似,起始这个操作就是要求出这个页对应的伙伴位码表中的对应位。
zone->free_pages += 1 << order;//在zone的空闲页计数器上加上这回空闲的页数。
while (order < MAX_ORDER-1) {
struct page *buddy1, *buddy2;
BUG_ON(area >= zone->free_area + MAX_ORDER);
if (!__test_and_change_bit(index, area->map))
break;//如果找到这页区对应的伙伴位码,但是在测试的时候就发现原来就是0,会必然改变成1的,这时我们会发现伙伴的状
//态不一样,我们就退出循环,没法把他们两个合并了。
buddy1 = base + (page_idx ^ (1 << order));//这个就是要合并的空闲内存区域的首页struct page指针。
buddy2 = base + page_idx;
BUG_ON(bad_range(zone, buddy1));//这里检查我们的struct page结构体是否在zoon的范围内。
BUG_ON(bad_range(zone, buddy2));
list_del(&buddy1->lru);//因为合并,则buddy1已经不再属于原来的order级别了,这样我们使lru从free_list列表断开。
mask <<= 1;
order++;//进行下一级别的合并做准备。
area++;//为了下一个free_area[]准备,这样我们才可以查看到free_area[n]->map中的伙伴位码。
index >>= 1;//会发现一个巧合,第一次进行合并开始,index=0,刚好和下面的语句匹配。
page_idx &= mask;//其实这里的mask我一直觉得只有使page_idx清零的作用,换句话合并永远都是从order的第一个区域
//开始。
}
list_add(&(base + page_idx)->lru, &area->free_list);//没重组的和重组的都把其首页的struct page结构体的lru成员,挂接到
//这个区域在所属zoon中对应的free_area的free_list链表上。
}
好了,这样我们就顺利返回到free_pages_bulk()函数中来。这个函数的返回值是ret,它表示释放了多少页内存到伙伴系统中。在这样我们可以再回到__free_hot_cold_page()函数看看。我们会根据具体的ret值去更新我们的count变量的。其实到这里,我们已经把整个free_hot_page()函数分析了一遍了。那么__free_pages()函数的分析已经完成了一半,现在我们把后面的一半也把它分析了。如果我们释放的内存不是一页的话,我们就调用函数__free_pages_ok()。这个函数就是直接通过伙伴机制来实现内存释放的。
void __free_pages_ok(struct page *page, unsigned int order)
{
LIST_HEAD(list);
int i;
arch_free_page(page, order);
mod_page_state(pgfree, 1 << order);//将当前处理器的页状态结构per_cpu_page_state中的空闲页计数器pgfree加
//1<<order,这个函数和inc_page_state()函数类似。
for (i = 0 ; i < (1 << order) ; ++i)
free_pages_check(__FUNCTION__, page + i);//再释放前要检测页是否有错误,如果有错误的话就必须进行检错。
list_add(&page->lru, &list);//把page->lru挂在临时链表list上。
kernel_map_pages(page, 1<<order, 0);
free_pages_bulk(page_zone(page), 1, &list, order);//释放从临时链表list开始的1个大小为2的order次方的连续区域的内存
//这个区域属于zoon=page_zone(page)。
}
好了,现在已经全部分析了一遍,关于Linux的连续页或是一页内存的释放有了比较深入的理解,我们接下来的文章会说说申请的大概过程。