前面章節我們介紹了memblock,其作用內核啓動初期,常用的內存分配器還未被初始化而不能使用,在此期間memblock是一種用於內存管理區域的方法。然後調用page_init來完成系統分頁機制的初始化工作,建立頁表,從而內核可以完成虛擬地址到物理地址的映射關係,本章主要是分析bootmem_init的流程。
1. bootm初始化
arm架構下, 在setup_arch中通過paging_init函數初始化內核分頁機制之後, 內核通過bootmem_init()
開始完成內存結點和內存區域的初始化工作,其函數定義在arch/arm/mm/init.c中,如下所示
void __init bootmem_init(void)
{
unsigned long min, max_low, max_high;
memblock_allow_resize(); -------------(1)
max_low = max_high = 0;
find_limits(&min, &max_low, &max_high);
early_memtest((phys_addr_t)min << PAGE_SHIFT,
(phys_addr_t)max_low << PAGE_SHIFT);
arm_memory_present(); -------------(2)
sparse_init(); -------------(3)
zone_sizes_init(min, max_low, max_high); -------------(4)
min_low_pfn = min;
max_low_pfn = max_low;
max_pfn = max_high;
}
- 通過memblock拿到limit,DRAM的起始地址的頁面號,分別爲min = 0x80000, max_low = 0xa0000,內存的結束地址max_high = 0xa0000,early_memtest做內存的Memtest使用,最終會賦值給min_low_pfn(內存塊的起始幀號),max_low_pfn(normal結束幀號),max_pfn(內存塊的結束幀號)。
- arm_memory_present是通過CONFIG_SPARSEMEM來定義的,對於現在ARM32該宏沒有定義,暫不分析,以後單獨討論。其主要是linux內核已經實現了內存熱插的支持,當一個linux系統不管運行在 物理環境 或者虛擬環境 時只要宿主能提供內存熱插拔機制,linux內核就能相應的增加或者減少內存。
- 啓動並運行bootmem分配器,對於ARM32位系統,該功能不支持,幾乎沒有做什麼
- zone_sizes_init()來初始化節點和管理區的一些數據項, 其中關鍵的是初始化了系統中各個內存域的頁幀邊界,保存在max_zone_pfn數組,從min_low_pfn到max_low_pfn是ZONE_NORMAL,max_low_pfn到max_pfn是ZONE_HIGHMEM。
2.zone_sizes_init初始化
static void __init zone_sizes_init(unsigned long min, unsigned long max_low,
unsigned long max_high)
{
unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES]; -------------(1)
struct memblock_region *reg;
memset(zone_size, 0, sizeof(zone_size));
zone_size[0] = max_low - min;
#ifdef CONFIG_HIGHMEM
zone_size[ZONE_HIGHMEM] = max_high - max_low;
#endif
/*
* Calculate the size of the holes.
* holes = node_size - sum(bank_sizes)
*/
memcpy(zhole_size, zone_size, sizeof(zhole_size)); -------------(2)
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_low) {
unsigned long low_end = min(end, max_low);
zhole_size[0] -= low_end - start;
}
#ifdef CONFIG_HIGHMEM
if (end > max_low) {
unsigned long high_start = max(start, max_low);
zhole_size[ZONE_HIGHMEM] -= end - high_start;
}
#endif
}
#ifdef CONFIG_ZONE_DMA -------------(3)
/*
* Adjust the sizes according to any special requirements for
* this machine type.
*/
if (arm_dma_zone_size)
arm_adjust_dma_zone(zone_size, zhole_size,
arm_dma_zone_size >> PAGE_SHIFT);
#endif
free_area_init_node(0, zone_size, min, zhole_size); -------------(4)
}
-
統計zone_size[0]和zone_size[ZONE_HIGHMEM]的大小,zone_size[0] = 0x20000,zone_size[ZONE_HIGHMEM] = 0
-
最終只是配置了zole_size[0],並且其值爲0
-
如果定義了CONFIG_ZONE_DMA,通過arm_dma_zone_size來配置DMA的內存域,該區域的長度依於處理器類型。在IA-32計算機上,一般的限制爲16MB,在我們現在使用的處理器上,CONFIG_ZONE_DMA沒有定義,所以只有ZONE_NORMAL和ZONE_HIGHMEM兩種。
-
進入到最關鍵的地方,free_area_init_node用來針對特定的節點進行初始化。
zone_size[]數據用於保持不同ZONE類型具有的頁表,zhole_size數組用於保持不同的ZONE類型具有的空洞的頁數,如下圖所示
接下來看看free_area_init_node的實現接口
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); -----------------(1)
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->kswapd_classzone_idx);
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn;
pgdat->per_cpu_nodestats = NULL;
#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
calculate_node_totalpages(pgdat, start_pfn, end_pfn, -----------------(2)
zones_size, zholes_size);
alloc_node_mem_map(pgdat); -----------------(3)
#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
reset_deferred_meminit(pgdat); -----------------(4)
free_area_init_core(pgdat); -----------------(5)
}
- 在NUMA有多個節點,而每個節點內,訪問內存的時間是相同的,不同的節點,訪問內存的時間可以不同。而對於UMA,只有一個節點,取得該node的pg_data_t數據結構變量,每個node都有一個pg_data_t變量描述,進行初始化工作。node_id=0,node_start_fn = 0x80000, start_fn = 0x80000
- 對節點長度和節點總可用頁面數進行初始化。calculate_node_totalpages函數是通過調用zone_spanned_pages_in_node和zone_absent_pages_in_node函數實現的,主要是爲pgdat的成員變量(包括空洞在內的總頁數(node_spanned_pages))和除空洞外的頁數(node_present_pages)設置值
- alloc_node_mem_map() 初始化節點的局部映射地址,即pg_data_t->node_mem_map。在NUMA中,全局mem_map指向系統第一個節點的地址,系統中每個節點的起始地址,都對應在全局mem_map的某個位置。在UMA中,全局mem_map就是節點的node_mem_map
- 由於CONFIG_DEFERRED_STRUCT_PAGE_INIT未定義,該函數爲空
- 調用free_area_init_core()來真正初始化每個struct zone中的成員,填充pgdat的ZONE結構體。
2.1 calculate_node_totalpages初始化
對於該函數主要是用來計算每一個zone的總頁數和實際頁數(不包含空洞),以及內存節點的總頁數和實際頁數(不包含空洞),其代碼實現如下
static void __meminit calculate_node_totalpages(struct pglist_data *pgdat,
unsigned long node_start_pfn,
unsigned long node_end_pfn,
unsigned long *zones_size,
unsigned long *zholes_size)
{
unsigned long realtotalpages = 0, totalpages = 0;
enum zone_type i;
for (i = 0; i < MAX_NR_ZONES; i++) {
struct zone *zone = pgdat->node_zones + i;
unsigned long zone_start_pfn, zone_end_pfn;
unsigned long size, real_size;
size = zone_spanned_pages_in_node(pgdat->node_id, i,
node_start_pfn,
node_end_pfn,
&zone_start_pfn,
&zone_end_pfn,
zones_size);
real_size = size - zone_absent_pages_in_node(pgdat->node_id, i,
node_start_pfn, node_end_pfn,
zholes_size);
if (size)
zone->zone_start_pfn = zone_start_pfn;
else
zone->zone_start_pfn = 0;
zone->spanned_pages = size;
zone->present_pages = real_size;
totalpages += size;
realtotalpages += real_size;
}
pgdat->node_spanned_pages = totalpages;
pgdat->node_present_pages = realtotalpages;
printk(KERN_DEBUG "On node %d totalpages: %lu\n", pgdat->node_id,
realtotalpages);
}
該函數主要計算各個ZONE區的page數目,對於ZONE區,其主要有以下3個
-
ZONE_DMA,該管理區是一些設備無法使用DMA訪問所有地址的範圍,因此特意劃分出來的一塊內存,專門用於特殊DMA訪問分配使用的區域。比如x86架構此區域爲0-16M。本處理器該區域不存在
-
ZONE_NORMAL:直接映射區,含有的頁面數爲0x20000
-
ZONE_HIGHMEM:高端內存管理區,申請的內存,需要內核進行map後才能訪問
-
ZONE_MOVABLE:這個區域是一個特殊的存在,主要是爲了支持memory hotplug功能,所以MOVABLE表示可移除,其實它也表示可遷移。本架構CPU不支持該功能。
簡單來說,可遷移的頁面不一定都在ZONE_MOVABLE中,但是ZONE_MOVABLE中的也頁面必須都是可遷移的,我們通過查看/proc/pagetypeinfo來看下實例:
ZONE_MOVABLE這個管理區,主要是和memory hotplug功能有關,爲什麼要設計內存熱插拔功能,主要是爲了如下兩點考慮:
1.邏輯內存熱插拔,對於虛擬機的支持,對於虛擬機按照需求來分配可用內存
2.物理內存熱插拔,對於NUMA服務器的支持,不需要的內存就設置爲offline,以降低功耗
3.優化內存碎片問題
2.2 alloc_node_mem_map
在linux內核中,所有的物理內存都用struct page結構來描述,這些對象以數組形式存放,而這個數組的地址就是mem_map。內核以節點node爲單位,每個node下的物理內存統一管理,也就是說在表示內存node的描述類型struct pglist_data中,有node_mem_map這個成員,其針對平坦型內存進行描述(CONFIG_FLAT_NODE_MEM_MAP)。如果系統只有一個pglist_data對象,那麼此對象下的node_mem_map即爲全局對象mem_map。函數alloc_remap()就是針對節點node的node_mem_map處理
static void __ref 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) ------------------(1)
return;
#ifdef CONFIG_FLAT_NODE_MEM_MAP
start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1); ------------------(2)
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); ------------------(3)
end = ALIGN(end, MAX_ORDER_NR_PAGES);
size = (end - start) * sizeof(struct page); ------------------(4)
map = alloc_remap(pgdat->node_id, size);
if (!map)
map = memblock_virt_alloc_node_nopanic(size, ------------------(5)
pgdat->node_id);
pgdat->node_mem_map = map + offset;
}
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}
- pgdat->node_spanned_pages此內存節點內無有效的內存,直接略過
- 起始地址必須對其,這個一般按照MB級別對齊即可,對齊後地址與真正開始地址之間的偏移大小,start = 0x80000,offset = 0
- 獲取節點內結束頁幀號pfn,然後對齊,end = 0xa0000,
- 計算需要的數組大小,需要注意end-start是頁幀個數(0xa0000 - 0x80000 = 0x40000),每個頁需要一個struct page對象,所以,這裏是乘關係,這樣得到整個node內所有以page爲單位描述需要佔據的內存
- 如果這裏分配失敗,則通過memblock管理算法分配內存。
2.3 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;
pgdat_resize_init(pgdat); ------------------(1)
#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
init_waitqueue_head(&pgdat->kswapd_wait);
init_waitqueue_head(&pgdat->pfmemalloc_wait);
#ifdef CONFIG_COMPACTION
init_waitqueue_head(&pgdat->kcompactd_wait);
#endif
pgdat_page_ext_init(pgdat);
spin_lock_init(&pgdat->lru_lock);
lruvec_init(node_lruvec(pgdat));
for (j = 0; j < MAX_NR_ZONES; j++) { ------------------(2)
struct zone *zone = pgdat->node_zones + j;
unsigned long size, realsize, freesize, memmap_pages;
unsigned long zone_start_pfn = zone->zone_start_pfn;
size = zone->spanned_pages;
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
*/
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
pr_warn(" %s zone: %lu pages exceeds freesize %lu\n",
zone_names[j], memmap_pages, freesize);
}
/* Account for reserved pages */
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->managed_pages = is_highmem_idx(j) ? realsize : freesize;
#ifdef CONFIG_NUMA
zone->node = nid;
#endif
zone->name = zone_names[j];
zone->zone_pgdat = pgdat;
spin_lock_init(&zone->lock);
zone_seqlock_init(zone);
zone_pcp_init(zone); ------------------(3)
if (!size)
continue;
set_pageblock_order();
setup_usemap(pgdat, zone, zone_start_pfn, size);
ret = init_currently_empty_zone(zone, zone_start_pfn, size); ------------------(4)
BUG_ON(ret);
memmap_init(size, nid, j, zone_start_pfn); ------------------(5)
}
}
-
主要是初始化struct pglist_data,首先初始化pgdat->node_size_lock自旋鎖初始化,初始化pgdat->kswapd_wait等待隊列,初始化頁換出守護進程創建空閒塊的大小
-
遍歷各個
zone
區域,進行如下初始化:-
根據spanned_pages和present_pages,調用calc_memmap_size計算管理該
zone
所需的struct page
結構所佔的頁面數memmap_pages
-
zone
中的freesize
表示可用的區域,需要減去memmap_pages
和dma_reserve的區域,如下開發板的Log打印所示:memmap
使用1024頁,DMA
保留0頁
-
-
計算
nr_kernel_pages
和nr_all_pages
的數量 -
初始化zone的其他變量和各種鎖
-
初始化zone結構體的per_cpu_pageset結構體變量pageset,per_cpu_pageset按CPU進行管理。它不直接返回夥伴系統,爲快速分配而按不同CPU持有頁。
-
初始化zone結構體的free_area結構體。
-
memmap_init函數在與頁幀具有1:1映射關係的頁數組中,向相應的頁幀的page結構體的flags成員設置PG_reserved位。
最後,當我們回顧bootmem_init
函數時,發現它基本上完成了linux物理內存框架的初始化,包括Node
, Zone
, Page Frame
,以及對應的數據結構等。