[内核内存] [arm64] 内存初始化2---dtb解析

linux内核版本:linux4.9.115(arm64)


Linux内核执行完stext函数后,内核MMU已经打开,内核进入了c代码运行阶段,进入了虚拟地址空间的世界。但此时内核通过虚拟地址了解的内存世界还太小。那么下一部需要对dtb进行映射,通过设备树文件和membloc模块让内核了解更为广阔的内存世界。

内核进入c代码运行阶段后,通过dtb文件中的memory节点去了解内存全局。下面是ls1043单板的memory节点描述:

1.//arc/arm64/boot/dts/freesclae/fsl-ls1043.dtsi
2.memory@80000000 {  
3.    device_type = "memory";  
4.    reg = <0x0 0x80000000 0 0x80000000>;  
5.          /* DRAM space 1, size: 2GiB DRAM */  
6.}; 

上诉节点描述了内存的起始地址和大小。内核在初始化化时会去读取dtb文件并解析memory节点内容,这些内存信息就会被保存近系统,图1是整体调用流程.
在这里插入图片描述

1.内核内存布局信息的获取

1.early_fixmap_init

Boot将dtb拷贝到内存中,且通过传递相关参数将dtb的物理地址告知内核。但是内核必须将dtb的相关物理地址映射到虚拟地址上,通过虚拟地址间接访问dtb文件。由于此时物理地址映射并没有完成。为了解决该问题提出了fixed map机制。

什么是fixed map机制?具体见前面的linux fixed map相关介绍。下面图2是利用fixed map机制完成fdt物理地址映射到内核虚拟地址空间的示意图。

在这里插入图片描述

2.Fix-map机制映射fdt物理地址

从图2可以看出,内核访问DTB物理地址,先将DTB所在的物理地址映射到内核虚拟地址空间的Fixed map区域,然后通过该虚拟地址区域间接访问DTB文件。

early_fixmap_init函数主要完成fixmap区域页表映射初始化,下面是该函数的代码实现:

1.//arc/arm64/mm/mmu.c
2.void __init early_fixmap_init(void)  
3.{  
4.    pgd_t *pgd;  
5.    pud_t *pud;  
6.    pmd_t *pmd;  
7.    //FIXADDR_START为fixed map区域起始地址定义在arc/arm64/include/asm/fixmap.h
8.    //#define FIXADDR_START	(FIXADDR_TOP - FIXADDR_SIZE)
9.    unsigned long addr = FIXADDR_START;  
10.    //pgd_offset_k(addr)获取addr虚拟地址对应于pgd在全局页表中的entry,这个pgd
11.    //全局页表就是swapper_pg_dir全局页表,其实整个内核系统就只有一个PGD
12.    pgd = pgd_offset_k(addr);  
13.    if (CONFIG_PGTABLE_LEVELS > 3 &&  
14.        !(pgd_none(*pgd) || pgd_page_paddr(*pgd) == __pa(bm_pud))) {  
15.        /* 
16.         * We only end up here if the kernel mapping and the fixmap 
17.         * share the top level pgd entry, which should only happen on 
18.         * 16k/4 levels configurations. 
19.         */  
20.        BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));  
21.        pud = pud_offset_kimg(pgd, addr);  
22.    } else {  
23.         //将bm_pud的物理地址写到pgd全局页表目录中
24.        pgd_populate(&init_mm, pgd, bm_pud);  
25.        pud = fixmap_pud(addr);  
26.    }  
27.    //将bm_pmd的物理地址写到pud页目录中
28.    pud_populate(&init_mm, pud, bm_pmd);  
29.    pmd = fixmap_pmd(addr);  
30.    //将bm_pte的物理地址写到pmd页表目录中。
31.    pmd_populate_kernel(&init_mm, pmd, bm_pte);  
32.    /* 
33.     * The boot-ioremap range spans multiple pmds, for which 
34.     * we are not prepared: 
35.     */  
36.    BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)  
37.             != (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));  
38.  
39.    if ((pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)))  
40.         || pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_END))) {  
41.       	...
42.        	...
43.    }  
44.}  

