6.2.4 页面管理机制的初步建立

    为了对页面管理机制作出初步准备,Linux使用了一种叫bootmem分配器(bootmem allocator)的机制,这种机制仅仅用在系统引导时,它为整个物理内存建立起一个页面位图。这个位图建立在从start_pfn开始的地方,也就是说,内核映象终点_end上方的地方。这个位图用来管理低区(例如小于896MB),因为在0896MB的范围内,有些页面可能保留,有些页面可能有空洞,因此,建立这个位图的目的就是要搞清楚哪一些物理页面是可以动态分配的。用来存放位图的数据结构为bootmem_data(在mm/numa.c中)

       typedef struct bootmem_data {

           unsigned long node_boot_start;

           unsigned long node_low_pfn;

           void *node_bootmem_map;

           unsigned long last_offset;

           unsigned long last_pos;

       } bootmem_data_t;

 

·      node_boot_start表示存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)。

·      node_low_pfn表示物理内存的顶点,最高不超过896MB

·      node_bootmem_map指向bootmem位图

·      last_offset 用来存放在前一次分配中所分配的最后一个字节相对于last_pos的位移量。

·      last_pos 用来存放前一次分配的最后一个页面的页面号。这个域用在__alloc_bootmem_core()函数中,通过合并相邻的内存来减少内部碎片。

下面介绍与bootmem相关的几个函数,这些函数位于mm/bootmeme.c中。

1. init_bootmem()函数

unsigned long __init init_bootmem (unsigned long start, unsigned long pages)

{

         max_low_pfn = pages;

         min_low_pfn = start;

         return(init_bootmem_core(&contig_page_data, start, 0, pages));

}

   这个函数仅在初始化时用来建立bootmem分配器。这个函数实际上是init_bootmem_core()函数的封装函数。init_bootmem()函数的参数start表示内核映象结束处的页面号,而pages表示物理内存顶点所在的页面号。而函数init_bootmem_core()就是对contig_page_data变量进行初始化。下面我们来看一下对该变量的定义:

 

int numnodes = 1;       /* Initialized for UMA platforms */

 

static bootmem_data_t contig_bootmem_data;

pg_data_t contig_page_data = { bdata: &contig_bootmem_data };

 

     变量contig_page_data的类型就是前面介绍过的pg_data_t数据结构。每个pg_data_t数据结构代表着一片均匀的、连续的内存空间。在连续空间UMA结构中,只有一个节点contig_page_data,而在NUMA结构或不连续空间UMA结构中,有多个这样的数据结构。系统中各个节点的pg_data_t数据结构通过node_next连接在一起成为一个链。有一个全局量pgdat_list则指向这个链。从上面的定义可以看出,contig_page_data是链中的第一个节点。这里假定整个物理空间为均匀的、连续的,以后若发现这个假定不能成立,则将新的pg_data_t结构加入到链中。

pg_data_t结构中有个指针bdatacontig_page_data被初始化为指向bootmem_data_t数据结构。下面我们来看init_bootmem_core()函数的具体代码:

/*

  * Called once to set up the allocator itself.

    */

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;

         unsigned long mapsize = ((end - start)+7)/8;

 

         pgdat->node_next = pgdat_list;

         pgdat_list = pgdat;

 

         mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL);

         bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT);

         bdata->node_boot_start = (start << PAGE_SHIFT);

         bdata->node_low_pfn = end;

 

        /*

          * Initially all pages are reserved - setup_arch() has to

          * register free RAM areas explicitly.

         */

         memset(bdata->node_bootmem_map, 0xff, mapsize);

 

         return mapsize;

}

    下面对这一函数给予说明:

·      变量mapsize存放位图的大小。(end - start)给出现有的页面数,再加个7是为了向上取整,除以8就获得了所需的字节数(因为每个字节映射8个页面)。

·      变量pgdat_list用来指向节点所形成的循环链表首部,因为只有一个节点,因此使pgdat_list指向自己。

