1. 啟動過程中的記憶體初始化 首先我們來看看start_kernel是如何初始化系統的, start_kerne定義在 "init/main.c?v=4.7, line 479" 其代碼很複雜, 我們只截取出其中與記憶體管理初始化相關的部分, 如下所示 table th:nth of type(1){ ...
1. 啟動過程中的記憶體初始化
首先我們來看看start_kernel是如何初始化系統的, start_kerne定義在init/main.c?v=4.7, line 479
其代碼很複雜, 我們只截取出其中與記憶體管理初始化相關的部分, 如下所示
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中設置一個最近存取頁面的高速緩存硬體機制,當進行存儲器訪問時,先檢查要訪問的頁面是否在高速緩存中. |
1.1 setup_arch函數初始化記憶體流程
前面我們的內核從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 | 初始化記憶體管理 |
該函數主要執行瞭如下操作
- 使用arm64_memblock_init來完成memblock機制的初始化工作, 至此memblock分配器接受系統中系統中記憶體的分配工作
- 調用paging_init來完成系統分頁機制的初始化工作, 建立頁表, 從而內核可以完成虛擬記憶體的映射和轉換工作
- 最後調用bootmem_init來完成實現buddy記憶體管理所需要的工作
1.2 (第一階段)啟動過程中的記憶體分配器
在初始化過程中, 還必須建立記憶體管理的數據結構, 以及很多事務. 因為內核在記憶體管理完全初始化之前就需要使用記憶體. 在系統啟動過程期間, 使用了額外的簡化悉尼股市的記憶體管理模塊, 然後在初始化完成後, 將舊的模塊丟棄掉.
這個階段的記憶體分配其實很簡單, 因此我們往往稱之為記憶體分配器(而不是記憶體管理器), 早期的內核中記憶體分配器使用的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做了初始化.
|
1.3 今日內容(第二階段(一)--初始化記憶體管理數據結構)
我們之前講了在memblock完成之後, 記憶體初始化開始進入第二階段, 第二階段是一個漫長的過程, 它執行了一系列複雜的操作, 從體繫結構相關信息的初始化慢慢向上層展開, 其主要執行瞭如下操作
特定於體繫結構的設置
在完成了基礎的記憶體結點和記憶體域的初始化工作以後, 我們必須剋服一些硬體的特殊設置
- 在初始化記憶體的結點和記憶體區域之前, 內核先通過pagging_init初始化了內核的分頁機制, 這樣我們的虛擬運行空間就初步建立, 並可以完成物理地址到虛擬地址空間的映射工作.
在arm64架構下, 內核在start_kernel()->setup_arch()
中通過arm64_memblock_init( )
完成了memblock的初始化之後, 接著通過setup_arch()->paging_init()開始初始化分頁機制
paging_init負責建立只能用於內核的頁表, 用戶空間是無法訪問的. 這對管理普通應用程式和內核訪問記憶體的方式,有深遠的影響
- 在分頁機制完成後, 內核通過setup_arch()->bootmem_init開始進行記憶體基本數據結構(記憶體結點pg_data_t, 記憶體域zone和頁幀)的初始化工作, 就是在這個函數中, 內核開始從體繫結構相關的部分逐漸展開到體繫結構無關的部分, 在zone_sizes_init->free_area_init_node中開始, 內核開始進行記憶體基本數據結構的初始化, 也不再依賴於特定體繫結構無關的層次
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伙伴系統.
2 初始化前的準備工作
2.1 回到setup_arch函數(當前已經完成的工作)
現在我們回到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()函數開始執行
體繫結構相關的代碼需要在啟動期間建立如下信息
- 系統中各個記憶體域的頁幀邊界,保存在max_zone_pfn數組
早期的內核還需記錄各結點頁幀的分配情況,保存在全局變數early_node_map中
內核提供了一個通用的框架, 用於將上述信息轉換為伙伴系統預期的節點和記憶體域數據結構, 但是在此之前各個體繫結構必須自行建立相關結構.
2.2 bootmem_init函數初始化記憶體結點和管理域
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();
}
2.3 zone_sizes_init函數
在初始化記憶體結點和記憶體域之前, 內核首先通過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)函數來完成,
3 free_area_init_nodes初始化NUMA管理數據結構
註意
此部分內容參照
free_area_init_nodes初始化了NUMA系統中所有結點的pg_data_t和zone、page的數據, 並列印了管理區信息, 該函數定義在mm/page_alloc.c?v=4.7, line 6460
3.1 代碼註釋
// 初始化各個節點的所有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);
}
}
3.2 設置可使用的頁幀編號
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提供的信息直接獲得。
3.3 構建其他記憶體域的頁幀區間
接下來構建其他記憶體域的頁幀區間,方法很直接:第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
3.4 建立結點數據結構
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標誌
4 free_area_init_node初始化UMA記憶體結點
free_area_init_nodes函數初始化所有結點的pg_data_t和zone、page的數據,並列印了管理區信息.
4.1 free_area_init_node函數註釋
該函數定義在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);
}
4.2 流程分析
- calculate_node_totalpages函數累計各個記憶體域的頁數,計算結點中頁的總數。對連續記憶體模型而言,這可以通過zone_sizes_init完成,但calculate_node_totalpages還考慮了記憶體空洞,該函數定義在mm/page_alloc.c, line 5789
以下例子取自一個UMA系統, 具有512 MiB物理記憶體。
> dmesg
...
On node 0 totalpages: 131056
- alloc_node_mem_map(pgdat)函數分配了該節點的頁面描述符數組[pgdat->node_mem_map數組的記憶體分配.
- 繼續調用free_area_init_core函數,繼續初始化該節點的pg_data_t結構,初始化zone以及page結構 , free_area_init_core函數是初始化zone的核心
4.3 alloc_node_mem_map函數
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的初始化
5 free_area_init_core初始化記憶體域zone
初始化記憶體域數據結構涉及的繁重工作由free_area_init_core執行,它會依次遍歷結點的所有記憶體域, 該函數定義在mm/page_alloc.c?v=4.7, line 5932
5.1 free_area_init_core函數代碼註釋
/*
* 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);
}
/* ...... */
}
5.2 流程講解
初始化記憶體域數據結構涉及的繁重工作由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
我們比較感興趣的是調用的兩個輔助函數
- zone_pcp_init嘗試初始化該記憶體域的per-CPU緩存, 定義在mm/page_alloc.c?v=4.7, line 5443
- init_currently_empty_zone初始化free_area列表,並將屬於該記憶體域的所有page實例都設置為初始預設值。正如前文的討論,調用了memmap_init_zone來初始化記憶體域的頁, 定義在mm/page_alloc.c?v=4.7, line 5458
我們還可以回想前文提到的,所有頁屬性起初都設置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);
}
/* ...... */
}
6 memmap_init初始化page頁面
在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的初始化
7 總結
7.1 start_kernel啟動流程
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;
|
7.2 pidhash_init配置高端記憶體
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]);
7.3 build_all_zonelists初始化每個記憶體節點的zonelists
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;
| 對於代碼中的判斷條件一般不會成立,因為頁數會最夠多(記憶體較大)