Linux内存管理(21)--fixmap addresses原理

作者 by adtxl / 2022-03-31 / 暂无评论 / 391 个足迹

转载自http://www.wowotech.net/memory_management/440.html

1. 引言

fixmap是一段固定地址映射。kernel预留一段虚拟地址空间。因此虚拟地址是在编译的时候确定。fixmap可以用来做什么?kernel启动初期,由于此时的kernel已经运行在虚拟地址上。因此我们访问具体的物理地址是不行的,必须建立虚拟地址和物理地址的映射,然后通过虚拟地址访问才可以。例如:dtb中包含bootloader传递过来的内存信息,我们需要解析dtb,但是我们得到的是dtb的物理地址。因此访问之前必须创建映射,创建映射又需要内存。但是由于所有的内存管理子系统还没有ready。因此我们不能使用ioremap接口创建映射。为此kernel提出fixmap的解决方案。

注:文章代码分析基于linux-4.15,架构基于aarch64(ARM64)。

2. fixmap空间分配

fixmap虚拟地址空间又被平均分成两个部分permanent fixed addresses和temporary fixed addresses。permanent fixed addresses是永久映射,temporary fixed addresses是临时映射。永久映射是指在建立的映射关系在kernel阶段不会改变,仅供特定模块一直使用。临时映射就是模块使用前创建映射,使用后解除映射。

fixmap区域又被继续细分,分配给不同模块使用。kernel中定义枚举类型作为index,根据index可以计算该模拟在fixmap区域的虚拟地址。

enum fixed_addresses {
    FIX_HOLE,
#define FIX_FDT_SIZE        (MAX_FDT_SIZE + SZ_2M)
    FIX_FDT_END,
    FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,    /* 1 */
    FIX_EARLYCON_MEM_BASE,                                   /* 2 */
    FIX_TEXT_POKE0,
    __end_of_permanent_fixed_addresses,
#define NR_FIX_BTMAPS       (SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS    7
#define TOTAL_FIX_BTMAPS    (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
    FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
    FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,  /* 3 */
    FIX_PTE,
    FIX_PMD,
    FIX_PUD,
    FIX_PGD,
    __end_of_fixed_addresses
};
#define FIXADDR_SIZE    (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START   (FIXADDR_TOP - FIXADDR_SIZE) 
  1. FIX_FDT映射设备树使用。在ARM64架构,大小是4M。
  2. early console使用,大小1页。1页虚拟地址空间完全够了,毕竟串口操作相关寄存器没有几个。
  3. early_ioremap()接口使用,这部分属于动态映射。大小是7 × 256KB。

fixmap区域是地址空间范围从FIXADDR_START到FIXADDR_TOP结束。FIXADDR_SIZE是permanent fixed addresses区域的大小。我对这个地方很奇怪,为什么不包括temporary fixed addresses区域的大小呢?如果你知道可以告诉我。fixmap区域可以想象成一块内存以页为单位被平均分成__end_of_permanent_fixed_addresses块。而这些枚举值就是这块内存的index。因此虚拟地址可以根据index进行计算。计算方法如下。

#define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x)    ((FIXADDR_TOP - ((x) & PAGE_MASK)) >> PAGE_SHIFT) 

FIX_FDT是给dtb创建映射的区域。例如需要得到FDT的虚拟地址,即可以利用__fix_to_virt(FIX_FDT)得到虚拟地址。之所以FIX_FDT放在枚举的最前面,是因为我们针对dtb映射采用section mapping要求虚拟地址2M对齐。FIXADDR_TOP地址本身是2M对齐的,因此FIXADDR_TOP - (FIX_FDT << PAGE_SHIFT)可以很容易2M对齐。

3. fixmap初始化

fixmap初始化操作在early_fixmap_init函数中完成。主要是建立PGD/PUD/PMD页表。early_fixmap_init实现如下(简化部分代码逻辑)。

