兩個問題:
1、系統是怎么知道物理內存的?linux內存管理學習(1):物理內存探測
2、在內存管理真正初始化之前,內核的代碼執行需要分配內存該怎么處理?
在Linux內核啟動初期,完整的內存管理系統如Buddy System和Slab分配器尚未初始化完成。
此時,內核通過memblock機制臨時管理物理內存空間。memblock作為早期內存管理器,負責記錄所有可用的DRAM區域,并處理啟動階段的內存分配與保留請求,例如為內核代碼、設備樹或初始化數據分配內存。
當內核繼續初始化并建立起Buddy System和Slab分配器等核心內存管理組件后,會在mem_init()?函數中完成內存管理權的移交。
此時,memblock分配器會將剩余的內存釋放給Buddy System統一管理,后續所有動態內存分配均由Buddy System和Slab分配器等高級機制接管,而memblock僅保留調試或特殊場景下的輔助功能。
分析memblock算法,可以從幾點入手:
1、 memblock算法初始化;
2、 memblock算法管理內存的申請和釋放;
在分析 memblock 之前,我們需要先理清系統內存的總體使用情況。內存按照用途和生命周期可以分為三類:
(1)靜態內存:永久分配給內核的固定內存區域,不可被動態分配或回收。包含內容如:
- 內核代碼段(_text? ~ _etext?):存放內核的可執行代碼。
- 內核數據段(_data? ~ _edata?):存放全局變量、靜態變量等。
- BSS段(__bss_start? ~ __bss_stop?):存放未初始化的靜態變量。
- 設備樹(FDT, Flattened Device Tree):描述硬件信息,由 Bootloader 傳遞。
- initramfs/initrd:臨時根文件系統,用于早期用戶空間初始化。
這些區域在系統啟動時就被占用,不會被釋放或重新分配。必須通過 memblock_reserve() 進行保護,防止被錯誤分配。
(2)預留內存:系統預先保留的專用內存,通常用于硬件加速或特殊設備。
- GPU/Camera/VPU:多媒體處理需要大塊連續物理內存(如 64MB~1GB)
- DMA 緩沖區:某些外設要求物理連續內存(如 DMA Engine)
- 安全相關區域:如 TEE(Trusted Execution Environment)占用的內存
這些區域不參與常規內存分配,但可能由驅動按需啟用或釋放。通過 memblock_reserve()? 或設備樹 reserved-memory? 節點預留
(3)動態內存:內核可自由分配和管理的物理內存,是系統最寶貴的資源
- 啟動初期:由 memblock? 進行簡單分配
- Buddy System 就緒后:由頁分配器(alloc_pages()?)管理,支持按頁分配
- SLAB/SLUB 就緒后:提供小塊內存分配(kmalloc()?)
可被用戶空間(通過 mmap)或內核(vmalloc、kmalloc)動態使用,可能因內存碎片或外設占用而面臨大塊連續內存不足的問題
其中memblock_reserved_init_regions主要包括上述靜態內存和預留內存空間
一、MEMBLOCK 內存分配器進行初始化
初始化入口:
start_kernel--> setup_arch--> e820__memblock_setup
內存memblock基本初始化包括兩個部分:
- 根據硬件實際的物理內存初始化內核內存可見區域
- 標記內核已經使用的內存 (設備樹、內核鏡像等)
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS];
#endif/** memblock 內存分配器的全局實例初始化* 使用 __initdata_memblock 標記表示該數據結構僅在內核初始化階段使用* 初始化完成后這部分內存可以被釋放*/
struct memblock memblock __initdata_memblock = {/* 可用內存區域(memory)初始化 */.memory.regions = memblock_memory_init_regions, /* 初始靜態分配的內存區域數組 */.memory.cnt = 1, /* 初始設為1表示有一個空條目(dummy entry) */.memory.max = INIT_MEMBLOCK_REGIONS, /* 初始最大區域數,通常為128 */.memory.name = "memory", /* 用于調試識別的名稱 *//* 保留內存區域(reserved)初始化 */ .reserved.regions = memblock_reserved_init_regions, /* 初始靜態分配的保留區域數組 */.reserved.cnt = 1, /* 初始設為1表示有一個空條目 */.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS, /* 初始最大保留區域數,通常為128 */.reserved.name = "reserved", /* 用于調試識別的名稱 *//* 全局控制參數初始化 */.bottom_up = false, /* 默認從高地址向低地址分配策略 */.current_limit = MEMBLOCK_ALLOC_ANYWHERE, /* 初始無分配地址限制 */
};
memblock把物理內存劃分為若干內存區,按使用類型分別放在memory和reserved兩個集合(數組)中,memory即動態內存的集合,reserved集合包括靜態內存和預留內存
?
?
每個數組包含了 128 個內存區域。我們可以在 INIT_MEMBLOCK_REGIONS 宏定義中看到它:
#define INIT_MEMBLOCK_REGIONS 128
#define INIT_PHYSMEM_REGIONS 4#ifndef INIT_MEMBLOCK_RESERVED_REGIONS
# define INIT_MEMBLOCK_RESERVED_REGIONS INIT_MEMBLOCK_REGIONS
#endif
內核和memblock相關的數據結構:
// 1、struct memblock 結構體描述分配器整體特性
struct memblock {/* 內存分配方向控制 */bool bottom_up; /* true: 從低地址向高地址分配內存;false: 從高地址向低地址分配(默認) *//* 內存分配的安全邊界 */phys_addr_t current_limit; /* memblock_alloc()能分配的最高物理地址,用于防止分配到未映射或不安全的區域 */struct memblock_type memory; /* 所有可用RAM區域的連續列表,從固件獲取(如e820/EFI內存映射) */struct memblock_type reserved; /* 所有保留/已分配的區域,包括:* - 內核靜態區域(代碼段、數據段、BSS段)* - 設備保留內存(GPU、DMA緩沖區)* - 早期動態分配的內存 */
};// 2、struct memblock_type 結構體用于維護特定內存類型集合
struct memblock_type {unsigned long cnt; /* 當前實際存儲的內存區域數量。例如:系統中有3塊可用的物理內存區域 */unsigned long max; /* regions數組的當前最大容量,當cnt == max時需要動態擴容 */phys_addr_t total_size; /* 該類型所有內存區域的總大小。例如:所有保留區域加起來共256MB */struct memblock_region *regions; /* 動態分配的內存區域數組。每個元素描述一個連續內存區域 */char *name; /* 該內存類型的名稱字符串。例如:"memory"、"reserved"等,主要用于調試輸出 */
};// 3、struct memblock_region 結構體代表被管理的內存區塊
struct memblock_region {phys_addr_t base; /* 內存區域的起始物理地址。例如:0x80000000(2GB處開始)*/phys_addr_t size; /* 內存區域的長度(字節數)。例如:0x20000000(512MB大小)*/enum memblock_flags flags; /* 區域屬性標志位,可能取值:* MEMBLOCK_NONE - 默認無特殊屬性* MEMBLOCK_HOTPLUG - 支持熱插拔的內存* MEMBLOCK_MIRROR - 鏡像內存區域* MEMBLOCK_NOMAP - 不映射到內核地址空間 */#ifdef CONFIG_NUMA /* 僅在NUMA(非統一內存訪問)系統生效 */int nid; /* NUMA節點ID,表示該內存屬于哪個物理節點。例如:0表示第一個NUMA節點 */
#endif
};
這三個結構體: memblock, memblock_type 和 memblock_region 是 Memblock 的主要組成部分。現在我們可以進一步了解 Memblock 和 它的初始化過程了。
1、e820__memblock_setup
這段代碼是 Linux 內核中用于初始化內存塊(memblock)分配器的函數,主要作用是根據 BIOS 提供的 E820 內存映射表來設置系統的物理內存布局。
// 將BIOS提供的E820內存信息導入memblock分配器
void __init e820__memblock_setup(void)
{... .../** 允許memblock動態擴容:* - 初始靜態分配128個區域(INIT_MEMBLOCK_REGIONS)* - 當E820條目超過128時自動擴容* - 安全原因:此時已知道保留區域,擴容不會覆蓋關鍵內存*/memblock_allow_resize();/* 遍歷所有E820條目 */for (i = 0; i < e820_table->nr_entries; i++) {struct e820_entry *entry = &e820_table->entries[i];/* 檢查地址范圍是否溢出 */end = entry->addr + entry->size;if (end != (resource_size_t)end)continue; // 跳過非法地址范圍/* 處理特殊保留內存(如調試保留區) */if (entry->type == E820_TYPE_SOFT_RESERVED) {memblock_reserve(entry->addr, entry->size); // 加入reserved列表continue;}/* 僅處理可用內存類型 */if (entry->type != E820_TYPE_RAM && // 普通可用內存entry->type != E820_TYPE_RESERVED_KERN) // 內核保留可用內存continue;/* 將可用內存加入memblock.memory */memblock_add(entry->addr, entry->size);}/** 內存對齊修剪:* - 確保所有區域邊界按PAGE_SIZE(通常4K)對齊* - 避免Buddy System出現部分頁的問題*/memblock_trim_memory(PAGE_SIZE);/* 打印memblock最終狀態(調試用) */memblock_dump_all();
}
- 遍歷E820內存映射表,將可用內存添加到memblock.memory
- 將特殊保留內存標記到memblock.reserved
- 確保所有內存區域按頁對齊,打印最終內存布局信息
接著看memblock_add
1.1 memblock_add
// memblock_add - 添加新的內存區域到memblock管理
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{// 計算區域結束地址(包含邊界)phys_addr_t end = base + size - 1;memblock_dbg("%s: [%pa-%pa] %pS\n", __func__,&base, &end, (void *)_RET_IP_);return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}// memblock_add_range - 將新的內存區域添加到memblock管理系統中
static int __init_memblock memblock_add_range(...)
{... .../* 空數組特殊處理(首次添加)*/if (type->regions[0].size == 0) {type->regions[0] = (struct memblock_region){.base = base, .size = size, .flags = flags, .nid = nid};type->total_size = size;return 0;}repeat:/* 兩階段控制:重置基礎參數 */base = obase;int nr_new = 0; // 需要新增的區域計數/* 遍歷現有區域處理重疊 */for_each_memblock_type(idx, type, rgn) {phys_addr_t rbase = rgn->base;phys_addr_t rend = rbase + rgn->size;/* 跳過無重疊區域 */if (rbase >= end) break;if (rend <= base) continue;/* 處理左側非重疊部分 */if (rbase > base) {nr_new++;if (insert) memblock_insert_region(type, idx++, base, rbase-base, nid, flags);}base = min(rend, end); // 推進處理位置}/* 處理右側剩余部分 */if (base < end) {nr_new++;if (insert)memblock_insert_region(type, idx, base, end-base, nid, flags);}/* 第一階段:數組擴容 */if (!insert) {while (type->cnt + nr_new > type->max)if (memblock_double_array(type, obase, size) < 0)return -ENOMEM;insert = true;goto repeat; // 跳轉執行第二階段} /* 第二階段:合并區域 */else {memblock_merge_regions(type);return 0;}
}
memblock內存管理機制的核心工作流程可分為四個關鍵步驟:
- 初始化處理:當檢測到memblock管理的內存區域為空時,直接將當前待添加的內存空間作為首個管理單元插入。
- 重疊檢測與處理:在非空狀態下,算法會先檢查新區域與現有區域是否存在地址重疊。若發現重疊,則自動剔除重疊部分,僅將有效的非重疊內存段加入管理系統。
- 動態擴容機制:當預設的128個region管理單元不足時,通過memblock_double_array()函數動態擴展存儲空間,確保能容納更多內存區域信息。
- 區域合并優化:最終調用memblock_merge_regions()函數,將地址連續且屬性相同的相鄰內存區域合并為更大的連續區塊。
這套機制的核心作用是將BIOS提供的e820內存布局信息,特別是標記為"usable"的可用內存區域,精確轉換為memblock.memory中的規范化管理單元。e820探測到多少個usable內存塊,就對應多少個region,這些region嚴格按地址從低到高排列,且保證沒有重疊。
圖示,詳細見Linux Kernel:啟動時內存管理(MemBlock 分配器)一、Bootmem 與 Memblock 系統初始化 - 掘金
?
?
memblock_trim_memory:將 memblock 中所有內存區域的起始地址(start)和結束地址(end)按照指定的對齊大小(通常為 PAGE_SIZE?)進行邊界調整,確保每個區域滿足以下條件:
- 起始地址(start):向上對齊到 PAGE_SIZE? 的倍數
- 結束地址(end):向下對齊到 PAGE_SIZE 的倍數
memblock_dump_all:內存布局信息打印
二、memblock 內存分配與回收
2.1、memblock_alloc
使用默認參數分配內存(自動選擇位置),具體調用流程與內容如下:
--> memblock_alloc--> memblock_alloc_try_nid(--> memblock_alloc_internal(size, align, min_addr, max_addr, nid, false);static void *__init memblock_alloc_internal(phys_addr_t size, phys_addr_t align,phys_addr_t min_addr, phys_addr_t max_addr,int nid, bool exact_nid)
{... ...// 安全檢查:如果slab分配器已就緒(memblock不應再被使用)if (WARN_ON_ONCE(slab_is_available()))return kzalloc_node(size, GFP_NOWAIT, nid); // 降級到slab分配// 確保分配范圍不超過memblock的當前限制if (max_addr > memblock.current_limit)max_addr = memblock.current_limit;// 首次嘗試:在[min_addr, max_addr]范圍內分配alloc = memblock_alloc_range_nid(size, align, min_addr, max_addr, nid,exact_nid);// 若失敗,放寬限制:允許分配低于min_addr的內存if (!alloc && min_addr)alloc = memblock_alloc_range_nid(size, align, 0, max_addr, nid,exact_nid);// 轉換物理地址為虛擬地址if (!alloc)return NULL;return phys_to_virt(alloc);
}// memblock_alloc_range_nid - 在指定范圍和NUMA節點分配啟動內存塊
phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size,phys_addr_t align, phys_addr_t start,phys_addr_t end, int nid,bool exact_nid)
{... ...// 4. 主要分配邏輯(可能多次嘗試)
again:// 4.1 優先嘗試在指定節點和范圍內分配found = memblock_find_in_range_node(size, align, start, end, nid,flags);if (found && !memblock_reserve(found, size)) // 成功則保留該區域goto done;// 4.2 若允許回退且指定了具體節點,嘗試任意節點分配if (nid != NUMA_NO_NODE && !exact_nid) {found = memblock_find_in_range_node(size, align, start,end, NUMA_NO_NODE,flags);if (found && !memblock_reserve(found, size))goto done;}// 4.3 處理內存鏡像情況:首次失敗后嘗試非鏡像區域if (flags & MEMBLOCK_MIRROR) {flags &= ~MEMBLOCK_MIRROR; // 清除鏡像標志pr_warn("Could not allocate %pap bytes of mirrored memory\n",&size);goto again; // 重新嘗試分配}// 5. 所有嘗試失敗后返回0return 0;// 6. 分配成功后的處理
done:/* 跳過kasan_init的高頻分配檢測 */if (end != MEMBLOCK_ALLOC_KASAN)/** 設置min_count=0避免kmemleak報告內存泄漏。* 因為這些內存塊通常只通過物理地址引用,kmemleak無法追蹤。*/kmemleak_alloc_phys(found, size, 0, 0);return found; // 返回分配的內存物理地址
}// 在指定范圍和節點內查找空閑區域
static phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size, phys_addr_t align,phys_addr_t start, phys_addr_t end,int nid, enum memblock_flags flags)
{/* 1. 處理特殊end標志 */if (end == MEMBLOCK_ALLOC_ACCESSIBLE ||end == MEMBLOCK_ALLOC_KASAN)end = memblock.current_limit; // 使用memblock的當前地址限制/* 2. 避免分配第一個物理頁(0x0-0xFFF)*/start = max_t(phys_addr_t, start, PAGE_SIZE); // 至少從PAGE_SIZE開始end = max(start, end); // 確保end >= start/* 3. 根據分配策略選擇搜索方向 */if (memblock_bottom_up())// 自底向上搜索(低地址優先)return __memblock_find_range_bottom_up(start, end, size, align,nid, flags);else// 自頂向下搜索(高地址優先,默認策略)return __memblock_find_range_top_down(start, end, size, align,nid, flags);
}
主要邏輯:從可用內存區中找一塊大小為 size 的物理內存區塊, 然后調用 memblock_reseve() 函數在找到的情況下,將這塊物理內存區塊加入到預留區內
2.2、memblock_free
釋放已分配的內存區域,具體調用流程與內容如下:
/*** memblock_free - 釋放由memblock_alloc_xx()分配的啟動內存塊* 釋放先前分配的內存塊,但不會將內存返還給伙伴系統(Buddy Allocator)。* 僅從memblock.reserved中移除標記。*/
int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size)
{... ...// 1. 通知kmemleak停止追蹤該物理內存范圍kmemleak_free_part_phys(base, size);// 2. 從memblock.reserved中移除該區域return memblock_remove_range(&memblock.reserved, base, size);
}/*** kmemleak_free_part_phys - 解除對物理內存范圍的泄漏追蹤* 將物理地址轉換為虛擬地址后調用標準釋放接口。*/
void __ref kmemleak_free_part_phys(phys_addr_t phys, size_t size)
{// 僅處理低端內存(若未配置HIGHMEM或地址在lowmem范圍內)if (!IS_ENABLED(CONFIG_HIGHMEM) || PHYS_PFN(phys) < max_low_pfn)kmemleak_free_part(__va(phys), size); // __va轉換為虛擬地址
}// memblock_remove_range - 從指定memblock類型中移除內存區域
static int __init_memblock memblock_remove_range(struct memblock_type *type,phys_addr_t base, phys_addr_t size)
{ ... ...// 1. 定位與目標范圍重疊的region區間ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);// 2. 反向遍歷并移除region(避免索引錯位)for (i = end_rgn - 1; i >= start_rgn; i--)memblock_remove_region(type, i);... ...
}// 從memblock類型中移除指定region: 該函數會更新總大小、壓縮region數組,并處理空數組的特殊情況。
static void __init_memblock memblock_remove_region(struct memblock_type *type, unsigned long r)
{// 1. 從總大小中減去被移除region的大小type->total_size -= type->regions[r].size;// 2. 移動后續region填補空缺(內存拷貝)memmove(&type->regions[r], &type->regions[r + 1],(type->cnt - (r + 1)) * sizeof(type->regions[r]));type->cnt--; // region計數減1/* 3. 處理空數組的特殊情況 */if (type->cnt == 0) {WARN_ON(type->total_size != 0); // 驗證一致性:總大小應為0// 重置為初始空狀態type->cnt = 1;type->regions[0].base = 0;type->regions[0].size = 0;type->regions[0].flags = 0;memblock_set_region_node(&type->regions[0], MAX_NUMNODES);}
}
memblock_isolate_range() 將要移除的物理內存區從 reserved 內存區中分離出來,將 start_rgn 和 end_rgn(該內存區塊的起始、結束索引號)返回回去
memblock_remove_region() 將這些索引對應的內存區塊從內存區中移除,這里具體做法為調用 memmove 函數將 r 索引之后的內存區塊全部往前挪一個位置,這樣 r 索引對應的內存區塊就被移除了,如果移除之后,內存區不含有任何內存區塊,那么就初始化該內存區
三、memblock釋放和移交管理權
當內核完成關鍵子系統初始化后,內存管理將進入從 memblock 到伙伴系統(Buddy System)的移交階段。這一重要過渡由 mm_init() 函數主導,其核心是通過 memblock_free_all() 實現控制權轉移,具體流程如下:
--> mm_init--> mem_init--> memblock_free_all
四、memblock內存分配API概覽
4.1、初始化與設置
- memblock_allow_resize()?: 允許動態調整memblock數組大小
- memblock_set_bottom_up()?: 設置內存分配方向(自底向上或自頂向下)
- memblock_set_current_limit()?: 設置當前內存分配限制地址
4.2、內存區域管理
- memblock_add(base, size)?: 添加新的可用內存區域(參數:基址,大小)
- ?memblock_remove(base, size)?: 移除指定的內存區域
- memblock_reserve(base, size)?: 保留(預留)指定的內存區域
- memblock_free(base, size)?: 釋放已分配的內存區域
- memblock_mark_hotplug()?/clear_hotplug()?: 設置/清除內存區域的熱插拔屬性
- memblock_mark_nomap()?/clear_nomap()?: 設置/清除內存區域的"不映射到內核"屬性
4.3、內存分配函數
- memblock_phys_alloc(size, align)?: 分配指定大小和對齊的物理內存
- ?memblock_alloc(size, align)?: 使用默認參數分配內存(自動選擇位置)
- memblock_alloc_node(size, align, nid)?: 在指定NUMA節點上分配內存
- memblock_alloc_from(size, align, min_addr)?: 從指定最小地址開始分配
- memblock_alloc_low(size, align)?: 分配低端內存(通常低于4GB)
4.4、查詢函數
- memblock_is_memory(addr)?: 檢查地址是否屬于可用內存區域
- memblock_is_reserved(addr)?: 檢查地址是否屬于保留區域
- ?memblock_phys_mem_size()?: 返回所有內存區域的總大小
- ?memblock_reserved_size()?: 返回所有保留區域的總大小
-
?memblock_start_of_DRAM()?/end_of_DRAM()?: 獲取DRAM內存的起始/結束地址
五、總結:
memblock 是 Linux 內核在初始化階段使用的臨時物理內存管理器,其核心設計圍繞以下幾點:
(1)分區管理
- memblock.memory?:記錄所有可用物理內存(由 BIOS/e820 探測到的 usable? 區域)
- memblock.reserved?:記錄所有已分配或保留的內存(內核代碼、設備預留等)。
(2)內存管理
- 申請內存:僅將目標區域從 memory? 移到 reserved?,不修改原始 memory? 布局。
- 釋放內存:極少使用(多數早期內存為永久分配),僅從 reserved? 中移除。
(3)與后續內存管理的銜接
- memblock 記錄所有物理內存信息。
- 內核初始化后期,Buddy System 通過 memblock_free_all()? 接管可用內存。
- Buddy System 直接從 memblock.memory? 提取空閑區域,忽略 reserved? 中的已分配部分
- memblock 僅保留調試接口,不再參與主動管理。
參考文檔:
(3 封私信 / 79 條消息) Linux Kernel:啟動時內存管理(MemBlock 分配器) - 知乎
linux 內核內存機制之e820(linux啟動時,利用e820讀取物理內存信息) - jinzi - 博客園
Linux內存初始化過程(ZZ) | L&H SITE
Linux內存都去哪了:(1)分析memblock在啟動過程中對內存的影響 - ArnoldLu - 博客園
(3 封私信 / 79 條消息) linux內存管理(一)內存初始化 - 知乎
【計算子系統】內存管理之一:地址映射
Linux內核內存管理(1):內存塊 - memblock-CSDN博客
early manage:memblock - Linux Book
【原創】(二)Linux物理內存初始化 - LoyenWang - 博客園
【linux 內存管理】memblock算法簡單梳理_linux memblock-CSDN博客
3.10.2.4. linux物理內存初始化(memblock) — ywg_dev_doc 0.1 文檔
Linux Kernel:啟動時內存管理(MemBlock 分配器)一、Bootmem 與 Memblock 系統初始化 - 掘金