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级映射的内存分布图:
下面是页面大小为4KB,地址宽度为48位,3级映射的内存分布图:
2.示例4KB大小页面,48位地址宽度,4级映射
如果输入的虚拟地址最高位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级页表
- 当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:表示页在内存中。