从上面的代码中可以看出,early_fixmap_init函数中完成初始化的只是所有中间level的Translation table的entry,最后一个level则是在各个具体的模块进行的创建的,对于DTB而言,这发生在fixmap_remap_fdt函数中。上述过程可由下图3表示

在这里插入图片描述

3.early_fixmap_init转换表初始化过程

ps:

  1. 对于fixed-mapped address这段虚拟地址空间,由于是位于内核空间,因此PGD当然就是复用swapper进程的PGD了(其实整个系统就一个PGD),而其他level的Translation table则是静态定义的位于内核bss段(在文件arch/arm64/mm/mmu.c),由于所有的Translation table都在kernel image mapping 的范围内,因此内核可以毫无压力的访问,并创建fixed-mapped address这段虚拟地址空间对应的PUD、PMD 和PTE的entry。)
  2. 系统对dtb的大小有限制,不能大于2MB,这样要求主要是为了保证创建地址映射的时候不会分配其它的 translation table page,所有的translation table都必须静态定义。因为此时内存管理模块还没有初始化,哪怕membloc模块也没有初始化不具有内存的布局信息并不能进行动态分配内存的操作。

2.early_ioremap_init

一般传统驱动模块都是使用ioremap函数来完成地址映射的,但是该函数必须依赖伙伴系统来创建某个level的Translation table(若该转换表不存在)。于是在内核启动初期需要通过early_ioremap机制让设备寄存器对内存进行访问。一些硬件需要在内存管理系统运行起来之前就需要工作,内核采early_ioremap机制来映射内存给这些硬件驱动使用。并且这些硬件驱动在使用完early_ioremap的地址后需要尽快的释放掉这些内存,这样才能保证其他硬件模块继续使用。因此early_ioremap采用的是Fixed map的temporary fixmap段虚拟地址。early_ioremap_init函数会调用early_ioremap_setup代码如下:

1.void __init early_ioremap_setup(void)  
2.{  
3.    int i;  
4.  
5.    for (i = 0; i < FIX_BTMAPS_SLOTS; i++)  
6.        if (WARN_ON(prev_map[i]))  
7.            break;  
8.  
9.    for (i = 0; i < FIX_BTMAPS_SLOTS; i++)  
10.        slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);  
11.}  

从代码可见它的实现是依赖fixmap中的转换表,所以它必须要在early_fixmap_init之后才能运行。具体实现细节不详细介绍,因为跟内存管理关系不大。简要用LoyenWang博主的图片介绍下其调用过程:ioremap的空间为7 * 256K的区域,保存在slot_vir[]数组中,当需要进行IO操作的时候,最终会调用到__early_ioremap函数,在该函数中去填充对应的pte entry,从而完成最终的虚拟地址和物理地址的映射(prev_map存放的是申请的fixed map区域的虚拟地址,pre_size表示申请虚拟地址长度)。

在这里插入图片描述

4.early_ioremap_init转换表初始化过程

3.setup_machine_fdt

在这里插入图片描述

5.读取dtb文件memory node函数流程图

上图是该函数调用流程图,下面介绍代码实现细节:

1.//arc/arm64/setup.c
2.void __init setup_arch(char **cmdline_p)  
3.{  
4.    ...... 
5.    early_fixmap_init();  
6.    early_ioremap_init();  
7.    /*__fdt_pointer,dtb所在的物理地址由bootloader通过x0寄存器传递过来 
8.     *(bootloader)只传递了这么一个参数,通过该参数能获得很多信息 
9.    */  
10.    setup_machine_fdt(__fdt_pointer);  
11.    .......
12.} 

