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

  了解了Linux内存管理的基本知识后,我们来研究一下Linux内存的分配和释放。我们会分两个部分来研究的。我们向来探讨系统启动初期内存的分配和释放。这个阶段可以申请到大片的物理内存,但是要注意到以下两点!

  1)其实系统启动初期是指在init_mem()之前的阶段,在这个阶段由于只需要少量的内存(内核编译需要的内存,页帧位码表,页表,initrd),所以可以申请到大片的物理内存,但是我们还要注意到的是,如果在这个阶段不释放这些内存的话,init_mem()以后的内核是不可以再对这部分内存操作的(分配和释放)。

  2)这个阶段,我们只可以分配低端内存,分配不了高端内存,因为这个时候内存是没有内核逻辑地址的。

  好了,现在我们开始来研究与这个阶段有关的函数,通过这些函数的分析,我们可以更清楚内存管理是怎样的。我们先来研究bootmem_init()系统启动阶段内存管理初始化。这个函数是在start_kernel()的setup_arch()的paging_init()调用的。我们现在来看看paging_init()的具体代码。

  void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
 void *zero_page;
 int node;

 bootmem_init(mi);

struct meminfo *mi其实是从setup_arch()函数传过来的全局变量,这个全局变量是meminfo,这个变量记录了系统内存信息,具体来说是记录了bank的信息。我们来看看bootmem_init(mi)具体做些什么!

