6.2.2  物理内存的探测

 我们知道,BIOS不仅能引导操作系统,还担负着加电自检和对资源的扫描探测,其中就包括了对物理内存的自检和扫描(你刚开机时所看到的信息就是此阶段BIOS显示的信息)。对于这个阶段中获得的内存信息可以通过BIOS调用“int 0x15”加以检查。由于Linux内核不能作BIOS调用,因此内核本身就得代为检查,并根据获得的信息生成一幅物理内存构成图,这就是上面所介绍的e820图,然后通过上面提到的参数块传给内核。使得内核能知道系统中内存资源的配置。之所以称为e820图,是因为在通过int 0x15查询内存的构成时要把调用参数之一设置成0xe820

页机制启用以后,与内存管理相关的操作就是调用init/main.c中的start_kernel()函数,start_kernel()函数要调用一个叫setup_arch()的函数,setup_arch()位于arch/i386/kernel/setup.c文件中,我们所关注的与物理内存探测相关的内容就在这个函数中。

1setup_arch()函数

   这个函数比较繁琐和冗长,下面我们只对setup_arch()中与内存相关的内容给予描述。

·      首先调用setup_memory_region()函数,这个函数处理内存构成图(map,并把内存的分布信息存放在全局变量e820中,后面会对此函数进行具体描述。

·      调用parse_mem_cmdline(cmdline_p)函数。在特殊的情况下,有的系统可能有特殊的RAM空间结构,此时可以通过引导命令行中的选择项来改变存储空间的逻辑结构,使其正确反映内存的物理结构。此函数的作用就是分析命令行中的选择项,并据此对数据结构e820中的内容作出修正,其代码也在setup.c中。

·      宏定义:

              #define PFN_UP(x)       (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)

              #define PFN_DOWN(x)     ((x) >> PAGE_SHIFT)

              #define PFN_PHYS(x)     ((x) << PAGE_SHIFT)

 

PFN_UP() PFN_DOWN()都是将地址x转换为页面号(PFNPage Frame Number的缩写),二者之间的区别为:PFN_UP()返回大于x的第一个页面号,而PFN_DOWN()返回小于x的第一个页面号。宏PFN_PHYS()返回页面号x的物理地址。

·      宏定义

              /*

              * 128MB for vmalloc and initrd

              */

              #define VMALLOC_RESERVE (unsigned long)(128 << 20)

              #define MAXMEM (unsigned long)(-PAGE_OFFSET-VMALLOC_RESERVE)

              #define MAXMEM_PFN PFN_DOWN(MAXMEM)

              #define MAX_NONPAE_PFN (1 << 20)

     对这几个宏描述如下:

VMALLOC_RESERVE :为vmalloc()函数访问内核空间所保留的内存区,大小为128MB

MAXMEM :内核能够直接映射的最大RAM容量,为1GB128MB896MB-PAGE_OFFSET就等于1GB

    MAXMEM_PFN :返回由内核能直接映射的最大物理页面数。

MAX_NONPAE_PFN :给出在4GB之上第一个页面的页面号。当页面扩充(PAE)功能启用时,才能访问4GB以上的内存。

·      获得内核映像之后的起始页面号

              /*

              * partially used pages are not usable - thus

              * we are rounding upwards:

              */

              start_pfn = PFN_UP(__pa(&_end));

在上一节已说明,宏__pa()返回给定虚拟地址的物理地址。其中标识符_end表示内核映像在内核空间的结束位置。因此,存放在变量start_pfn中的值就是紧接着内核映像之后的页面号。

·      找出可用的最高页面号

 

              /*

              * Find the highest page frame number we have available

              */

              max_pfn = 0;

              for (i = 0; i < e820.nr_map; i++) {

                   unsigned long start, end;

                   /* RAM? */

                   if (e820.map[i].type != E820_RAM)

                       continue;

                   start = PFN_UP(e820.map[i].addr);

                   end = PFN_DOWN(e820.map[i].addr + e820.map[i].size);

                   if (start >= end)

                       continue;

                   if (end > max_pfn)

                       max_pfn = end;

              }

上面这段代码循环查找类型为E820_RAM(可用RAM)的内存区,并把最后一个页面的页面号存放在max_pfn中。

·      确定最高和最低内存范围

              /*

              * Determine low and high memory ranges:

              */

              max_low_pfn = max_pfn;

              if (max_low_pfn > MAXMEM_PFN) {

            max_low_pfn = MAXMEM_PFN;

              #ifndef CONFIG_HIGHMEM

              /* Maximum memory usable is what is directly addressable */

            printk(KERN_WARNING "Warning only %ldMB will be used.\n",

                                                        MAXMEM>>20);

                   if (max_pfn > MAX_NONPAE_PFN)

                       printk(KERN_WARNING "Use a PAE enabled kernel.\n");

                   else

                       printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");

              #else /* !CONFIG_HIGHMEM */

              #ifndef CONFIG_X86_PAE

                   if (max_pfn > MAX_NONPAE_PFN) {

                       max_pfn = MAX_NONPAE_PFN;

                       printk(KERN_WARNING "Warning only 4GB will be used.\n");

                         printk(KERN_WARNING "Use a PAE enabled kernel.\n");

            }

              #endif /* !CONFIG_X86_PAE */

              #endif /* !CONFIG_HIGHMEM */

              }

 

有两种情况:

(1)         如果物理内存RAM大于896MB,而小于4GB,则选用CONFIG_HIGHMEM选项来进行访问。

(2)         如果物理内存RAM大于4GB,则选用CONFIG_X86_PAE(启用PAE模式)来进行访问。

    上面这段代码检查了这两种情况,并显示适当的警告信息。

       

              #ifdef CONFIG_HIGHMEM

              highstart_pfn = highend_pfn = max_pfn;

              if (max_pfn > MAXMEM_PFN) {

                   highstart_pfn = MAXMEM_PFN;

                   printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",

                    pages_to_mb(highend_pfn - highstart_pfn));

              }

              #endif

 