static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;               /* 1 */
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;/* 1 */
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;/* 1 */

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

    pgd = pgd_offset_k(addr);
    if (pgd_none(*pgd))
        __pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);   /* 2 */
    pud = fixmap_pud(addr);
    if (pud_none(*pud))
        __pud_populate(pud, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);   /* 2 */
    pmd = fixmap_pmd(addr);
    __pmd_populate(pmd, __pa_symbol(bm_pte), PMD_TYPE_TABLE);       /* 2 */

    BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
             != (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));       /* 3 */

    if ((pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)))
            || pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_END))) {     /* 3 */
        WARN_ON(1);
    }
}
  1. 静态定义数组作为页表(PUD/PMD/PTE)使用。PGD页表使用swapper_pg_dir
  2. FIXADDR_START为虚拟地址,建立页表映射关系。假设计算FDT的虚拟地址为addr = __fix_to_virt(FIX_FDT),必然addr是2M对齐的一个地址。由上面的分析可知,FIXADDR_START的值位于[addr - 2M, addr]之间。因此访问[addr - 2M, addr]之间的虚拟地址不需要再建立PUD/PMD页表项。只需要设置PTE页表对应的页表项即可。
  3. [FIX_BTMAP_BEGIN, FIX_BTMAP_END]区域给动态映射使用,保证该区域正好位于[addr - 2M, addr]之间,必须检查动态映射区域小于2M。定义的PTE页表数组其实是给动态映射使用的。当我们需要访问物理地址A,从[addr - 2M, addr]区域找到一个合适的虚拟地址B,填充B地址对应的PTE页表项即可访问。这也是early_ioremap的实现原理。

经过early_fixmap_init函数的探究,我们也可以得到一个结论:为了以page为单位进行映射,必须保证FIX_FDT和__end_of_fixed_addresses之间的虚拟地址空间必须小于2M。如果有超过2M的部分就要使用section mapping(因为只有一个PTE页表),以2M为单位映射。

4. early ioremap初始化

如果你希望kernel启动早期使用ioremap操作,其实是不行的。我们必须借助early ioremap接口。early ioremap是基于fixmap实现。初始化在early_ioremap_init完成。简化部分代码如下。

static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;

void __init early_ioremap_setup(void)
{
    int i;

    for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
        slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}

void __init early_ioremap_init(void)
{
    early_ioremap_setup();
}

early ioremap利用slot管理映射,最多支持FIX_BTMAPS_SLOTS个映射,每个映射最大支持映射256KB。slot_virt数组存储每个slot的虚拟地址首地址。prev_map数组用来记录已经分配出去的虚拟地址,数组值为0代表没有分配。prev_size记录映射的size。

4. 创建FDT映射

创建FDT映射的函数是__fixmap_remap_fdt,实现如下。

void *__init __fixmap_remap_fdt(phys_addr_t dt_phys,
        int *size, pgprot_t prot)
{
    const u64 dt_virt_base = __fix_to_virt(FIX_FDT);                 /* 1 */
    int offset;
    void *dt_virt;

    BUILD_BUG_ON(dt_virt_base % SZ_2M);                              /* 2 */

    BUILD_BUG_ON(__fix_to_virt(FIX_FDT_END) >> SWAPPER_TABLE_SHIFT !=
             __fix_to_virt(FIX_BTMAP_BEGIN) >> SWAPPER_TABLE_SHIFT); /* 3 */

    offset = dt_phys % SWAPPER_BLOCK_SIZE;
    dt_virt = (void *)dt_virt_base + offset;

    create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),  /* 4 */
            dt_virt_base, SWAPPER_BLOCK_SIZE, prot);

    *size = fdt_totalsize(dt_virt);
    if (offset + *size > SWAPPER_BLOCK_SIZE)
        create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE), dt_virt_base,
               round_up(offset + *size, SWAPPER_BLOCK_SIZE), prot);  /* 5 */

    return dt_virt;
}
  1. 根据__fix_to_virt计算fdt虚拟地址。
  2. 虚拟地址必须2M对齐。主要是为了section mapping。
  3. early_fixmap_init函数中,已经建立了PUD/PMD页表。为了让映射过程不用分配额外的PMD页表内存,这里必须保证FDT所在的虚拟地址范围落在early_fixmap_init函数建立的PMD页表范围内。
  4. 创建页表。这里映射2M空间。
  5. 如果dtb文件结尾地址超过上一个建立映射的地址范围,就必须紧接着再映射2M空间。

独特见解