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()函数检测系统物理内存。如下:

[cpp]  view plain copy
  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中,如下:

[cpp]  view plain copy
  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()的代码,其它代码基本一样。

[cpp]  view plain copy
  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中,如下:

[cpp]  view plain copy
  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中,如下:

[cpp]  view plain copy
  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(),如下:

[cpp]  view plain copy
  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()函数,如下:

[cpp]  view plain copy
  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内核中用于建立内存管理框架的基础。例如建立初始化页表映射、管理区等都会用到它。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值