Linux内存管理之三 Linux内存初始化

有了armv8架构访问内存的理解,我们来看下linux在内存这块的初始化就更容易理解了。

创建启动页表:

在汇编代码阶段的head.S文件中,负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。

  • identity map:是指把idmap_text区域的物理地址映射到相等的虚拟地址上,这种映射完成后,其虚拟地址等于物理地址。idmap_text区域都是一些打开MMU相关的代码。

  • kernel image map:将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行映射。

arch/arm64/kernel/head.S:
ENTRY(stext)
        bl      preserve_boot_args
        bl      el2_setup                       // Drop to EL1, w0=cpu_boot_mode
        adrp    x23, __PHYS_OFFSET
        and     x23, x23, MIN_KIMG_ALIGN - 1    // KASLR offset, defaults to 0
        bl      set_cpu_boot_mode_flag
        bl      __create_page_tables
        /*
         * The following calls CPU setup code, see arch/arm64/mm/proc.S for
         * details.
         * On return, the CPU will be ready for the MMU to be turned on and
         * the TCR will have been set.
         */
        bl      __cpu_setup                     // initialise processor
        b       __primary_switch
ENDPROC(stext)

__create_page_tables主要执行的就是identity map和kernel image map:

 __create_page_tables:
......
        create_pgd_entry x0, x3, x5, x6
        mov     x5, x3                          // __pa(__idmap_text_start)
        adr_l   x6, __idmap_text_end            // __pa(__idmap_text_end)
        create_block_map x0, x7, x3, x5, x6

        /*
         * Map the kernel image (starting with PHYS_OFFSET).
         */
        adrp    x0, swapper_pg_dir
        mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)
        add     x5, x5, x23                     // add KASLR displacement
        create_pgd_entry x0, x5, x3, x6
        adrp    x6, _end                        // runtime __pa(_end)
        adrp    x3, _text                       // runtime __pa(_text)
        sub     x6, x6, x3                      // _end - _text
        add     x6, x6, x5                      // runtime __va(_end)
        create_block_map x0, x7, x3, x5, x6
 ......

其中调用create_pgd_entry进行PGD及所有中间level(PUD, PMD)页表的创建,调用create_block_map进行PTE页表的映射。关于四级页表的关系如下图所示,这里就不进一步解释了。

汇编结束后的内存映射关系如下图所示:

当执行完上面的map之后,MMU就已经打开了并且开始进入C代码运行阶段,那么下一步就要对dtb进行映射了。

fixmap区之dtb map:

在执行setup_arch中,会最先进行early_fixmap_init(),这个函数就是用来map dtb的,但是它只会建立dtb对应的这段物理地址中间level的页表entry,而最后一个level的页表映射则通过setup_machine_fdt函数里的fixmap_remap_fdt来创建。

void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{
     void *dt_virt;
     int size;

     dt_virt = __fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);
     if (!dt_virt)
         return NULL;

     memblock_reserve(dt_phys, size);
     return dt_virt;
 }

fixmap_remap_fdt主要是为fdt建立地址映射,在该函数的最后,顺便就调用memblock_reserve保留了该段内存。

可以看出dtb的映射采用的是fixmap,所谓fixmap就是固定映射,它需要我们明确的知道想要映射的物理地址,并把这段地址映射到想要映射的虚拟地址上。当然这里固定映射还有些片面,因为在fixmap机制实现上,也有支持动态分配虚拟地址的功能,这个功能主要用于临时fixmap映射(这个临时映射就是用来执行early ioremap使用的。),而dtb的映射属于永久映射。

fixmap区之early ioremap:

对于一些硬件需要在内存管理系统起来之前就要工作的,我们就可以使用这种机制来映射内存给这些硬件driver使用。各个模块在使用完early ioremap的地址后,需要尽快把这段映射的虚拟地址释放掉,这样才能反复被其他模块继续申请使用。

early_ioremap_init会调用early_ioremap_setup:

可见它的实现是依赖fixmap的,所以它必须要在early_fixmap_init之后才能运行。

注意:如果想要在伙伴系统初始化之前进行设备寄存器的访问,那么可以考虑early IO remap机制。

至此我们已经知道dtb和early ioremap都是在fixmap区的,如下图:

系统内存的布局:

完成dtb的map之后,内核可以访问这一段的内存了,通过解析dtb中的内容,内核可以勾勒出整个内存布局的情况,为后续内存管理初始化奠定基础。这一步主要在setup_machine_fdt中完成。这里就不看代码了,其调用流程是:setup_machine_fdt->early_init_dt_scan->early_init_dt_scan_nodes

就像注释中所示内核根据dtb的不同node勾勒出choosen node,root node,memory node相应内存区域。

除了这3个node,还有一个reserved-memory node,它是在上面讲到dtb map的时候fixmap_remap_fdt函数做的。下面我们看下这4个node的具体实现。

  • 该节点有一个bootargs属性,该属性定义了内核的启动参数,比如mem= xx,此外,还处理initrd相关的property,并保存在initrd_start和initrd_end这两个全局变量中。

  • root node 与内存无关,暂时不详述,以后有机会讲到device tree系列再详述。

  • memory node

 

通过memblock_add加入到memblock.memory对应的memblock_type链表中进行管理。

接下来到arm64_memblock_init函数:

void __init arm64_memblock_init(void)
{
......
     memblock_reserve(__pa_symbol(_text), _end - _text); 1.kernel image保留区
 #ifdef CONFIG_BLK_DEV_INITRD
     if (initrd_start) {
         memblock_reserve(initrd_start, initrd_end - initrd_start); 2.initrd保留区
         /* the generic initrd code expects virtual addresses */
         initrd_start = __phys_to_virt(initrd_start);
         initrd_end = __phys_to_virt(initrd_end);
     }
 #endif
     early_init_fdt_scan_reserved_mem(); 3.dts中配置为保留的区域
......
}
  • reserve内核代码、数据区等(_text到_end那一段,具体的内容可以参考内核链接脚本)

  • 保留initital ramdisk image区域(从initrd_start到initrd_end区域)

  • reserved-memory node 如下所示:

通过上面的一系列操作,需要动态管理的内存已经被放到了memory type和reserved type这两个region中了,现在内存已经被memblock模块所管理了,这只是启动后的第一步......

我们接着看linux初始化内存的下半部分,等内存初始化后就可以进入真正的内存管理了,初始化我总结了一下,大体分为三步:

  1. 物理内存进系统前

  2. 用memblock模块来对内存进行管理

  3. 页表映射

  4. zone初始化

前两步在linux里分别对应如下操作:

  1. fixed map 加载dtb :Uboot会将kernel image和dtb拷贝到内存中,并且将dtb物理地址告知kernel

  2. 系统解析dtb里的内存参数:kernel需要从该物理地址上读取到dtb文件并解析,才能得到最终的内存信息

上面两个步骤可以简单参考上篇文章,本文在上面2个步骤的基础上延续向下讲,进入 paging_init()和 bootmem_init()

paging_init

void __init paging_init(void)
{
 phys_addr_t pgd_phys = early_pgtable_alloc();//分配一页大小的物理内存放进pgd
 pgd_t *pgd = pgd_set_fixmap(pgd_phys);

 map_kernel(pgd);//将内核的各个段进行映射 .text .init .data .bss
 map_mem(pgd);//将memblock子系统添加的物理内存进行映射(将物理地址映射到线性区域)

 /*
  * We want to reuse the original swapper_pg_dir so we don't have to
  * communicate the new address to non-coherent secondaries in
  * secondary_entry, and so cpu_switch_mm can generate the address with
  * adrp+add rather than a load from some global variable.
  *
  * To do this we need to go via a temporary pgd.
  */
 cpu_replace_ttbr1(__va(pgd_phys));//切换页表
 memcpy(swapper_pg_dir, pgd, PGD_SIZE);//将新建立的页表内容替换swapper_pg_dir页表内容
 cpu_replace_ttbr1(lm_alias(swapper_pg_dir));

 pgd_clear_fixmap();
 memblock_free(pgd_phys, PAGE_SIZE);

 /*
  * We only reuse the PGD from the swapper_pg_dir, not the pud + pmd
  * allocated with it.
  */
 memblock_free(__pa_symbol(swapper_pg_dir) + PAGE_SIZE,
        SWAPPER_DIR_SIZE - PAGE_SIZE);
}
  • early_pgtable_alloc:分配一页大小的物理内存放进pgd

  • map_kernel(pgd):将内核的各个段进行映射(.text .init .data .bss)

  • map_mem(pgd):将memblock子系统添加的物理内存进行映射(将物理地址映射到线性区域)

