欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

【原创】(二)Linux物理内存初始化

程序员文章站 2023-01-22 12:31:02
背景 By 鲁迅 By 高尔基 说明: 1. Kernel版本:4.14 2. ARM64处理器,Contex A53,双核 3. 使用工具:Source Insight 3.5, Visio 1. 介绍 让我们思考几个朴素的问题? 1. 系统是怎么知道物理内存的? 2. 在内存管理真正初始化之前, ......

背景

  • read the fucking source code! --by 鲁迅
  • a picture is worth a thousand words. --by 高尔基

说明:

  1. kernel版本:4.14
  2. arm64处理器,contex-a53,双核
  3. 使用工具:source insight 3.5, visio

1. 介绍

让我们思考几个朴素的问题?

  1. 系统是怎么知道物理内存的?
  2. 在内存管理真正初始化之前,内核的代码执行需要分配内存该怎么处理?

我们先来尝试回答第一个问题,看过dts文件的同学应该见过memory的节点,以arch/arm64/boot/dts/freescale/fsl-ls208xa.dtsi为例:

    memory@80000000 {
        device_type = "memory";
        reg = <0x00000000 0x80000000 0 0x80000000>;
              /* dram space - 1, size : 2 gb dram */
    };

这个节点描述了内存的起始地址及大小,事实上内核在解析dtb文件时会去读取该memory节点的内容,从而将检测到的内存注册进系统。

那么新的问题又来了?uboot会将kernel imagedtb拷贝到内存中,并且将dtb物理地址告知kernelkernel需要从该物理地址上读取到dtb文件并解析,才能得到最终的内存信息,dtb的物理地址需要映射到虚拟地址上才能访问,但是这个时候paging_init还没有调用,也就是说物理地址的映射还没有完成,那该怎么办呢?没错,fixed map机制出现了。

第二个问题答案:当所有物理内存添加进系统后,在mm_init之前,系统会使用memblock模块来对内存进行管理。

开启探索之旅吧!

2. early_fixmap_init

简单来说,fixed map指的是虚拟地址中的一段区域,在该区域中所有的线性地址是在编译阶段就确定好的,这些虚拟地址需要在boot阶段去映射到物理地址上。
来张图片看看虚拟地址空间:
【原创】(二)Linux物理内存初始化

图中fixed: 0xffffffbefe7fd000 - 0xffffffbefec00000,描述的就是fixed map的区域。

那么这段区域中的详细一点的布局是怎样呢?看看arch/arm64/include/asm/fixmap.h中的enum fixed_address结构就清晰了,图来了:
【原创】(二)Linux物理内存初始化

从图中可以看出,如果要访问dtb所在的物理地址,那么需要将该物理地址映射到fixed map中的区域,然后访问该区域中的虚拟地址即可。访问io空间也是一样的道理,下文也会讲述到。

那么来看看early_fixmap_init函数的关键代码吧:

void __init early_fixmap_init(void)
{
    pgd_t *pgd;
    pud_t *pud;
    pmd_t *pmd;
    unsigned long addr = fixaddr_start;              /* (1) */

    pgd = pgd_offset_k(addr);           /* (2) */
    if (config_pgtable_levels > 3 &&
        !(pgd_none(*pgd) || pgd_page_paddr(*pgd) == __pa_symbol(bm_pud))) {
        /*
         * we only end up here if the kernel mapping and the fixmap
         * share the top level pgd entry, which should only happen on
         * 16k/4 levels configurations.
         */
        bug_on(!is_enabled(config_arm64_16k_pages));
        pud = pud_offset_kimg(pgd, addr);
    } else {
        if (pgd_none(*pgd))
            __pgd_populate(pgd, __pa_symbol(bm_pud), pud_type_table);          /* (3) */
        pud = fixmap_pud(addr);
    }
    if (pud_none(*pud))
        __pud_populate(pud, __pa_symbol(bm_pmd), pmd_type_table);    /* (4) */
    pmd = fixmap_pmd(addr);
    __pmd_populate(pmd, __pa_symbol(bm_pte), pmd_type_table);        /* (5) */
......
}

关键点:

  1. fixaddr_start,定义了fixed map区域的起始地址,位于arch/arm64/include/asm/fixmap.h中;
  2. pgd_offset_k(addr),获取addr地址对应pgd全局页表中的entry,而这个pgd全局页表正是swapper_pg_dir全局页表;
  3. bm_pud的物理地址写到pgd全局页目录表中;
  4. bm_pmd的物理地址写到pud页目录表中;
  5. bm_pte的物理地址写到pmd页表目录表中;

bm_pud/bm_pmd/bm_pte是三个全局数组,相当于是中间的页表,存放各级页表的entry,定义如下:

static pte_t bm_pte[ptrs_per_pte] __page_aligned_bss;
static pmd_t bm_pmd[ptrs_per_pmd] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[ptrs_per_pud] __page_aligned_bss __maybe_unused;

