5.2.1 拷贝可用内存区信息
首先setup_arch第一步要做的就是保存arch/x86/kernel/head_32.S初始化的new_cpu_data数据到boot_cpu_data中。new_cpu_data主要是保存CPU的相关信息,在哪儿初始化的?还记得我们在arch/x86/kernel/head_32.S中忽略过的checkCPUtype吗?就在那里,感兴趣的同学可以去探究一下。
784行,setup_memory_map()函数,进入start_kernel内核初始化函数中第一个内存管理函数就是由它来实现。
1204 void __init setup_memory_map(void) 1205 { 1206 char *who; 1207 1208 who = x86_init.resources.memory_setup(); 1209 memcpy(&e820_saved, &e820, sizeof(struct e820map)); 1210 printk(KERN_INFO "BIOS-provided physical RAM map:/n"); 1211 e820_print_map(who); 1212 } |
setup_memory_map()最终调用x86_init.resources.memory_setup()实现对e820内存图的优化,并根据boot_params中 e820_map字段的值来设置全局变量e820的值。这个全局变量是一个e820map结构:
struct e820map {
__u32 nr_map;
struct e820entry map[E820_X_MAX];
};
还记得吗?我们在执行实模式下代码main函数的时候调用了一个detect_memory_e820()函数,当时该函数通过BIOS服务程序int 0x15获得系统启动后的所有可用空间,共有boot_params.e820_entries个可用空间,每块空间作为boot_params.e820_map[]数组的元素,存放着他们的起始地址、大小和元素。
这里说个题外话,探测一个PC机内存的最好方法是就是通过调用INT 0x15,eax = 0xe820来实现。这个功能在2002年以后被所有PC机所使用,这是唯一能够探测超过4G大小内存的方案,当然,这个方法也可以被认为是内存的最终检测方法。实际上,这个函数返回一个非排序列表,这个列表包含了那些没有使用的项,并且可能返回存在覆盖的区域。在linux中每个列表项被存放在ES:EDI指定的内存区域中。每个项均有一定的格式:即2个8字节字段,一个2字节字段。我们前面看见了,对于内存探测的实现由函数detect_memory_e820来实现的,在这个函数中,使用了一个do...while()循环来实现,并将所探测的内容写入boot_params.e820_map数组中。
那么,x86_init.resources.memory_setup()是个什么函数呢?在arch/x86/kernel/x86_init.c的32行,我们看到又有一个__initdata的定义:
32struct x86_init_ops x86_init __initdata = { 33 34 .resources = { 35 .probe_roms = x86_init_noop, 36 .reserve_resources = reserve_standard_io_resources, 37 .memory_setup = default_machine_specific_memory_setup, 38 }, 39 40 .mpparse = { 41 .mpc_record = x86_init_uint_noop, 42 .setup_ioapic_ids = x86_init_noop, 43 .mpc_apic_id = default_mpc_apic_id, 44 .smp_read_mpc_oem = default_smp_read_mpc_oem, 45 .mpc_oem_bus_info = default_mpc_oem_bus_info, 46 .find_smp_config = default_find_smp_config, 47 .get_smp_config = default_get_smp_config, 48 }, 49 50 .irqs = { 51 .pre_vector_init = init_ISA_irqs, 52 .intr_init = native_init_IRQ, 53 .trap_init = x86_init_noop, 54 }, 55 56 .oem = { 57 .arch_setup = x86_init_noop, 58 .banner = default_banner, 59 }, 60 61 .paging = { 62 .pagetable_setup_start = native_pagetable_setup_start, 63 .pagetable_setup_done = native_pagetable_setup_done, 64 }, 65 66 .timers = { 67 .setup_percpu_clockev = setup_boot_APIC_clock, 68 .tsc_pre_init = x86_init_noop, 69 .timer_init = hpet_time_init, 70 }, 71 72 .iommu = { 73 .iommu_init = iommu_init_noop, 74 }, 75 76 .pci = { 77 .init = x86_default_pci_init, 78 .init_irq = x86_default_pci_init_irq, 79 .fixup_irqs = x86_default_pci_fixup_irqs, 80 }, 81}; |
这里,跟其他__initdata数据一样,在编译的时候就存放在init数据区了。那么x86_init.resources.memory_setup也就是37行的那个default_machine_specific_memory_setup函数了。所以我们看到这个函数:
1166char *__init default_machine_specific_memory_setup(void) 1167{ 1168 char *who = "BIOS-e820"; 1169 u32 new_nr; 1170 /* 1171 * Try to copy the BIOS-supplied E820-map. 1172 * 1173 * Otherwise fake a memory map; one section from 0k->640k, 1174 * the next section from 1mb->appropriate_mem_k 1175 */ 1176 new_nr = boot_params.e820_entries; 1177 sanitize_e820_map(boot_params.e820_map, 1178 ARRAY_SIZE(boot_params.e820_map), 1179 &new_nr); 1180 boot_params.e820_entries = new_nr; 1181 if (append_e820_map(boot_params.e820_map, boot_params.e820_entries) 1182 < 0) { 1183 u64 mem_size; 1184 1185 /* compare results from other methods and take the greater */ 1186 if (boot_params.alt_mem_k 1187 < boot_params.screen_info.ext_mem_k) { 1188 mem_size = boot_params.screen_info.ext_mem_k; 1189 who = "BIOS-88"; 1190 } else { 1191 mem_size = boot_params.alt_mem_k; 1192 who = "BIOS-e801"; 1193 } 1194 1195 e820.nr_map = 0; 1196 e820_add_region(0, LOWMEMSIZE(), E820_RAM); 1197 e820_add_region(HIGH_MEMORY, mem_size << 10, E820_RAM); 1198 } 1199 1200 /* In case someone cares... */ 1201 return who; 1202} |
首先1177行为所有可用内存区空间进行“消毒”。至于sanitize“消毒”算法,有兴趣的同学,特别是对信息安全感兴趣的同学可以去研究一下。最重点的是1181行调用append_e820_map函数,将boot_params.e820_map[]数组中所有的内存区物理信息拷贝到全局数据e820中,最终返回"BIOS-e820"字符串,赋给setup_memory_map()函数中的内部变量who。
之后setup_memory_map函数将e820的数据保存到e820_saved中,相当于备份。最后调用e820_print_map打印出内存分布图,注意这里,e820.nr_map就是可以的内存区的数量:
151void __init e820_print_map(char *who) 152{ 153 int i; 154 155 for (i = 0; i < e820.nr_map; i++) { 156 printk(KERN_INFO " %s: %016Lx - %016Lx ", who, 157 (unsigned long long) e820.map[i].addr, 158 (unsigned long long) 159 (e820.map[i].addr + e820.map[i].size)); 160 e820_print_type(e820.map[i].type); 161 printk(KERN_CONT "/n"); 162 } 163} |
793~796行,调用系统编译生成的数据来初始化init_mm的start_code、end_code、end_data以及.brk等字段;同时也初始化code_resource的start、end字段和data_resource的start、end字段以及bss_resource的start、end字段。这个init_mm还记得吗?初始化0号进程的时候,它的task_struct的active_mm就被赋成了这个。它是一个mm_struct结构,在编译vmlinux的时候就只给他初始化了一下字段:
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.cpu_vm_mask = CPU_MASK_ALL,
};
这里初始化它的start_code、end_code、end_data以及.brk等字段分别为_text、_etext、_edata和_brk_end,是一步极其重要的过程,表明了当前0号进程进程的虚拟内存技术就可以用了。至于后面那几个resource,通过870~872行代码将其作为一个IO resource保存起来,有关IO端口的相关知识,请查阅博客“Linux I/O体系结构”。