内存管理五 物理内存初始化

一、概序
  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, "memory@0") != 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
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文无所谓版权,欢迎转载。

猜你喜欢

转载自blog.csdn.net/frank_zyp/article/details/82867404