事实上,early_fixmap_init只是建立了一个映射的框架,具体的物理地址和虚拟地址的映射没有去填充,这个是由使用者具体在使用时再去填充对应的pte entry。比如像fixmap_remap_fdt()函数,就是典型的填充pte entry的过程,完成最后的一步映射,然后才能读取dtb文件。

来一张图片就懂了,是透彻的懂了:
【原创】(二)Linux物理内存初始化

3. early_ioremap_init

如果在boot早期需要操作io设备的话,那么ioremap就用上场了,由于跟实际的内存管理关系不太大,不再太深入的分析。
【原创】(二)Linux物理内存初始化

简单来说,ioremap的空间为7 * 256k的区域,保存在slot_vir[]数组中,当需要进行io操作的时候,最终会调用到__early_ioremap函数,在该函数中去填充对应的pte entry,从而完成最终的虚拟地址和物理地址的映射。

4. memblock

上文讲的内容都只是铺垫,为了能正确访问dtb文件并且解析得到物理地址信息。从入口到最终添加的调用过程如下图:
【原创】(二)Linux物理内存初始化

所以,这个章节的重点就是memblock模块,这个是早期的内存分配管理器,我不禁想起了之前在nuttx中的内存池实现了,细节已然不太清晰了,但是框架性的思维都大同小异。

4.1 结构体

【原创】(二)Linux物理内存初始化

总共由三个数据结构来描述:

  • struct memblock定义了一个全局变量,用来维护所有的物理内存;
  • struct memblock_type代表系统中的内存类型,包括实际使用的内存和保留的内存;
  • struct memblock_region用来描述具体的内存区域,包含在struct memblock_type中的regions数组中,最多可以存放128个。

直接上个代码吧:

static struct memblock_region memblock_memory_init_regions[init_memblock_regions] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[init_memblock_regions] __initdata_memblock;
#ifdef config_have_memblock_phys_map
static struct memblock_region memblock_physmem_init_regions[init_physmem_regions] __initdata_memblock;
#endif

struct memblock memblock __initdata_memblock = {
    .memory.regions     = memblock_memory_init_regions,
    .memory.cnt     = 1,    /* empty dummy entry */
    .memory.max     = init_memblock_regions,
    .memory.name        = "memory",

    .reserved.regions   = memblock_reserved_init_regions,
    .reserved.cnt       = 1,    /* empty dummy entry */
    .reserved.max       = init_memblock_regions,
    .reserved.name      = "reserved",

#ifdef config_have_memblock_phys_map
    .physmem.regions    = memblock_physmem_init_regions,
    .physmem.cnt        = 1,    /* empty dummy entry */
    .physmem.max        = init_physmem_regions,
    .physmem.name       = "physmem",
#endif

    .bottom_up      = false,
    .current_limit      = memblock_alloc_anywhere,
};

定义的memblock为全局变量,在定义的时候就进行了初始化。初始化的时候,regions指向的也是静态全局的数组,其中数组的大小为init_memblock_regions,也就是128个,限制了这些内存块的个数了,实际在代码中可以看到,当超过这个数值时,数组会以2倍的速度动态扩大。

初始化完了后,大体是这个样子的:
【原创】(二)Linux物理内存初始化

4.2 memblock_add/memblock_remove

memblock子模块,基本的逻辑都是围绕内存的添加和移除操作来展开,最终是通过调用memblock_add_range/memblock_remove_range来实现的。

  • memblock_add_range
    【原创】(二)Linux物理内存初始化

图中的左侧是函数的执行流程图,执行效果是右侧部分。右侧部分画的是一个典型的情况,实际的情况可能有多种,但是核心的逻辑都是对插入的region进行判断,如果出现了物理地址范围重叠的部分,那就进行split操作,最终对具有相同flagregion进行merge操作。

  • memblock_remove_range
    该函数执行的一个典型case效果如下图所示:
    【原创】(二)Linux物理内存初始化

假如现在需要移除掉一片区域,而该区域跨越了多个region,则会先调用memblock_isolate_range来对这片区域进行切分,最后再调用memblock_isolate_range对区域范围内的region进行移除操作。

当调用memblock_alloc函数进行地址分配时,最后也是调用memblock_add_range来实现的,申请的这部分内存最终会添加到reserved类型中,毕竟已经分配出去了,其他人也不应该使用了。

5. arm64_memblock_init

当物理内存都添加进系统之后,arm64_memblock_init会对整个物理内存进行整理,主要的工作就是将一些特殊的区域添加进reserved内存中。函数执行完后,如下图所示:
【原创】(二)Linux物理内存初始化

  • 其中浅绿色的框表示的都是保留的内存区域, 剩下的部分就是可以实际去使用的内存了。

物理内存大体面貌就有了,后续就需要进行内存的页表映射,完成实际的物理地址到虚拟地址的映射了。

那就待续吧。

【原创】(二)Linux物理内存初始化