linux内核工程师 3.02节 linux x86物理内存探测

在内核建立相应的内存管理区之前,先要进行物理内存的探测,获取相关的内存信息。对于X86架构,内核在void main()中调用detect_memory()来进行物理内存的探测。

[cpp]  view plain  copy
  1. void main(void)  
  2. {          
  3.     ...  
  4.     ...  
  5.          /* Detect memory layout */  
  6.     detect_memory();  
  7.     ...  
  8.     ...  
  9. }  


detect_memory()中分别通过调用0xE820H,0xE801H和0x88H从BIOS中获取内存布局和相关的信息。

[cpp]  view plain  copy
  1. <span style="font-size:12px;">int detect_memory(void)  
  2. {  
  3.     int err = -1;  
  4.   
  5.     if (detect_memory_e820() > 0)  
  6. </span><span style="font-size:12px;">       err = 0;  
  7.   
  8.     if (!detect_memory_e801())  
  9.         err = 0;  
  10.   
  11.     if (!detect_memory_88())  
  12.         err = 0;  
  13.   
  14.     return err;  
  15. }  
  16. </span>  


这三个函数都通过int 0x15触发BIOS中断来获取相关信息

 

下面我们着重分析detect_memory_e820().在此之前先了解一个关键的数据结构--struct e820entry.该结构用来保存一个物理内存段的地址信息以及类型。

[cpp]  view plain  copy
  1. <span style="font-size:12px;">struct e820entry {  
  2.     __u64 addr; /* start of memory segment */  
  3.     __u64 size; /* size of memory segment */  
  4.     __u32 type; /* type of memory segment */  
  5. } __attribute__((packed));</span>  

addr:该内存段的起始地址

size:该内存段的大小

type:该内存段的类型,可分为Usable (normal) RAM,Reserved - unusable,ACPI reclaimable memory,ACPI NVS memory,Area containing bad memory,要获取所有的内存段的信息,detect_memory_e820()通过一个do_while循环来不断触发int 0x15中断来获取每个内存段的信息,并且将这些信息保存在一个struct e820entry类型的数组中,即boot_params.e820_map,下面是相关代码

[cpp]  view plain  copy
  1. <span style="font-size:12px;">static int detect_memory_e820(void)  
  2. {  
  3.     int count = 0;  
  4.     struct biosregs ireg, oreg;  
  5.     struct e820entry *desc = boot_params.e820_map;  
  6.     static struct e820entry buf; /* static so it is zeroed */  
  7.   
  8.     initregs(&ireg);  
  9.     ireg.ax  = 0xe820;  
  10.     ireg.cx  = sizeof buf;  
  11.     ireg.edx = SMAP;  
  12.     ireg.di  = (size_t)&buf;  
  13.   
  14.     /* 
  15.      * Note: at least one BIOS is known which assumes that the 
  16.      * buffer pointed to by one e820 call is the same one as 
  17.      * the previous call, and only changes modified fields.  Therefore, 
  18.      * we use a temporary buffer and copy the results entry by entry. 
  19.      * 
  20.      * This routine deliberately does not try to account for 
  21.      * ACPI 3+ extended attributes.  This is because there are 
  22.      * BIOSes in the field which report zero for the valid bit for 
  23.      * all ranges, and we don't currently make any use of the 
  24.      * other attribute bits.  Revisit this if we see the extended 
  25.      * attribute bits deployed in a meaningful way in the future. 
  26.      */  
  27.   
  28.     do {  
  29.         intcall(0x15, &ireg, &oreg); /*触发0x15中断*/  
  30.         ireg.ebx = oreg.ebx; /* for next iteration... */  
  31.   
  32.         /* BIOSes which terminate the chain with CF = 1 as opposed 
  33.            to %ebx = 0 don't always report the SMAP signature on 
  34.            the final, failing, probe. */  
  35.         if (oreg.eflags & X86_EFLAGS_CF)  
  36.             break;  
  37.   
  38.         /* Some BIOSes stop returning SMAP in the middle of 
  39.            the search loop.  We don't know exactly how the BIOS 
  40.            screwed up the map at that point, we might have a 
  41.            partial map, the full map, or complete garbage, so 
  42.            just return failure. */  
  43.         if (oreg.eax != SMAP) {  
  44.             count = 0;  
  45.             break;  
  46.         }  
  47.   
  48.         *desc++ = buf;/*保存获取的内存段信息*/  
  49.         count++;      /*获取的内存段数目加1*/  
  50.     } while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));  
  51.   
  52.     return boot_params.e820_entries = count;  
  53. }</span>  


