首先咱們來看看start_kernel是如何初始化系統的, start_kerne定義在init/main.c?v=4.7, line 479node
其代碼很複雜, 咱們只截取出其中與內存管理初始化相關的部分, 以下所示linux
asmlinkage __visible void __init start_kernel(void) { setup_arch(&command_line); mm_init_cpumask(&init_mm); setup_per_cpu_areas(); build_all_zonelists(NULL, NULL); page_alloc_init(); /* * These use large bootmem allocations and must precede * mem_init(); * kmem_cache_init(); */ mm_init(); kmem_cache_init_late(); kmemleak_init(); setup_per_cpu_pageset(); rest_init(); }
函數 | 功能 |
---|---|
setup_arch | 是一個特定於體系結構的設置函數, 其中一項任務是負責初始化自舉分配器 |
mm_init_cpumask | 初始化CPU屏蔽字 |
setup_per_cpu_areas | 函數(查看定義)給每一個CPU分配內存,並拷貝.data.percpu段的數據. 爲系統中的每一個CPU的per_cpu變量申請空間. 在SMP系統中, setup_per_cpu_areas初始化源代碼中(使用per_cpu宏)定義的靜態per-cpu變量, 這種變量對系統中每一個CPU都有一個獨立的副本. 此類變量保存在內核二進制影像的一個獨立的段中, setup_per_cpu_areas的目的就是爲系統中各個CPU分別建立一份這些數據的副本 在非SMP系統中這是一個空操做 |
build_all_zonelists | 創建並初始化結點和內存域的數據結構 |
mm_init | 創建了內核的內存分配器, 其中經過mem_init停用bootmem分配器並遷移到實際的內存管理器(好比夥伴系統) 而後調用kmem_cache_init函數初始化內核內部用於小塊內存區的分配器 |
kmem_cache_init_late | 在kmem_cache_init以後, 完善分配器的緩存機制, 當前3個可用的內核內存分配器slab, slob, slub都會定義此函數 |
kmemleak_init | Kmemleak工做於內核態,Kmemleak 提供了一種可選的內核泄漏檢測,其方法相似於跟蹤內存收集器。當獨立的對象沒有被釋放時,其報告記錄在 /sys/kernel/debug/kmemleak中, Kmemcheck可以幫助定位大多數內存錯誤的上下文 |
setup_per_cpu_pageset | 初始化CPU高速緩存行, 爲pagesets的第一個數組元素分配內存, 換句話說, 其實就是第一個系統處理器分配 因爲在分頁狀況下,每次存儲器訪問都要存取多級頁表,這就大大下降了訪問速度。因此,爲了提升速度,在CPU中設置一個最近存取頁面的高速緩存硬件機制,當進行存儲器訪問時,先檢查要訪問的頁面是否在高速緩存中. |
前面咱們的內核從start_kernel開始, 進入setup_arch(), 並完成了早期內存分配器的初始化和設置工做.算法
void __init setup_arch(char **cmdline_p) { /* 初始化memblock */ arm64_memblock_init( ); /* 分頁機制初始化 */ paging_init(); bootmem_init(); }
流程 | 描述 |
---|---|
arm64_memblock_init | 初始化memblock內存分配器 |
paging_init | 初始化分頁機制 |
bootmem_init | 初始化內存管理 |
該函數主要執行了以下操做c#
在初始化過程當中, 還必須創建內存管理的數據結構, 以及不少事務. 由於內核在內存管理徹底初始化以前就須要使用內存. 在系統啓動過程期間, 使用了額外的簡化悉尼股市的內存管理模塊, 而後在初始化完成後, 將舊的模塊丟棄掉.數組
這個階段的內存分配其實很簡單, 所以咱們每每稱之爲內存分配器(而不是內存管理器), 早期的內核中內存分配器使用的bootmem引導分配器, 它基於一個內存位圖bitmap, 使用最優適配算法來查找內存, 可是這個分配器有很大的缺陷, 最嚴重的就是內存碎片的問題, 所以在後來的內核中將其捨棄《而使用了新的memblock機制. memblock機制的初始化在arm64上是經過arm64_memblock_init函數來實現的緩存
start_kernel() |---->page_address_init() | 考慮支持高端內存 | 業務:初始化page_address_pool鏈表; | 將page_address_maps數組元素按索引降序插入 | page_address_pool鏈表; | 初始化page_address_htable數組. | |---->setup_arch(&command_line); | 初始化特定體系結構的內容 | |---->arm64_memblock_init( ); | 初始化引導階段的內存分配器memblock | |---->paging_init(); | 分頁機制初始化 | |---->bootmem_init(); [當前位置] | 始化內存數據結構包括內存節點, 內存域和頁幀page | |---->arm64_numa_init(); | 支持numa架構 | |---->zone_sizes_init(min, max); 來初始化節點和管理區的一些數據項 | |---->free_area_init_node | 初始化內存節點 | |---->free_area_init_core | 初始化zone | |---->memmap_init | 初始化page頁面 | |---->memblock_dump_all(); | 初始化完成, 顯示memblock的保留的全部內存信息 | |---->build_all_zonelist() | 爲系統中的zone創建後備zone的列表. | 全部zone的後備列表都在 | pglist_data->node_zonelists[0]中; | | 期間也對per-CPU變量boot_pageset作了初始化. |
咱們以前講了在memblock完成以後, 內存初始化開始進入第二階段, 第二階段是一個漫長的過程, 它執行了一系列複雜的操做, 從體系結構相關信息的初始化慢慢向上層展開, 其主要執行了以下操做安全
在完成了基礎的內存結點和內存域的初始化工做之後, 咱們必須克服一些硬件的特殊設置數據結構
在arm64架構下, 內核在start_kernel()->setup_arch()
中經過arm64_memblock_init( )
完成了memblock的初始化以後, 接着經過setup_arch()->paging_init()開始初始化分頁機制架構
paging_init負責創建只能用於內核的頁表, 用戶空間是沒法訪問的. 這對管理普通應用程序和內核訪問內存的方式,有深遠的影響app
bootmem_init() 始化內存數據結構包括內存節點, 內存域和頁幀page | |---->arm64_numa_init(); | 支持numa架構 | |---->zone_sizes_init(min, max); 來初始化節點和管理區的一些數據項 | |---->free_area_init_node | 初始化內存節點 | |---->free_area_init_core | 初始化zone | |---->memmap_init | 初始化page頁面 | |---->memblock_dump_all(); | 初始化完成, 顯示memblock的保留的全部內存信息
對相關數據結構的初始化是從全局啓動函數start_kernel中開始的, 該函數在加載內核並激活各個子系統以後執行. 因爲內存管理是內核一個很是重要的部分, 所以在特定體系結構的設置步驟中檢測並肯定系統中內存的分配狀況後, 會當即執行內存管理的初始化.
最後咱們的內存管理器已經初始化並設置完成, 能夠投入運行了, 所以內核將內存管理的工做從早期的內存分配器(bootmem或者memblock)移交到咱們的buddy夥伴系統.
如今咱們回到start_kernel()
->setup_arch()
函數
void __init setup_arch(char **cmdline_p) { /* 初始化memblock */ arm64_memblock_init( ); /* 分頁機制初始化 */ paging_init(); bootmem_init(); }
到目前位置咱們已經完成了以下工做
memblock已經經過arm64_memblock_init完成了初始化, 至此係統中的內存能夠經過memblock分配了
paging_init完成了分頁機制的初始化, 至此內核已經佈局了一套完整的虛擬內存空間
至此咱們全部的內存均可以經過memblock機制來分配和釋放, 儘管它實現的笨拙而簡易, 可是已經足夠咱們初始化階段使用了, 反正內核頁不可能指着它過一生, 而咱們也經過pagging_init建立了頁表, 爲內核提供了一套可供內核和進程運行的虛擬運行空間, 咱們能夠安全的進行內存的分配了
所以該是時候初始化咱們強大的buddy系統了.
內核接着setup_arch()->bootmem_init()函數開始執行
體系結構相關的代碼須要在啓動期間創建以下信息
早期的內核還需記錄各結點頁幀的分配狀況,保存在全局變量early_node_map中
內核提供了一個通用的框架, 用於將上述信息轉換爲夥伴系統預期的節點和內存域數據結構, 可是在此以前各個體系結構必須自行創建相關結構.
arm64架構下, 在setup_arch中經過paging_init函數初始化內核分頁機制以後, 內核經過bootmem_init()
開始完成內存結點和內存區域的初始化工做, 該函數定義在arch/arm64/mm/init.c, line 306
void __init bootmem_init(void) { unsigned long min, max; min = PFN_UP(memblock_start_of_DRAM()); max = PFN_DOWN(memblock_end_of_DRAM()); early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT); max_pfn = max_low_pfn = max; arm64_numa_init(); /* * Sparsemem tries to allocate bootmem in memory_present(), so must be * done after the fixed reservations. */ arm64_memory_present(); sparse_init(); zone_sizes_init(min, max); high_memory = __va((max << PAGE_SHIFT) - 1) + 1; memblock_dump_all(); }
在初始化內存結點和內存域以前, 內核首先經過setup_arch()-->bootmem_init()-->zone_sizes_init()來初始化節點和管理區的一些數據項, 其中關鍵的是初始化了系統中各個內存域的頁幀邊界,保存在max_zone_pfn數組.
[zone_sizes_init](zone_sizes_init函數定義在arch/arm64/mm/init.c?v=4.7, line 92, 因爲arm64支持NUMA和UMA兩種存儲器架構, 所以該函數依照NUMA和UMA, 有兩種不一樣的實現.函數定義在arch/arm64/mm/init.c?v=4.7, line 92, 因爲arm64支持NUMA和UMA兩種存儲器架構, 所以該函數依照NUMA和UMA, 有兩種不一樣的實現.
#ifdef CONFIG_NUMA static void __init zone_sizes_init(unsigned long min, unsigned long max) { unsigned long max_zone_pfns[MAX_NR_ZONES] = {0}; if (IS_ENABLED(CONFIG_ZONE_DMA)) max_zone_pfns[ZONE_DMA] = PFN_DOWN(max_zone_dma_phys()); max_zone_pfns[ZONE_NORMAL] = max; free_area_init_nodes(max_zone_pfns); } #else static void __init zone_sizes_init(unsigned long min, unsigned long max) { struct memblock_region *reg; unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES]; unsigned long max_dma = min; memset(zone_size, 0, sizeof(zone_size)); /* 4GB maximum for 32-bit only capable devices */ #ifdef CONFIG_ZONE_DMA max_dma = PFN_DOWN(arm64_dma_phys_limit); zone_size[ZONE_DMA] = max_dma - min; #endif zone_size[ZONE_NORMAL] = max - max_dma; memcpy(zhole_size, zone_size, sizeof(zhole_size)); for_each_memblock(memory, reg) { unsigned long start = memblock_region_memory_base_pfn(reg); unsigned long end = memblock_region_memory_end_pfn(reg); if (start >= max) continue; #ifdef CONFIG_ZONE_DMA if (start < max_dma) { unsigned long dma_end = min(end, max_dma); zhole_size[ZONE_DMA] -= dma_end - start; } #endif if (end > max_dma) { unsigned long normal_end = min(end, max); unsigned long normal_start = max(start, max_dma); zhole_size[ZONE_NORMAL] -= normal_end - normal_start; } } free_area_init_node(0, zone_size, min, zhole_size); } #endif /* CONFIG_NUMA */
在獲取了三個管理區的頁面數後, NUMA架構下經過free_area_init_nodes()來完成後續工做, 其中核心函數爲free_area_init_node(),用來針對特定的節點進行初始化, 因爲UMA架構下只有一個內存結點, 所以直接經過free_area_init_node來完成內存結點的初始化
截至到目前爲止, 體系結構相關的部分已經結束了, 各個體系結構已經自行創建了本身所需的一些底層數據結構, 這些結構創建好之後, 內核將繁重的內存數據結構建立和初始化的工做交給free_area_init_node(s)函數來完成,
注意
此部份內容參照
free_area_init_nodes初始化了NUMA系統中全部結點的pg_data_t和zone、page的數據, 並打印了管理區信息, 該函數定義在mm/page_alloc.c?v=4.7, line 6460
// 初始化各個節點的全部pg_data_t和zone、page的數據 void __init free_area_init_nodes(unsigned long *max_zone_pfn) { unsigned long start_pfn, end_pfn; int i, nid; /* Record where the zone boundaries are * 全局數組arch_zone_lowest_possible_pfn * 用來存儲各個內存域可以使用的最低內存頁幀編號 */ memset(arch_zone_lowest_possible_pfn, 0, sizeof(arch_zone_lowest_possible_pfn)); /* 全局數組arch_zone_highest_possible_pfn * 用來存儲各個內存域可以使用的最高內存頁幀編號 */ memset(arch_zone_highest_possible_pfn, 0, sizeof(arch_zone_highest_possible_pfn)); /* 輔助函數find_min_pfn_with_active_regions * 用於找到註冊的最低內存域中可用的編號最小的頁幀 */ arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); /* max_zone_pfn記錄了各個內存域包含的最大頁幀號 */ arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; /* 依次遍歷,肯定各個內存域的邊界 */ for (i = 1; i < MAX_NR_ZONES; i++) { /* 因爲ZONE_MOVABLE是一個虛擬內存域 * 不與真正的硬件內存域關聯 * 該內存域的邊界老是設置爲0 */ if (i == ZONE_MOVABLE) continue; /* 第n個內存域的最小頁幀 * 即前一個(第n-1個)內存域的最大頁幀 */ arch_zone_lowest_possible_pfn[i] = arch_zone_highest_possible_pfn[i-1]; /* 不出意外,當前內存域的最大頁幀 * 由max_zone_pfn給出 */ arch_zone_highest_possible_pfn[i] = max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); } arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; /* Find the PFNs that ZONE_MOVABLE begins at in each node */ memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); /* 用於計算進入ZONE_MOVABLE的內存數量 */ find_zone_movable_pfns_for_nodes(); /* Print out the zone ranges * 將各個內存域的最大、最小頁幀號顯示出來 */ pr_info("Zone ranges:\n"); for (i = 0; i < MAX_NR_ZONES; i++) { if (i == ZONE_MOVABLE) continue; pr_info(" %-8s ", zone_names[i]); if (arch_zone_lowest_possible_pfn[i] == arch_zone_highest_possible_pfn[i]) pr_cont("empty\n"); else pr_cont("[mem %#018Lx-%#018Lx]\n", (u64)arch_zone_lowest_possible_pfn[i] << PAGE_SHIFT, ((u64)arch_zone_highest_possible_pfn[i] << PAGE_SHIFT) - 1); } /* Print out the PFNs ZONE_MOVABLE begins at in each node */ pr_info("Movable zone start for each node\n"); for (i = 0; i < MAX_NUMNODES; i++) { /* 對每一個結點來講,zone_movable_pfn[node_id] * 表示ZONE_MOVABLE在movable_zone內存域中所取得內存的起始地址 * 內核確保這些頁將用於知足符合ZONE_MOVABLE職責的內存分配 */ if (zone_movable_pfn[i]) { /* 顯示各個內存域的分配狀況 */ pr_info(" Node %d: %#018Lx\n", i, (u64)zone_movable_pfn[i] << PAGE_SHIFT); } } /* Print out the early node map */ pr_info("Early memory node ranges\n"); for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) pr_info(" node %3d: [mem %#018Lx-%#018Lx]\n", nid, (u64)start_pfn << PAGE_SHIFT, ((u64)end_pfn << PAGE_SHIFT) - 1); /* Initialise every node */ mminit_verify_pageflags_layout(); setup_nr_node_ids(); /* 代碼遍歷全部的活動結點, * 並分別對各個結點調用free_area_init_node創建數據結構, * 該函數須要結點第一個可用的頁幀做爲一個參數, * 而find_min_pfn_for_node則從early_node_map數組提取該信息 */ for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); free_area_init_node(nid, NULL, find_min_pfn_for_node(nid), NULL); /* Any memory on that node * 根據node_present_pages字段判斷結點具備內存 * 則在結點位圖中設置N_HIGH_MEMORY標誌 * 該標誌只表示結點上存在普通或高端內存 * 所以check_for_regular_memory * 進一步檢查低於ZONE_HIGHMEM的內存域中是否有內存 * 並據此在結點位圖中相應地設置N_NORMAL_MEMORY */ if (pgdat->node_present_pages) node_set_state(nid, N_MEMORY); check_for_memory(pgdat, nid); } }
free_area_init_nodes首先必須分析並改寫特定於體系結構的代碼提供的信息。其中,須要對照在zone_max_pfn和zone_min_pfn中指定的內存域的邊界,計算各個內存域可以使用的最低和最高的頁幀編號。使用了兩個全局數組來存儲這些信息:
參見mm/page_alloc.c?v=4.7, line 259)
static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES]; static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];
經過max_zone_pfn傳遞給free_area_init_nodes的信息記錄了各個內存域包含的最大頁幀號。 free_area_init_nodes將該信息轉換爲一種更方便的表示形式,即以[low, high]形式描述各個內 存域的頁幀區間,存儲在前述的全局變量中(我省去了對這些變量填充字節0的初始化過程):
void __init free_area_init_nodes(unsigned long *max_zone_pfn) { /* ...... */ arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; /* Find the PFNs that ZONE_MOVABLE begins at in each node */ memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); /* 用於計算進入ZONE_MOVABLE的內存數量 */ find_zone_movable_pfns_for_nodes(); /* 依次遍歷,肯定各個內存域的邊界 */ for (i = 1; i < MAX_NR_ZONES; i++) { /* 因爲ZONE_MOVABLE是一個虛擬內存域 * 不與真正的硬件內存域關聯 * 該內存域的邊界老是設置爲0 */ if (i == ZONE_MOVABLE) continue; /* 第n個內存域的最小頁幀 * 即前一個(第n-1個)內存域的最大頁幀 */ arch_zone_lowest_possible_pfn[i] = arch_zone_highest_possible_pfn[i-1]; /* 不出意外,當前內存域的最大頁幀 * 由max_zone_pfn給出 */ arch_zone_highest_possible_pfn[i] = max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); } /* ...... */ }
輔助函數find_min_pfn_with_active_regions用於找到註冊的最低內存域中可用的編號最小的頁幀。該內存域沒必要必定是ZONE_DMA,例如,在計算機不須要DMA內存的狀況下也能夠是ZONE_NORMAL。最低內存域的最大頁幀號能夠從max_zone_pfn提供的信息直接得到。
接下來構建其餘內存域的頁幀區間,方法很直接:第n個內存域的最小頁幀,即前一個(第n-1個)內存域的最大頁幀。當前內存域的最大頁幀由max_zone_pfn給出
void __init free_area_init_nodes(unsigned long *max_zone_pfn) { /* ...... */ arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; /* Find the PFNs that ZONE_MOVABLE begins at in each node */ memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); /* 用於計算進入ZONE_MOVABLE的內存數量 */ find_zone_movable_pfns_for_nodes(); /* ...... */ }
因爲ZONE_MOVABLE是一個虛擬內存域,不與真正的硬件內存域關聯,該內存域的邊界老是設置爲0。回憶前文,可知只有在指定了內核命令行參數kernelcore或movablecore之一時,該內存域纔會存在. 該內存域通常開始於各個結點的某個特定內存域的某一頁幀號。相應的編號在find_zone_movable_pfns_for_nodes裏計算。
如今能夠向用戶提供一些有關已肯定的頁幀區間的信息。舉例來講,其中可能包括下列內容(輸出取自AMD64系統,有4 GiB物理內存):
> dmesg Zone PFN ranges: DMA 0 0 -> 4096 DMA32 4096 -> 1048576 Normal 1048576 -> 1245184
free_area_init_nodes剩餘的部分遍歷全部結點,分別創建其數據結構
void __init free_area_init_nodes(unsigned long *max_zone_pfn) { /* 輸出有關內存域的信息 */ /* ...... */ /* 代碼遍歷全部的活動結點, * 並分別對各個結點調用free_area_init_node創建數據結構, * 該函數須要結點第一個可用的頁幀做爲一個參數, * 而find_min_pfn_for_node則從early_node_map數組提取該信息 */ for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); free_area_init_node(nid, NULL, find_min_pfn_for_node(nid), NULL); /* Any memory on that node * 根據node_present_pages字段判斷結點具備內存 * 則在結點位圖中設置N_HIGH_MEMORY標誌 * 該標誌只表示結點上存在普通或高端內存 * 所以check_for_regular_memory * 進一步檢查低於ZONE_HIGHMEM的內存域中是否有內存 * 並據此在結點位圖中相應地設置N_NORMAL_MEMORY */ if (pgdat->node_present_pages) node_set_state(nid, N_MEMORY); check_for_memory(pgdat, nid); } /* ...... */ }
代碼遍歷全部活動結點,並分別對各個結點調用free_area_init_node創建數據結構。該函數須要結點第一個可用的頁幀做爲一個參數,而find_min_pfn_for_node則從early_node_map數組提取該信息。
若是根據node_present_pages字段判斷結點具備內存,則在結點位圖中設置N_HIGH_MEMORY標誌。咱們知道該標誌只表示結點上存在普通或高端內存,所以check_for_regular_memory進一步檢查低於ZONE_HIGHMEM的內存域中是否有內存,並據此在結點位圖中相應地設置N_NORMAL_MEMORY標誌
free_area_init_nodes函數初始化全部結點的pg_data_t和zone、page的數據,並打印了管理區信息.
該函數定義在mm/page_alloc.c?v=4.7, line 6076
void __paginginit free_area_init_node(int nid, unsigned long *zones_size, unsigned long node_start_pfn, unsigned long *zholes_size) { pg_data_t *pgdat = NODE_DATA(nid); unsigned long start_pfn = 0; unsigned long end_pfn = 0; /* pg_data_t should be reset to zero when it's allocated */ WARN_ON(pgdat->nr_zones || pgdat->classzone_idx); reset_deferred_meminit(pgdat); pgdat->node_id = nid; pgdat->node_start_pfn = node_start_pfn; #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP get_pfn_range_for_nid(nid, &start_pfn, &end_pfn); pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid, (u64)start_pfn << PAGE_SHIFT, end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0); #else start_pfn = node_start_pfn; #endif /* 首先累計各個內存域的頁數 * 計算結點中頁的總數 * 對連續內存模型而言 * 這能夠經過zone_sizes_init完成 * 但calculate_node_totalpages還考慮了內存空洞 */ calculate_node_totalpages(pgdat, start_pfn, end_pfn, zones_size, zholes_size); /* 分配了該節點的頁面描述符數組 * [pgdat->node_mem_map數組的內存分配 */ alloc_node_mem_map(pgdat); #ifdef CONFIG_FLAT_NODE_MEM_MAP printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", nid, (unsigned long)pgdat, (unsigned long)pgdat->node_mem_map); #endif /* 對該節點的每一個區[DMA,NORMAL,HIGH]的的結構進行初始化 */ free_area_init_core(pgdat); }
如下例子取自一個UMA系統, 具備512 MiB物理內存。
> dmesg ... On node 0 totalpages: 131056
alloc_node_mem_map負責初始化一個簡單但很是重要的數據結構。如上所述,系統中的各個物理內存頁,都對應着一個struct page實例。該結構的初始化由alloc_node_mem_map執行
static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat) { unsigned long __maybe_unused start = 0; unsigned long __maybe_unused offset = 0; /* Skip empty nodes */ if (!pgdat->node_spanned_pages) return; #ifdef CONFIG_FLAT_NODE_MEM_MAP start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1); offset = pgdat->node_start_pfn - start; /* ia64 gets its own node_mem_map, before this, without bootmem */ if (!pgdat->node_mem_map) { unsigned long size, end; struct page *map; /* * The zone's endpoints aren't required to be MAX_ORDER * aligned but the node_mem_map endpoints must be in order * for the buddy allocator to function correctly. */ end = pgdat_end_pfn(pgdat); end = ALIGN(end, MAX_ORDER_NR_PAGES); size = (end - start) * sizeof(struct page); map = alloc_remap(pgdat->node_id, size); if (!map) map = memblock_virt_alloc_node_nopanic(size, pgdat->node_id); pgdat->node_mem_map = map + offset; } #ifndef CONFIG_NEED_MULTIPLE_NODES /* * With no DISCONTIG, the global mem_map is just set as node 0's */ if (pgdat == NODE_DATA(0)) { mem_map = NODE_DATA(0)->node_mem_map; #if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM) if (page_to_pfn(mem_map) != pgdat->node_start_pfn) mem_map -= offset; #endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */ } #endif #endif /* CONFIG_FLAT_NODE_MEM_MAP */ }
沒有頁的空結點顯然能夠跳過。若是特定於體系結構的代碼還沒有創建內存映射(這是可能的,例如,在IA-64系統上),則必須分配與該結點關聯的全部struct page實例所需的內存。各個體系結構能夠爲此提供一個特定的函數。但目前只有在IA-32系統上使用不連續內存配置時是這樣。在全部其餘的配置上,則使用普通的自舉內存分配器進行分配。請注意,代碼將內存映射對齊到夥伴系統的最大分配階,由於要使全部的計算都工做正常,這是必需的。
指向該空間的指針不只保存在pglist_data實例中,還保存在全局變量mem_map中,前提是當前考察的結點是系統的第0個結點(若是系統只有一個內存結點,則老是這樣)。mem_map是一個全局數組,在講解內存管理時,咱們會常常遇到, 定義在mm/memory.c?v=4.7, line 85
struct page *mem_map;
而後在free_area_init_node函數的最後, 經過free_area_init_core來完成內存域zone的初始化
初始化內存域數據結構涉及的繁重工做由free_area_init_core執行,它會依次遍歷結點的全部內存域, 該函數定義在mm/page_alloc.c?v=4.7, line 5932
/* * Set up the zone data structures: * - mark all pages reserved * - mark all memory queues empty * - clear the memory bitmaps * * NOTE: pgdat should get zeroed by caller. */ static void __paginginit free_area_init_core(struct pglist_data *pgdat) { enum zone_type j; int nid = pgdat->node_id; int ret; /* 初始化pgdat->node_size_lock自旋鎖 */ pgdat_resize_init(pgdat); #ifdef CONFIG_NUMA_BALANCING spin_lock_init(&pgdat->numabalancing_migrate_lock); pgdat->numabalancing_migrate_nr_pages = 0; pgdat->numabalancing_migrate_next_window = jiffies; #endif #ifdef CONFIG_TRANSPARENT_HUGEPAGE spin_lock_init(&pgdat->split_queue_lock); INIT_LIST_HEAD(&pgdat->split_queue); pgdat->split_queue_len = 0; #endif /* 初始化pgdat->kswapd_wait等待隊列 */ init_waitqueue_head(&pgdat->kswapd_wait); /* 初始化頁換出守護進程建立空閒塊的大小 * 爲2^kswapd_max_order */ init_waitqueue_head(&pgdat->pfmemalloc_wait); #ifdef CONFIG_COMPACTION init_waitqueue_head(&pgdat->kcompactd_wait); #endif pgdat_page_ext_init(pgdat); /* 遍歷每一個管理區 */ for (j = 0; j < MAX_NR_ZONES; j++) { struct zone *zone = pgdat->node_zones + j; unsigned long size, realsize, freesize, memmap_pages; unsigned long zone_start_pfn = zone->zone_start_pfn; /* size爲該管理區中的頁框數,包括洞 */ size = zone->spanned_pages; /* realsize爲管理區中的頁框數,不包括洞 / realsize = freesize = zone->present_pages; /* * Adjust freesize so that it accounts for how much memory * is used by this zone for memmap. This affects the watermark * and per-cpu initialisations * 調整realsize的大小,即減去page結構體佔用的內存大小 */ /* memmap_pags爲包括洞的全部頁框的page結構體所佔的大小 */ memmap_pages = calc_memmap_size(size, realsize); if (!is_highmem_idx(j)) { if (freesize >= memmap_pages) { freesize -= memmap_pages; if (memmap_pages) printk(KERN_DEBUG " %s zone: %lu pages used for memmap\n", zone_names[j], memmap_pages); } else /* 內存不夠存放page結構體 */ pr_warn(" %s zone: %lu pages exceeds freesize %lu\n", zone_names[j], memmap_pages, freesize); } /* Account for reserved pages * 調整realsize的大小,即減去DMA保留頁的大小 */ if (j == 0 && freesize > dma_reserve) { freesize -= dma_reserve; printk(KERN_DEBUG " %s zone: %lu pages reserved\n", zone_names[0], dma_reserve); } if (!is_highmem_idx(j)) nr_kernel_pages += freesize; /* Charge for highmem memmap if there are enough kernel pages */ else if (nr_kernel_pages > memmap_pages * 2) nr_kernel_pages -= memmap_pages; nr_all_pages += freesize; /* * Set an approximate value for lowmem here, it will be adjusted * when the bootmem allocator frees pages into the buddy system. * And all highmem pages will be managed by the buddy system. */ /* 設置zone->spanned_pages爲包括洞的頁框數 */ zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; #ifdef CONFIG_NUMA /* 設置zone中的節點標識符 */ zone->node = nid; /* 設置可回收頁面比率 */ zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) / 100; /* 設置slab回收緩存頁的比率 */ zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; #endif /* 設置zone的名稱 */ zone->name = zone_names[j]; /* 初始化各類鎖 */ spin_lock_init(&zone->lock); spin_lock_init(&zone->lru_lock); zone_seqlock_init(zone); /* 設置管理區屬於的節點對應的pg_data_t結構 */ zone->zone_pgdat = pgdat; /* 初始化cpu的頁面緩存 */ zone_pcp_init(zone); /* For bootup, initialized properly in watermark setup */ mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages); /* 初始化lru相關成員 */ lruvec_init(&zone->lruvec); if (!size) continue; set_pageblock_order(); /* 定義了CONFIG_SPARSEMEM該函數爲空 */ setup_usemap(pgdat, zone, zone_start_pfn, size); /* 設置pgdat->nr_zones和zone->zone_start_pfn成員 * 初始化zone->free_area成員 * 初始化zone->wait_table相關成員 */ ret = init_currently_empty_zone(zone, zone_start_pfn, size); BUG_ON(ret); /* 初始化該zone對應的page結構 */ memmap_init(size, nid, j, zone_start_pfn); } /* ...... */ }
初始化內存域數據結構涉及的繁重工做由free_area_init_core執行,它會依次遍歷結點的全部內存域
static void __paginginit free_area_init_core(struct pglist_data *pgdat) { enum zone_type j; int nid = pgdat->node_id; int ret; /* ...... */ /* 遍歷每一個管理區 */ for (j = 0; j < MAX_NR_ZONES; j++) { struct zone *zone = pgdat->node_zones + j; unsigned long size, realsize, freesize, memmap_pages; unsigned long zone_start_pfn = zone->zone_start_pfn; /* size爲該管理區中的頁框數,包括洞 */ size = zone->spanned_pages; /* realsize爲管理區中的頁框數,不包括洞 / realsize = freesize = zone->present_pages; /* ...... */ }
內存域的真實長度,可經過跨越的頁數減去空洞覆蓋的頁數而獲得。這兩個值是經過兩個輔助函數計算的,我不會更詳細地討論了。其複雜性實質上取決於內存模型和所選定的配置選項,但全部變體最終都沒有什麼意外之處
static void __paginginit free_area_init_core(struct pglist_data *pgdat) { /* ...... */ if (!is_highmem_idx(j)) nr_kernel_pages += freesize; /* Charge for highmem memmap if there are enough kernel pages */ else if (nr_kernel_pages > memmap_pages * 2) nr_kernel_pages -= memmap_pages; nr_all_pages += freesize; /* * Set an approximate value for lowmem here, it will be adjusted * when the bootmem allocator frees pages into the buddy system. * And all highmem pages will be managed by the buddy system. */ /* 設置zone->spanned_pages爲包括洞的頁框數 */ zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; #ifdef CONFIG_NUMA /* 設置zone中的節點標識符 */ zone->node = nid; /* 設置可回收頁面比率 */ zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) / 100; /* 設置slab回收緩存頁的比率 */ zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; #endif /* 設置zone的名稱 */ zone->name = zone_names[j]; /* 初始化各類鎖 */ spin_lock_init(&zone->lock); spin_lock_init(&zone->lru_lock); zone_seqlock_init(zone); /* 設置管理區屬於的節點對應的pg_data_t結構 */ zone->zone_pgdat = pgdat; /* ...... */ }
內核使用兩個全局變量跟蹤系統中的頁數。nr_kernel_pages統計全部一致映射的頁,而nr_all_pages還包括高端內存頁在內free_area_init_core始化爲0
咱們比較感興趣的是調用的兩個輔助函數
咱們還能夠回想前文提到的,全部頁屬性起初都設置MIGRATE_MOVABLE。 此外,空閒列表是在zone_init_free_lists中初始化的
static void __paginginit free_area_init_core(struct pglist_data *pgdat) { /* ...... */ { /* 初始化cpu的頁面緩存 */ zone_pcp_init(zone); /* 設置pgdat->nr_zones和zone->zone_start_pfn成員 * 初始化zone->free_area成員 * 初始化zone->wait_table相關成員 */ ret = init_currently_empty_zone(zone, zone_start_pfn, size); BUG_ON(ret); /* 初始化該zone對應的page結構 */ memmap_init(size, nid, j, zone_start_pfn); } /* ...... */ }
在free_area_init_core初始化內存管理區zone的過程當中, 經過memmap_init函數對每一個內存管理區zone的page內存進行了初始化
memmap_init函數定義在mm/page_alloc.c?v=4.7, line
#ifndef __HAVE_ARCH_MEMMAP_INIT #define memmap_init(size, nid, zone, start_pfn) \ memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY) #endif
memmap_init_zone函數完成了page的初始化工做, 該函數定義在mm/page_alloc.c?v=4.7, line 5139
至此,節點和管理區的關鍵數據已完成初始化,內核在後面爲內存管理作得一個準備工做就是將全部節點的管理區都鏈入到zonelist中,便於後面內存分配工做的進行
內核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化
start_kernel() |---->page_address_init() | 考慮支持高端內存 | 業務:初始化page_address_pool鏈表; | 將page_address_maps數組元素按索引降序插入 | page_address_pool鏈表; | 初始化page_address_htable數組. | |---->setup_arch(&command_line); | |---->setup_per_cpu_areas(); | 爲per-CPU變量分配空間 | |---->build_all_zonelist() | 爲系統中的zone創建後備zone的列表. | 全部zone的後備列表都在 | pglist_data->node_zonelists[0]中; | | 期間也對per-CPU變量boot_pageset作了初始化. | |---->page_alloc_init() |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); | 不考慮熱插拔CPU | |---->pidhash_init() | 詳見下文. | 根據低端內存頁數和散列度,分配hash空間,並賦予pid_hash | |---->vfs_caches_init_early() |---->dcache_init_early() | dentry_hashtable空間,d_hash_shift, h_hash_mask賦值; | 同pidhash_init(); | 區別: | 散列度變化了(13 - PAGE_SHIFT); | 傳入alloc_large_system_hash的最後參數值爲0; | |---->inode_init_early() | inode_hashtable空間,i_hash_shift, i_hash_mask賦值; | 同pidhash_init(); | 區別: | 散列度變化了(14 - PAGE_SHIFT); | 傳入alloc_large_system_hash的最後參數值爲0; |
void pidhash_init(void) |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), | 0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096); | 根據nr_kernel_pages(低端內存的頁數),分配哈希數組,以及各個哈希 | 數組元素下的哈希鏈表的空間,原理以下: | number = nr_kernel_pages; | number >= (18 - PAGE_SHIFT) 根據散列度得到數組元素個數 | number = roundup_pow_of_two(number); | pidhash_shift = max{x | 2**x <= number} | size = number * sizeof(*pid_hash); | 使用位圖分配器分配size空間,將返回值付給pid_hash; | |---->pidhash_size = 1 << pidhash_shift; | |---->for(i = 0; i < pidhash_size; i++) | INIT_HLIST_HEAD(&pid_hash[i]);
void build_all_zonelists(void) |---->set_zonelist_order() |---->current_zonelist_order = ZONELIST_ORDER_ZONE; | |---->__build_all_zonelists(NULL); | Memory不支持熱插拔, 爲每一個zone創建後備的zone, | 每一個zone及本身後備的zone,造成zonelist | |---->pg_data_t *pgdat = NULL; | pgdat = &contig_page_data;(單node) | |---->build_zonelists(pgdat); | 爲每一個zone創建後備zone的列表 | |---->struct zonelist *zonelist = NULL; | enum zone_type j; | zonelist = &pgdat->node_zonelists[0]; | |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); | 爲pgdat->node_zones[0]創建後備的zone,node_zones[0]後備的zone | 存儲在node_zonelist[0]內,對於node_zone[0]的後備zone,其後備的zone | 鏈表以下(只考慮UMA體系,並且不考慮ZONE_DMA): | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; | node_zonelist[0]._zonerefs[0].zone_idx = 2; | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; | node_zonelist[0]._zonerefs[1].zone_idx = 1; | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; | node_zonelist[0]._zonerefs[2].zone_idx = 0; | | zonelist->_zonerefs[3].zone = NULL; | zonelist->_zonerefs[3].zone_idx = 0; | |---->build_zonelist_cache(pgdat); |---->pdat->node_zonelists[0].zlcache_ptr = NULL; | UMA體系結構 | |---->for_each_possible_cpu(cpu) | setup_pageset(&per_cpu(boot_pageset, cpu), 0); |詳見下文 |---->vm_total_pages = nr_free_pagecache_pages(); | 業務:得到全部zone中的present_pages總和. | |---->page_group_by_mobility_disabled = 0; | 對於代碼中的判斷條件通常不會成立,由於頁數會最夠多(內存較大)