Kernel 内存管理

Kernel 内存管理

Posted by Jeff on January 7, 2021

Kernel 内存管理

AArch64 Linux通常使用以下配置:

  • 4KB页面, 使用3级或4级转换表,支持39位(512GB)或48位(256TB)的虚拟地址。
  • 64KB页面,使用2级转换表,支持42位(4TB)虚拟地址

他们的内存布局是一致的

以内核defconfig默认的4KB page + 4 levels配置为例,LINUX在arm架构上把虚拟地址空间划分为2个空间, 虚拟地址和内核地址, 每个空间最大支持256TB.

start                                            end                                                     Size              Use


0x0000_0000_0000_0000          0x0000_FFFF_FFFF_FFFF              256TB            user

0xFFFF_0000_0000_0000          0xFFFF_FFFF_FFFF_FFFF             256TB            kernel

引用:Memory Layout on AArch64 Linux


宏开关配置:

VA_BITS=48,PAGE_SIZE=4k

CONFIG_ARM64_VA_BITS_48=y

CONFIG_ARM64_VA_BITS=48

CONFIG_ARM64_4K_PAGES=y

宏定义:

Kernel4.0 aarch64

代码路径:arch/arm64/include/asm/memory.h

#define VA_BITS				(CONFIG_ARM64_VA_BITS)

#define PAGE_OFFSET			(UL(0xffffffffffffffff) << (VA_BITS - 1))

#define MODULES_END			(PAGE_OFFSET)

#define MODULES_VADDR	(MODULES_END - SZ_64M)

#define PCI_IO_END			(MODULES_VADDR - SZ_2M)

#define PCI_IO_START		(PCI_IO_END - PCI_IO_SIZE)

#define FIXADDR_TOP			(PCI_IO_START - SZ_2M)

#define TASK_SIZE_64		(UL(1) << VA_BITS)

代码路径:arch/arm64/include/asm/pgtable.h

//(例如VA_BITS=48, PAGE_SHIFT=12的情况下)
//(1UL << (VA_BITS - PAGE_SHIFT)) 表示48位的有效虚拟地址,一共可以表示多数个page页
//再乘以sizeof(struct page), 表示需要多数内存来存储struct page
//也就是说VMEMMAP是用来存储所有页面的struct page结构体的

#define VMEMMAP_SIZE		ALIGN((1UL << (VA_BITS - PAGE_SHIFT)) * sizeof(struct page), PUD_SIZE)

#define VMALLOC_START		(UL(0xffffffffffffffff) << VA_BITS)

#define VMALLOC_END		(PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K)

代码路径:arch/arm64/include/asm/fixmap.h

#define FIXADDR_SIZE	(__end_of_permanent_fixed_addresses << PAGE_SHIFT)

#define FIXADDR_START	(FIXADDR_TOP - FIXADDR_SIZE)

计算各个区的地址:

VA_BITS=48

.

PAGE_OFFSET = (UL(0xffffffffffffffff) « (VA_BITS - 1)) = 0xFFFF800000000000

.

MODULES_END = PAGE_OFFSET = 0xFFFF800000000000

.

MODULES_VADDR = (MODULES_END - SZ_64M) = 0xFFFF800000000000 - 0x04000000 = 0xFFFF7FFFFC000000

.

PCI_IO_END = (MODULES_VADDR - SZ_2M) = 0xFFFF7FFFFC000000 - 0x00200000 = 0xFFFF7FFFFBE00000

.

PCI_IO_SIZE = SZ_16M = 0x01000000

.

PCI_IO_START = (PCI_IO_END - PCI_IO_SIZE) = 0xFFFF7FFFFBE00000 - 0x01000000 =0xFFFF7FFFFAE00000

.

PUD_SIZE = 0x40000000

.

VMEMMAP_SIZE = ALIGN((1UL « (VA_BITS - PAGE_SHIFT)) * sizeof(struct page), PUD_SIZE) = 0x40000000000

.

VMALLOC_START = (UL(0xffffffffffffffff) « VA_BITS) = 0xFFFF000000000000

.

VMALLOC_END = (PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K) = 0xFFFF800000000000 - 0x40000000 - 0x40000000000 - 0x00010000 = 0xFFFF7BFFBFFF0000

.

FIXADDR_SIZE = (__end_of_permanent_fixed_addresses « PAGE_SHIFT) = 2«12 =0x2000

.

FIXADDR_START = (FIXADDR_TOP - FIXADDR_SIZE) = (PCI_IO_START - SZ_2M) - FIXADDR_SIZE = 0xFFFF_7FFF_FAE0_0000 - 0x20_0000 - 0x2000 = 0xFFFF7FFFFABFE000