如果使用了CONFIG_HIGHMEM 选项,上面这段代码仅仅打印出大于896MB的可用物理内存数量。

·      初始化引导时的分配器

              * Initialize the boot-time allocator (with low memory only):

              */

              bootmap_size = init_bootmem(start_pfn, max_low_pfn);

 

通过调用init_bootmem()函数,为物理内存页面管理机制的建立做初步准备,为整个物理内存建立起一个页面位图。这个位图建立在从start_pfn开始的地方,也就是说,把内核映像终点_end上方的若干页面用作物理页面位图。在前面的代码中已经搞清楚了物理内存顶点所在的页面号为max_low_pfn,所以物理内存的页面号一定在0max_low_pfn之间。可是,在这个范围内可能有空洞(hole),另一方面,并不是所有的物理内存页面都可以动态分配。建立这个位图的目的就是要搞清楚哪一些物理内存页面可以动态分配的。后面会具体描述bootmem分配器。

 

·      bootmem 分配器,登记全部低区(0896MB)的可用RAM页面

              /*

              * Register fully available low RAM pages with the

              * bootmem allocator.

              */

              for (i = 0; i < e820.nr_map; i++) {

                   unsigned long curr_pfn, last_pfn, size;

              /*

              * Reserve usable low memory

              */

             if (e820.map[i].type != E820_RAM)

                continue;

              /*

              * We are rounding up the start address of usable memory:

              */

                   curr_pfn = PFN_UP(e820.map[i].addr);

                   if (curr_pfn >= max_low_pfn)

                       continue;

              /*

              * ... and at the end of the usable range downwards:

              */

              last_pfn = PFN_DOWN(e820.map[i].addr + e820.map[i].size);

                   if (last_pfn > max_low_pfn)

                       last_pfn = max_low_pfn;

              /*

              * .. finally, did all the rounding and playing

              * around just make the area go away?

              */

                   if (last_pfn <= curr_pfn)

                       continue;

                   size = last_pfn - curr_pfn;

                   free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size));

              }

 

