內存管理 | 內存初始化【轉】


轉自:https://zhuanlan.zhihu.com/p/355205941

介紹完內存初始化過程中最為重要的一個數據結構后,我們就正式開始跟着代碼從start_kernel一步一步了解內存初始化的整個流程。我們再次借用初始化第一章節的代碼流程圖。

 

setup_arch

setup_arch是一個特定於體系結構的設置函數。

setup_machine_fdt

 

void __init setup_arch(char **cmdline_p) { /*  * 重要數據結構,內核通過machine_desc結構來控制系統體系架構相關部分的初始化  * machine_desc機構提的成員包含了體系架構相關部分的幾個最重要的初始化函數  * 包括map_io、init_irq、init_machine、phys_io、timer等  */ const struct machine_desc *mdesc; ... mdesc = setup_machine_fdt(__atags_pointer); ... }

setup_machine_fdt函數用於獲取內核前期初始化所需的bootargs,cmdline等系統引導參數 。

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys) { ... early_init_dt_scan_nodes(); ... } void __init early_init_dt_scan_nodes(void) { /* Retrieve various information from the /chosen node */ of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); /* Initialize {size,address}-cells info */ of_scan_flat_dt(early_init_dt_scan_root, NULL); /* Setup memory, calling early_init_dt_add_memory_arch */ of_scan_flat_dt(early_init_dt_scan_memory, NULL); }

early_init_dt_scan_nodes函數中通過of_scan_flat_dt函數掃描整個設備樹,實際動作是在回調函數中完成的。early_init_dt_scan_chosen是對chosen節點的操作,主要是將節點下的bootargs屬性的字符串拷貝到boot_command_line指向的內存中。early_init_dt_scan_root是根據節點的#address-cells屬性和#size-cells屬性初始化全局變量dt_root_size_size_cells和dt_root_addr_cells,如果沒有設置屬性的話這里就使用默認值。early_init_dt_scan_memory是對內存的初始化。

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data) { ... base = dt_mem_next_cell(dt_root_addr_cells, &reg); size = dt_mem_next_cell(dt_root_size_cells, &reg); early_init_dt_add_memory_arch(base, size); }

對於dt_root_addr_cells和dt_root_size_cells的使用,我們可以看出根節點的#address-cells屬性和#size-cells屬性都是用來描述內存地址和大小的,得到每塊內存的起始地址和大小后,再調用early_init_dt_add_memory_arch函數。

void __init early_init_dt_add_memory_arch(u64 base, u64 size) { ... /* Add the chunk to the MEMBLOCK list */ if (add_mem_to_memblock) { if (validate_mem_limit(base, &size)) memblock_add(base, size); } }

在比較完內核對地址和大小的一系列要求后,最后調用memblock_add將內存塊加入內存。

setup_dma_zone

setup_dma_zone傳遞的參數是mdesc,根據mdesc->dma_zone_size設置DMA區域的大小arm_dma_zone_size和DMA區域的結束地址arm_dma_limit。

adjust_lowmem_bounds

void __init adjust_lowmem_bounds(void) { ... vmalloc_limit = (u64)(uintptr_t)vmalloc_min - PAGE_OFFSET + PHYS_OFFSET; /*  * The first usable region must be PMD aligned. Mark its start  * as MEMBLOCK_NOMAP if it isn't  * 四級頁表結構,PMD對應中間頁目錄  */ for_each_memblock(memory, reg) { if (!memblock_is_nomap(reg)) { if (!IS_ALIGNED(reg->base, PMD_SIZE)) { phys_addr_t len; len = round_up(reg->base, PMD_SIZE) - reg->base; memblock_mark_nomap(reg->base, len); } break; } } for_each_memblock(memory, reg) { phys_addr_t block_start = reg->base; phys_addr_t block_end = reg->base + reg->size; if (memblock_is_nomap(reg)) continue; if (reg->base < vmalloc_limit) { if (block_end > lowmem_limit) lowmem_limit = min_t(u64, vmalloc_limit, block_end); /*  * 找到第一個非pmd對齊的頁面,然后將memblock_limit指向該頁面。  * 這取決於將限制向下舍入為pmd對齊,此限制發生在此函數的末尾。  * 使用此算法,幾乎任何存儲體的開始或結束都可以不按PMD對齊。  * 唯一的例外是存儲體0的開始必須是部分對齊的,  * 因為否則在映射存儲體0的開始時就需要分配內存,  * 這是在映射任何可用內存之前發生的。  */ if (!memblock_limit) { if (!IS_ALIGNED(block_start, PMD_SIZE)) memblock_limit = block_start; else if (!IS_ALIGNED(block_end, PMD_SIZE)) memblock_limit = lowmem_limit; } } } arm_lowmem_limit = lowmem_limit; high_memory = __va(arm_lowmem_limit - 1) + 1; if (!memblock_limit) memblock_limit = arm_lowmem_limit; ... }

