Linux内核ARM64页表映射,对于ARM64架构来说,目前基于ARMv8-A架构的处理器最大可以支持到48根地址线,也就是寻址248的虚拟地址空间,即虚拟地址空间范围为0x0000_0000_0000_0000~0x0000_FFFF_FFFF_FFFF,共256TB。理论上完全可以做到64根地址线,那么最大就可以寻找到264的虚拟地址空间。但是对于目前的应用来说,256TB的虚拟地址空间已经足够使用了。因为如果支持64位虚拟地址空间,意味着处理器设计需要考虑更多的地址线, CPU的设计复杂度会增大。
基于ARMv8-A架构的处理器的虚拟地址分成两个区域。一个是从0x0000_0000_0000_0000到0x0000_FFFF_FFFF_FFFF,另外一个是从0xFFFF_0000_0000_0000到0xFFFF_FFFF_FFFF_FFFF。
基于ARMv8-A架构的处理器可以通过配置CONFIG_ARM64_ VA_BITS这个宏来设置虚拟地址的宽度。
[arch/arm64/Kconfig]
config ARM64_VA_BITS
int
default 39 if ARM64_VA_BITS_39
default 42 if ARM64_VA_BITS_42
default 48 if ARM64 VA BITS 48
另外基于ARMv8-A架构的处理器支持的最大物理地址宽度也是48位。
Linux内存空间布局与地址映射的粒度和地址映射的层级有关。基于ARMv8-A架构的处理器支持的页面大小可以是4KB、16KB或者64KB。映射的层级可以是3级或者4级。
下面是页面大小为4KB,地址宽度为48位,4级映射的内存分布图:
AArch64 Linux memory layout with 4KB pages + 4 levels:
Start End Size Use
-----------------------------------------------------------------
0000000000000000 0000ffffffffffff 256TB user
ffff000000000000 ffffffffffffffff 256TB kernel
下面是页面大小为4KB,地址宽度为48位,3级映射的内存分布图:
AArch64 Linux memory layout with 4KB pages + 3 levels:
Start End Size Use
------------------------------------------------------------
0000000000000000 0000007fffffffff 512GB user
ffffff8000000000 ffffffffffffffff 512GB kernel
Linux内核的documentation/arm64/memory.txt文件中还有其他不同配置的内存分布图。
我们的QEMU实验平台配置4KB大小页面,48位地址宽度,4级映射,下面以此为蓝本介绍ARM64的地址映射过程。
如下图所示,地址转换过程如下。
基于ARMv8-A架构的处理器虚拟地址查找(4KB页)
(1)如果输入的虚拟地址最高位bit[63]为1,那么这个地址是用于内核空间的,页表的基地址寄存器用TTBR1_EL1(Translation Table Base Register 1)。如果bit[63]
等于0,那么这个虚拟地址属于用户空间,页表基地址寄存器用TTBR0。
(2)TTBRx寄存器保存了第0级页表的基地址(L0 Table base address, Linux内核中称为PGD), L0页表中有512个表项(Table Descriptor),以虚拟地址的bit[47:39]作为索引值在\20页表中查找相应的表项。每个表项的内容含有下一级页表的基地址,即\21页表(Linux内核中称为PUD)的基地址。
(3)PUD页表中有512个表项,以虚拟地址的bit[38:30]为索引值在PUD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L2页表(Linux内核中称为PMD)的基地址。
(4)PMD页表中有512个表项,以虚拟地址的bit[29:21]为索引值在PMD表中查找相应的表项,每个表项的内容含有下一级页表的基地址,即L3页表(Linux内核中称为PTE)的基地址。
(5)在PTE页表中,以虚拟地址的bit[20:12]
为索引值在PTE表中查找相应的表项,每个PTE表项中含有最终的物理地址的bit[47:12]
,和虚拟地址中bit[11:0]
合并成最终的物理地址,完成地址翻译过程。
在内核初始化阶段会对内核空间的页表进行一一映射,实现的函数依然是create_mapping()。
[start_kenrel-> setup_arch->paging_init->map_mem->__map_memblock->
create_mapping]
static void __ref create_mapping(phys_addr_t phys, unsigned long virt,
phys_addr_t size, pgprot_t prot)
{
if (virt < VMALLOC_START) {
pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside
kernel range\n",
&phys, virt);
return;
}
__create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
size, prot, early_alloc);
}
首先会做虚拟地址的检查,低于VMALLOC_START的地址空间不是有效的内核虚拟地址空间。VMALLOC_START等于0xffff_0000_0000_0000。
PGD页表的基地址和ARM32内核一样,通过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链接文件计算得来,在我们QEMU实验平台,它的地址是0xffff80000095f800。
下面要通过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))
在pgtable-hwdef.h头文件中,定义了PGDIR_SHIFT、PUD_SHIFT和PMD_SHIFT的宏。在我们QEMU的ARM64的实验平台上,定义了4级页表,也就是CONFIG_ARM64_PGTABLE_LEVELS等于4,另外VA_BITS定义为48。那么通过计算可以得到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。
[arch/arm64/include/asm/pgtable-hwdef.h]
#define PTRS_PER_PTE (1 << (PAGE_SHIFT - 3))
/*
* PMD_SHIFT determines the size a level 2 page table entry can map.
*/
#if CONFIG_ARM64_PGTABLE_LEVELS > 2
#define PMD_SHIFT ((PAGE_SHIFT - 3) * 2 + 3) //20
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE-1))
#define PTRS_PER_PMD PTRS_PER_PTE
#endif
/*
* PUD_SHIFT determines the size a level 1 page table entry can map.
*/
#if CONFIG_ARM64_PGTABLE_LEVELS > 3
#define PUD_SHIFT ((PAGE_SHIFT - 3) * 3 + 3) //30
#define PUD_SIZE (_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK (~(PUD_SIZE-1))
#define PTRS_PER_PUD PTRS_PER_PTE
#endif
/*
* PGDIR_SHIFT determines the size a top-level page table entry can map
* (depending on the configuration, this level can be 0, 1 or 2).
*/
#define PGDIR_SHIFT ((PAGE_SHIFT - 3) * CONFIG_ARM64_PGTABLE_LEVELS +
3) //39
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
#define VA BITS (CONFIG ARM64 VA BITS)
这里CONFIG_ARM64_VA_BITS一般定义为48。假设页表的层数大于3, PGDIR_SHIFT为39,那么pgd_index()就是以虚拟地址中第39~48位作为偏移量,代码里先把虚拟地址右移39位,然后再与上PTRS_PER_PGD。
在__create_mapping()
函数中,以PGDIR_SIZE为步长遍历内存区域[virt, virt+size]
,然后通过调用alloc_init_pud()来初始化PGD页表项内容和下一级页表PUD。pgd_addr_end()以PGDIR_SIZE为步长。
/*
* Create the page directory entries and any necessary page tables for the
* mapping specified by 'md'.
*/
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);
alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);
phys += next - addr;
} while (pgd++, addr = next, addr ! = end);
}
下面看alloc_init_pud()函数。
[create_mapping->__create_mapping-> alloc_init_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)) {
pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
pgd_populate(mm, pgd, pud);
}
pud = pud_offset(pgd, addr);
do {
next = pud_addr_end(addr, end);
/*
* For 4K granule only, attempt to put down a 1GB block
*/
if (use_1G_block(addr, next, phys)) {
pud_t old_pud = *pud;
set_pud(pud, __pud(phys |
pgprot_val(mk_sect_prot(prot))));
/*
* If we have an old value for a pud, it will
* be pointing to a pmd table that we no longer
* need (from swapper_pg_dir).
*
* Look up the old pmd table and free it.
*/
if (! pud_none(old_pud)) {
flush_tlb_all();
if (pud_table(old_pud)) {
phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
if (! WARN_ON_ONCE(slab_is_available()))
memblock_free(table, PAGE_SIZE);
}
}
} else {
alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
}
phys += next - addr;
} while (pud++, addr = next, addr ! = end);
}
alloc_init_pud()函数会做如下事情。
(1)通过pgd_none()判断当前PGD表项内容是否为空。如果PGD表项内容为空,说明下一级页表为空,那么需要动态分配下一级页表。下一级页表PUD一共有PTRS_PER_PUD个页表项,即512个表项,然后通过pgd_populate()把刚分配的PUD页表设置到相应的PGD页表项中。
(2)通过pud_offset()来获取相应的PUD表项。这里会通过pud_index()宏来计算索引值,计算方法和pgd_index()函数类似,最终使用虚拟地址的bit[38~30]位来做索引值。
(3)接下来以PUD_SIZE(即1<<30
, 1GB)为步长,通过while循环来设置下一级页表。
(4)use_1G_block()函数会判断是否使用1GB大小的block来映射?当这里要映射的大小内存块正好是PUD_SIZE,那么只需要映射到PUD就好了,接下来的PMD和PTE页表等到真正需要使用时再映射,通过set_pud()函数来设置相应的PUD表项。
(5)如果use_1G_block()函数判断不能通过1GB大小来映射,那么就需要调用alloc_init_pmd()函数来进行下一级页表的映射。
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)) {
pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));
if (pud_sect(*pud)) {
/*
* need to have the 1G of mappings continue to be
* present
*/
split_pud(pud, pmd);
}
pud_populate(mm, pud, pmd);
flush_tlb_all();
}
pmd = pmd_offset(pud, addr);
do {
next = pmd_addr_end(addr, end);
/* try section mapping first */
if (((addr | next | phys) & ~SECTION_MASK) == 0) {
pmd_t old_pmd =*pmd;
set_pmd(pmd, __pmd(phys |
pgprot_val(mk_sect_prot(prot))));
/*
* Check for previous table entries created during
* boot (__create_page_tables) and flush them.
*/
if (! pmd_none(old_pmd)) {
flush_tlb_all();
if (pmd_table(old_pmd)) {
phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
if (! WARN_ON_ONCE(slab_is_available()))
memblock_free(table, PAGE_SIZE);
}
}
} else {
alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
prot, alloc);
}
phys += next - addr;
} while (pmd++, addr = next, addr ! = end);
}
alloc_init_pmd()函数用于配置PMD页表,主要做如下事情。
(1)首先判断PUD页表项的内容是否为空?如果为空,表示PUD指向的下一级页表PMD不存在,需要动态分配PMD页表。分配PTRS_PER_PMD个页表项,即512个,然后通过pud_populate()来设置pud页表项。
(2)通过pmd_offset()宏来获取相应的PUD表项。这里会通过pud_index()来计算索引值,计算方法和pgd_index()函数类似,最终使用虚拟地址的bit[29:21]位来做索引值。
(3)接下来以PMD_SIZE(即1<<21
, 2MB)为步长,通过while循环来设置下一级页表。
(4)如果虚拟区间的开始地址addr和结束地址next,以及物理地址phys都与SECTION_SIZE(2MB)大小对齐,那么直接设置PMD页表项,不需要映射下一级页表。下一级页表等到需要用时再映射也来得及,所以这里直接通过set_pmd()设置PMD页表项。
(5)如果映射的内存不是和SECTION_SIZE对齐的,那么需要通过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);
}
PTE页表是4级页表的最后一级,alloc_init_pte()配置PTE页表项。
(1)首先判断PMD表项的内容是否为空?如果为空,说明下一级页表不存在,需要动态分配512个页表项,然后通过__pmd_populate()函数来设置PMD页表项。
(2)通过pte_offset_kernel()宏来索引到相应的PTE页表项。索引值可以通过pte_index()来计算,最终会使用虚拟地址bit[20:12]
来做索引值。
(3)接下来以PAGE_SIZE即4KB大小为步长,通过while循环来设置PTE页表项。
酷客网相关文章:
评论前必须登录!
注册