Chinaunix首页 | 论坛 | 博客
  • 博客访问: 187882
  • 博文数量: 40
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 418
  • 用 户 组: 普通用户
  • 注册时间: 2013-06-26 22:37
文章存档

2015年(4)

2014年(27)

2013年(9)

我的朋友

分类: LINUX

2015-01-19 22:09:04

浅析linux内核内存管理之物理内存探测

作者:李万鹏
转载自:http://tomhibolu.iteye.com/blog/1214876


在系统boot的时候,kernel通过0x15中断获得机器内存容量。有三种参数88H(只能探测最大64MB的内存),E801H(得到大 小),E802H(获得memory map)。这个memory map称为E820图,在kernel的初始化代码中会将这个memory map复制到一个kernel中的数据结构e820map里,kernel需要通过这个结构来计算可用的内存容量。
Cpp代码
  1. struct e820map {  
  2.     int nr_map;  
  3.     struct e820entry {  
  4.     unsigned long long addr;    /* start of memory segment */  
  5.     unsigned long long size;    /* size of memory segment */  
  6.     unsigned long type;     /* type of memory segment */  
  7.     } map[E820MAX];  
  8. };  
  • 这里的nr_map是内存段的数量
  • 每个内存段由struct e820entry表示
  • addr字段表示内存段的起始地址
  • size字段表示内存段的大小
  • type表示内存段的类型,比如E820_RAM表示可用内存
  • E820MAX是一个宏,为32,说明最多可以有32个内存段
在setup_arch函数中有这么两句,调用mach_specific_memory_setup将E820图复制到kernel中的数据结构中,包括了系统保留的段和空闲段,通过print_memory_map函数打印出来。
Cpp代码
  1. printk(KERN_INFO "BIOS-provided physical RAM map:\n");  
  2. print_memory_map(machine_specific_memory_setup());  
下面来看machine_specific_memory_setup函数的实现:
Cpp代码
  1. static char * __init machine_specific_memory_setup(void)  
  2. {  
  3.     char *who;  
  4.   
  5.   
  6.     who = "BIOS-e820";  
  7.   
  8.     /* 
  9.      * Try to copy the BIOS-supplied E820-map. 
  10.      * 
  11.      * Otherwise fake a memory map; one section from 0k->640k, 
  12.      * the next section from 1mb->appropriate_mem_k 
  13.      */  
  14.     sanitize_e820_map(E820_MAP, &E820_MAP_NR);  
  15.     if (copy_e820_map(E820_MAP, E820_MAP_NR) < 0) {  
  16.         unsigned long mem_size;  
  17.   
  18.         /* compare results from other methods and take the greater */  
  19.         if (ALT_MEM_K < EXT_MEM_K) {  
  20.             mem_size = EXT_MEM_K;  
  21.             who = "BIOS-88";  
  22.         } else {  
  23.             mem_size = ALT_MEM_K;  
  24.             who = "BIOS-e801";  
  25.         }  
  26.   
  27.         e820.nr_map = 0;  
  28.         add_memory_region(0, LOWMEMSIZE(), E820_RAM);  
  29.         add_memory_region(HIGH_MEMORY, mem_size << 10, E820_RAM);  
  30.     }  
  31.     return who;  
  32. }  
  • 首先调用sanitize_e820_map函数将重叠的去除
  • 调用copy_e820_map函数将E820图copy到struct e820map结构中
  • 如果BIOS没有提供该信息(在较古老的机器上可能是这样),内存自身生成一个表,0~0x9f000 ,1MB~E801或88找到的最大值