adjust_lowmem_bounds負責為lowmem/highmem設置邊界。它需要先進行系統設置,然后才能進行內存塊保留。在進行內存塊保留時,也可以從系統中刪除內存。低內存/高內存邊界和內存尾部可能會受到刪除操作的影響,但刪除后內存並未重新計算。雖然在某些系統上,這種情況是無害的,而在其他系統上,這可能導致將錯誤的范圍傳遞給主內存分配器。所以在完成所有保留后,需要通過重新計算lowmem/highmem邊界來更正此問題。這也就是為什么adjust_lowmem_bounds函數會被調用兩次。

arm_memblock_init

void __init arm_memblock_init(const struct machine_desc *mdesc) { /* Register the kernel text, kernel data and initrd with memblock. */ /* 預留內核鏡像內存,其中包括.text,.data,.init */ memblock_reserve(__pa(KERNEL_START), KERNEL_END - KERNEL_START); arm_initrd_init(); /*  * 預留vector page內存  * 如果CPU支持向量重定向(控制寄存器的V位),則CPU中斷向量被映射到這里。  */ arm_mm_memblock_reserve(); /* reserve any platform specific memblock areas */ if (mdesc->reserve) mdesc->reserve(); //預留架構相關的內存,這里包括內存屏障和安全ram  early_init_fdt_reserve_self(); //預留設備樹自身加載所占內存  early_init_fdt_scan_reserved_mem(); //初始化設備樹掃描reserved-memory節點預留內存  /* reserve memory for DMA contiguous allocations */ dma_contiguous_reserve(arm_dma_limit); //內核配置參數或命令行參數中預留的DMA連續內存  arm_memblock_steal_permitted = false; memblock_dump_all(); }

由arm_memblock_init函數可以看出設置保留內存的4種方法:

  1. machine的reserve接口中設置:mdesc->reserve();
  2. 設備樹reserved-memory節點中設置;
  3. 配置文件:Device Drivers> Generic Driver Options> DMA Contiguous Memory Allocator;
  4. 啟動參數添加字段 mem=size;

paging_init

void __init paging_init(const struct machine_desc *mdesc) { void *zero_page; /*  * prepare_page_table()  * 在內存使用之前,需要首先清理頁表信息。  * 在prepare_page_table函數中對三段地址使用pmd_clear來清理一級頁表的內容  * 0~MODULES_VADDR,MODULES_VADDR~PAGE_OFFSET,arm_lowmem_limit~VMALLOC_START  */ prepare_page_table(); /*  * map_lowmem()  * 將lowmem部分的一級頁表即PGD頁表填充初始化。  * 1MB對齊部分的物理內存會被初始化PGD中,不足1MB的會通過PTE來映射。  * 對此,boot階段初始化的頁表就被覆蓋了。  */ map_lowmem(); memblock_set_current_limit(arm_lowmem_limit); dma_contiguous_remap(); early_fixmap_shutdown(); devicemaps_init(mdesc); kmap_init(); tcm_init(); top_pmd = pmd_off_k(0xffff0000); /* allocate the zero page. */ zero_page = early_alloc(PAGE_SIZE); bootmem_init(); empty_zero_page = virt_to_page(zero_page); __flush_dcache_page(NULL, empty_zero_page); /* Compute the virt/idmap offset, mostly for the sake of KVM */ kimage_voffset = (unsigned long)&kimage_voffset - virt_to_idmap(&kimage_voffset); }

paging_init主要完成初始化內核的分頁機制,通過對boot階段頁表的覆蓋,並填充新的一級頁表,這樣我們的虛擬內存空間就初步建立,並可以完成物理地址到虛擬地址的映射工作了。

在paging_init中最為重要的函數要數bootmem_init(),接下來我們來詳細介紹一下bootmem_init。

void __init bootmem_init(void) { unsigned long min, max_low, max_high; memblock_allow_resize(); max_low = max_high = 0; /* 通過find_linits找出物理內存開始幀號、結束幀號和NORMAL區域的結束幀號 */ find_limits(&min, &max_low, &max_high); early_memtest((phys_addr_t)min << PAGE_SHIFT, (phys_addr_t)max_low << PAGE_SHIFT); /*  * Sparsemem tries to allocate bootmem in memory_present(),  * so must be done after the fixed reservations  */ /*  * 遍歷所有memory region,每個memory region分成1G大小的section,並設置section在位  * 函數中調用memory_present函數:  * sparse_index_init(section,nid):  * 遍歷所有的section,為其分配“struct mem_section”實例,需要注意  * 1.如果memory region不是按照section對齊的,那么最后一個section會有空洞,即沒有對應的物理頁  * 2.SECTIONS_PER_ROOT即一個物理頁面可以存放多少“struct mem_section”實例,由於目前內存是按照物理頁面來管理的,  * 所以一次會分配一個物理頁面來存放“struct mem_section”實例,稱為一個ROOT,  * 如果“struct mem_section”實例很多的話,可能需要分配多個物理頁面。  * ms->section_mem_map = sparse_encode_early_nid(nid) | SECTION_MARKED_PRESENT:  * 用來設置section在位  */ arm_memory_present(); /*  * sparse_init() needs the bootmem allocator up and running.  */ /* 初始化section機制  * 初始化mem_section數組使之與每一個section映射  * 初始化section_mem_map與page映射  */ sparse_init(); /*  * Now free the memory - free_area_init_node needs  * the sparse mem_map arrays initialized by sparse_init()  * for memmap_init_zone(), otherwise all PFNs are invalid.  */ zone_sizes_init(min, max_low, max_high); /*  * This doesn't seem to be used by the Linux memory manager any  * more, but is used by ll_rw_block. If we can get rid of it, we  * also get rid of some of the stuff above as well.  */ min_low_pfn = min; max_low_pfn = max_low; max_pfn = max_high; }

bootmem_init函數中提到了一個新的機制——Sparsemem Memory Model。之前在基礎知識中我們學過,內存的最基本單位是page,但在Sparse Memory模型中,section是管理內存online/offline的最小內存單元。在添加此模型的補丁中,作者描述了該模型的幾大優勢:

  1. 對於有內存空洞的設備,減少系統struct page使用的內存
  2. 內存熱插拔系統需要使用
  3. 在NUMA系統上支持內存的重疊

section就是幾個page組合而成,比page更大一些的內存區域,但又比node的范圍要小。這樣整個系統的物理內存就被分成一個個section,並由mem_section結構體表示。而這個結構體中保存了該section范圍的struct page結構體的地址。

bootmem_init函數中另一個重要的函數是zone_sizes_init, 先看以下zone_sizes_init的函數調用圖:

 

  1. calculate_node_totalpages:從名字就可以看出這個函數是用來統計node結點中的頁面數的,而統計方法如圖所示:

 

  1. free_area_init_core:主要完成struct pglist_data結構中的字段函數初始化,比如初始化pglist_data內部使用的鎖和隊列,並初始化它所管理的各個zone。

build_all_zonelists

pagin_init完成了分頁機制的初始化,然后bootmem_init完成了內存結點和內存域的初始化工作,此時,數據結構已經基本准備完畢,之后要做的就是將所有節點的內存域都鏈入到zonelists中,方便后面內存分配的工作。

build_all_zonelists中將大部分內存相關的工作都交給了__build_all_zonelists,后者又對系統中的各個NUMA結點分別調用了build_zonelists。

static void __build_all_zonelists(void *data) { int nid; int __maybe_unused cpu; pg_data_t *self = data; static DEFINE_SPINLOCK(lock); spin_lock(&lock); #ifdef CONFIG_NUMA  memset(node_load, 0, sizeof(node_load)); #endif  /*  * This node is hotadded and no memory is yet present. So just  * building zonelists is fine - no need to touch other nodes.  */ if (self && !node_online(self->node_id)) { build_zonelists(self); } else { for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); build_zonelists(pgdat); } ... } spin_unlock(&lock); }

for_each_online_node遍歷了系統中所有的活動結點。如果是UMA系統只有一個結點,build_zonelists只調用了一次,就對所有的內存創建了內存域列表。NUMA系統調用該函數的次數等於結點的個數,每次調用都會對一個不同的結點生成內存域數據。build_zonelists傳入的參數是一個指向pgdat_t實例的指針參數,該數據結構包含了結點的所有信息。由於UMA和NUMA架構下結點的層次結構有很大的區別,因此,內核分別提供了兩套不同的build_zonelists接口。但大體實現方法都是通過for循環遍歷所有結點並加入到zonelists中。

參考資料

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM