Linux内存从0到1学习笔记(一,内存简介)

Posted 高桐@BILL

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内存从0到1学习笔记(一,内存简介)相关的知识,希望对你有一定的参考价值。

一、写在前面;

首先我们通过一张图三个维度,来了解下“内存”在操作系统中的概念;

内存管理比较复杂,涉及的内容包括用户空间、内核空间和硬件;

用户空间即Linux Kernel暴露给用户态的系统调用,如brk,mmap等。通常会通过libc封装成常见的c函数,如mmap(),malloc(),free()等。大多数开发者都是在这个领域活动。

内核空间即为用户态提供系统调用的地方,因此在内存方面,内核空间往往需要处理系统调用请求。如sys_brk,sys_mmap等。内核空间除了处理用户态的系统调用外,还包括VMA管理,缺页中断,匿名页面等模块。为系统运行提供保障。

硬件层包括处理器的MMU,TLB和高速缓存部件,以及板载的物理内存,如LPDDR和DDR等。

二、ARM64内核内存的分布

ARM64架构处理器采用了48位物理寻址机制,最多可以寻找256TB的物理地址空间。对于目前的应用来说,这已经足够了。不需要扩展到64位的物理寻址地址。虚拟地址同样最多支持48位寻址。所以在处理器架构设计上,把虚拟地址空间划分为两个空间,每个空间最多支持256TB。这两个空间分别为用户空间和内核空间。

  • 用户空间:0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF
  • 内核空间:0xFFFF 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF

64位Linux内核中没有高端内存,因为48位的寻址空间已经很大了。

ARM64在Linux5.0内核的内存分布

 (1)用户空间:0x0000_0000_0000_0000到0x0000_ffff_ffff_ffff,一共有256TB。
(2)非规范区域。
(3)内核空间:0xffff_0000_0000_0000到0xffff_ffff_ffff_ffff,一共有256TB。 

        Linux内核提供了一个转储(dump)页表的调试接口。需要在内核配置文件(以.config结尾)中使能CONFIG_ARM64_PTDUMP_DEBUGFS配置选项。该调试接口为/sys/kernel/debug/kernel_page_tables。

这个是Linux4.14版本内核kernel内存初始化,打印出内核的内存分布如下:

 我们首先通过代码,来了解下上面的信息是如何打印出来的,代码链路如下:

--> (linux/init/main.c) asmlinkage __visible void __init start_kernel(void)

--> (linux/init/main.c) static void __init mm_init(void)

--> ​(root/arch/arm64/mm/init.c)void __init mem_init(void)

参考kernel源码如下:

init.c « mm « arm64 « arch - kernel/git/stable/linux.git - Linux kernel stable tree

/*
 * mem_init() marks the free areas in the mem_map and tells us how much memory
 * is free.  This is done after various parts of the system have claimed their
 * memory after the kernel image.
 */
void __init mem_init(void)

	if (swiotlb_force == SWIOTLB_FORCE ||
	    max_pfn > (arm64_dma_phys_limit >> PAGE_SHIFT))
		swiotlb_init(1);
	else
		swiotlb_force = SWIOTLB_NO_FORCE;

	set_max_mapnr(pfn_to_page(max_pfn) - mem_map);

#ifndef CONFIG_SPARSEMEM_VMEMMAP
	free_unused_memmap();
#endif
	/* this will put all unused low memory onto the freelists */
	free_all_bootmem();

	kexec_reserve_crashkres_pages();

	mem_init_print_info(NULL);

#define MLK(b, t) b, t, ((t) - (b)) >> 10
#define MLM(b, t) b, t, ((t) - (b)) >> 20
#define MLG(b, t) b, t, ((t) - (b)) >> 30
#define MLK_ROUNDUP(b, t) b, t, DIV_ROUND_UP(((t) - (b)), SZ_1K)

	pr_notice("Virtual kernel memory layout:\\n");
#ifdef CONFIG_KASAN
	pr_notice("    kasan   : 0x%16lx - 0x%16lx   (%6ld GB)\\n",
		MLG(KASAN_SHADOW_START, KASAN_SHADOW_END));