·      接下来的一句使memsize成为下一个4的倍数(4CPU的字长)。例如,假设有40个物理页面,因此,我们可以得出memsize5个字节。所以,上面的操作就变为(5+(41))&~(4-1)(00001000&11111100),最低的两位变为0,其结果为8。这就有效地使memsize变为4的倍数。

·      phys_to_virt(mapstart << PAGE_SHIFT)把给定的物理地址转换为虚地址。

·      用节点的起始物理地址初始化node_boot_start(这里为0x00000000

·      用物理内存节点的页面号初始化node_low_pfn

·      初始化所有被保留的页面,即通过把页面中的所有位都置为1来标记保留的页面

·      返回位图的大小。

2. free_bootmem()函数

    这个函数把给定范围的页面标记为空闲(即可用),也就是,把位图中某些位清0,表示相应的物理内存可以投入分配。

    原函数为:

       void __init free_bootmem (unsigned long addr, unsigned long size)

    {

 

        return(free_bootmem_core(contig_page_data.bdata, addr, size));

     }

从上面可以看出,free_bootmem()是个封装函数,实际的工作是由free_bootmem_core()函数完成的:

static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long 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;

        unsigned long end = (addr + size)/PAGE_SIZE;

 

        if (!size) BUG();

          if (end > bdata->node_low_pfn)

                BUG();

 

           /*

          * 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 (!test_and_clear_bit(i, bdata->node_bootmem_map))

                         BUG();

         }

}

对此函数的解释如下:

·      变量edix被初始化为页面总数。

·      变量end被初始化为最后一个页面的页面号。

·      进行两个可能的条件检查.

·      start初始化为第一个页面的页面号(向上取整),而sidx(start index)初始化为相对于node_boot_start.的页面号。

·      清位图中从sidxeidx的所有位,即把这些页面标记为可用。

3. reserve_bootmem()函数

  这个函数用来保留页面。为了保留一个页面,只需要在bootmem位图中把相应的位置为1即可。

  原函数为:

  void __init reserve_bootmem (unsigned long addr, unsigned long size)

   {

         reserve_bootmem_core(contig_page_data.bdata, addr, size);

   }

  reserve_bootmem()为封装函数,实际调用的是reserve_bootmem_core()函数:

  static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)

{

         unsigned long i;

         /*

          * round up, partially reserved pages are considered

          * fully reserved.

         */

         unsigned long sidx = (addr - bdata->node_boot_start)/PAGE_SIZE;

         unsigned long eidx = (addr + size - bdata->node_boot_start +

                                                         PAGE_SIZE-1)/PAGE_SIZE;

         unsigned long end = (addr + size + PAGE_SIZE-1)/PAGE_SIZE;

 

         if (!size) BUG();

 

         if (sidx < 0)

                 BUG();

         if (eidx < 0)

                 BUG();

        if (sidx >= eidx)

                 BUG();

         if ((addr >> PAGE_SHIFT) >= bdata->node_low_pfn)

                 BUG();

         if (end > bdata->node_low_pfn)

                 BUG();

         for (i = sidx; i < eidx; i++)

                 if (test_and_set_bit(i, bdata->node_bootmem_map))

                         printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE);

}

对此函数的解释如下:

·      sidx (start index)初始化为相对于node_boot_start的页面号

·      变量eidx初始化为页面总数(向上取整)。

·      变量end初始化为最后一个页面的页面号(向上取整)。

·      进行各种可能的条件检查.

·      把位图中从sidxeidx的所有位置1

4__alloc_bootmem()函数

   这个函数以循环轮转的方式从不同节点分配页面。因为在i386上只有一个节点,因此只循环一次。

   函数原型为:

       void * __alloc_bootmem (unsigned long size,

                               unsigned long align,

                               unsigned long goal);

       void * __alloc_bootmem_core (bootmem_data_t *bdata,

                                    unsigned long size,

                                    unsigned long align,

                                    unsigned long goal);

 

    其中__alloc_bootmem()为封装函数,实际调用的函数为__alloc_bootmem_core (),因为__alloc_bootmem_core ()函数比较长,下面分片断来进行仔细分析:

 

              unsigned long i, start = 0;

              void *ret;

              unsigned long offset, remaining_size;

              unsigned long areasize, preferred, incr;

              unsigned long eidx = bdata->node_low_pfn -

                           (bdata->node_boot_start >> PAGE_SHIFT);

 

   eidx初始化为本节点中现有页面的总数。

 

              if (!size) BUG();

              if (align & (align-1))

                         BUG();

 