copy_e820_map函数实现:
Cpp代码
  1. static int __init copy_e820_map(struct e820entry * biosmap, int nr_map)  
  2. {  
  3.     /* Only one memory region (or negative)? Ignore it */  
  4.     if (nr_map < 2)  
  5.         return -1;  
  6.   
  7.     do {  
  8.         unsigned long long start = biosmap->addr;  
  9.         unsigned long long size = biosmap->size;  
  10.         unsigned long long end = start + size;  
  11.         unsigned long type = biosmap->type;  
  12.   
  13.         /* Overflow in 64 bits? Ignore the memory map. */  
  14.         if (start > end)  
  15.             return -1;  
  16.   
  17.         /* 
  18.          * Some BIOSes claim RAM in the 640k - 1M region. 
  19.          * Not right. Fix it up. 
  20.          */  
  21.         if (type == E820_RAM) {  
  22.             if (start < 0x100000ULL && end > 0xA0000ULL) {  
  23.                 if (start < 0xA0000ULL)  
  24.                     add_memory_region(start, 0xA0000ULL-start, type);  
  25.                 if (end <= 0x100000ULL)  
  26.                     continue;  
  27.                 start = 0x100000ULL;  
  28.                 size = end - start;  
  29.             }  
  30.         }  
  31.         add_memory_region(start, size, type);  
  32.     } while (biosmap++,--nr_map);  
  33.     return 0;  
  34. }  
  • 至少BIOS与RAM不是一个内存段的,所以nr_map < 2肯定是不对的
  • 调用add_memory_region函数将E820图填充到struct e820map结构中
  • 如果类型为E820_RAM,即可用内存,判断这个范围是否覆盖 640KB~1MB,这段需要为ISA图形卡等保留的,所以这段要保留,如果谁覆盖了这段需要把这段抠除。物理地址从0x000a0000到 0x000fffff的范围通常留给BIOS例程,并且映射ISA图形卡上的内部内存。这个区域就是所有的IBM兼容PC上从640KB~1MB之间著名 的空洞:物理地址存在但被保留,对应的页框不能由操作系统使用。