其他两个函数还没弄太明白,只知道是用来获取扩展内存的大小的~其中0x88H获取的内存上限为64M(以1K为单位),而0xe801可以获取到64M以上的内存(以64K为单位),它们也将获取的信息保存在boot_params中以供后用。

       在start_kernel()-->setup_arch()中,我们可以看到很多与架构相关的初始化工作,接下来我们选择几个与内存管理相关的关键函数进行分析。

     

[cpp]  view plain  copy
  1. <span style="font-size:12px;">void __init setup_arch(char **cmdline_p)  
  2. {  
  3.     ...  
  4.     ...  
  5.     setup_memory_map();  
  6.     ...  
  7.     ...    
  8.     max_pfn = e820_end_of_ram_pfn(); /*找到最大的页框编号*/  
  9.     ...  
  10.     /* max_low_pfn get updated here */  
  11.     find_low_pfn_range();/*设定高、低内存的分界线*/  
  12.     ...  
  13.     ...   
  14. }  
  15.   
  16. </span>  



首先来看setup_memory_map().

 

[cpp]  view plain  copy
  1. void __init setup_memory_map(void)  
  2. {  
  3.     char *who;  
  4.   
  5.     who = x86_init.resources.memory_setup();  
  6.     memcpy(&e820_saved, &e820, sizeof(struct e820map));  
  7.     printk(KERN_INFO "BIOS-provided physical RAM map:\n");  
  8.     e820_print_map(who);  
  9. }  


该函数中,通过结构体x86_init_resources调用memory_setup(),在x86_init.c中,我们可以看到该函数指针的初始化