进行条件检查

 

              /*

              * We try to allocate bootmem pages above 'goal'

              * first, then we try to allocate lower pages.

              */

              if (goal && (goal >= bdata->node_boot_start) &&

                   ((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {

                         preferred = goal - bdata->node_boot_start;

              } else

           preferred = 0;

              preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;

                                                  

开始分配后首选页的计算分为两步:

    1)如果goal为非0且有效,则给preferred赋初值,否则,其初值为0

2)根据参数align 来对齐preferred的物理地址。

 

              areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;

 

获得所需页面的总数(向上取整)

 

                  incr = align >> PAGE_SHIFT ? : 1;

 

根据对齐的大小来选择增加值。除非大于4K(很少见),否则增加值为1

 

              restart_scan:

                   for (i = preferred; i < eidx; i += incr) {

                 unsigned long j;

                   if (test_bit(i, bdata->node_bootmem_map))

                             continue;

 

这个循环用来从首选页面号开始,找到空闲的页面号。test_bit()宏用来测试给定的位,如果给定位为1,则返回1

 

           for (j = i + 1; j < i + areasize; ++j) {

               if (j >= eidx)

                   goto fail_block;

               if (test_bit (j, bdata->node_bootmem_map))

                  goto fail_block;

           }

 

这个循环用来查看在首次满足内存需求以后,是否还有足够的空闲页面。如果没有空闲页,就跳到fail_block

          

   start = i;

       goto found;

 

如果一直到了这里,则说明从i开始找到了足够的页面,跳过fail_block并继续。

 

                  fail_block:;

                 }

             if (preferred) {

                preferred = 0;

                goto restart_scan; 

                   }

            return NULL;

 

如果到了这里,从首选页面中没有找到满足需要的连续页面,就忽略preferred的值,并从0开始扫描。如果preferred1,但没有找到满足需要的足够页面,则返回NULL

 

            found:

已经找到足够的内存,继续处理请求。

 

                 if (start >= eidx)

                     BUG();

 

进行条件检查。

 

       /*

       * Is the next page of the previous allocation-end the start

       * of this allocation's buffer? If yes then we can 'merge'

       * the previous partial page with this allocation.

       */

       if (align <= PAGE_SIZE && bdata->last_offset

                              && bdata->last_pos+1 == start) {

           offset = (bdata->last_offset+align-1) & ~(align-1);

           if (offset > PAGE_SIZE)

               BUG();

           remaining_size = PAGE_SIZE-offset;

 

    if语句检查下列条件:

    1)所请求对齐的值小于页的大小(4k)。

2)变量last_offset为非0。如果为0,则说明前一次分配达到了一个非常好的页面边界,没有内部碎片。

3)检查这次请求的内存是否与前一次请求的内存是相临的,如果是,则把两次分配合在一起进行。