static void __init bootmem_init(struct meminfo *mi)
{
 struct node_info node_info[MAX_NUMNODES], *np = node_info;
 unsigned int bootmap_pages, bootmap_pfn, map_pg;
 int node, initrd_node;

 bootmap_pages = find_memend_and_nodes(mi, np);

对于struct node_info这个结构体是用来描述内存节点(node)的信息的,这里的node_info变量是这个结构体的数组,*np这是指向这个数组的指针,我们先来看看find_memend_and_nodes(mi, np);这个函数。

find_memend_and_nodes(struct meminfo *mi, struct node_info *np)
{
 unsigned int i, bootmem_pages = 0, memend_pfn = 0;

 for (i = 0; i < MAX_NUMNODES; i++) {
  np[i].start = -1U;
  np[i].end = 0;
  np[i].bootmap_pages = 0;
 }

这里是对刚才说的那个结构体进行初始化,一直把整个数组的每个单元初始化完为止。

for (i = 0; i < mi->nr_banks; i++) {
  unsigned long start, end;
  int node;

  if (mi->bank[i].size == 0) {
   /*
    * Mark this bank with an invalid node number
    */
   mi->bank[i].node = -1;
   continue;
  }
 英文注释说的很清楚,当有无效的bank的时候,就把-1这个无效值赋给bank.node成员。

 node = mi->bank[i].node;

 当发现有效的时候,就把bank属于哪个node赋值给局部变量node。

 if (node >= numnodes) {

      numnodes = node + 1;

      if (numnodes > MAX_NUMNODES)

         BUG();

 }

 这里要先说的numnodes这个是用来统计系统node的总数的,其实可以看出,如果bank里面的node成员永远记录的是0(说明属于0内存节点),numnodes的值是不会发生变化的,int numnodes = 1;可以看出这个变量的初始值为1,这样就说明只有一个内存节点,但是只要bank的node成员记录了别的数字的话,这样numnodes的值就会相应的递增了。

 start = O_PFN_UP(mi->bank[i].start);

 end   = O_PFN_DOWN(mi->bank[i].start + mi->bank[i].size);
 这里的start和end着两个变量是记录了内存节点的物理基址页号和物理终址页号,我们来分析一下O_PFN_UP这个宏是这么运作的。

 #define O_PFN_UP(x) (PAGE_ALIGN(x) >> PAGE_SHIFT)

 #define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK)

 #define PAGE_SIZE  (1UL << PAGE_SHIFT)
 #define PAGE_MASK  (~(PAGE_SIZE-1))

 首先我们要知道的是,对物理地址右移12位的话,就可以求出这个物理地址的所在页了。我在这里举个例子,首先要知道Linux里面的一页的大小为4096字节,所以可以看出PAGE_SIZE=对无符号整型常数1尽心左移12位=4096字节=4kb,这样就是一页的大小了。然后我们会发现PAGE_MASK=除了13位以上的全是1外,其他的都是0。好了,现在我假设我的addr的范围是在0~4kb的地址范围的话,PAGE_ALIGN(x)出来的永远是0,这样O_PFN_UP(x)都是0号页。对于O_PFN_DOWN类似的,读者自行分析。

 if (np[node].start > start)

    np[node].start = start;

 if (np[node].end < end)

    np[node].end = end;

 if (memend_pfn < end)

    memend_pfn = end;

 }
 好了,对于这个FOR循环我们做的东西就是把系统属于同一个node的bank组成一个内存节点,同时汇总出系统的node的总数,把每个node的起始物理页号,和结束物理页号记录了下来。好了,我们现在继续往下看。

 /*
  * Calculate the number of pages we require to
  * store the bootmem bitmaps.
  */

 很明显,一下这个小循环是计算出每个node(包括内存孔洞)的页帧位码表占得内存页数。

 for (i = 0; i < numnodes; i++) {

      if (np[i].end == 0)

         continue;

      np[i].bootmap_pages = bootmem_bootmap_pages(np[i].end -np[i].start);

      bootmem_pages += np[i].bootmap_pages;

 }

 我们来看看bootmembootmap_pages函数是做些什么的。代码如下:

 unsigned long __init bootmem_bootmap_pages (unsigned long pages)
{
 unsigned long mapsize;

 mapsize = (pages+7)/8;//pages是一个内存节点一共有多少页,起始页帧位码表是用每一位来表示一页的,可以看出8页可以用一个mapsize来表示,其中一个mapsize占一个存储单元,这样我们就可以求出一共占有多少个存储单元了。
 mapsize = (mapsize + ~PAGE_MASK) & PAGE_MASK;
 mapsize >>= PAGE_SHIFT;//以上两句求出所有存储单元占了多少页。

 return mapsize;
}

 函数返回后,统计处每个内存节点的页帧位码表占的总页数后,把它保存在每个bootmap_pages成员里面。接下来的局部变量bootmem_pages是保存所有内存节点的页帧位码表的总页数。

 high_memory = __va(memend_pfn << PAGE_SHIFT);//memend_pfn其实是记录着所有页,就是给high_memory记录着系统低端内存结束地址对应的内核线性地址。

 /*
  * This doesn't seem to be used by the Linux memory
  * manager any more.  If we can get rid of it, we
  * also get rid of some of the stuff above as well.
  */

 max_low_pfn = memend_pfn - O_PFN_DOWN(PHYS_OFFSET);//低端内存最大可能页数

 max_pfn = memend_pfn - O_PFN_DOWN(PHYS_OFFSET);//系统物理内存最大可能页数

 return bootmem_pages;
}

 好了,这样就返回到了最开始的bootmem_init()的函数了,现在bootmap_pages变量保存着bootmem_pages的信息,就是所有内存节点的页帧位码表所占空间的总页数。我们接下来看看下条语句。

 bootmap_pfn   = find_bootmap_pfn(0, mi, bootmap_pages);

 我们来看看find_bootmap_pfn这个函数的具体代码。

 find_bootmap_pfn(int node, struct meminfo *mi, unsigned int bootmap_pages)
{
   unsigned int start_pfn, bank, bootmap_pfn;

   start_pfn   = V_PFN_UP(&_end);

   我们来看看V_PFN_UP这个宏是这么样的,如下:

   #define V_PFN_UP(x) O_PFN_UP(__pa(x))
   #define O_PFN_UP(x) (PAGE_ALIGN(x) >> PAGE_SHIFT)

   很明显的是,_pa(_end)这样求出的是内核编译后本身所占空间的物理截止地址,再通过O_PFN_UP(x)这个宏就可以求出这个物理地址所在的页号。这样start_pfn局部变量保存着编译内核本身所占空间的物理截止地址的页号。

   bootmap_pfn = 0;

   for (bank = 0; bank < mi->nr_banks; bank ++) {

                       unsigned int start, end;

                       if (mi->bank[bank].node != node)

                                 continue;

                       上面的if,很明显是为了寻找属于内存节点0的bank,为什么要找这个bank,找出它来有什么用,我们接着往下看。

                       start = O_PFN_UP(mi->bank[bank].start);

                       end   = O_PFN_DOWN(mi->bank[bank].size +mi->bank[bank].start);

                       分别求出这个bank的起始页号和结束页号。

                       if (end < start_pfn)continue;//这里就是比较这个bank的结束页号是都小于上面说的内核本身所占空间的物理截止地址页号,其实就是看这个bank是否装的下编译后的内核本身。

                       if (start < start_pfn)start = start_pfn;//如果顺利的话,来到这句其实为了后面的比较做准备的,因为这个bank不只是装编译后内核本身的,还要装所有内核node的页帧位码表的。

                       if (end <= start)continue;

                       if (end - start >= bootmap_pages){

                                bootmap_pfn = start;

                                break;

                       }

 可以看出只有剩下的空间够装系统所有node的页帧位码表的时候,我们才算找到我们需要的bank的大小,同时bootmap_pfn变量记录着页帧位码表的起始页号。

     if (bootmap_pfn == 0)

              BUG();

     return bootmap_pfn;

}
函数返回后赋给变量bootmap_pfn,它记录了页帧位码表的起始页号。接下来我们有回到了bootmem_init()函数里面去了。

 initrd_node   = check_initrd(mi);//由于这个函数过于简单,读者可以自行分析,我在这里只是说说大概是怎么运作的。查询系统中有没有内存bank,看看那个bank可以包含initrd,这里涉及到两个全局变量:phys_initrd_start和phys_initrd_size。如果找不到包含initrd的bank,就把上述两个变量清零。

 map_pg = bootmap_pfn;
 np += numnodes - 1;//np指向系统中最后一个内存node对应的nod_info机构

 for (node = numnodes - 1; node >= 0; node--, np--) {

  /*
   * If there are no pages in this node, ignore it.
   * Note that node 0 must always have some pages.
   */

      if (np->end == 0) {//如果检测到内存节点0里面不包含任何页的话,着系统崩溃,因为内核和页帧位码表规定保存在0号节点内的。

                 if (node == 0)

                     BUG();

                 continue;

      }

      init_bootmem_node(NODE_DATA(node), map_pg, np->start, np->end);//我们先来关注一下NODE_DATA(node)这个宏。在将这个宏之前我先要讲一些前提知识,当系统配置了CONFIG_DISCONTIGMEM时,说明系统有多个node,这时每个node都会对应一个pg_data_t结构类型的discontig_node_data[n]的变量,同时每个变量的bdata成员是一个指针,指向一个结构类型为bootmem_data_t的node_bootmem_data[n]变量。pg_data_t是一个描述内存节点的结构体,bootmem_data_t是描述系统启动过程的结构体。好了,现在来看看这个宏的是什么:#define NODE_DATA(nid)  (&discontig_node_data[nid])。很明显,可以通过node号,我们就可以找到对应的描述这个node的discontig_node_data[n]变量。我们来看看上述的函数。

      unsigned long __init init_bootmem_node (pg_data_t *pgdat, unsigned long freepfn, unsigned long startpfn, unsigned long endpfn)
{
 return(init_bootmem_core(pgdat, freepfn, startpfn, endpfn));//pgdat=描述node的结构体,freepfn=页帧位码表的起始页号,startpfn=内存节点的开始页号,endpfn=node的截止页号
}

在往下看:

 static unsigned long __init init_bootmem_core (pg_data_t *pgdat,
 unsigned long mapstart, unsigned long start, unsigned long end)
{
 bootmem_data_t *bdata = pgdat->bdata;//通过bdata成员可以取出每个node对应的bootmem_data_t成员
 unsigned long mapsize = ((end - start)+7)/8;//求出这个node的页帧位码表的占几个存储单元,即这个表的大小

 pgdat->pgdat_next = pgdat_list;//pgdat_list是一个指向0号node对应的pg_data_t的结构变量,当系统配置了CONFIG_DISCONTIGMEM时候有多个node,这样就是对应着discontig_node_data[0]。如果系统只有一个node得话,则对应contig_node_data。
 pgdat_list = pgdat;//结合上面的那条语句,你会发现其实每次node的pgdat_next成员是被附上上一个节点的pg_data_t结构体的,再结合前面的那个大循环体,在循环结束的时候,我们最终会处理到0号node的,这时的pgdat_list是指向0号node的。这样就得到一个链表pgdat_list。

 mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL);//求出这个node的页帧位码表物理起始页号
 bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT);//由页号求出这个物理起始页地址,再转换成虚拟的
 bdata->node_boot_start = (start << PAGE_SHIFT);//node的物理基址
 bdata->node_low_pfn = end;//node的物理终止页号

 /*
  * Initially all pages are reserved - setup_arch() has to
  * register free RAM areas explicitly.
  */
 memset(bdata->node_bootmem_map, 0xff, mapsize);//把node的页帧位码表的每一位置1,禁止使用这个页。

 return mapsize;
}
好了,现在我们再次回到bootmem_init()这个函数,其实上条语句就干了两件事情,第一就是建立了一张链表,第二就是把每个页帧位码表都置1了,就是禁止使用相应的页。

  free_bootmem_node_bank(node, mi);//我们还是进去这个函数看看吧。

  static inline void free_bootmem_node_bank(int node, struct meminfo *mi)
{
 pg_data_t *pgdat = NODE_DATA(node);//找到对应的discontig_data_t[n]
 int bank;

 for (bank = 0; bank < mi->nr_banks; bank++)
  if (mi->bank[bank].node == node)
   free_bootmem_node(pgdat, mi->bank[bank].start, mi->bank[bank].size);//很明显这个循环就是为了寻找属于这个node的所有bank,找到一个bank,就调用一个free_bootmem_node函数,我们来看看这个函数是干些什么的。
}

  void __init free_bootmem_node (pg_data_t *pgdat, unsigned long physaddr, unsigned long size)
{
 free_bootmem_core(pgdat->bdata, physaddr, size);//pgdat=这个node对应的pg_data_t结构体,physaddr=bank的物理起始地址,size=bank的物理大小。
}

  static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)//启动阶段释放内存的核心操作函数,addr要释放内存空间的物理起始地址,size要释放内存空间的大小,字节数。
{
 unsigned long i;
 unsigned long start;
 /*
  * round down end of usable mem, partially free pages are
  * considered reserved.
  */
 unsigned long sidx;
 unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;//求出这个bank占了多少页
 unsigned long end = (addr + size)/PAGE_SIZE;//求出这个bank的截止页号

 BUG_ON(!size);//不能释放空内存,否则系统崩溃
 BUG_ON(end > bdata->node_low_pfn);//不能释放超出本node德内存,否则系统崩溃

 if (addr < bdata->last_success)
  bdata->last_success = addr;//last_success记录着最后一次在本内存节点成功申请的内存的起始位置,对于这里,本人理解不了。

 /*
  * Round up the beginning of the address.
  */
 start = (addr + PAGE_SIZE-1) / PAGE_SIZE;//一定是一重新的一页开始释放的,如果起始位置已经在前面的页开始了,则不可以从前面的页开始释放。
 sidx = start - (bdata->node_boot_start/PAGE_SIZE);//这个开始页相对于内存节点的物理起始页的偏移量

 for (i = sidx; i < eidx; i++) {
  if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))//如果其中有的位码已经是0的话,系统崩溃。这里就是把相应的页对应的位先测试再清零。
   BUG();
 }
}

 我们再回到bootmem_init()这个函数来,我们来看看下条语句:

 map_pg += np->bootmap_pages;//np->bootmap_pages代表这个node的页帧位码表所占的页数,这样map_pg就指向下一个内存节点的页帧位码表的开始页号。

 if (node == 0)

     reserve_node_zero(bootmap_pfn, bootmap_pages);//保护0号内存节点中内核本身所占空间、内核后的页帧位码表空间、内核前的16kb页表空间、页表前16kb空间,将这些内存对应的页帧位码设置为1.

 }

#ifdef CONFIG_BLK_DEV_INITRD//如果系统支持INITRD
 if (phys_initrd_size && initrd_node >= 0) {//如果系统确定定义了INITRD空间
  reserve_bootmem_node(NODE_DATA(initrd_node), phys_initrd_start,
         phys_initrd_size);//保护INITRD空间,将这个空间每一位码置1
  initrd_start = __phys_to_virt(phys_initrd_start);//将全局变量initrd_start保存initrd所占空间的起始的线性逻辑地址
  initrd_end = initrd_start + phys_initrd_size;//全局变量initrd_end保存initrd所占空间的结束的线性逻辑地址
 }
#endif

 BUG_ON(map_pg != bootmap_pfn + bootmap_pages);
}
最后还是很不容易全部分析完了。

          

    

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值