[cpp]  view plain  copy
  1. <span style="font-size:12px;">struct x86_init_ops x86_init __initdata = {  
  2.   
  3.     .resources = {  
  4.         .probe_roms     = x86_init_noop,  
  5.         .reserve_resources  = reserve_standard_io_resources,  
  6.         .memory_setup       = default_machine_specific_memory_setup,  
  7.     },</span>  

跟踪找到default_machine_specific_memory_setup(),

[cpp]  view plain  copy
  1. <span style="font-size:12px;"></span>   
[cpp]  view plain  copy
  1. <span style="font-size:12px;">char *__init default_machine_specific_memory_setup(void)  
  2. {  
  3.     char *who = "BIOS-e820";  
  4.     u32 new_nr;  
  5.     /* 
  6.      * Try to copy the BIOS-supplied E820-map. 
  7.      * 
  8.      * Otherwise fake a memory map; one section from 0k->640k, 
  9.      * the next section from 1mb->appropriate_mem_k 
  10.      */  
  11.     new_nr = boot_params.e820_entries;  
  12.     sanitize_e820_map(boot_params.e820_map,  /*消除重叠的内存段*/  
  13.             ARRAY_SIZE(boot_params.e820_map),  
  14.             &new_nr);  
  15.     boot_params.e820_entries = new_nr;  
  16.     /*将内存布局的信息从boot_params.e820_map拷贝到struct e820map e820*/  
  17.     if (append_e820_map(boot_params.e820_map, boot_params.e820_entries)  
  18.       < 0) {  
  19.         u64 mem_size;  
  20.   
  21.         /* compare results from other methods and take the greater */  
  22.         if (boot_params.alt_mem_k  
  23.             < boot_params.screen_info.ext_mem_k) {  
  24.             mem_size = boot_params.screen_info.ext_mem_k;  
  25.             who = "BIOS-88";  
  26.         } else {  
  27.             mem_size = boot_params.alt_mem_k;  
  28.             who = "BIOS-e801";  
  29.         }  
  30.   
  31.         e820.nr_map = 0;  
  32.         e820_add_region(0, LOWMEMSIZE(), E820_RAM);  
  33.         e820_add_region(HIGH_MEMORY, mem_size << 10, E820_RAM);  
  34.     }  
  35.   
  36.     /* In case someone cares... */  
  37.     return who;  
  38. }</span>  

可以看到该函数主要完成两个功能:

1.消除内存段的重叠部分

2.将内存布局信息从boot_params.e820_map拷贝到e820中

跟踪append_e820_map()-->__append_e820_map()

[cpp]  view plain  copy
  1. <span style="font-size:12px;">static int __init __append_e820_map(struct e820entry *biosmap, int nr_map)  
  2. {  
  3.     while (nr_map) {  
  4.         u64 start = biosmap->addr;  
  5.         u64 size = biosmap->size;  
  6.         u64 end = start + size;  
  7.         u32 type = biosmap->type;  
  8.   
  9.         /* Overflow in 64 bits? Ignore the memory map. */  
  10.         if (start > end)  
  11.             return -1;  
  12.   
  13.         e820_add_region(start, size, type);  
  14.   
  15.         biosmap++;  
  16.         nr_map--;  
  17.     }  
  18.     return 0;  
  19. }</span>  


其中传给biosmap的参数为boot_params.e820_map,即内存布局的起始地址,传给nr_map的参数为boot_params.e820_entries,即获取的内存段的数目。在该函数中,对每个段调用e820_add_region,来看看这个函数

[cpp]  view plain  copy
  1. <span style="font-size:12px;">void __init e820_add_region(u64 start, u64 size, int type)  
  2. {  
  3.     __e820_add_region(&e820, start, size, type);  
  4. }</span>  

 

[cpp]  view plain  copy
  1. <span style="font-size:12px;">static void __init __e820_add_region(struct e820map *e820x, u64 start, u64 size,  
  2.                      int type)  
  3. {  
  4.     int x = e820x->nr_map;  
  5.   
  6.     if (x >= ARRAY_SIZE(e820x->map)) {  
  7.         printk(KERN_ERR "Ooops! Too many entries in the memory map!\n");  
  8.         return;  
  9.     }  
  10.   
  11.     e820x->map[x].addr = start;  
  12.     e820x->map[x].size = size;  
  13.     e820x->map[x].type = type;  
  14.     e820x->nr_map++;  
  15. }</span>  


最终由__e820_add_region()函数将内存段的信息保存到e820的map数组中。

下面再来看看e820_end_of_ram_pfn(),该函数用来遍历所有内存段的页框,找到低端内存的最大页框编号

[cpp]  view plain  copy
  1. <span style="font-size:12px;">unsigned long __init e820_end_of_ram_pfn(void)  
  2. {  
  3.     return e820_end_pfn(MAX_ARCH_PFN, E820_RAM);  
  4. }</span>  


E820_RAM代表可用内存

MAX_ARCH_PFN的定义如下

[cpp]  view plain  copy
  1. <span style="font-size:12px;">#ifdef CONFIG_X86_32  
  2. # ifdef CONFIG_X86_PAE  
  3. #  define MAX_ARCH_PFN      (1ULL<<(36-PAGE_SHIFT))  
  4. # else  
  5. #  define MAX_ARCH_PFN      (1ULL<<(32-PAGE_SHIFT))  
  6. # endif  
  7. #else /* CONFIG_X86_32 */  
  8. # define MAX_ARCH_PFN MAXMEM>>PAGE_SHIFT  
  9. #endif</span>  

 

这里可以看到如果定义了CONFIG_X86_32,那么在没使用PAE的情况下,MAX_ARCH_PFN的值就为4GB内存对应的页框号,而如果定义了则MAX_ARCH_PFN的值为MAXMEM对应的页框号,我们来看MAXMEM的定义:

[cpp]  view plain  copy
  1. <span style="font-size:12px;">#define MAXMEM    (VMALLOC_END - PAGE_OFFSET - __VMALLOC_RESERVE)  
  2.   
  3. </span>  


这里面的宏都和线性地址的最后1GB有关,其中__VANALLOC_RESERVE为128M,下图说明了第4GB的内存划分

结合这个图我们可以得出MAXMEM为一个略小于896M的值(896M-8K-4M-4M),即略小于低端内存的上限,高端内存的起始地址,这样是为了避免某些情况下产生的OOPS。

走得貌似有点远了,再回到e820_end_of_ram_pfn(),跟踪e820_end_pfn(MAX_ARCH_PFN, E820_RAM):

[cpp]  view plain  copy
  1. <span style="font-size:12px;">static unsigned long __init e820_end_pfn(unsigned long limit_pfn, unsigned type)  
  2. {  
  3.     int i;  
  4.     unsigned long last_pfn = 0;  
  5.     unsigned long max_arch_pfn = MAX_ARCH_PFN;  
  6.   
  7.     for (i = 0; i < e820.nr_map; i++) {  /*循环遍历内存布局数组*/  
  8.         struct e820entry *ei = &e820.map[i];  
  9.         unsigned long start_pfn;  
  10.         unsigned long end_pfn;  
  11.   
  12.         if (ei->type != type)  
  13.             continue;  
  14.   
  15.         start_pfn = ei->addr >> PAGE_SHIFT;  
  16.         end_pfn = (ei->addr + ei->size) >> PAGE_SHIFT;  
  17.   
  18.         if (start_pfn >= limit_pfn)/*起始地址大于MAX_ARCH_PFN,无视之*/  
  19.             continue;  
  20.         if (end_pfn > limit_pfn) { /*结束地址大于MAX_ARCH_PFN则直接最大页框编号设为MAX_ARCH_PFN*/  
  21.                                       
  22.             last_pfn = limit_pfn;  
  23.             break;  
  24.         }  
  25.         if (end_pfn > last_pfn)    /*该内存段的末地址大于之前找到的最大页框编号, 
  26.                                 则重置最大页框编号*/  
  27.             last_pfn = end_pfn;  
  28.     }  
  29.   
  30.     if (last_pfn > max_arch_pfn)  
  31.         last_pfn = max_arch_pfn;  
  32.   
  33.     printk(KERN_INFO "last_pfn = %#lx max_arch_pfn = %#lx\n",  
  34.              last_pfn, max_arch_pfn);  
  35.     return last_pfn;  
  36. }</span>  

 

最后分析一个函数,setup_arch()-->find_low_pfn_range().该函数用来划分低端内存和高端内存的界限,也可以说是确定高端内存的起始地址。再看这个函数之前,还得先看几个宏定义

[cpp]  view plain  copy
  1. <span style="font-size:12px;">#define MAXMEM_PFN    PFN_DOWN(MAXMEM)  
  2. #define PFN_UP(x)   (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)  
  3. #define PFN_DOWN(x) ((x) >> PAGE_SHIFT)</span>  

可以看出

PFN_DOWN(x)计算出地址值x对应的页框的编号

PFN_UP(x)计算出地址值对应的页框的下一个页框的编号

MAXMEM_PFN为MAXMEM对应的页框编号

再看具体的代码:

[cpp]  view plain  copy
  1. <span style="font-size:12px;">void __init find_low_pfn_range(void)  
  2. {  
  3.     /* it could update max_pfn */  
  4.   
  5.     if (max_pfn <= MAXMEM_PFN)    /*实际物理内存小于等于低端内存896M*/  
  6.         lowmem_pfn_init();  
  7.     else                  /*实际的物理内存大于896M*/  
  8.         highmem_pfn_init();  
  9. }</span>  


当实际的物理内存小于等于低端内存时,由lowmem_pfn_init()进行分配

[cpp]  view plain  copy
  1.   
[cpp]  view plain  copy
  1. <span style="font-size:12px;">void __init lowmem_pfn_init(void)  
  2. {  
  3.     /* max_low_pfn is 0, we already have early_res support */  
  4.     /*将分界线初始化为实际物理内存的最大页框号,由于系统的内存小于896M, 
  5.     所以全部内存为低端内存,如需要高端内存,则从中分一部分出来进行分配*/  
  6.     max_low_pfn = max_pfn;  
  7.   
  8.     if (highmem_pages == -1)  
  9.         highmem_pages = 0;  
  10. #ifdef CONFIG_HIGHMEM  /*如果用户定义了HIGHMEM,即需要分配高端内存*/  
  11.     if (highmem_pages >= max_pfn) {       /*如果高端内存的页起始地址>=最大页框号,则无法分配*/  
  12.         printk(KERN_ERR MSG_HIGHMEM_TOO_BIG,  
  13.             pages_to_mb(highmem_pages), pages_to_mb(max_pfn));  
  14.         highmem_pages = 0;  
  15.     }  
  16.     if (highmem_pages) {  
  17.         /*这个条件保证低端内存不能小于64M*/  
  18.         if (max_low_pfn - highmem_pages < 64*1024*1024/PAGE_SIZE) {  
  19.             printk(KERN_ERR MSG_LOWMEM_TOO_SMALL,  
  20.                 pages_to_mb(highmem_pages));  
  21.             highmem_pages = 0;  
  22.         }  
  23.         max_low_pfn -= highmem_pages; /*设定好低、高端内存的分界线*/  
  24.     }  
  25. #else  
  26.     if (highmem_pages)  
  27.         printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");  
  28. #endif  
  29. }  
  30. </span>  

 
 

 

当实际的物理内存大于896M,由highmem_pfn_init()进行分配

[cpp]  view plain  copy
  1. <span style="font-size:12px;">void __init highmem_pfn_init(void)  
  2. {  
  3.     max_low_pfn = MAXMEM_PFN; /*设定高端内存和低端内存的分界线*/  
  4.       
  5.                                   
  6.     if (highmem_pages == -1)  /*未设定高端内存的页框数*/  
  7.         highmem_pages = max_pfn - MAXMEM_PFN;  /*默认为最大页框数减去MAXMEM_PFN*/  
  8.   
  9.     if (highmem_pages + MAXMEM_PFN < max_pfn)      /*高端内存页框数加上MAXMEM_PFN小于最大页框数*/  
  10.         max_pfn = MAXMEM_PFN + highmem_pages;  /*将最大页框数下调到前两者的和*/  
  11.   
  12.     if (highmem_pages + MAXMEM_PFN > max_pfn){     /*申请的高端内存超过范围则不分配*/  
  13.         printk(KERN_WARNING MSG_HIGHMEM_TOO_SMALL,  
  14.             pages_to_mb(max_pfn - MAXMEM_PFN),  
  15.             pages_to_mb(highmem_pages));  
  16.         highmem_pages = 0;  
  17.     }  
  18. #ifndef CONFIG_HIGHMEM  
  19.     /* Maximum memory usable is what is directly addressable */  
  20.     printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20);  
  21.     if (max_pfn > MAX_NONPAE_PFN)  
  22.         printk(KERN_WARNING "Use a HIGHMEM64G enabled kernel.\n");  
  23.     else  
  24.         printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");  
  25.     max_pfn = MAXMEM_PFN;  
  26. #else /* !CONFIG_HIGHMEM */  
  27. #ifndef CONFIG_HIGHMEM64G  
  28.     if (max_pfn > MAX_NONPAE_PFN) {  
  29.         max_pfn = MAX_NONPAE_PFN;  
  30.         printk(KERN_WARNING MSG_HIGHMEM_TRIMMED);  
  31.     }  
  32. #endif /* !CONFIG_HIGHMEM64G */  
  33. #endif /* !CONFIG_HIGHMEM */  
  34. }  
  35. </span>  




至此,已将内存探测和高低端内存的划分分析完了,接下来再分析管理区的初始化。

猜你喜欢

转载自blog.csdn.net/zjy900507/article/details/80682436