首页 > 学院 > 开发设计 > 正文

Linux内存管理:ARM Memory Layout以及mmu配置

2019-11-09 19:04:06
字体:
来源:转载
供稿:网友

在内核进行page初始化以及mmu配置之前,首先需要知道整个memory map。

1. ARM Memory Layout

这里写图片描述

PAGE_OFFSET Start address of Kernel space 0xC000_0000

lowmem Kernel direct-mapped RAM region (1:1 mapping) Maximum 896M

HIGH_MEMORY End address of lowmem PAGE_OFFSET + MEMORY_SIZE

pkmap 用来把HIGHMEM page 永久映射到 kernel space 2MB (这个大小每个平台不一样) kmap() / kunmap()

Page gap To against out-of-bounds errors 8MB

vmalloc vmalloc() / ioremap() space

DMA DMA memory mapping region

Fixmap kmap()可能会进入睡眠,所以不能用在中断上下文等地方. 所以Fixmap就是用于在中断上下文中把 highmem映射到内核空间的. Mapping HIGHMEM pages atomically kmap_atomic() :Fixmap在使用这个函数,所以可以在中断上下文中使用

Vector CPU vectors are mapped here

Modules Kernel modules inserted via insmod are placed here 16MB (14MB, if HIGHMEM is enabled)

在内核初始化的时候,上面说的lowmemory中,还需要去除一些reserved memory。这些预留的内存是供一些外设使用的。下面来看一下预留内存的去除方式以及内核怎么读取预留的。 (这里不包含具体的内存分配内容,比如slab或者buddy系统等)。

2. 在bootloader判断物理内核地址范围之后,会修改相应的device tree节点。

以高通平台为例,bootloader中有如下函数会负责更新device tree中的memory node

int update_device_tree() { ... ret = fdt_path_offset(fdt, "/memory"); offset = ret; ret = target_dev_tree_mem(fdt, offset); ...}

“/memory”一般定义在sekeleton.dtsi,这也是为什么虽然skeleton.dtsi文件里边都是空的内容,但还是需要include这个文件的原因。

//skeleton64.dtsi/ { #address-cells = <2>; #size-cells = <2>; cpus { }; soc { }; chosen { }; aliases { }; memory { device_type = "memory"; reg = <0 0 0 0>; };};

然后在kernel里调用如下函数来读取memory大小等赋值给memblock变量:

setup_machine_fdt(){ ... of_scan_flat_dt(early_init_dt_scan_memory, NULL); ...}int __init early_init_dt_scan_memory(unsigned long node, const char *uname, int depth, void *data){ const char *type = of_get_flat_dt_PRop(node, "device_type", NULL); const __be32 *reg, *endp; int l; /* We are scanning "memory" nodes only */ if (type == NULL) { /* * The longtrail doesn't have a device_type on the * /memory node, so look for the node called /memory@0. */ if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0) return 0; } else if (strcmp(type, "memory") != 0) return 0; reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l); if (reg == NULL) reg = of_get_flat_dt_prop(node, "reg", &l); if (reg == NULL) return 0; endp = reg + (l / sizeof(__be32)); pr_debug("memory scan node %s, reg size %d, data: %x %x %x %x,/n", uname, l, reg[0], reg[1], reg[2], reg[3]); while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) { u64 base, size; base = dt_mem_next_cell(dt_root_addr_cells, &reg); size = dt_mem_next_cell(dt_root_size_cells, &reg); if (size == 0) continue; pr_debug(" - %llx , %llx/n", (unsigned long long)base, (unsigned long long)size); early_init_dt_add_memory_arch(base, size); } return 0;}

3. 内核读到device tree节点之后,会把所有的内存范围保留在memblock里边。然后去掉所有预先保留的内存(比如高通msm平台预留给modem的内存等)。把内核分成lowmemory和highmemory等

在内核启动之后,

start_kernel()->setup_arch()->setup_arch()->sanity_check_meminfo()

的时候打印的memblock的内容为:

