1. 程式人生 > >記憶體管理五 實體記憶體初始化

記憶體管理五 實體記憶體初始化

一、概序
  linux記憶體管理包含記憶體初始化、頁表對映過程、核心記憶體佈局圖、夥伴系統、SLAB分配器、vmalloc、malloc、mmap缺頁中斷等內容。按層分可以分為使用者空間、核心空間和硬體層,下面的圖可以詳細的說明:
  引用於奔跑吧linux核心

二、核心記憶體分佈
1、實體記憶體大小:
  現在裝置的管理方式都是通過裝置數來管理,memory的大小也同樣在裝置樹的DTS中定義如下:

	memory {
				device_type = "memory";
				reg = <0 0x80000000 0 0x20000000>;
		};

在核心啟動start_kernel函式中回去獲取對應的memory的起始地址和大小,並加入到memblock中,因為在核心啟動的初期,夥伴系統等分配的記憶體的方式還未初始化好,所以通過memblock的方式來分配管理記憶體。其呼叫流程如下:

start_kernel -> setup_arch -> setup_machine_fdt -> early_init_dt_scan_nodes -> early_init_dt_scan_memory
int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
				     int depth, void *data)
{	
	/* We are scanning "memory" nodes only */
	if (type == NULL) {
		if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "
[email protected]
") != 0) return 0; } else if (strcmp(type, "memory") != 0) return 0; 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); //在這裡通過一系列的處理後加入到memblock中 return 0; } void __init __weak early_init_dt_add_memory_arch(u64 base, u64 size) { memblock_add(base, size); }

2、記憶體的管理方式:
  在start_kernel ->…-> create_mapping中建立頁表後,核心就可以對實體記憶體進行管理了,管理方式如下:
    a、記憶體被劃分為很多node的節點,通過pg_data_t型別的結構體來管理;
    b、每一個node又劃分為不同簇即zone來管理,一般劃分為ZONE_DMA/ZONE_NOMAL/ZONE_HIGHEM ;
     通過struct zone_struct的結構體來描敘;
    c、頁面(page)是頁面分配的基本單位,記憶體中的每個頁都會建立一個struct page來管理。
  其中zone的型別在如下檔案中定義:

enum zone_type {
	ZONE_DMA,            //0~15MB,此部分簇的記憶體用來執行DMA操作
	ZONE_NORMAL,    //16MB~895MB,這個區包含的都是能正常對映的頁
	ZONE_HIGHMEM,  //896MB~實體記憶體結束,這部分在“高階記憶體”中使用到,64bit的的kernel一般無此zone
}

zone的初始化在start_kernel( ) -> free_area_init_core中在free_area_init_core函式中會依次建立每一個zone,另外在start_kernel->build_zonelists_node的中會初始化zonelist的結構體夥伴系統會從zonelist中開始分配記憶體。

3、使用者空間和核心空間劃分
  linux的使用者核心空間劃分,對於ARM32bit和ARM64bit的核心存在一定的差異,因為32bit的linux的能使用的虛擬地址的空間為4G,當實體記憶體大於4G時即無法訪問,故存在高階記憶體的概率(後續詳細講解)。但ARM64bit的結構採用48位物理定址機制,最大可以訪問256TB的實體地址空間,對於目前的應用來說,完全已經足夠,不需要擴充套件到64位的物理定址。虛擬地址同樣最大支援48位定址,所以把虛擬地址空間劃分為兩個空間,每個空間最大支援256TB。
(1)32bit 核心空間劃分:
  32bit的核心使用者空間和核心空間劃分通常按照3:1劃分,當然也支援2:2來劃分,由以下程式碼控制,預設會選擇VMSPLIT_3G,即使用者空間大小為3G,核心空間的大小為1G。PAGE_OFFSET用於計算實體地址無虛擬地址的偏移量。

kernel-3.18/arch/arm/Kconfig
choice
	prompt "Memory split"
	depends on MMU
	default VMSPLIT_3G

	config VMSPLIT_3G
		bool "3G/1G user/kernel split"
	config VMSPLIT_2G
		bool "2G/2G user/kernel split"
	config VMSPLIT_1G
		bool "1G/3G user/kernel split"
endchoice

config PAGE_OFFSET
	hex
	default PHYS_OFFSET if !MMU
	default 0x40000000 if VMSPLIT_1G
	default 0x80000000 if VMSPLIT_2G
	default 0xC0000000

下面看ARM32核心的分佈圖,在linux核心啟動的會打印出對應的核心空間佈局圖如下:

[    0.000000] <0>-(0)[0:swapper]Virtual kernel memory layout:
[    0.000000] <0>    vector  :     0xffff0000 - 0xffff1000   (   4 kB)
[    0.000000] <0>    fixmap  :    0xffc00000 - 0xfff00000   (3072 kB)
[    0.000000] <0>    vmalloc :   0xe1000000 - 0xff800000   ( 488 MB)
[    0.000000] <0>    lowmem  : 0xc0000000 - 0xe0800000   ( 520 MB)
[    0.000000] <0>    pkmap   :   0xbfe00000 - 0xc0000000   (   2 MB)
[    0.000000] <0>    modules :  0xbf000000 - 0xbfe00000   (  14 MB)
[    0.000000] <0>      .text :       0xc0008000 - 0xc0e00000   (14304 kB)
[    0.000000] <0>      .init :        0xc1200000 - 0xc1400000   (2048 kB)
[    0.000000] <0>      .data :     0xc1400000 - 0xc1526154   (1177 kB)
[    0.000000] <0>       .bss :      0xc1528000 - 0xc1867984   (3327 kB)

其中核心編譯出來的Bootimage本身佔據了記憶體空間的_text段到 _end段,分為如下幾段,在核心編譯出來的System.map檔案中可以查詢到:
  - 程式碼段:_text和_etext為程式碼段的起始地址和結束地址,包含了核心編譯成的核心程式碼;
  - init段:__init_begin和__init_end中間的部分,包含了模組初始化的資料;
  -資料段:_sdata和_edata之間的部分,包含核心的變數;
  -BSS段:__bss_start和__bss_stop之間的部分,包含初始化為0的所有靜態全域性變數;
  
  使用者空間和核心空間使用3:1的方式劃分,核心空間1GB的空間部分空間可以直接訪問實體地址,稱為線性對映區,實體地址的[0:760M]的記憶體被線性對映到[3GB:3GB+760M]的虛擬地址上。線性對映區的實體地址與虛擬地址的相差PAGE_OFFSET(3GB/0xc0000000)的大小,可以通過如下函式實現轉換:

//kernel-3.18/arch/arm/include/asm/memory.h
static inline phys_addr_t __virt_to_phys(unsigned long x)
{
	return (phys_addr_t)x - PAGE_OFFSET + PHYS_OFFSET;
}

static inline unsigned long __phys_to_virt(phys_addr_t x)
{
	return x - PHYS_OFFSET + PAGE_OFFSET;
}

核心把實體記憶體低於760M的稱為線性對映區(Normal memory),高於760M的實體記憶體稱為高階記憶體(High memory),由於32位系統的定址能力只有4GB,對於實體記憶體760M~4GB的情況,保留240MB的虛擬地址空間劃分用於動態對映高階記憶體,這樣核心空間就可以訪問到全部的4GB的實體記憶體了。對於實體記憶體高於4GB的請,可以使用LPE機制來拓展實體記憶體訪問。其中記憶體佈局圖如下:
 空間劃分圖
  
(2)64bit 核心空間劃分:
  ARM64架構的核心採用48位物理定址,最大可以訪問256TB的實體地址空間,通常吧虛擬地址空間劃分為兩個空間,每個空間最大支援256TB,劃分如下:
  -使用者空間:0x0000 0000 0000 0000 - 0x0000 ffff ffff ffff 256TB
  -核心空間:0xffff 0000 0000 0000 - 0xffff ffff ffff ffff 256TB
  64位的核心沒有高階記憶體的概念,器記憶體分佈圖如下:
在這裡插入圖片描述

三、實體記憶體初始化
  核心分配記憶體的方式大部分都是通過或者間接的通過buddy系統來管理,使用者在在申請記憶體時,夥伴系統會分配一塊合適的記憶體塊(2^order page)給使用者,在使用者釋放時回收。order的最大值為MAX_ORDER,即11,故每個記憶體塊連結串列包含2/4/8/16…1024個連續的頁面。實體記憶體分為不同的zone來管理,不同的zone都通過如下結構體來管理:

struct zone {
	/* zone watermarks, access with *_wmark_pages(zone) macros */
	unsigned long watermark[NR_WMARK];//zone記憶體水位相關
    
    /* free areas of different sizes */
	struct free_area	free_area[MAX_ORDER]; //空閒頁面
    
	unsigned long		*pageblock_flags;     //頁面型別
    ......
}

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};

上面可以看出zone的結構體中中有free_area 的陣列,陣列的大小是MAX_ORDER,free_area的資料結構中有MIGRATE_TYPES個連結串列,其關係如下圖:

夥伴系統空閒頁塊管理圖

MIGRATE_TYPES的的型別也在mmzone.h的檔案中定義:

enum {
	MIGRATE_UNMOVABLE,
	MIGRATE_MOVABLE,
	MIGRATE_RECLAIMABLE,
	MIGRATE_PCPTYPES,	/* the number of types on the pcp lists */
	MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
	MIGRATE_TYPES
};

在核心初始化的時候,使用的是memblock的方式來管理記憶體的,當核心準備好的時候,會遍歷所有的memblock的記憶體塊,找到記憶體塊的起始地址和結束地址以2^order的方式加入的夥伴系統中。

start_kernel -> mm_init -> mem_init-> free_all_bootmem -> free_low_memory_core_early
static unsigned long __init free_low_memory_core_early(void)
{
	unsigned long count = 0;
	phys_addr_t start, end;

	memblock_clear_hotplug(0, -1);
    //遍歷所有memblock的起始地址和結束地址
	for_each_free_mem_range(i, NUMA_NO_NODE, MEMBLOCK_NONE, &start, &end,NULL)
    //開始釋放記憶體到夥伴系統中
	count += __free_memory_core(start, end);

	return count;
}

__free_memory_core經過一系列處理後會呼叫到夥伴系統的核心函式__free_pages來釋放記憶體:

__free_memory_core -> __free_pages_memory -> __free_pages_bootmem -> __free_pages_boot_core -> __free_pages
static void __init __free_pages_boot_core(struct page *page,
					unsigned long pfn, unsigned int order)
{
	unsigned int nr_pages = 1 << order;
	struct page *p = page;
	unsigned int loop;

	prefetchw(p);
	for (loop = 0; loop < (nr_pages - 1); loop++, p++) {
		prefetchw(p + 1);
		__ClearPageReserved(p);
		set_page_count(p, 0);
	}
	__ClearPageReserved(p);
	set_page_count(p, 0);

	page_zone(page)->managed_pages += nr_pages;
	set_page_refcounted(page);
	__free_pages(page, order);
}

作者:frank_zyp
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文無所謂版權,歡迎轉載。