这个循环仔细检查所有可以使用的RAM,并调用free_bootmem()函数把这些可用RAM标记为可用。这个函数调用以后,只有类型为1(可用RAM)的内存被标记为可用的,参看后面对这个函数的具体描述。

·      保留内存

              /*

              * Reserve the bootmem bitmap itself as well. We do this in two

              * steps (first step was init_bootmem()) because this catches

              * the (very unlikely) case of us accidentally initializing the

              * bootmem allocator with an invalid RAM area.

              */

              reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +

                            bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));

 

这个函数把内核和bootmem位图所占的内存标记为“保留”。 HIGH_MEMORY1MB,即内核开始的地方,后面还要对这个函数进行具体描述

·      页机制的初始化

              paging_init();

       这个函数初始化分页内存管理所需要的数据结构,参见后面的详细描述。

2 setup_memory_region() 函数

这个函数用来处理BIOS的内存构成图和把这个构成图拷贝到全局变量e820中。如果操作失败,就创建一个伪内存构成图。这个函数的主要操作为:

·      调用sanitize_e820_map()函数,以删除内存构成图中任何重叠的部分,因为BIOS所报告的内存构成图可能有重叠。

·      调用copy_e820_map()进行实际的拷贝。

·      如果操作失败,创建一个伪内存构成图,这个伪构成图有两部分:0640K1M到最大物理内存。

·      打印最终的内存构成图

3copy_e820_map() 函数

   函数原型为:

    static int __init sanitize_e820_map(struct e820entry * biosmap, char * pnr_map)

  其主要操作为:

·      如果物理内存区间小于2,那肯定出错。因为BIOS至少和RAM属于不同的物理区间。

if (nr_map < 2)

           return -1;

·      BIOS构成图中读出一项

       do {

           unsigned long long start = biosmap->addr;

           unsigned long long size = biosmap->size;

           unsigned long long end = start + size;

           unsigned long type = biosmap->type;

·      进行检查

       /* Overflow in 64 bits? Ignore the memory map. */

           if (start > end)

               return -1;

·      一些BIOS640K1MB之间的区间作为RAM来用,这是不符合常规的。因为从0xA0000开始的空间用于图形卡,因此,在内存构成图中要进行修正。如果一个区的起点在0xA0000以下,而终点在1MB之上,就要将这个区间拆开成两个区间,中间跳过从0xA00001MB边界之间的那一部分。

/*

              * Some BIOSes claim RAM in the 640k - 1M region.

              * Not right. Fix it up.

              */

                         if (type == E820_RAM) {

                             if (start < 0x100000ULL && end > 0xA0000ULL) {

                                if (start < 0xA0000ULL)

                                    add_memory_region(start, 0xA0000ULL-start, type)                                                  

                           if (end <= 0x100000ULL)

                                    continue;

                                start = 0x100000ULL;

                           size = end - start;

                    }

                         }

          

                         add_memory_region(start, size, type);

              } while (biosmap++,--nr_map);

              return 0;

4 add_memory_region() 函数

   这个函数的功能就是在e820中增加一项,其主要操作为:

·      获得已追加在e820中的内存区数

       int x = e820.nr_map;

·      如果数目已达到最大(32),则显示一个警告信息并返回

       if (x == E820MAX) {

           printk(KERN_ERR "Oops! Too many entries in

                                                the memory map!\n");

           return;

       }

·      e820中增加一项,并给nr_map1

       e820.map[x].addr = start;

       e820.map[x].size = size;

       e820.map[x].type = type;

       e820.nr_map++;

 

5 print_memory_map() 函数

      这个函数把内存构成图在控制台上输出,函数本身比较简单,在此给出一个运行实例。例如函数的输出为(BIOS所提供的物理RAM区间):

      

       BIOS-e820: 0000000000000000 - 00000000000a0000 (usable)

       BIOS-e820: 00000000000f0000 - 0000000000100000 (reserved)

       BIOS-e820: 0000000000100000 - 000000000c000000 (usable)

       BIOS-e820: 00000000ffff0000 - 0000000100000000 (reserved)