分类:
Linux
kernel是怎么管理内存的呢?从启动的角度来看,怎么看kernel怎么建立内存管理模块。还是需要从全局变量的角度来看。
1. early_ioremap--固定映射FIXMAP
ioremap的作用是将IO和BIOS以及物理地址空间映射到在896M至1G的128M的地址空间内,使得kernel能够访问该空间并进行相应的读写操作。
- early_ioremap_init(){
- for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
- slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i); //将所有fixed_address里的索引的虚拟地址放入slot_virt
- pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));//获取FIX_BTMAP_BEGIN索引指向的虚拟地址所对应的pmd表项.一个虚拟地址转换到物理地址需要通过pgd(global directory), pud (page upper direcotory), pmd (page middle directory)和pt(page table),这里需要的是pmd的地址
- memset(bm_pte, 0, sizeof(bm_pte)); //初始化bm_pte为全0,bm_pte就是fixed_address所要使用到的页表的空间
- pmd_populate_kernel(&init_mm, pmd, bm_pte);//将bm_pte这个页表设置为fixed_address所指向的虚拟地址所要使用的页表
if (pmd != early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END))){ //如果FIX_BTMAP_END所属的pmd和FIX_BTMAP_BEGIN所属的pmd不同那么就报警
//省略打印报警信息若干
}
}
slot_virt数组是一个向量表,每一个表项都被初始化成为一个页面的起始地址。该地址由_fix_to_vir宏对于(FIX_BTMAP_BEGIN-NR_FIX_BTMAPS*i)进行转换获得。
先看一下_fix_to_virt的定义
- #define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
按照定义,可以看到ioremap所使用的虚拟地址空间是从FIXADDR_TOP(FIXADDR_TOP=0xfffff000的定义在pgtable_32.c里面,指向4G虚拟地址空间中最后的一页)开始往下延伸的空间。
再回过头来看FIX_BTMAP_BEGIN的定义。FIX_BTMAP_BEGIN的定义是在fixmap.h的枚举类型fixed_address里。这个枚举类型中定义了有哪些需要使用到ioremap的预定义的索引。例如fixed_address.FIX_HOLE实际值是0,其指向是memory中的第一个4K空间,而经过_fix_to_virt转换后就变成了指向0xfffff000这个页面。
而FIX_BTMAP_BEGIN实际代表的是fixed_address里面最后一个索引。所以slot_virt里面实际上是所有fixed_address中的索引的虚拟地址的。
对于ioremap的使用需要通过early_memremap和early_iounmap进行。由于对应于ioremap的内存空间是有限的,所以对于ioremap空间的使用遵照使用结束马上释放的原则。这就是说early_memremap和early_iounmap必须配对使用并且访问结束必须马上执行unmap。
- static void __init __iomem *
- __early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
- {
- unsigned long offset;
- resource_size_t last_addr;
- unsigned int nrpages;
- enum fixed_addresses idx0, idx;
- int i, slot;
- WARN_ON(system_state != SYSTEM_BOOTING);
- slot = -1;
- for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {//pre_map[]是一个索引与slot_virt[]一一对应,这段for的含义在于找到一个没有被使用过的slot_virt[i]的页面,该slot_virt[i]所指向的虚拟页面地址就是将会和实际物理地址phys_addr相绑定的虚拟地址。
- if (!prev_map[i]) {
- slot = i;
- break;
- }
- }
- if (slot < 0) {
- printk(KERN_INFO "early_iomap(%08llx, %08lx) not found slot\n",
- (u64)phys_addr, size);
- WARN_ON(1);
- return NULL;
- }
- if (early_ioremap_debug) {
- printk(KERN_INFO "early_ioremap(%08llx, %08lx) [%d] => ",
- (u64)phys_addr, size, slot);
- dump_stack();
- }
- /* Don't allow wraparound or zero size */
- last_addr = phys_addr + size - 1;
- if (!size || last_addr < phys_addr) {
- WARN_ON(1);
- return NULL;
- }
- prev_size[slot] = size;
- /*
- * Mappings have to be page-aligned
- */
- offset = phys_addr & ~PAGE_MASK; //offset是页内的偏移
- phys_addr &= PAGE_MASK; //现在phys_addr就是起始页面的地址
- size = PAGE_ALIGN(last_addr + 1) - phys_addr;//现在size就是指出了到底占据了多少个页面的大小
- /*
- * Mappings have to fit in the FIX_BTMAP area.
- */
- nrpages = size >> PAGE_SHIFT; //到底我们需要多少页面
- if (nrpages > NR_FIX_BTMAPS) {
- WARN_ON(1);
- return NULL;
- }
- /*
- * Ok, go for it..
- */
- idx0 = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;//找到空闲slot所对应的fixed_address中的索引号
- idx = idx0;
- while (nrpages > 0) {
- early_set_fixmap(idx, phys_addr, prot);//在bm_ptes中将指定的idx索引的页表项填充为对应的物理地址使得bm_pte[idx]指向正确的物理页面地址
- phys_addr += PAGE_SIZE;
- --idx;
- --nrpages;
- }
- if (early_ioremap_debug)
- printk(KERN_CONT "%08lx + %08lx\n", offset, slot_virt[slot]);
- prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]); //返回phys_addr所指向的虚拟地址
- return prev_map[slot];
- }
而early_iounmap的过程基本相反,其将bm_pte[]相关的pte,pre_map[]和pre_size清除。
2. e820
e820是BIOS中负责向OS报告当前内存区域使用情况的中断服务。kernel在实模式的时候曾经调用过e820记录下kernel启动前系统初始化的时候所被使用到的内存区域和可以使用的空闲区域。这些内存区域就包含了BIOS所占用的区域,硬件所占用的区域。
Kernel需要把这些由BIOS和硬件占用的内存区域加以保留来确保不会覆盖这些区域而破坏之后对于该区域的使用。
而kernel的代码很显然并没有把e820仅仅当成保存BIOS和硬件预留内存区域的作用,一些其他需要标记的内存区域也被放在e820里面。
- struct e820map e820; //全局e820
- struct e820map e820_saved; //全局e820的备份
最初的e820初始化由setup_arch中的setup_memory_map()发起:
- void __init setup_memory_map(void)
- {
- char *who;
- who = x86_init.resources.memory_setup();//在一般的pc中这个函数指向e820.c中的default_machine_specific_memory_setup
- memcpy(&e820_saved, &e820, sizeof(struct e820map));//备份e820到e820_saved
- printk(KERN_INFO "BIOS-provided physical RAM map:\n");
- e820_print_map(who); //打印调试信息
- }
- char *__init default_machine_specific_memory_setup(void)
- {
- char *who = "BIOS-e820";
- u32 new_nr;
- /*
- * Try to copy the BIOS-supplied E820-map.
- *
- * Otherwise fake a memory map; one section from 0k->640k,
- * the next section from 1mb->appropriate_mem_k
- */
- new_nr = boot_params.e820_entries;
- sanitize_e820_map(boot_params.e820_map,
- ARRAY_SIZE(boot_params.e820_map),
- &new_nr);//santitize_e820_map的作用就是对指定e820结构中内存区域进行排序合并以保证其中没有重复和重叠的内存区域。这里boot_params.e820_map就是我们在实模式中调用BIOS e820的服务而获得的BIOS和硬件所使用的内存区域。
- boot_params.e820_entries = new_nr;
- if (append_e820_map(boot_params.e820_map, boot_params.e820_entries)//append_e820_map的作用是将指定的e820结构的内存区域合并到全局e820中去。返回小于0代表boot_params.e820_map中没有可以合并的内容也就是说BIOS e820调用显然错误了。此时全局e820是空的。
- < 0) {
- u64 mem_size;
- /* compare results from other methods and take the greater */
- if (boot_params.alt_mem_k
- < boot_params.screen_info.ext_mem_k) {//如果BIOS int15 ah=e801汇报的物理内存小于通过int15 ah=88汇报的可用内存。就按照int15 ah=88来计算物理内存,否则按照int15 ah=e801来计算物理内存
- mem_size = boot_params.screen_info.ext_mem_k;
- who = "BIOS-88";
- } else {
- mem_size = boot_params.alt_mem_k;
- who = "BIOS-e801";
- }
- e820.nr_map = 0;
- e820_add_region(0, LOWMEMSIZE(), E820_RAM); //将0-640K的内存空间添加到全局e820中,标记为E820_RAM可用内存区域
- e820_add_region(HIGH_MEMORY, mem_size << 10, E820_RAM); //将1M-最大物理内存之间的空间添加到全局e820中,标记为E820_RAM可用内存区域
- }
- /* In case someone cares... */
- return who;
- }
之后,kernel对于hdr.setup_data进行解析。hdr.setup_data是一个单向列表。按照Documentation\x86\Boot.txt的解释是bootloader传给OS的除了hdr所预设规定的内容之外的信息。
- static void __init parse_setup_data(void)
- {
- struct setup_data *data;
- u64 pa_data;
- if (boot_params.hdr.version < 0x0209)
- return;
- pa_data = boot_params.hdr.setup_data;
- while (pa_data) { //如果setup_data不为NULL
- data = early_memremap(pa_data, PAGE_SIZE); //由于pa_data是实际的物理地址,所以需要将其映射到ioremap固定映射区间FIXMAP。
- switch (data->type) {
- case SETUP_E820_EXT://如果setup_data要求对于E820进行扩展。
- parse_e820_ext(data, pa_data);//将setup_data所指定的e820扩展加入全局e820中
- break;
- default:
- break;
- }
- pa_data = data->next;
- early_iounmap(data, PAGE_SIZE); //unmap
- }
- }
接着,kernel对于setup_data本身进行保护,将其加入全局e820并且标记为E820_RESERVE_KERN
- static void __init e820_reserve_setup_data(void)
- {
- struct setup_data *data;
- u64 pa_data;
- int found = 0;
- if (boot_params.hdr.version < 0x0209)
- return;
- pa_data = boot_params.hdr.setup_data;
- while (pa_data) {
- data = early_memremap(pa_data, sizeof(*data));
- e820_update_range(pa_data, sizeof(*data)+data->len,
- E820_RAM, E820_RESERVED_KERN);//将setup_data本身标记为E820_RESERVED_KERN
- found = 1;
- pa_data = data->next;
- early_iounmap(data, sizeof(*data));
- }
- if (!found)
- return;
- sanitize_e820_map(e820.map, ARRAY_SIZE(e820.map), &e820.nr_map);
- memcpy(&e820_saved, &e820, sizeof(struct e820map));//将改变过的e820复制到e820_saved
- printk(KERN_INFO "extended physical RAM map:\n");
- e820_print_map("reserve setup_data");
- }
最后通过finish_e820_parsing()结束对e820的初始化。
3. memblock - Kernel对于物理内存使用情况的记录
- struct memblock {
- phys_addr_t current_limit;
- phys_addr_t memory_size; /* Updated by memblock_analyze() */
- struct memblock_type memory;
- struct memblock_type reserved;
- };
memblock.memory.regions是可用的memory region的集合,而memory.reserved.regions是需要保留的memory region以阻止分配的集合。
memblock的初始化从i386_default_early_setup开始。i386_default_early_setup分别调用了进行最初的初始化。
- memblock_init(); //初始化memblock.memory和memblock.reserved将他们清空。
- memblock_x86_reserve_range(__pa_symbol(&_text), __pa_symbol(&__bss_stop), "TEXT DATA BSS"); //将kernel本身的text,data,bss三个段的进行保留。
- #ifdef CONFIG_BLK_DEV_INITRD
- /* Reserve INITRD */
- if (boot_params.hdr.type_of_loader && boot_params.hdr.ramdisk_image) {
- /* Assume only end is not page aligned */
- u64 ramdisk_image = boot_params.hdr.ramdisk_image;
- u64 ramdisk_size = boot_params.hdr.ramdisk_size;
- u64 ramdisk_end = PAGE_ALIGN(ramdisk_image + ramdisk_size);
- memblock_x86_reserve_range(ramdisk_image, ramdisk_end, "RAMDISK");//将initrd所使用的ramdisk区域进行保留
- }
- #endif
对于memblock的基本操作有以下几个函数:
- //将给定的物理地址所指定的memory region加入到指定的memblock(memblock.reserved或者是memblock.memory)中。新加入的memory region需要经过检查,如果与原先的memory region有重叠,则需要合并在原先的memory region中,否则的话就新建一个memory region.
- static long __init_memblock memblock_add_region(struct memblock_type *type, phys_addr_t base, phys_addr_t size);
- //从指定的memblock中移除指定物理地址所指定的memory region.如果所指定的区域是存在区域的一部分,则涉及到调整region大小,或者将一个region拆分成为两个region.
- static long __init_memblock __memblock_remove(struct memblock_type *type, phys_addr_t base, phys_addr_t size);
- //使用该函数可以向kernel申请一块可用的物理内存。实际的操作是在memblock.memory中找到合适的内存,将其从memblock.memory去除,加入到memblock.reserved中以标记其已经被使用。
- phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align);
- //使用该函数来释放由memblock_alloc申请到的物理内存,释放的内存会从memblock.reserved中移除,并加入memblock.memory中
- long __init_memblock memblock_free(phys_addr_t base, phys_addr_t size);