如果以上三个条件都满足,则用前一次分配中最后一页剩余的空间初始化remaining_size

 

           if (size < remaining_size) {

               areasize = 0;

               // last_pos unchanged

               bdata->last_offset = offset+size;

               ret = phys_to_virt(bdata->last_pos*PAGE_SIZE

                             + offset + bdata->node_boot_start);

 

如果请求内存的大小小于前一次分配中最后一页中的可用空间,则没必要分配任何新的页。变量last_offset增加到新的偏移量,而last_pos保持不变,因为没有增加新的页。把这次新分配的起始地址存放在变量ret中。宏phys_to_virt()返回给定物理地址的虚地址。

 

           } else {

               remaining_size = size - remaining_size;

               areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE;

               ret = phys_to_virt(bdata->last_pos*PAGE_SIZE

                              + offset + bdata->node_boot_start);

               bdata->last_pos = start+areasize-1;

               bdata->last_offset = remaining_size;

 

所请求的大小大于剩余的大小。首先求出所需的页面数,然后更新变量last_pos last_offset

例如,在前一次分配中,如果分配了9k,则占用3个页面,内部碎片为12k-9k=3k。因此,page_offset1k,且剩余大小为3k。如果新的请求为1k,则第3个页面本身就能满足要求,但是,如果请求的大小为10k,则需要新分配((10 k- 3k) + PAGE_SIZE-1)/PAGE_SIZE,即2个页面,因此,page_offset3k

 

           }

           bdata->last_offset &= ~PAGE_MASK;

       } else {

           bdata->last_pos = start + areasize - 1;

           bdata->last_offset = size & ~PAGE_MASK;

           ret = phys_to_virt(start * PAGE_SIZE +

                                         bdata->node_boot_start);

          }

 

如果因为某些条件未满足而导致不能进行合并,则执行这段代码,我们刚刚把last_pos last_offset直接设置为新的值,而未考虑它们原先的值。last_pos值还要加上所请求的页面数,而新page_offset值的计算就是屏蔽掉除了获得页偏移量位的所有位,即“size &  PAGE_MASK”, PAGE_MASK 0x00000FFF,用PAGE_MASK的求反正好得到页的偏移量。

 

             /*

              * Reserve the area now:

              */

      

              for (i = start; i < start+areasize; i++)

                         if (test_and_set_bit(i, bdata->node_bootmem_map))

                             BUG();

              memset(ret, 0, size);

              return ret;

 

现在,我们有了内存,就需要保留它。宏test_and_set_bit()用来测试并置位,如果某位原先的值为0,则它返回0,如果为1,则返回1。还有一个条件判断语句,进行条件判断(这种条件出现的可能性非常小,除非RAM坏)。然后,把这块内存初始化为0,并返回给调用它的函数。

 

5. free_all_bootmem()函数

  这个函数用来在引导时释放页面,并清除bootmem分配器。

   函数原型为:

       void free_all_bootmem (void);

       void free_all_bootmem_core(pg_data_t *pgdat);

 

同前面的函数调用形式类似,free_all_bootmem()为封装函数,实际调用free_all_bootmem_core()函数。下面,我们对free_all_bootmem_core()函数分片断来介绍:

 

              struct page *page = pgdat->node_mem_map;

              bootmem_data_t *bdata = pgdat->bdata;

              unsigned long i, count, total = 0;

              unsigned long idx;

      

              if (!bdata->node_bootmem_map) BUG();

              count = 0;

              idx = bdata->node_low_pfn - (bdata->node_boot_start

                                                 >> PAGE_SHIFT);

   idx初始化为从内核映象结束处到内存顶点处的页面数。

 

       for (i = 0; i < idx; i++, page++) {

           if (!test_bit(i, bdata->node_bootmem_map)) {

               count++;

               ClearPageReserved(page);

               set_page_count(page, 1);

               __free_page(page);

           }

       }

 

搜索bootmem位图,找到空闲页,并把mem_map中对应的项标记为空闲。set_page_count()函数把page结构的count域置1,而__free_page()真正的释放页面,并修改伙伴(buddy)系统的位图。

 

              total += count;

      

              /*

              * Now free the allocator bitmap itself, it's not

              * needed anymore:

              */

              page = virt_to_page(bdata->node_bootmem_map);

              count = 0;

              for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start

                           >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE;

                                                         i++,page++) {

                         count++;

                         ClearPageReserved(page);

                         set_page_count(page, 1);

                   __free_page(page);

              }

 

   获得bootmem位图的地址, 并释放它所在的页面。   

 

              total += count;

              bdata->node_bootmem_map = NULL;

              return total;

 

       把该存储节点的bootmem_map域置为NULL,并返回空闲页面的总数。