linux 內存探測和初始化

  1、內存探測

    linux在被bootloader加載到內存後, cpu最初執行的內核代碼是arch/x86/boot/header.S彙編文件中的_start例程,設置好頭部header,其中包括大量的bootloader參數。接着是其中的start_of_setup例程,這個例程在做了一些準備工作後會通過call main跳轉到arch/x86/boot/main.c:main()函數處執行,這就是衆所周知的x86下的main函數,它們都工作在實模式下。在這個main函數中我們可以第一次看到與內存管理相關的代碼,這段代碼調用detect_memory()函數檢測系統物理內存。如下:

  1. void main(void)  
  2. {  
  3.     /* First, copy the boot header into the "zeropage" */  
  4.     copy_boot_params(); /* 把頭部各參數複製到boot_params變量中 */  
  5.   
  6.     /* End of heap check */  
  7.     init_heap();  
  8.   
  9.     /* Make sure we have all the proper CPU support */  
  10.     if (validate_cpu()) {  
  11.         puts("Unable to boot - please use a kernel appropriate "  
  12.              "for your CPU.\n");  
  13.         die();  
  14.     }  
  15.   
  16.     /* Tell the BIOS what CPU mode we intend to run in. */  
  17.     set_bios_mode();  
  18.   
  19.     /* Detect memory layout */  
  20.     detect_memory(); /* 內存探測函數 */  
  21.   
  22.     /* Set keyboard repeat rate (why?) */  
  23.     keyboard_set_repeat();  
  24.   
  25.     /* Query MCA information */  
  26.     query_mca();  
  27.   
  28.     /* Query Intel SpeedStep (IST) information */  
  29.     query_ist();  
  30.   
  31.     /* Query APM information */  
  32. #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)  
  33.     query_apm_bios();  
  34. #endif  
  35.   
  36.     /* Query EDD information */  
  37. #if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)  
  38.     query_edd();  
  39. #endif  
  40.   
  41.     /* Set the video mode */  
  42.     set_video();  
  43.   
  44.     /* Parse command line for 'quiet' and pass it to decompressor. */  
  45.     if (cmdline_find_option_bool("quiet"))  
  46.         boot_params.hdr.loadflags |= QUIET_FLAG;  
  47.   
  48.     /* Do the last things and invoke protected mode */  
  49.     go_to_protected_mode();  
  50. }  
    內存探測的實現在arch/x86/boot/memory.c中,如下:

  1. int detect_memory(void)  
  2. {  
  3.     int err = -1;  
  4.   
  5.     if (detect_memory_e820() > 0)  
  6.         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. }  
    由上面的代碼可知,linux內核會分別嘗試調用detect_memory_e820()、detcct_memory_e801()、detect_memory_88()獲得系統物理內存佈局,這3個函數都在memory.c中實現,它們內部其實都會以內聯彙編的形式調用bios中斷以取得內存信息,該中斷調用形式爲int 0x15,同時調用前分別把AX寄存器設置爲0xe820h、0xe801h、0x88h,關於0x15號中斷有興趣的可以去查詢相關手冊。下面分析detect_memory_e820()的代碼,其它代碼基本一樣。

  1. #define SMAP    0x534d4150  /* ASCII "SMAP" */  
  2.   
  3. static int detect_memory_e820(void)  
  4. {  
  5.     int count = 0; /* 用於記錄已檢測到的物理內存數目 */  
  6.     struct biosregs ireg, oreg;  
  7.     struct e820entry *desc = boot_params.e820_map;  
  8.     static struct e820entry buf; /* static so it is zeroed */  
  9.   
  10.     initregs(&ireg); /* 初始化ireg中的相關寄存器 */  
  11.     ireg.ax  = 0xe820;  
  12.     ireg.cx  = sizeof buf; /* e820entry數據結構大小 */  
  13.     ireg.edx = SMAP; /* 標識 */  
  14.     ireg.di  = (size_t)&buf; /* int15返回值的存放處 */  
  15.   
  16.     /* 
  17.      * Note: at least one BIOS is known which assumes that the 
  18.      * buffer pointed to by one e820 call is the same one as 
  19.      * the previous call, and only changes modified fields.  Therefore, 
  20.      * we use a temporary buffer and copy the results entry by entry. 
  21.      * 
  22.      * This routine deliberately does not try to account for 
  23.      * ACPI 3+ extended attributes.  This is because there are 
  24.      * BIOSes in the field which report zero for the valid bit for 
  25.      * all ranges, and we don't currently make any use of the 
  26.      * other attribute bits.  Revisit this if we see the extended 
  27.      * attribute bits deployed in a meaningful way in the future. 
  28.      */  
  29.   
  30.     do {  
  31.         /* 在執行這條內聯彙編語句時輸入的參數有:  
  32.         eax寄存器=0xe820  
  33.         dx寄存器=’SMAP’  
  34.         edi寄存器=desc  
  35.         ebx寄存器=next  
  36.         ecx寄存器=size  
  37.           
  38.          返回給c語言代碼的參數有:  
  39.         id=eax寄存器 
  40.         rr=edx寄存器  
  41.         ext=ebx寄存器 
  42.         size=ecx寄存器  
  43.         desc指向的內存地址在執行0x15中斷調用時被設置  
  44.         */    
  45.         intcall(0x15, &ireg, &oreg);  
  46.         ireg.ebx = oreg.ebx; /* 選擇下一個 */  
  47.   
  48.         /* BIOSes which terminate the chain with CF = 1 as opposed 
  49.            to %ebx = 0 don't always report the SMAP signature on 
  50.            the final, failing, probe. */  
  51.         if (oreg.eflags & X86_EFLAGS_CF)  
  52.             break;  
  53.   
  54.         /* Some BIOSes stop returning SMAP in the middle of 
  55.            the search loop.  We don't know exactly how the BIOS 
  56.            screwed up the map at that point, we might have a 
  57.            partial map, the full map, or complete garbage, so 
  58.            just return failure. */  
  59.         if (oreg.eax != SMAP) {  
  60.             count = 0;  
  61.             break;  
  62.         }  
  63.   
  64.         *desc++ = buf; /* 將buf賦值給desc */  
  65.         count++; /* 探測數加一 */  
  66.     } while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));  
  67.     /* 將內存塊數保持到變量中 */  
  68.     return boot_params.e820_entries = count;  
  69. }  
    由於歷史原因,一些I/O設備也會佔據一部分內存物理地址空間,因此係統可以使用的物理內存空間是不連續的,系統內存被分成了很多段,每個段的屬性也是不一樣的。int 0x15查詢物理內存時每次返回一個內存段的信息,因此要想返回系統中所有的物理內存,我們必須以迭代的方式去查詢。detect_memory_e820()函數把int 0x15放到一個do-while循環裏,每次得到的一個內存段放到struct e820entry裏,而struct e820entry的結構正是e820返回結果的結構。像其它啓動時獲得的結果一樣,最終都會被放到boot_params裏,探測到的各個內存段情況被放到了boot_params.e820_map。
    這裏存放中斷返回值的e820entry結構,以及表示內存圖的e820map結構均位於arch/x86/include/asm/e820.h中,如下:

  1. struct e820entry {  
  2.     __u64 addr; /* 內存段的開始 */  
  3.     __u64 size; /* 內存段的大小 */  
  4.     __u32 type; /* 內存段的類型 */  
  5. } __attribute__((packed));  
  6.   
  7. struct e820map {  
  8.     __u32 nr_map;  
  9.     struct e820entry map[E820_X_MAX];  
  10. };  
    內存探測用於檢測出系統有多少個通常不連續的內存區塊。之後要建立一個描述這些內存塊的內存圖數據結構,這就是上面的e820map結構,其中nr_map爲檢測到的系統中內存區塊數,不能超過E820_X_MAX(定義爲128),map數組描述各個內存塊的情況,包括其開始地址、內存塊大小、類型。

     對於32位的系統,通過調用鏈arch/x86/boot/main.c:main()--->arch/x86/boot/pm.c:go_to_protected_mode()--->arch/x86/boot/pmjump.S:protected_mode_jump()--->arch/i386/boot/compressed/head_32.S:startup_32()--->arch/x86/kernel/head_32.S:startup_32()--->arch/x86/kernel/head32.c:i386_start_kernel()--->init/main.c:start_kernel(),到達衆所周知的Linux內核啓動函數start_kernel(),這裏會調用setup_arch()完成與體系結構相關的一系列初始化工作,其中就包括各種內存的初始化工作,如內存圖的建立、管理區的初始化等等。對x86體系結構,setup_arch()函數在arch/x86/kernel/setup.c中,如下:

  1. void __init setup_arch(char **cmdline_p)  
  2. {  
  3.     /* ...... */  
  4.   
  5.     x86_init.oem.arch_setup();  
  6.   
  7.     setup_memory_map(); /* 建立內存圖 */  
  8.     parse_setup_data();  
  9.     /* update the e820_saved too */  
  10.     e820_reserve_setup_data();  
  11.   
  12.     /* ...... */  
  13.   
  14.     /* 
  15.      * partially used pages are not usable - thus 
  16.      * we are rounding upwards: 
  17.      */  
  18.     max_pfn = e820_end_of_ram_pfn(); /* 找出最大可用內存頁面幀號 */  
  19.   
  20.     /* preallocate 4k for mptable mpc */  
  21.     early_reserve_e820_mpc_new();  
  22.     /* update e820 for memory not covered by WB MTRRs */  
  23.     mtrr_bp_init();  
  24.     if (mtrr_trim_uncached_memory(max_pfn))  
  25.         max_pfn = e820_end_of_ram_pfn();  
  26.   
  27. #ifdef CONFIG_X86_32  
  28.     /* max_low_pfn在這裏更新 */  
  29.     find_low_pfn_range(); /* 找出低端內存的最大頁幀號 */  
  30. #else  
  31.     num_physpages = max_pfn;  
  32.   
  33.     /* ...... */  
  34.   
  35.     /* max_pfn_mapped在這更新 */  
  36.     /* 初始化內存映射機制 */  
  37.     max_low_pfn_mapped = init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT);  
  38.     max_pfn_mapped = max_low_pfn_mapped;  
  39.   
  40. #ifdef CONFIG_X86_64  
  41.     if (max_pfn > max_low_pfn) {  
  42.         max_pfn_mapped = init_memory_mapping(1UL<<32,  
  43.                              max_pfn<<PAGE_SHIFT);  
  44.         /* can we preseve max_low_pfn ?*/  
  45.         max_low_pfn = max_pfn;  
  46.     }  
  47. #endif  
  48.   
  49.     /* ...... */  
  50.   
  51.     initmem_init(0, max_pfn); /* 啓動內存分配器 */  
  52.   
  53.     /* ...... */  
  54.   
  55.     x86_init.paging.pagetable_setup_start(swapper_pg_dir);  
  56.     paging_init(); /* 建立完整的頁表 */  
  57.     x86_init.paging.pagetable_setup_done(swapper_pg_dir);  
  58.   
  59.     /* ...... */  
  60. }  
    幾乎所有的內存初始化工作都是在setup_arch()中完成的,主要的工作包括:
    (1)建立內存圖:setup_memory_map();
    (2)調用e820_end_of_ram_pfn()找出最大可用頁幀號max_pfn,調用find_low_pfn_range()找出低端內存區的最大可用頁幀號max_low_pfn。
    (2)初始化內存映射機制:init_memory_mapping();
    (3)初始化內存分配器:initmem_init();
    (4)建立完整的頁表:paging_init()。
    2、建立內存圖
    內存探測完之後,就要建立描述各內存塊情況的全局內存圖結構了。函數爲setup_arch()--->arch/x86/kernel/e820.c:setup_memory_map(),如下:

  1. void __init setup_memory_map(void)  
  2. {  
  3.     char *who;  
  4.     /* 調用x86體系下的memory_setup函數 */  
  5.     who = x86_init.resources.memory_setup();  
  6.     /* 保存到e820_saved中 */  
  7.     memcpy(&e820_saved, &e820, sizeof(struct e820map));  
  8.     printk(KERN_INFO "BIOS-provided physical RAM map:\n");  
  9.     /* 打印輸出 */  
  10.     e820_print_map(who);  
  11. }  
    該函數調用x86_init.resources.memory_setup()實現對BIOS e820內存圖的設置和優化,然後將全局e820中的值保存在e820_saved中,並打印內存圖。Linux的內存圖保存在一個全局的e820變量中,還有其備份e820_saved,這兩個全局的e820map結構變量均定義在arch/x86/kernel/e820.c中。memory_setup()函數是建立e820內存圖的核心函數,從arch/x86/kernel/x86_init.c中可知,x86_init.resources.memory_setup()就是e820.c中的default_machine_specific_memory_setup()函數,如下:

  1. char *__init default_machine_specific_memory_setup(void)  
  2. {  
  3.     char *who = "BIOS-e820";  
  4.     u32 new_nr;  
  5.     /* 
  6.      * 複製BIOS提供的e820內存圖,否則僞造一個內存圖:一塊爲0-640k,接着的 
  7.      * 下一塊爲1mb到appropriate_mem_k的大小 
  8.      */  
  9.     new_nr = boot_params.e820_entries;  
  10.     /* 將重疊的去除 */  
  11.     sanitize_e820_map(boot_params.e820_map,  
  12.             ARRAY_SIZE(boot_params.e820_map),  
  13.             &new_nr);  
  14.     /* 去掉重疊的部分後得到的內存塊個數 */  
  15.     boot_params.e820_entries = new_nr;   
  16.     /* 將其複製到全局變量e820中,小於0時,爲出錯處理 */  
  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. }  
  39.   
  40. /* 
  41.  * 複製BIOS e820內存圖到一個安全的地方。如果我們在裏面,則要進行重疊檢查 
  42.  * 如果我們用的是現代系統,則設置代碼將給我們提供一個可以使用的內存圖,以便 
  43.  * 用它來建立內存。如果不是現代系統,則將僞造一個內存圖 
  44.  */  
  45. static int __init append_e820_map(struct e820entry *biosmap, int nr_map)  
  46. {  
  47.     /* Only one memory region (or negative)? Ignore it */  
  48.     if (nr_map < 2)  
  49.         return -1;  
  50.   
  51.     return __append_e820_map(biosmap, nr_map);  
  52. }  
  53.   
  54. static int __init __append_e820_map(struct e820entry *biosmap, int nr_map)  
  55. {  
  56.     while (nr_map) { /* 循環nr_map次調用,添加內存塊到e820 */  
  57.         u64 start = biosmap->addr;  
  58.         u64 size = biosmap->size;  
  59.         u64 end = start + size;  
  60.         u32 type = biosmap->type;  
  61.   
  62.         /* Overflow in 64 bits? Ignore the memory map. */  
  63.         if (start > end)  
  64.             return -1;  
  65.         /* 添加函數 */  
  66.         e820_add_region(start, size, type);  
  67.   
  68.         biosmap++;  
  69.         nr_map--;  
  70.     }  
  71.     return 0;  
  72. }  
  73.   
  74. void __init e820_add_region(u64 start, u64 size, int type)  
  75. {  
  76.     __e820_add_region(&e820, start, size, type);  
  77. }  
  78.   
  79. /* 
  80.  * 添加一個內存塊到內存e820內存圖中 
  81.  */  
  82. static void __init __e820_add_region(struct e820map *e820x, u64 start, u64 size,  
  83.                      int type)  
  84. {  
  85.     int x = e820x->nr_map;  
  86.   
  87.     if (x >= ARRAY_SIZE(e820x->map)) {  
  88.         printk(KERN_ERR "Ooops! Too many entries in the memory map!\n");  
  89.         return;  
  90.     }  
  91.   
  92.     e820x->map[x].addr = start;  
  93.     e820x->map[x].size = size;  
  94.     e820x->map[x].type = type;  
  95.     e820x->nr_map++;  
  96. }  
    從以上代碼可知,內存圖設置函數memory_setup()    把從BIOS中探測到的內存塊情況(保存在boot_params.e820_map中)做重疊檢測,把重疊的內存塊去除,然後調用append_e820_map()將它們添加到全局的e920變量中,具體完成添加工作的函數是__e820_add_region()。到這裏,物理內存就已經從BIOS中讀出來存放到全局變量e820中,e820是linux內核中用於建立內存管理框架的基礎。例如建立初始化頁表映射、管理區等都會用到它。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章