1.//arc/arm64/setup.c
2.static void __init setup_machine_fdt(phys_addr_t dt_phys)  
3.{  
4.    //返回dtb所在的虚拟地址dt_virt
5.    void *dt_virt = fixmap_remap_fdt(dt_phys);----->(A)  
6.  
7.    if (!dt_virt || !early_init_dt_scan(dt_virt)) {  -------(B)
8.       ......  
9.    }  
10.  
11.    dump_stack_set_arch_desc("%s (DT)", of_flat_dt_get_machine_name());  
12.}  

fixmap_remap_fdt

fixmap_remap_fdt传入dtb所在的物理地址dt_phys返回其虚拟地址dt_virt,此时运行内核cpu已经开启了MMU。下面是其实现代码细节和调用流程:

1.//arc/arm64/mm/mmu.c
2.void *__init fixmap_remap_fdt(phys_addr_t dt_phys)  
3.{  
4.    void *dt_virt;  
5.    int size;  
6.    //完成fdt PTE表entry的内容填写,返回fdt起始虚拟地址dt_virt和空间大小size
7.    dt_virt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);  
8.    if (!dt_virt)  
9.        return NULL;  
10.    /*把DTB所占的内存添加到memblock管理模块的reserve模块里,这样后续内存分配不会使用这段内存。在后面
11. 	*会使用memblock_free()把该内存释放
12. 	*/
13.    memblock_reserve(dt_phys, size);  
14.    return dt_virt;  
13.}  
1.//arc/arm64/mm/mmu.c
2.void *__init __fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot)  
3.{  
      //为dtb提供虚拟地址的base,静态定义事先预留
15.    const u64 dt_virt_base = __fix_to_virt(FIX_FDT);  
16.    int offset;  de
17.    void *dt_virt;  
18.  
19.    BUILD_BUG_ON(MIN_FDT_ALIGN < 8);  
       //检查物理地址的对齐,必须MIN_FDT_ALIGN对齐
20.    if (!dt_phys || dt_phys % MIN_FDT_ALIGN)  
21.        return NULL;  
22.    //检查虚拟地址的对齐,必须SZ_2M对齐
23.    BUILD_BUG_ON(dt_virt_base % SZ_2M);  
24.    /*
25. 	*保证FDT所在的虚拟地址范围落在early_fixmap_init函数建立的PMD范围内以为在early_fixmap_init已经建
26. 	*立了PUD和PMD,不能让其额外浪费PMD内存
        */
27.    BUILD_BUG_ON(__fix_to_virt(FIX_FDT_END) >> SWAPPER_TABLE_SHIFT !=  
28.             __fix_to_virt(FIX_BTMAP_BEGIN) >> SWAPPER_TABLE_SHIFT);  
29.    //物理地址偏移(2M空间范围,末尾21位)
30.    offset = dt_phys % SWAPPER_BLOCK_SIZE;
31.    //偏移后实际的虚拟地址  
32.    dt_virt = (void *)dt_virt_base + offset;  
33.  
34.    //根据提供的物理地址和虚拟地址设置页表的entry
35.    create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),  
            dt_virt_base, SWAPPER_BLOCK_SIZE, prot);  
36.  
37.    //根据实际的虚拟地址访问物理地址空间内容,即FDT文件内容,此处检测DTB文件首部内容是否是DTB魔数
38.    if (fdt_magic(dt_virt) != FDT_MAGIC)  
39.        return NULL;  
40.    //获取dtb文件大小
41.    *size = fdt_totalsize(dt_virt); 
42.    //DTB大小不能超过2M 
43.    if (*size > MAX_FDT_SIZE)  
44.        return NULL;  
45.    //如果dtb文件结尾的地址空间超过了上面建立的2M地址范围,需要紧接着再映射2M地址空间
46.    if (offset + *size > SWAPPER_BLOCK_SIZE)  
47.         create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),	dt_virt_base,  			
48. 						round_up(offset + *size, SWAPPER_BLOCK_SIZE), prot);  
50.    return dt_virt;  
38.}  

early_init_dt_scan