.

.


</br>


QEMU + AARCH64 + 2G内存内核输出内存布局信息:


</br>


内存布局图


</br>


知识点:

kasan:

KASAN是一个动态检测内存错误的工具, 原理是利用额外的内存标记可用内存的状态. 
这部分额外的内存被称作shadow memory(影子区)。
KASAN将1/8的内存用作shadow memory。

modules:

128MB的内核模块区域,是内核模块使用的虚拟地址空间

vmalloc:

vmalloc函数使用的虚拟地址空间,kernel image也在vmalloc区域,
内核镜像的起始地址 = KIMAGE_ADDR + TEXT_OFFSET, TEXT_OFFSET是内存中的内核镜像相对内存起始位置的偏移。

内核镜像相关

.text:

代码段。 _text是代码段的起始地址,_etext是结束地址, kernel image放在这段位置。

.rodata:

read-only-data. 常量区,存放程序中定义为const的全局变量。

.init:

对应大部分模块初始化的数据,初始化结束后就会释放这部分内存。

.data:

数据段。 包含内核大部分已初始化的全局变量。

.bss:

静态内存分配段。 包含所有未初始化或初始化为0的静态全局变量。

fixed:

固定映射区。 在内核的启动过程中,有些模块需要使用虚拟内存并mapping到指定的物理地址上,
而且,这些模块也没有办法等待完整的内存管理模块初始化之后再进行地址映射。
因此,linux kernel固定分配了一些fixmap的虚拟地址,这些地址有固定的用途,使用该地址的模块在初始化的时候,
将这些固定分配的地址mapping到指定的物理地址上去。(Fix-Mapped Addresses)

PCI I/O:

pci设备的I/O地址空间

vmemmap:

内存的物理地址如果不连续的话,就会存在内存空洞(稀疏内存),vmemmap就用来存放稀疏内存的page结构体的数据的虚拟地址空间。

memory:

线性映射区,范围是【0xffff_8000_0000_0000, 0xffff_ffff_ffff_ffff】, 一共有128TB, 
但这里代码对应的是memblock_start_of_DRAM()和memblock_end_of_DRAM()函数。
memory根据实际物理内存大小做了限制,所以memroy显示了实际能够访问的内存区


</br>


关于地址转换

__virt_to_phys():


	.ltorg

	.align	3
	.type	__switch_data, %object
__switch_data:
	.quad	__mmap_switched
	.quad	__bss_start			// x6
	.quad	__bss_stop			// x7
	.quad	processor_id			// x4
	.quad	__fdt_pointer			// x5
	.quad	memstart_addr			// x6
	.quad	init_thread_union + THREAD_START_SP // sp

/*
 * The following fragment of code is executed with the MMU on in MMU mode, and
 * uses absolute addresses; this is not position independent.
 */
__mmap_switched:
	adr	x3, __switch_data + 8

	ldp	x6, x7, [x3], #16
1:	cmp	x6, x7
	b.hs	2f
	str	xzr, [x6], #8			// Clear BSS
	b	1b
2:
	ldp	x4, x5, [x3], #16
	ldr	x6, [x3], #8
	ldr	x16, [x3]
	mov	sp, x16
	str	x22, [x4]			// Save processor ID
	str	x21, [x5]			// Save FDT pointer
	str	x24, [x6]			// Save PHYS_OFFSET
	mov	x29, #0
	b	start_kernel
ENDPROC(__mmap_switched)

------------------------------------------------------------

/*
 * Calculate the start of physical memory.
 */
__calc_phys_offset:
// adr伪指令用于将一个地址加载到寄存器,
// 取到的是相对于PC寄存器的地址,由于此刻PC寄存器中值是物理地址,
// 所以x0中取到的即是标号1处的物理地址
	adr	x0, 1f 
// 将标号1处的的前八字节(.的虚拟地址)给x1,后八字节即PAGE_OFFSET给x2
	ldp	x1, x2, [x0]

// .处的物理地址减去虚拟地址,即x28中保存的是物理地址相对虚拟地址的偏移	  
	sub	x28, x0, x1

// 计算出kernel image起始的物理地址给x24
// PAGE_OFFSET+虚拟地址与物理地址偏移	   
	add	x24, x2, x28  
	ret
ENDPROC(__calc_phys_offset)
	.align 3
 
// 用来定义一个quad word也就是4字(8字节),“.”表示当前虚拟地址  
1:	.quad	.
//此处是说将PAGE_OFFSET放置在这个位置,占用8个字节
	.quad	PAGE_OFFSE