<6>[0.000000] [0:wapper:0] sanity_check_meminfo memblock.memory.cnt=2<6>[0.000000] [0:wapper:0] pys_addr vmalloc_limit = 0xa9c00000<6>[0.000000] [0:wapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x2fd00000<6>[0.000000] [0:wapper:0] count = 2 , reg->base =0xb0000000 , reg->size =0x30000000<6>[0.000000] [0:wapper:0] arm_lowmem_limit =0xa9c00000

内存分为两个CS: CS1基地址为0x80000000,大小为0x30000000。 CS2基地址为0xb0000000,大小为0x30000000。 所以物理内存开始地址为0x800000000,总的大小为1.5GB。 但中间缺了0x2fd00000到0x30000000的3MB大小的内存,哪里去了??(应该是bootloader改的~~,预留了sec_debug相关的内存)

这段3MB里边,包含了sec_dbg的内容,但大小没有3MB这么大,其余的用作什么了还得查<0>[0.000000] [0:swapper:0] sec_dbg_setup: str=@0xaff00008<0>[0.000000] [0:swapper:0] sec_dbg_setup: secdbg_paddr = 0xaff00008<0>[0.000000] [0:swapper:0] sec_dbg_setup: secdbg_size = 0x80000

之后会调用如下函数,读取memory相关的device tree内容,预留modem,audio等相关的内存:

setup_arch()->arm_memblock_init()->dma_contiguous_reserve()->dma_contiguous_early_removal_fixup()

这时打印的内容为:

<6>[0.000000] [0:swapper:0] arm_lowmem_limit =0xa9c00000<6>[0.000000] [0:swapper:0] cma: Found external_image__region@0, memory base 0x85500000, size 19 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found modem_adsp_region@0, memory base 0x86800000, size 88 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found pheripheral_region@0, memory base 0x8c000000, size 6 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found venus_region@0, memory base 0x8c600000, size 5 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found secure_region@0, memory base 0x00000000, size 109 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found qseecom_region@0, memory base 0x00000000, size 13 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found audio_region@0, memory base 0x00000000, size 3 MiB, limit 0xffffffff<6>[0.000000] [0:swapper:0] cma: Found splash_region@8E000000, memory base 0x8e000000, size 20 MiB, limit 0xffffffff

读取的dts文件内容可以找到,,内容如下:

{ memory { #address-cells = <2>; #size-cells = <2>;/* Additionally Reserved 6MB for TIMA and Increased the TZ app size * by 2MB [total 8 MB ] */ external_image_mem: external_image__region@0 { linux,reserve-contiguous-region; linux,reserve-region; linux,remove-completely; reg = <0x0 0x85500000 0x0 0x01300000>; label = "external_image_mem"; }; modem_adsp_mem: modem_adsp_region@0 { linux,reserve-contiguous-region; linux,reserve-region; linux,remove-completely; reg = <0x0 0x86800000 0x0 0x05800000>; label = "modem_adsp_mem"; }; peripheral_mem: pheripheral_region@0 { linux,reserve-contiguous-region; linux,reserve-region; linux,remove-completely; reg = <0x0 0x8C000000 0x0 0x0600000>; label = "peripheral_mem"; }; venus_mem: venus_region@0 { linux,reserve-contiguous-region; linux,reserve-region; linux,remove-completely; reg = <0x0 0x8C600000 0x0 0x0500000>; label = "venus_mem"; }; secure_mem: secure_region@0 { linux,reserve-contiguous-region; reg = <0 0 0 0x6D00000>; label = "secure_mem"; }; qseecom_mem: qseecom_region@0 { linux,reserve-contiguous-region; reg = <0 0 0 0xD00000>; label = "qseecom_mem"; }; audio_mem: audio_region@0 { linux,reserve-contiguous-region; reg = <0 0 0 0x314000>; label = "audio_mem"; }; cont_splash_mem: splash_region@8E000000 { linux,reserve-contiguous-region; linux,reserve-region; reg = <0x0 0x8E000000 0x0 0x1400000>; label = "cont_splash_mem"; }; };};

之后在

setup_arch()->arm_memblock_init()->dma_contiguous_reserve()->dma_contiguous_early_removal_fixup()还会调用一次sanity_check_meminfo()函数

这时打印的内容变成了

<6>[0.000000] [0:swapper:0] pys_addr vmalloc_limit = 0xa9c00000<6>[0.000000] [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000<6>[0.000000] [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000<6>[0.000000] [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000<6>[0.000000] [0:swapper:0] arm_lowmem_limit =0xb1200000

比较两次调用sanity_check_meminfo()函数打印的log,可以看到扣除的内存范围,这些里边只有external_image_mem,modem_adsp_mem,peripheral_mem,venus_mem这几个被扣除了。 后面的secure_region,qseecom_region,audio_region,splash_region哪去了??(这部分被ion memory预留!!)

以下是扣除的内容

external_image_mem: 0x85500000~0x86800000 大小为 19MBmodem_adsp_mem :0x86800000 ~0x8C000000 大小为 88MBperipheral_mem : 0x8C000000 ~ 0x8C600000 大小为6MBvenus_mem:0x8c600000 ~ 0x8cb00000 大小为5MBsecure_mem : 0xd9000000~ 0xe0000000 大小为112MB //这个与上面的109MB相比大小被调整,为什么?qseecom_region : 0xd8000000 ~ 0xd9000000 大小为16MB////这个与上面的109MB相比大小也被调整,为什么?audio_mem : 0xd7c00000 大小为4MB//大小被调整splash_region : 0x8E000000~ 0x8F400000 大小为20MBdefault region :0xa9400000 ~ 0xa9c00000 大小为8MB

external_image_mem,modem_adsp_mem,peripheral_mem,venus_mem这些被扣除前后,memblock的 内容如下:

<6>[0.000000] [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x2fd00000<6>[0.000000] [0:swapper:0] count = 2 , reg->base =0xb0000000 , reg->size =0x30000000//第一次打印的时候是这样的,第二次打印就变成下面这样了<6>[0.000000] [0:swapper:0] count = 1 , reg->base =0x80000000 , reg->size =0x5500000<6>[0.000000] [0:swapper:0] count = 2 , reg->base =0x8cb00000 , reg->size =0x23200000<6>[0.000000] [0:swapper:0] count = 3 , reg->base =0xb0000000 , reg->size =0x30000000<6>[0.000000] [0:swapper:0] arm_lowmem_limit =0xb1200000//vmalloc被cmdline设置为了340MB,所以vmalloc_limit= 0xb1200000//(0xff000000 - 0x15400000(340MB)的值,也就是从0xff00000开始减去vmalloc大小得到的值)。//这个值被调整完之后变成arm_lowmem_limit = 0xa9c00000。 //但第二次被sanity_check_meminfo()函数打印的时候被调整成了0xb1200000,怎么调整的??//arm_lowmem_limit这个是最终划分Lowmemory和其他vmalloc区域的标准。//从下面的可以看到lowmemory地址最大的区域就是0xf000000~0xf120000。最大地址就到0xf1200000,和arm_lowmem_limit是一样的。//highmemory的开始地址是high_memory的值,大小如下://high_memory = __va(arm_lowmem_limit - 1) + 1; //这个值加上VMALLOC_OFFSET即为vmalloc的开始地址//#define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET)//VMALLOC_OFFSET一般为8MB

整个内存的示意图 这里写图片描述

<6>[0.000000] [0:swapper: 0] Memory: 1243908K/1448960K available (10539K kernel code, 1363K rwdata, 4472K rodata, 1417K init, 5844K bss, 205052K reserved, 632832K highmem)<6>[0.000000] [0:swapper: 0] Virtual kernel memory layout:<6>[0.000000] [0:swapper: 0] vector : 0xffff0000 - 0xffff1000 ( 4 kB)<6>[0.000000] [0:swapper: 0] fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB)<6>[0.000000] [0:swapper: 0] arm_lowmem_limit = 0xf1200000 <6>[0.000000] [0:swapper: 0] <6>[0.000000] [0:swapper: 0] start_phys : 0xf0000000 end_phys : 0x20000000 <6>[0.000000] [0:swapper: 0] vmalloc : 0xf1200000 - 0xff000000 ( 222 MB)<6>[0.000000] [0:swapper: 0] lowmem : 0xf0000000 - 0xf1200000 ( 18 MB)<6>[0.000000] [0:swapper: 0] start_phys : 0xccb00000 end_phys : 0xefd00000 <6>[0.000000] [0:swapper: 0] vmalloc : 0xefd00000 - 0xf0000000 ( 3 MB)<6>[0.000000] [0:swapper: 0] lowmem : 0xccb00000 - 0xefd00000 ( 562 MB)<6>[0.000000] [0:swapper: 0] start_phys : 0xc0000000 end_phys : 0xc5500000 <6>[0.000000] [0:swapper: 0] vmalloc : 0xc5500000 - 0xccb00000 ( 118 MB)<6>[0.000000] [0:swapper: 0] lowmem : 0xc0000000 - 0xc5500000 ( 85 MB)<6>[0.000000] [0:swapper: 0] pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)<6>[0.000000] [0:swapper: 0] modules : 0xbf000000 - 0xbfe00000 ( 14 MB)<6>[0.000000] [0:swapper: 0] .text : 0xc0008000 - 0xc0fa8ec4 (16004 kB)<6>[0.000000] [0:swapper: 0] .init : 0xc1000000 - 0xc1162480 (1418 kB)<6>[0.000000] [0:swapper: 0] .data : 0xc1164000 - 0xc12b8de4 (1364 kB)<6>[0.000000] [0:swapper: 0] .bss : 0xc12c1b3c - 0xc1876b78 (5845 kB)

contig_page_data里边node_zones的Normal和HighMem的 zone_start_pfn,spanned_pages正好对应上面的地址。

Normal: zone_start_pfn = 0x80000000 zone_start_pfn加上spanned_pages的个数,算一下地址正好是arm_lowmem_limit的值HighMem: zone_start_pfn的值也是正好等于arm_lowmem_limit的值。 zone_start_pfn加上spanned_pages的值也正好等于0xE0000000。

4. 根据上述处理之后,内核得到可用的内存大小以及范围。然后通过mmu配置等,做内存分页(paging)。

不管是x86架构还是ARM架构,现在大部分CPU访问内存,一般通过MMU来实现虚拟内存和物理内存的转换。 以下是一个简单的示意图。(如果要详细分析的话,要看MMU分几层,每个page大小怎么配置等等!!参考ARM架构的书) 这里写图片描述

在ARM平台,二级页表和三级页表可以选择用。但目前为止没有见过三级页表的,所以略过三级页表,只看一下二级页表的。

//在/kernel/arch/arm/include/asm/pgtable.h文件里边#ifdef CONFIG_ARM_LPAE #include <asm/pgtable-3level.h>#else#include <asm/pgtable-2level.h>#endif

设置一个page大小。这里先略过去寄存器的设置以及page大小类型等。这部分可以参考arm developer’s guide。 先看一下Linux里边在哪里定义page大小的。

//kernel/include/asm-generic/page.h文件里边#define PAGE_SHIFT 12#define PAGE_SIZE (1UL << PAGE_SHIFT)//12是最常看到的4k大小的page。

以ARM二级页表为例,一级页表和二级页表的种类有两种。

//page大小为4K,按下面的组织方式都可以map最大4G的内存地址空间。1. 一级页表是4096,二级页表是2562. 一级页表是2048,二级页表是512//在ARM Linux中,分别定义了PTRS_PER_PGD,PTRS_PER_PMD,PTRS_PER_PTE分别表示原本三级的页表,但如果是二级页表的话。这三个值分别定义为如下:#define PTRS_PER_PTE 512#define PTRS_PER_PMD 1#define PTRS_PER_PGD 2048//上面的值正好对应1级页表2048,二级页表512的组织方式。二级页表中,PUD,PMD没有用。//一级页表4096,二级页表256这样的配置,就可以定义成如下:#define PTRS_PER_PTE 256#define PTRS_PER_PMD 1#define PTRS_PER_PGD 4096

页表的示意图如下:

这里写图片描述

create_mapping()函数具体负责页表的生成。

//create_mapping()有几个调用路径1. devicemaps_init()->create_mapping()2. map_lowmem()->create_mapping()3. iotable_init()->create_mapping()4. debug_ll_io_init()->create_mapping()

可以看一下create_mapping()函数怎么按照物理和对应的虚拟内存,构建页表。

下面举一个例子看一下某个task访问某个虚拟地址是怎么一步一步转成物理地址的。

Linux内核进程,访问的地址都是内核范围之内的,只要做一个简单的偏移就可以在物理地址和虚拟地址之间进行转换,就不多说了。 用户进程,其page table的地址,都会保存在其task struct的mm或者active_mm的pgd中。可以根据这个地址,按照页表的分配方式来算。 这里写图片描述

从用户进程的task_struct中可以知道pgd的地址,当然页表分配方式上面已经讲了,这里是4096,256的分配方式。如果这个进程中,访问的虚拟地址是0x01206000。按照下面的方式可以算出来是0x578DB000。 这里写图片描述

按照ARM Developer’s Guide中的图,来看一下是怎么一步一步算出来的。

这里写图片描述

虚拟地址是:0x01206000Translation table base addre就是pgd的地址(保存在协处理器CP15:C2中),从上面的task_struct->active_mm->pgd可以看到就是0xDD7E3380虚拟地址0x01206000 * 0xFFF000000 ,这个是取虚拟地址前面12bit,然后右移20位,就是0x12,等于18。这个值要乘以4,加上pgd地址。因为第一级页表有4096个,页表的每一个项是4个字节,所以就要乘以4。故,要取的地址就是0xDD318048。这个地址里边的值就是0x53C6381。这个值乘以0xFFFFFF00就是第二级页表的基地址0x53C6300。取0x01206000虚拟地址的中间8bit,右移8位,然后乘以4,加到上面算出来的二级页表基地址0x53C6300这个上面去。算出来的值就是0x53C6318。这个地址的值是0x578DBC7F。0x578DBC7F * 0xFFFFF000 加上虚拟地址*0x00000FFF的值,就是0x578DB000。这个就是最终要访问的物理地址。

用户进程的内存管理

这里写图片描述 1. 进程数据结构: task_struct 2. 进程内存管理数据结构: mm_struct mmap: 进程分配的所有内存的链表头 pgd: page global directory 的地址 3. 进程分配的内存,由vm_area_struct管理 vm_start and vm_end: 虚拟内存的开始地址和结束地址

下图是用户进程访问的虚拟内存通过pgd转换成物理地址的示意图,在前面已经详细讲过: 这里写图片描述


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表