early_init_dt_scan主要是对dtb进行早期的扫描工作,这里面和内存初始化有关的函数是early_init_dt_scan_memory,下面是简要介绍函数的调用流程和实现细节:

early_init_dt_scan
		-----early_init_dt_verify(对dtb头进行检查)
   		 	  -----early_init_dt_scan_nodes
1.//driver/of/fdt.c  
2.void __init early_init_dt_scan_nodes(void)  
3.{  
4.    /* 
5. 	   *扫描chosen节点,并把bootargs属性值拷贝到boot_command_line中如果定义了CONFIG_CMDLINE这个宏即通过
6. 	   *config文件配置命令行,则把配置的命令行参数也拷贝到boot_command_line
8.     */  
9.    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);  
10.  
11.    /* Initialize {size,address}-cells info */  
12.    of_scan_flat_dt(early_init_dt_scan_root, NULL);  
13.  
14.    /* Setup memory, calling early_init_dt_add_memory_arch */  
15.    of_scan_flat_dt(early_init_dt_scan_memory, NULL);  
15.}  

of_scan_flat_dt

of_scan_flat_dt对dtb里面的所有节点进行扫描,用提供的回调函数循环处理节点信息,回调函数返回0继续扫描,返回非0结束扫描,当扫描到最后一个节点也会结束扫描。

early_init_dt_scan_memory对dtb中的memory node进行解析

1.//drivers/of/fdt.c  
2.int __init early_init_dt_scan_memory(unsigned long node, const char *uname, 
3.                     int depth, void *data)  
4.{  
4.    const char *type = of_get_flat_dt_prop(node, "device_type", NULL);  
5.    const __be32 *reg, *endp;  
6.    int l;  
7. 	   //此处挑选节点的device_type是否是memory节点,不是直接跳过该节点,但是ppc32架构属特殊情况需额外处理    
8.    if (type == NULL) {  
9.        /* 
10.         * The longtrail doesn't have a device_type on the 
11.         * /memory node, so look for the node called /memory@0. 
12.         */  
13.        if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)  
14.            return 0;  
15.    } else if (strcmp(type, "memory") != 0)  
16.        return 0;  
17.    //memory node的物理地址信息保存在"linux,usable-memory"或者"reg"属性中
18.    reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);  
19.    if (reg == NULL)  
20.        reg = of_get_flat_dt_prop(node, "reg", &l);  
21.    if (reg == NULL)  
22.        return 0;  
23.    //l / sizeof(__be32)是reg属性值的cell数目,reg指向第一个cell,endp指向最后一个cell
24.    endp = reg + (l / sizeof(__be32));  
25.  
26.    pr_debug("memory scan node %s, reg size %d,\n", uname, l);  
27.    /*
28. 	*memory node的reg属性值其实就是一个数组,数组中的每一个entry都是base address和size的二元组。解
29.     *析reg属性需要两个参数,dt_root_addr_cells和dt_root_size_cells,这两个参数分别定义了root节点的
30. 	*子节点(比如说memory node)reg属性中base address和size的cell数目,如果等于1,基地址(或者
31. 	*size)用一个32-bit的cell表示。对于ARMv8,一般dt_root_addr_cells和dt_root_size_cells等
32. 	*于2,表示基地址(或者size)用两个32-bit的cell表示dt_root_addr_cells和
33. 	*dt_root_size_cells这两个参数的解析在early_init_dt_scan_root中完成
34. 	*/
35.    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {  
36.        u64 base, size;  
37.  
38.        base = dt_mem_next_cell(dt_root_addr_cells, &reg);  
39.        size = dt_mem_next_cell(dt_root_size_cells, &reg);  
40.  
41.        if (size == 0)  
42.            continue;  
43.        pr_debug(" - %llx ,  %llx\n", (unsigned long long)base,  
44.            (unsigned long long)size);  
45.         //向系统注册该memory node内存区域,通过memblock_add完成添加
46.        early_init_dt_add_memory_arch(base, size);  
47.    }  
48.  
49.    return 0;  
48.}