__calc_phys_offset的主要作用就是通过计算物理地址与虚拟地址偏移,保存到x28, 而kernel image的虚拟地址为PAGE_OFFSET,因此可以获知kernel image的物理地址,将kernel image的物理地址保存到x24中

#define PHYS_OFFSET		({ memstart_addr; })
PHYS_OFFSET = 0x4000_0000
PAGE_OFFSET = (UL(0xffffffffffffffff) << (VA_BITS - 1)) = 0xFFFF_8000_0000_0000
#define __virt_to_phys(x)	(((phys_addr_t)(x) - PAGE_OFFSET + PHYS_OFFSET))


</br>

实例:

在内核定义一个变量bits,获取其虚拟地址然后调用__virt_to_phys转换成物理地址

int bits=0;

phys_addr_t test= __virt_to_phys(&bits);

&bits = 0xffff800000ed3eb4

test = 0xffff800000ed3eb4 - PAGE_OFFSET + PHYS_OFFSET = 0xffff_8000_00ed_3eb4 - 0xFFFF_8000_0000_0000 + 0x4000_0000 = 0x40ed_3eb4


</br>
</br>
</br>



AARCH64 、 Kernel5.11.0-rc2 内存布局



源码宏定义:

计算原理同上

内存布局图

地址转换的差异:

/*
 * The linear kernel range starts at the bottom of the virtual address space.
 */
 
#define __is_lm_address(addr)	(((u64)(addr) & ~PAGE_OFFSET) < (PAGE_END - PAGE_OFFSET))

#define __lm_to_phys(addr)	(((addr) & ~PAGE_OFFSET) + PHYS_OFFSET)

#define __kimg_to_phys(addr)	((addr) - kimage_voffset)

#define __virt_to_phys_nodebug(x) ({				

	phys_addr_t __x = (phys_addr_t)(__tag_reset(x));
	
	__is_lm_address(__x) ? __lm_to_phys(__x) : __kimg_to_phys(__x);
	
})

#define __virt_to_phys(x)	__virt_to_phys_nodebug(x)

#define __phys_addr_symbol(x)	__pa_symbol_nodebug(x)

//-------------------------
__virt_to_phys()函数调用__is_lm_address()判断地址类型
PAGE_OFFSET=0xFFFF0000_0000_0000  那么~PAGE_OFFSET=0x0000_FFFF_FFFF_FFFF
PAGE_END=0xFFFF_8000_0000_0000
PHYS_OFFSET=0x4000_0000
所以:__is_lm_address(addr) = (((u64)(addr) & 0x0000_FFFF_FFFF_FFFF) < (0xFFFF_8000_0000_0000 - 0xFFFF_0000_0000_0000))

1.__is_lm_address(addr)==1时,即 addr & 0x0000_FFFF_FFFF_FFFF 大于 0x0000_0000_0000 并且 小于0x8000_0000_0000 
	调用__lm_to_phys(addr)计算物理地址
	例如在kernel中:
	int *test = (int *)kmalloc(sizeof(int),GFP_KERNEL);
	// (gdb) p /x test
	// test = 0xFFFF_0000_0141_CD00,即(0xFFFF_0000_0141_CD00 &0x0000_FFFF_FFFF_FFFF)< (0xFFFF_8000_0000_0000 - 0xFFFF_0000_0000_0000) = 0x0141_CD00 < 0x8000_0000_0000 成立,即 __is_lm_address(addr) = 1
	所以__lm_to_phys(addr)=(((addr) & ~PAGE_OFFSET) + PHYS_OFFSET)=0xFFFF_0000_0141_CD00 &0x0000_FFFF_FFFF_FFFF+ 0x4000_0000 = 0x0141_CD00 + 0x4000_0000 = 0x4141_CD00

2.__is_lm_address(addr)==0时,即 addr & 0x0000_FFFF_FFFF_FFFF 大于 0x8000_0000_0000
	调用__kimg_to_phys(addr)计算物理地址
	例如全局变量swapper_pg_dir
	swapper_pg_dir = 0xFFFF_8000_10AA_7000 ,0xFFFF_8000_10AA_7000 & 0x0000_FFFF_FFFF_FFFF= 0x8000_10AA_7000 < 0x8000_0000_0000 不成立 ,即 __is_lm_address(addr) = 0
	所以__kimg_to_phys(addr) = 0xFFFF_8000_10AA_7000 - kimage_voffset = 0xFFFF_8000_10AA_7000 - 0xFFFF_7FFF_CFE0_0000 = 0x40CA_7000