ARM64页表映射

发布时间 2023-04-13 09:18:03作者: yooooooo

1.ARMv8-A架构

基于ARMv8-A架构的处理器最大可以支持到48根地址线,也就是寻址2的48次方的虚拟地址空间,即虚拟地址空间范围为0x0000_0000_0000_0000~0x0000_FFFF_FFFF_FFFF,共256TB。

一个是从0x0000_0000_0000_0000到0x0000_FFFF_FFFF_FFFF,另外一个是从0xFFFF_0000_0000_0000到0xFFFF_FFFF_FFFF_FFFF。

基于ARMv8-A架构的处理器支持的页面大小可以是4KB、16KB或者64KB。映射的层级可以是3级或者4级(地址范围是不一样的)。

下面是页面大小为4KB,地址宽度为48位,4级映射的内存分布图:
image

下面是页面大小为4KB,地址宽度为48位,3级映射的内存分布图:

image

2.示例4KB大小页面,48位地址宽度,4级映射

image

如果输入的虚拟地址最高位bit[63]为1,那么这个地址是用于内核空间的,页表的基地址寄存器用TTBR1_EL1(Translation Table Base Register 1)。如果bit[63]等于0,那么这个虚拟地址属于用户空间,页表基地址寄存器用TTBR0。

TTBRx寄存器保存了第0级页表的基地址(L0 Table base address, Linux内核中称为PGD), L0页表中有512个表项(Table Descriptor),以虚拟地址的bit[47:39]作为索引值在L0页表中查找相应的表项。每个表项的内容含有下一级页表的基地址,即L1页表(Linux内核中称为PUD)的基地址。

PUD页表中有512个表项,以虚拟地址的bit[38:30]为索引值在PUD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L2页表(Linux内核中称为PMD)的基地址。

PMD页表中有512个表项,以虚拟地址的bit[29:21]为索引值在PMD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L3页表(Linux内核中称为PTE)的基地址。

在PTE页表中,以虚拟地址的bit[20:12]为索引值在PTE表中查找相应的表项,每个PTE表项中含有最终的物理地址的bit[47:12],和虚拟地址中bit[11:0]合并成最终的物理地址,完成地址翻译过程。

3.示例39位有效位,4KB大小页面,3级页表

image

  • 当CONFIG_PGTABLE_LEVELS=4时:pgd-->pud-->pmd-->pte;
  • 当CONFIG_PGTABLE_LEVELS=3时,没有PUD页表:pgd(pud)-->pmd-->pte;

4.pgd_offset_k函数

通过pgd_offset_k()宏来得到具体的PGD页面目录项的表项。首先通过init_mm数据结构的pgd成员来获取PGD页表的基地址,然后通过pgd_index()来计算PGD页表中的偏移量offset。

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(addr)     pgd_offset(&init_mm, addr)
 
#define pgd_offset(mm, addr)      ((mm)->pgd+pgd_index(addr))
 
/* to find an entry in a page-table-directory */
#define pgd index(addr)     (((addr) >> PGDIR SHIFT) & (PTRS PER PGD - 1))

通过计算可以得到PGDIR_SHIFT等于39, PUD_SHIFT等于30, PMD_SHIFT等于21。
每级页表的页表项数目分别用PTRS_PER_PGD、PTRS_PER_PUD、PTRS_PER_PMD和PTRS_PER_PTE来表示,都等于512。
PGDIR_SIZE宏表示一个PGD页表项能覆盖的内存范围大小为512GB。PUD_SIZE等于1GB, PMD_SIZE等于2MB, PAGE_SIZE等于4KB。

5.create_mapping函数

通过init_mm数据结构的pgd成员来获取, swapper_pg_dir全局变量指向PGD页表基地址。

[arch/arm64/kernel/vmlinux.lds.S]
 
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;

[arch/arm64/include/asm/page.h]

#define SWAPPER_PGTABLE_LEVELS  (CONFIG_ARM64_PGTABLE_LEVELS - 1)
#define SWAPPER DIR SIZE     (SWAPPER PGTABLE LEVELS * PAGE SIZE)

假设CONFIG_ARM64_PGTABLE_LEVELS定义为4,那么SWAPPER_DIR_SIZE大小就等于3个PAGE_SIZE的大小。从vmlinux.lds.S链接文件可以看到,PGD页表的大小定义为3个PAGE_SIZE。swapper_pg_dir的起始地址由vmlinux.lds.S链接文件计算得来。

6. alloc_init_pte()配置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)) {
        pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
        if (pmd_sect(*pmd))
              split_pmd(pmd, pte);
        __pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);
        flush_tlb_all();
    }
    BUG_ON(pmd_bad(*pmd));

    pte = pte_offset_kernel(pmd, addr);
    do {
        set_pte(pte, pfn_pte(pfn, prot));
        pfn++;
    } while (pte++, addr += PAGE_SIZE, addr ! = end);
}

首先判断PMD表项的内容是否为空?如果为空,说明下一级页表不存在,需要动态分配512个页表项,然后通过__pmd_populate()函数来设置PMD页表项。
通过pte_offset_kernel()宏来索引到相应的PTE页表项。索引值可以通过pte_index()来计算,最终会使用虚拟地址bit[20:12]来做索引值。
接下来以PAGE_SIZE即4KB大小为步长,通过while循环来设置PTE页表项。将pteval写入pteptr所指的页表项中:

define set_pte(pteptr, pteval) (*(pteptr) = pteval)

PTE_DIRTY:CPU在写操作时会设置该标志位,表示对应页面被写过,为脏页。
PTE_YOUNG:CPU访问该页时会设置该标志位。在页面换出时,如果该标志位置位了,说明该页刚被访问过,页面是young的,不适合把该页换出,同时清除该标志位。
PTE_PRESENT:表示页在内存中。