完成DTB映射后,内核可以访问这段内存。内核可以通过DTB文件的解析了解到内存的布局情况,为后续内存的初始化管理做好准备工作。而内存布局的信息收集主要来自下列几个途径:

  1. choosen node。该节点有一个bootargs属性,该属性定义了内核的启动参数,而在启动参数中,可能包括了mem=nn[KMG]这样的参数项。initrd-start和initrd-end参数定义了initial ramdisk image的物理地址范围。
  2. memory node。这个节点主要定义了系统中的物理内存布局。主要的布局信息是通过reg属性来定义的,该属性定义了若干的起始地址和size条目。
  3. DTB header中的memreserve域。对于dts而言,这个域是定义在root node之外的一行字符串,例如:/memreserve/ 0x05e00000 0x00100000;,memreserve之后的两个值分别定义了起始地址和size。对于dtb而言,memreserve这个字符串被DTC解析并称为DTB header中的一部分。更具体的信息可以参考device tree基础文档,了解DTB的结构。
  4. reserved-memory node。这个节点及其子节点定义了系统中保留的内存地址区域。保留内存有两种,一种是静态定义的,用reg属性定义的address和size。另外一种是动态定义的,只是通过size属性定义了保留内存区域的长度,或者通过alignment属性定义对齐属性,动态定义类型的子节点的属性不能精准的定义出保留内存区域的起始地址和长度。在建立地址映射方面,可以通过no-map属性来控制保留内存区域的地址映射关系的建立。

内核收集了内存布局信息后会通过memblock模块来对这些内存进行管理。最后内存资源被保存在memblock的memory type数组中。

4.arm64_memblock_init

此函数主要是整理内存区域,将一些特殊的区域添加到memblock内存管理模块中去

1.void __init arm64_memblock_init(void)  
2.{  
3.        ......  
4.        ......  
5.    reserve_elfcorehdr();  
6.    //预留内存,当内核crashing后进行相关操作的空间
7.    reserve_crashkernel();  
8.    //将内核代码段设置位reserved类型  
9.    memblock_reserve(__pa(_text), _end - _text);  
10.#ifdef CONFIG_BLK_DEV_INITRD  
10.    if (initrd_start) {  
11.        //将内核initrd段设置位reserved类型  
12.        memblock_reserve(initrd_start, initrd_end - initrd_start);  
13.  
14.        /* the generic initrd code expects virtual addresses */  
15.        initrd_start = __phys_to_virt(initrd_start);  
16.        initrd_end = __phys_to_virt(initrd_end);  
17.    }  
19.#endif  
18.    //将dtb中的reserved-memory区域设置位reserved类型  
19.    early_init_fdt_scan_reserved_mem();  
20.  
21.    /* 4GB maximum for 32-bit only capable devices */  
22.    if (IS_ENABLED(CONFIG_ZONE_DMA))  
23.        arm64_dma_phys_limit = max_zone_dma_phys();  
24.    else  
25.        arm64_dma_phys_limit = PHYS_MASK + 1;  
26.    /*
27.     *在arm64架构中不再需要高端内存,为了与原先接口保存兼容,内核将高端内存的起始地址设置位物理内存的
28. 	*结束地址
29.     */
30.    high_memory = __va(memblock_end_of_DRAM() - 1) + 1;  
31.    //申请公共区域CMA  
32.    dma_contiguous_reserve(arm64_dma_phys_limit);  
33.  
34.    memblock_allow_resize();  
36.}  

代码中可以看出arm64_memblock_init函数主要是remove一些no-map(不归内核管理)区域,并保留一些关键的区域如:内核镜像区域,ramdisk镜像和dtb中的reserved内存节点,最后还会申请一个公共区域CMA。该函数最终目的是通过membloc模块把内存中的空闲和被占用的区域进行分开管理。图6展示内核如何通过memblock模块内存布局的示意图:

在这里插入图片描述

6.memblock管理下的内核内存布局示意图
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值