调用add_memory_region添加相应的内存段到e820map:
Cpp代码
  1. static void __init add_memory_region(unsigned long long start,  
  2.                                   unsigned long long size, int type)  
  3. {  
  4.     int x;  
  5.   
  6.     if (!efi_enabled) {  
  7.             x = e820.nr_map;  
  8.   
  9.         if (x == E820MAX) {  
  10.             printk(KERN_ERR "Ooops! Too many entries in the memory map!\n");  
  11.             return;  
  12.         }  
  13.   
  14.         e820.map[x].addr = start;  
  15.         e820.map[x].size = size;  
  16.         e820.map[x].type = type;  
  17.         e820.nr_map++;  
  18.     }  
  19. /* add_memory_region */  
如果内存段数量达到了最大值E820MAX即32,则oops。
Cpp代码
  1. static void __init print_memory_map(char *who)  
  2. {  
  3.     int i;  
  4.   
  5.     for (i = 0; i < e820.nr_map; i++) {  
  6.         printk(" %s: %016Lx - %016Lx ", who,  
  7.             e820.map[i].addr,  
  8.             e820.map[i].addr + e820.map[i].size);  
  9.         switch (e820.map[i].type) {  
  10.         case E820_RAM:  printk("(usable)\n");  
  11.                 break;  
  12.         case E820_RESERVED:  
  13.                 printk("(reserved)\n");  
  14.                 break;  
  15.         case E820_ACPI:  
  16.                 printk("(ACPI data)\n");  
  17.                 break;  
  18.         case E820_NVS:  
  19.                 printk("(ACPI NVS)\n");  
  20.                 break;  
  21.         default:    printk("type %lu\n", e820.map[i].type);       
  22.                 break;  
  23.         }  
  24.     }  
  25. }  
调用print_memory_map打印出各个内存段的范围和类型,我的内存是2G的,打印结果如下:
Cpp代码
  1. [    0.000000]  BIOS-provided physical RAM map:  
  2. [    0.000000]  BIOS-e820: 0000000000000000 - 000000000009f000 (usable)  
  3. [    0.000000]  BIOS-e820: 000000000009f000 - 00000000000a0000 (reserved)  
  4. [    0.000000]  BIOS-e820: 00000000000f0000 - 0000000000100000 (reserved)  
  5. [    0.000000]  BIOS-e820: 0000000000100000 - 0000000001e00000 (usable)  
  6. [    0.000000]  BIOS-e820: 0000000001e00000 - 0000000001e80040 (reserved)  
  7. [    0.000000]  BIOS-e820: 0000000001e80040 - 000000007bed0000 (usable)  
  8. [    0.000000]  BIOS-e820: 000000007bed0000 - 000000007bed3000 (ACPI NVS)  
  9. [    0.000000]  BIOS-e820: 000000007bed3000 - 000000007bee0000 (ACPI data)  
  10. [    0.000000]  BIOS-e820: 000000007bee0000 - 000000007bf00000 (reserved)  
  11. [    0.000000]  BIOS-e820: 000000007c000000 - 0000000080000000 (reserved)  
  12. [    0.000000]  BIOS-e820: 00000000f0000000 - 00000000f4000000 (reserved)  
  13. [    0.000000]  BIOS-e820: 00000000fec00000 - 0000000100000000 (reserved)  
至此,kernel已经成功的通过0x 15参数E820H,得到BIOS的E820图,并将其填充内核中的e820map结构,供内核其他部分使用。


在setup_memory函数中会调用find_max_pfn,从e820map结构中获得可用内存的容量。下面来看几个宏:
Cpp代码
  1. #define PFN_UP(x)   (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)  
  2. #define PFN_DOWN(x) ((x) >> PAGE_SHIFT)  
  3. #define PFN_PHYS(x) ((x) << PAGE_SHIFT)  
  4.   
  5. /* 
  6.  * Reserved space for vmalloc and iomap - defined in asm/page.h 
  7.  */  
  8. #define MAXMEM (-__PAGE_OFFSET-__VMALLOC_RESERVE)  
  9. #define MAXMEM_PFN  PFN_DOWN(MAXMEM)  
  10. #define MAX_NONPAE_PFN  (1 << 20)  
  • PFN_UP,PFN_DOWN都是返回地址x对应的页帧号只是PFN_UP返回的是x地址下一个页的页帧号,PFN_DOWN返回的是x所在页的页帧号
  • PFN_PHYS获得页帧号对应的物理地址
  • MAXMEM是低端内存的最大值
  • MAXMEM_PFN是低端内存最大一个页的页帧号
  • MAX_NONPAE_PFN是给出4GB之上第一个页面的页面号

setup_memory是与体系结构密切相关的函数,跟踪其实现:
Cpp代码
  1. static unsigned long __init setup_memory(void)  
  2. {  
  3.     unsigned long bootmap_size, start_pfn, max_low_pfn;  
  4.   
  5.     /* 
  6.      * partially used pages are not usable - thus 
  7.      * we are rounding upwards: 
  8.      */  
  9.     start_pfn = PFN_UP(init_pg_tables_end);  
  10.   
  11.     find_max_pfn();  
  12.   
  13.     max_low_pfn = find_max_low_pfn();  
  14.   
  15. #ifdef CONFIG_HIGHMEM  
  16.     highstart_pfn = highend_pfn = max_pfn;  
  17.     if (max_pfn > max_low_pfn) {  
  18.         highstart_pfn = max_low_pfn;  
  19.     }  
  20.     printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",  
  21.         pages_to_mb(highend_pfn - highstart_pfn));  
  22. #endif  
  23.     printk(KERN_NOTICE "%ldMB LOWMEM available.\n",  
  24.             pages_to_mb(max_low_pfn));  
  25.     /* 
  26.      * Initialize the boot-time allocator (with low memory only): 
  27.      */  
  28.     bootmap_size = init_bootmem(start_pfn, max_low_pfn);  
  29.   
  30.     register_bootmem_low_pages(max_low_pfn);  
  31.   
  32.     /* 
  33.      * Reserve the bootmem bitmap itself as well. We do this in two 
  34.      * steps (first step was init_bootmem()) because this catches 
  35.      * the (very unlikely) case of us accidentally initializing the 
  36.      * bootmem allocator with an invalid RAM area. 
  37.      */  
  38.     reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +  
  39.              bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));  
  40.   
  41.     /* 
  42.      * reserve physical page 0 - it's a special BIOS page on many boxes, 
  43.      * enabling clean reboots, SMP operation, laptop functions. 
  44.      */  
  45.     reserve_bootmem(0, PAGE_SIZE);  
  46.   
  47.         。。。。。。。。。。。。  
  48.     return max_low_pfn;  
  49. }  
  • PFN_UP获得_end后第一个page的页帧号来初始化start_pfn
  • 调用find_max_low_pfn获得低端内存的最大页帧号
  • 如果配置了CONFIG_HIGHMEM,则初始化highstart_pfn变量
  • 调用init_bootmem初始化bootmem allocator
  • 将max_low_pfn,即直接内存映射部分的page设置为可用
  • 保留内核镜像(从0x100000开始,kernel code, kernel data, kernel bss),page 0,bootmem allocator的bitmap占用的空间
  • 然会低端内存的最大页帧号
