Linux内存初始化之页表映射(一)

时间:2024-04-06 16:34:29

内存初始化主要在start_kernel函数中,主要内容有:

1. 从dts中扫描所有物理内存和预留内存,并添加到memblock子系统

2. 给memblock中的所有非预留memory region建立页表映射

3. sparse内存模型初始化

4. 计算每个zone管理内存的大小以及每个zone的free list初始化

5. 释放所有空闲内存块到伙伴系统

6.初始化slab和vmalloc子系统

7.初始化kasan,memleak等各种内存检测模块

Linux内存初始化之页表映射(一)

1.1 内存扫描

  系统支持的内存,是怎么告诉内核的?在伙伴系统和slab没有初始化时,内存分配是怎么进行的?

系统支持的可用内存,一般都在DTS中声明,包含起始物理地址(base),和大小(size),在系统启动时,解析DTS并添加到memblock子系统. memblock子系统,是Linux启动阶段的内存分配接口.

函数调用关系:

start_kernel->setup_arch->setup_machine_fdt->early_init_dt_scan->early_init_dt_scan_nodes->early_init_dt_scan_memory

->early_init_dt_add_memory_arch->memblock_add
 

预留内存: 一些特殊用途,如记录reset reason,modem子系统的共享内存等,这些内存不会被伙伴系统或者slab系统管理.对用户是不可见的.CMA机制也会预留内存,但最终会被伙伴系统管理.需要专用的cma内存分配接口

memblockCMA的分析,可以参考对应的文章.

2.2页表的映射

2.2.1 Linux内核页表分级

内核启动时进行内存映射, map_mem()->create_mapping()

内核支持4级映射(PGD->PUD->PMD->PTE) ,支持的level由宏CONFIG_PGTABLE_LEVELS定义,目前为3级映射,也即PGD->PMD->PTE

从下图可以知道,每级页表分别使用虚拟地址的9位作为索引,也即每级页表大小为512.  虚拟地址最后12bit作为页内索引.从而表示一个具体的物理地址

Linux内存初始化之页表映射(一)

虚拟地址到物理地址转换

Linux内存初始化之页表映射(一)

2.2.2 页表映射

调用关系start_kernel->setup_arch->paging_init->map_mem->__map_memblock

static void  __create_mapping(struct mm_struct *mm, pgd_t *pgd,
                    phys_addr_t phys, unsigned long virt,
                    phys_addr_t size, pgprot_t prot,
                    void *(*alloc)(unsigned long size))
{
    unsigned long addr, length, end, next;

    addr = virt & PAGE_MASK;
    length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

    end = addr + length;//映射结束地址
    do {
        next = pgd_addr_end(addr, end);//下一个PGD的起始映射地址
        alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);//给一个PGD建立对应的PUD,PMD,PTE
        phys += next - addr;
    } while (pgd++, addr = next, addr != end);//映射下一个PGD表项
}

下面分析PUD的映射

static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
                  unsigned long addr, unsigned long end,
                  phys_addr_t phys, pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pud_t *pud;
    unsigned long next;

    if (pgd_none(*pgd)) {//如果PGD里没有填充PUD,则通过memblock接口分配PUD数组内存,这里我们没有用到PUD,所以PGD=PUD
        pud = alloc(PTRS_PER_PUD * sizeof(pud_t));//分配PUD数组
        pgd_populate(mm, pgd, pud);//用PUD的物理地址,填充PGD表项
    }

    pud = pud_offset(pgd, addr);//根据addr从PGD中找到对应的PUD项,这里PUD=PGD
    do {
        next = pud_addr_end(addr, end);//计算PUD结束地址
        alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);//给PUD建立PMT和PTE
        phys += next - addr;
    } while (pud++, addr = next, addr != end);//映射下一个PUD,我们这里没有用到PUD,所以直接进行PMD映射
}

PMD表的映射,跟PGD和PUD的映射类似。

static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
                  unsigned long addr, unsigned long end,
                  phys_addr_t phys, pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pmd_t *pmd;
    unsigned long next;

    /*
     * Check for initial section mappings in the pgd/pud and remove them.
     */
    if (pud_none(*pud) || pud_sect(*pud)) {//检查PUD表项是否分配内存

  1.         pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));

        pud_populate(mm, pud, pmd);
        flush_tlb_all();
    }
    BUG_ON(pud_bad(*pud));

    pmd = pmd_offset(pud, addr);//从PUD中提出对应的PMD表项
    do {
        next = pmd_addr_end(addr, end);//计算pmd的结束地址
            alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),//给PMD建立PTE表项
                       prot, alloc);
        phys += next - addr;
    } while (pmd++, addr = next, addr != end);//映射下一个PMD表项
}

最后映射PTE表

static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
                  unsigned long end, unsigned long pfn,
                  pgprot_t prot,
                  void *(*alloc)(unsigned long size))
{
    pte_t *pte;

    if (pmd_none(*pmd) || pmd_sect(*pmd)) {//检查PMD是否已经建立
        pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
        if (pmd_sect(*pmd))
            split_pmd(pmd, pte);
        __pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);//填充PMD表
        flush_tlb_all();
    }
    BUG_ON(pmd_bad(*pmd));

    pte = pte_offset_kernel(pmd, addr);//从PMD中找到对应的PTE
    do {
        set_pte(pte, pfn_pte(pfn, prot));//把物理页帧号设置到对应的PTE表项中,每个PTE表项对应一个4k的page
        pfn++;//映射下一个page
    } while (pte++, addr += PAGE_SIZE, addr != end);//循环映射下一个表项
}

详细的页表映射可以参考Linux启动时的页表映射