分類:
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);