下面来看查找最大内存的函数:
Cpp代码
  1. void __init find_max_pfn(void)  
  2. {  
  3.     int i;  
  4.   
  5.     max_pfn = 0;  
  6.     if (efi_enabled) {  
  7.         efi_memmap_walk(efi_find_max_pfn, &max_pfn);  
  8.         return;  
  9.     }  
  10.   
  11.     for (i = 0; i < e820.nr_map; i++) {  
  12.         unsigned long start, end;  
  13.         /* RAM? */  
  14.         if (e820.map[i].type != E820_RAM)  
  15.             continue;  
  16.         start = PFN_UP(e820.map[i].addr);  
  17.         end = PFN_DOWN(e820.map[i].addr + e820.map[i].size);  
  18.         if (start >= end)  
  19.             continue;  
  20.         if (end > max_pfn)  
  21.             max_pfn = end;  
  22.     }  
  23. }  
  • 循环遍历e820map,获得最大物理页帧号
Cpp代码
  1. unsigned long __init find_max_low_pfn(void)  
  2. {  
  3.     unsigned long max_low_pfn;  
  4.   
  5.     max_low_pfn = max_pfn;  
  6.     if (max_low_pfn > MAXMEM_PFN) {  
  7.         if (highmem_pages == -1)  
  8.             highmem_pages = max_pfn - MAXMEM_PFN;  
  9.         if (highmem_pages + MAXMEM_PFN < max_pfn)  
  10.             max_pfn = MAXMEM_PFN + highmem_pages;  
  11.         if (highmem_pages + MAXMEM_PFN > max_pfn) {  
  12.             printk("only %luMB highmem pages available, ignoring highmem size of %uMB.\n", pages_to_mb(max_pfn - MAXMEM_PFN), pages_to_mb(highmem_pages));  
  13.             highmem_pages = 0;  
  14.         }  
  15.         max_low_pfn = MAXMEM_PFN;  
  16. #ifndef CONFIG_HIGHMEM  
  17.         /* Maximum memory usable is what is directly addressable */  
  18.          printk(KERN_WARNING "Warning only %ldMB will be used.\n",  
  19.                     MAXMEM>>20);  
  20.         if (max_pfn > MAX_NONPAE_PFN)  
  21.             printk(KERN_WARNING "Use a PAE enabled kernel.\n");  
  22.         else  
  23.             printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");  
  24.         max_pfn = MAXMEM_PFN;  
  25. #else /* !CONFIG_HIGHMEM */  
  26. #ifndef CONFIG_X86_PAE  
  27.         if (max_pfn > MAX_NONPAE_PFN) {  
  28.             max_pfn = MAX_NONPAE_PFN;  
  29.             printk(KERN_WARNING "Warning only 4GB will be used.\n");  
  30.             printk(KERN_WARNING "Use a PAE enabled kernel.\n");  
  31.         }  
  32. #endif /* !CONFIG_X86_PAE */  
  33. #endif /* !CONFIG_HIGHMEM */  
  34.     } else {  
  35.         if (highmem_pages == -1)  
  36.             highmem_pages = 0;  
  37. #ifdef CONFIG_HIGHMEM  
  38.         if (highmem_pages >= max_pfn) {  
  39.             printk(KERN_ERR "highmem size specified (%uMB) is bigger than pages available (%luMB)!.\n", pages_to_mb(highmem_pages), pages_to_mb(max_pfn));  
  40.             highmem_pages = 0;  
  41.         }  
  42.         if (highmem_pages) {  
  43.             if (max_low_pfn-highmem_pages < 64*1024*1024/PAGE_SIZE){  
  44.                 printk(KERN_ERR "highmem size %uMB results in smaller than 64MB lowmem, ignoring it.\n", pages_to_mb(highmem_pages));  
  45.                 highmem_pages = 0;  
  46.             }  
  47.             max_low_pfn -= highmem_pages;  
  48.         }  
  49. #else  
  50.         if (highmem_pages)  
  51.             printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");  
  52. #endif  
  53.     }  
  54.     return max_low_pfn;  
  55. }  
  • 这里分两种情况进行讨论,一个是内存大于896MB,一个是内存小于896MB
  • max_low_pfn > MAXMEM_PFN下的#ifndef CONFIG_HIGHMEM会设置max_pfn = MAXMEM_PFN;看出如果不开启highmem,即使内存大于896MB,也只能使用896MB
阅读(2098) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~