#endif
	pr_notice("    modules : 0x%16lx - 0x%16lx   (%6ld MB)\\n",
		MLM(MODULES_VADDR, MODULES_END));
	pr_notice("    vmalloc : 0x%16lx - 0x%16lx   (%6ld GB)\\n",
		MLG(VMALLOC_START, VMALLOC_END));
	pr_notice("      .text : 0x%p" " - 0x%p" "   (%6ld KB)\\n",
		MLK_ROUNDUP(_text, _etext));
	pr_notice("    .rodata : 0x%p" " - 0x%p" "   (%6ld KB)\\n",
		MLK_ROUNDUP(__start_rodata, __init_begin));
	pr_notice("      .init : 0x%p" " - 0x%p" "   (%6ld KB)\\n",
		MLK_ROUNDUP(__init_begin, __init_end));
	pr_notice("      .data : 0x%p" " - 0x%p" "   (%6ld KB)\\n",
		MLK_ROUNDUP(_sdata, _edata));
	pr_notice("       .bss : 0x%p" " - 0x%p" "   (%6ld KB)\\n",
		MLK_ROUNDUP(__bss_start, __bss_stop));
	pr_notice("    fixed   : 0x%16lx - 0x%16lx   (%6ld KB)\\n",
		MLK(FIXADDR_START, FIXADDR_TOP));
	pr_notice("    PCI I/O : 0x%16lx - 0x%16lx   (%6ld MB)\\n",
		MLM(PCI_IO_START, PCI_IO_END));
#ifdef CONFIG_SPARSEMEM_VMEMMAP
	pr_notice("    vmemmap : 0x%16lx - 0x%16lx   (%6ld GB maximum)\\n",
		MLG(VMEMMAP_START, VMEMMAP_START + VMEMMAP_SIZE));
	pr_notice("              0x%16lx - 0x%16lx   (%6ld MB actual)\\n",
		MLM((unsigned long)phys_to_page(memblock_start_of_DRAM()),
		    (unsigned long)virt_to_page(high_memory)));
#endif
	pr_notice("    memory  : 0x%16lx - 0x%16lx   (%6ld MB)\\n",
		MLM(__phys_to_virt(memblock_start_of_DRAM()),
		    (unsigned long)high_memory));

#undef MLK
#undef MLM
#undef MLK_ROUNDUP

	/*
	 * Check boundaries twice: Some fundamental inconsistencies can be
	 * detected at build time already.
	 */
#ifdef CONFIG_COMPAT
	BUILD_BUG_ON(TASK_SIZE_32			> TASK_SIZE_64);
#endif

#ifdef CONFIG_SPARSEMEM_VMEMMAP
	/*
	 * Make sure we chose the upper bound of sizeof(struct page)
	 * correctly when sizing the VMEMMAP array.
	 */
	BUILD_BUG_ON(sizeof(struct page) > (1 << STRUCT_PAGE_MAX_SHIFT));
