Linux中的内存分配和释放之__free_pages()函数分析

本文详细分析了Linux内核中的__free_pages()函数,讲解了其在内存释放过程中的作用,包括free_hot_page()和__free_pages_ok()函数。文章介绍了冷热区内存管理机制,阐述了释放内存页时如何处理冷区和热区,并通过伙伴系统进行内存合并和释放。通过对struct page、struct zone和struct per_cpu_pages等数据结构的讨论,揭示了Linux内核中内存分配和释放的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  记得我们在上篇文章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的连续页或是一页内存的释放有了比较深入的理解,我们接下来的文章会说说申请的大概过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值