主要是完成通过memblock_add添加到系统中的物理内存映射,注意如果memblock设置了MEMBLOCK_NOMAP标志的话则不对其地址映射。

  • cpu_replace_ttbr1(__va(pgd_phys)):切换页表

  • memcpy(swapper_pg_dir, pgd, PGD_SIZE):将新建立的页表内容替换swapper_pg_dir页表内容

bootmem_init

void __init bootmem_init(void)
{
 unsigned long min, max;

 min = PFN_UP(memblock_start_of_DRAM());
 max = PFN_DOWN(memblock_end_of_DRAM());

 early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);

 max_pfn = max_low_pfn = max;

 arm64_numa_init();
 /*
  * Sparsemem tries to allocate bootmem in memory_present(), so must be
  * done after the fixed reservations.
  */
 arm64_memory_present();

 sparse_init();
 zone_sizes_init(min, max);

 memblock_dump_all();
}

「Linux是如何组织物理内存的?」

「node」

目前计算机系统有两种体系结构:

  • 非一致性内存访问 NUMA(Non-Uniform Memory Access)意思是内存被划分为各个node,访问一个node花费的时间取决于CPU离这个node的距离。每一个cpu内部有一个本地的node,访问本地node时间比访问其他node的速度快

  • 一致性内存访问 UMA(Uniform Memory Access)也可以称为SMP(Symmetric Multi-Process)对称多处理器。意思是所有的处理器访问内存花费的时间是一样的。也可以理解整个内存只有一个node。

「zone」

ZONE的意思是把整个物理内存划分为几个区域,每个区域有特殊的含义

enum zone_type {
#ifdef CONFIG_ZONE_DMA
 /*
  * ZONE_DMA is used when there are devices that are not able
  * to do DMA to all of addressable memory (ZONE_NORMAL). Then we
  * carve out the portion of memory that is needed for these devices.
  * The range is arch specific.
  *
  * Some examples
  *
  * Architecture  Limit
  * ---------------------------
  * parisc, ia64, sparc <4G
  * s390   <2G
  * arm   Various
  * alpha  Unlimited or 0-16MB.
  *
  * i386, x86_64 and multiple other arches
  *    <16M.
  */
 ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
 /*
  * x86_64 needs two ZONE_DMAs because it supports devices that are
  * only able to do DMA to the lower 16M but also 32 bit devices that
  * can only do DMA areas below 4G.
  */
 ZONE_DMA32,
#endif
 /*
  * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
  * performed on pages in ZONE_NORMAL if the DMA devices support
  * transfers to all addressable memory.
  */
 ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
 /*
  * A memory area that is only addressable by the kernel through
  * mapping portions into its own address space. This is for example
  * used by i386 to allow the kernel to address the memory beyond
  * 900MB. The kernel will set up special mappings (page
  * table entries on i386) for each page that the kernel needs to
  * access.
  */
 ZONE_HIGHMEM,
#endif
 ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
 ZONE_DEVICE,
#endif
 __MAX_NR_ZONES

};

「page」

代表一个物理页,在内核中一个物理页用一个struct page表示。

「page frame」:

为了描述一个物理page,内核使用struct page结构来表示一个物理页。假设一个page的大小是4K的,内核会将整个物理内存分割成一个一个4K大小的物理页,而4K大小物理页的区域我们称为page frame

「page frame num(pfn)」 :

pfn是对每个page frame的编号。故物理地址和pfn的关系是:

物理地址>>PAGE_SHIFT = pfn

「pfn和page的关系」

内核中支持了好几个内存模型:CONFIG_FLATMEM(平坦内存模型)CONFIG_DISCONTIGMEM(不连续内存模型)CONFIG_SPARSEMEM_VMEMMAP(稀疏的内存模型)目前ARM64使用的稀疏的类型模式


/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)

系统启动的时候,内核会将整个struct page映射到内核虚拟地址空间vmemmap的区域,所以我们可以简单的认为struct page的基地址是vmemmap,则:

vmemmap+pfn的地址就是此struct page对应的地址。

最后

至此linux对物理内存的初始化和虚拟地址和物理地址的映射关系算是告一段落,相信你已经知道 linux 虚拟寻址空间layout的来龙去脉,以及如何把物理内存通过node, zone, page frame来软件模拟。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值