#endif

	if (PAGE_SIZE >= 16384 && get_num_physpages() <= 128) 
		extern int sysctl_overcommit_memory;
		/*
		 * On a machine this small we won't get anywhere without
		 * overcommit, so turn it on by default.
		 */
		sysctl_overcommit_memory = OVERCOMMIT_ALWAYS;
	
  • kasan: KASAN是一个动态检测内存错误的工具。原理是利用额外的内存标记可用内存的状态. 这部分额外的内存被称作shadow memory(影子区)。KASAN将1/8的内存用作shadow memory。
  • modules: 内核模块区域,是内核模块使用的虚拟地址空间。内核模块通过insmod命令加载,会动态的映射到这里.
  • vmalloc: vmalloc函数申请的虚拟内存使用的虚拟地址空间范围
  • .text: 代码段。用于存放程序代码的区域, 编译时确定,且 只读。代码段由各个函数产生,函数的每一个语句将最终经过编绎和汇编生成二进制机器代码(具体生生哪种体系结构的机器代码由编译器决定)。_text是代码段的起始地址,_etext是结束地址, kernel image放在这段位置。
  • .rodata:(read-only-data)只读数据段,也就是你所说的常量区 。.rodata用来存放只读数据,比如printf语句中的格式字符串和开关语句的跳转表。存放程序中定义为const的全局变量。
  • .init: 对应大部分模块初始化的数据。程序初始化入口代码,在main()之前运行,初始化结束后就会释放这部分内存。__init_begin和__init_end分别为init段的起始地址和结束地址。
  • .data: 数据段。 .data段存储未初始化或初始化为0的全局变量(包括全局静态变量)、局部静态变量(static)
  • .bss: (Block Started by Symbol)静态内存分配段。BSS段存储未初始化或初始化为0的全局变量(包括全局静态变量)、局部静态变量(static)具体体现为一个占位符,并不给该段的数据分配空间,只是记录数据所需空间的大小。 .bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化。注意!.data和.bss在加载时合并到一个Segment(Data Segment)中,这个Segment是可读可写的。
  • fixed: 固定映射区。 在内核的启动过程中,有些模块需要使用虚拟内存并mapping到指定的物理地址上,而且,这些模块也没有办法等待完整的内存管理模块初始化之后再进行地址映射。因此,linux kernel固定分配了一些fixmap的虚拟地址,这些地址有固定的用途,使用该地址的模块在初始化的时候,讲这些固定分配的地址mapping到指定的物理地址上去。(Fix-Mapped Addresses)
  • PCI I/O: pci设备的I/O地址空间
  • vmemmap: 内存的物理地址如果不连续的话,就会存在内存空洞(稀疏内存),vmemmap就用来存放稀疏内存的page结构体的数据的虚拟地址空间。
  • memory: 线性映射区即物理内存线性的映射到内核空间中。memory根据实际物理内存大小做了限制,所以memroy显示了实际能够访问的内存区。最终是通过dts或acpi中配置的memory节点确定的,如下代码,上图中打印的数据显示为12G;
MLM(__phys_to_virt(memblock_start_of_DRAM()), (unsigned long)high_memory))
high_memory = __va(memblock_end_of_DRAM() - 1) + 1;

三、物理内存DTB映射

通过前面的介绍,我们大概对内存有了一个全局的认识,那么Linux内核是如何识别物理内存的呢?

实际上,内核通过解析dtb文件来读取memory节点的内容,从而识别到内存,并将检测到的内存注册到系统。

3.1 运行时查看DTB

3.1.1 /sys/firmware/fdt

进入/sys/firmware目录后便可看到二个文件,一个是devicetree文件夹,另一个是fdt(原始dtb文件,可以用hexdump -C fdt 将其打印出来查看就会发现里面的数据和dtb文件是一致的)。

3.1.2 /sys/firmware/devicetree/base/

以目录结构呈现的dtb文件。 根节点对应base目录, 每一个节点对应一个目录, 每一个属性对应一个文件

3.1.3 /sys/devices/platform

系统中所有的platform_device, 有来自设备树的, 也有来有.c文件中注册的
对于来自设备树的platform_device,可以进入 /sys/devices/platform/<设备名>/of_node 查看它的设备树属性(例如进入/sys/devices/platform/led/后若发现该目录下有of_node节点,就表明该platform_device来自设备树)

3.1.4 /proc/device-tree

是链接文件, 指向 /sys/firmware/devicetree/base

那么如何查看memory的node信息呢?如下:

以arch/arm64/boot/dts/freescale/fsl-ls208xa.dtsi为例,memory节点的信息常常如下面格式:

 uboot会将kernel image和dtb拷贝到内存中,并且将dtb物理地址告知kernel,kernel需要从该物理地址上读取到dtb文件并解析,才能得到最终的内存信息,dtb的物理地址需要映射到虚拟地址上才能访问.

以上是关于Linux内存从0到1学习笔记(一,内存简介)的主要内容,如果未能解决你的问题,请参考以下文章

Linux内存从0到1学习笔记(8.6 DMA-BUF简介)

Linux内存从0到1学习笔记(七,用户空间虚拟内存之三 - 内存映射)

Linux内存从0到1学习笔记(四,TLB)

Linux内存从0到1学习笔记(9.10 内存优化调试之panic_on_oom介绍

Linux内存从0到1学习笔记(五,内存分类)

Linux内存从0到1学习笔记(8.13 dma内存调试一)