临时虚拟地址空间映射
- RISC-V
- 2024-07-07
- 235热度
- 0评论
概述
为什么要做临时虚拟地址空间映射?
一旦开启MMU,PC的下一条指令地址会经过MMU转化,未开启MMU之前地址的翻译是不需要经过MMU转化直接访问。对应开启MMU之后,应该要使用虚拟地址,才能访问到正确的指令内存。
前面描述了虚拟地址转换为物理地址是通过MMU自动转换,但是需要给MMU创建好页表,这样MMU才能自动查询到物理地址。页表也是对应的物理内存,也是需要分配的,在正常系统运行时,页表的分配可以通过系统的内存管理接口获取到,但是在系统刚运行时,内存管理并没有初始化好,无法调用接口分配到页表,这样即使使能MMU但是找不到页表,也无法翻译出物理地址。
而临时虚拟地址空间映射就是要解决这样的问题,对于页表可以提前定义一个静态的全局数组来代替,填充好页表,使能MMU时,可以从物理地址访问过度到虚拟地址访问,等后期的内存初始化好后,可以分配页表了,再重新映射一遍。
在使能MMU后,要访问内核代码,又要访问设备树?要解决这个问题,内核会先做那些临时虚拟地址空间映射?
(1)固定虚拟地址映射(fixmap):Linux内核分配了一段固定的虚拟地址范围,这段地址范围称为fixmap,专门用于映射到设备树,early console寄存器等使用,用于在使能MMU时,在未完成内存初始化时可以通过fixmap访问到设备树。
(2) 内核代码的映射(kernel):开启MMU后,内核代码的指令访问都是虚拟地址,所以需要马上进行映射。内核代码的映射了,虚拟地址范围在编译时就确定了,需要处理的是分配到页表进行填充,而各级页表使用的是事先先分配的全局数组,填充好各级页表即可。
(1)内核vmlinux被加载到内存的0x80200000的位置,DTB被加载到0x82200000的位置。
(2)fixmap这段地址编译时由FIXADDR_START和FIXADDR_TOP确定,这段虚拟地址是固定的,如上图例子在0xffffffcefee00000~0xffffffceff000000范围。
(3)kernel地址范围编译的时候确定,从PAGE_OFFSET开始,如上图是0xffffffe000000000开始,与readelf -h工具链查看的入口地址一致。
(4)两段虚拟地址共用了一个根目录页表early_pg_dir[512],因为对应3级页表根目录页表每个表项寻址范围1G,512个表项共计512G范围,是完全足够的。
(5)kernel虚拟地址使用的粗粒度映射,二级页目录为early_pmd[512],每个页表项2M寻址访问,10M的空间占用了5个页表项。
(6)fixmap虚拟地址目前主要用于映射到DTB,本身的映射范围有2M的空间,预留了1M的空间用于DTB,DTB使用的细粒度映射,二级页表使用的是fixmap_pmd[512],三级页表使用的是fixmap_pte[512]。
(7)上述涉及的4个页表都是定义的全局数组,位于vmlinux中被事先加载到物理内存中。
(8)fixmap的地址在PAGE_OFFSET下面,与kernel之间还有vmalloc和pci_io的地址空间。即FIXADDR_TOP= PCI_IO_START,PCI_IO_END=VMEMMAP_START,VMALLOC_END=PAGE_OFFSET - 1。
asmlinkage void __init setup_vm(uintptr_t dtb_pa)
{
uintptr_t va, end_va;
uintptr_t load_pa = (uintptr_t)(&_start);
uintptr_t load_sz = (uintptr_t)(&_end) - load_pa;
uintptr_t map_size = best_map_size(load_pa, MAX_EARLY_MAPPING_SIZE);
①_start和_end分别为内核加载到的物理地址起始和结束位置。
va_pa_offset = PAGE_OFFSET - load_pa;
pfn_base = PFN_DOWN(load_pa);
②va_pa_offset内核虚拟地址相对物理地址的偏移量,pfn_base是内核开始地址对应的pfn,
即叶帧号。
/*
* Enforce boot alignment requirements of RV32 and
* RV64 by only allowing PMD or PGD mappings.
*/
BUG_ON(map_size == PAGE_SIZE);
/* Sanity check alignment and size */
BUG_ON((PAGE_OFFSET
BUG_ON((load_pa
BUG_ON(load_sz > MAX_EARLY_MAPPING_SIZE);
/* Setup early PGD for fixmap */
create_pgd_mapping(early_pg_dir, FIXADDR_START,
(uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);
③填充FIXADDR_START虚拟地址根目录页表项,因为映射大小PGDIR_SIZE,所以只填充根目录页表项。页表项PFN指向下一级页表的物理地址,为fixmap_pgd_next,本章实验是3级页表,所以为fixmap_pmd[512],页表项的属性为PAGE_TABLE(0x1)。最终的填充内容就是上图
early_pg_dir[315]=(0x80a83000 >> 12 ) << 10 | 0x01 = 0x202a0c01,格式见3.3.2章节实际的物理地址需要右移12转化为PPN,然后再左移10
#ifndef __PAGETABLE_PMD_FOLDED
/* Setup fixmap PMD */
create_pmd_mapping(fixmap_pmd, FIXADDR_START,
(uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE);
④填充FIXADDR_START二级页表所在页表项,因为映射的地址大小时PMD_SIZE,所以只填充二级页表。页表项中指向的下一级页表为fixmap_pte[512],页表项属性为PAGE_TABLE。最终的填充内容就是上图
fixmap_pmd[503] = (0x80a85000 >> 12) << 10 | 0x01 = 0x202a1401。
经过③和④步骤,就将FIXADDR_START~FIXADDR_TOP这段虚拟地址空间的PGD/PMD页表填充好了,但是这段虚拟地址空间最终映射到那块物理地址了?最终会映射到DBT存在的那段物理地址,而DBT会在接下来再进行映射,所以了PTE页表项暂时先不填充,待到后续再进行填充。
/* Setup trampoline PGD and PMD */
create_pgd_mapping(trampoline_pg_dir, PAGE_OFFSET,
(uintptr_t)trampoline_pmd, PGDIR_SIZE, PAGE_TABLE);
create_pmd_mapping(trampoline_pmd, PAGE_OFFSET,
load_pa, PMD_SIZE, PAGE_KERNEL_EXEC);
⑤ 这里还对PAGE_OFFSET~PAGE_OFFSET+2M的空间做了一个临时粗粒度映射,主要的目的是用于后续使能MMU时,能够无缝从物理地址切换到虚拟地址,以至于不要让访问的地址查找不到页表而进入异常。用到的根页表是trampoline_pg_dir[512]和PMD页表是trampoline_pmd[512]。trampoline_pg_dir看起来有点多余,因为接下来会对内核做映射,页表使用early_pg_dir和early_pmd_dir。
#else
/* Setup trampoline PGD */
create_pgd_mapping(trampoline_pg_dir, PAGE_OFFSET,
load_pa, PGDIR_SIZE, PAGE_KERNEL_EXEC);
#endif
/*
* Setup early PGD covering entire kernel which will allows
* us to reach paging_init(). We map all memory banks later
* in setup_vm_final() below.
*/
end_va = PAGE_OFFSET + load_sz;
for (va = PAGE_OFFSET; va < end_va; va += map_size)
create_pgd_mapping(early_pg_dir, va,
load_pa + (va - PAGE_OFFSET),
map_size, PAGE_KERNEL_EXEC);
⑥对虚拟地址PAGE_OFFSET~end_va范围进行映射,使用early_pg_dir[512]作为根目录页表,由于映射的大小时map_size=0x200000;所以在填充根目录页表后,会继续向下遍历下一级PMD页表,填充到PMD页表时map_size=PMD_SIZE,页表项属性为PAGE_KERNEL_EXEC就不再往下遍历填充PTE页表了,这样就完成一个PGD/PMD页表的填充,达成一个PAGE_OFFSET~end_va地址范围的粗粒度映射,对应上图的10M空间。
/* Create fixed mapping for early FDT parsing */
end_va = __fix_to_virt(FIX_FDT) + FIX_FDT_SIZE;
for (va = __fix_to_virt(FIX_FDT); va < end_va; va += PAGE_SIZE)
create_pte_mapping(fixmap_pte, va,
dtb_pa + (va - __fix_to_virt(FIX_FDT)),
PAGE_SIZE, PAGE_KERNEL);
⑦__fix_to_virt(FIX_FDT)~__fix_to_virt(FIX_FDT) + FIX_FDT_SIZE这段虚拟地址空间映射到DBT物理空间,这段虚拟地址空间在FIXMAP范围内,由于FIXMAP的虚拟地址空间已经填充好了PGD/PMD页表项,要映射到DTB只需要填充PTE页表项目即可,所以这里调用的是create_pte_mapping填充PTE页表。
/* Save pointer to DTB for early FDT parsing */
dtb_early_va = (void *)fix_to_virt(FIX_FDT) + (dtb_pa & ~PAGE_MASK);
/* Save physical address for memblock reservation */
dtb_early_pa = dtb_pa;
⑧dtb_early_va保存的就是dtb起始的虚拟地址,dtb_early_pa是dtb的其实物理地址。
}
页表填充
所谓映射,实际上分配页表,然后填充页表项。那么页表的填充涉及到3个函数,分别对应的就是PGD/PMD/PTE页表项目的填充。
static void __init create_pgd_mapping(pgd_t *pgdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
-pgd:页表的地址
-va:映射的虚拟地址
-pa:当sz等于PGDIR_SIZE/PMD_SIZE时,pa就是下一级页表的物理地址。否则就是实际的物理地址。PGD页表项的映射地址范围,3级页表是1G,PMD页表项的映射地址范围,3级页表是2M。
-sz:映射的大小
-prot:页表项的属性
static void __init create_pmd_mapping(pmd_t *pmdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
static void __init create_pte_mapping(pte_t *ptep,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
根目录页表填充
static void __init create_pgd_mapping(pgd_t *pgdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pgd_next_t *nextp;
phys_addr_t next_phys;
uintptr_t pgd_index = pgd_index(va);
① 将虚拟地址转化为页表的index,((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)
if (sz == PGDIR_SIZE) {
if (pgd_val(pgdp[pgd_index]) == 0)
pgdp[pgd_index] = pfn_pgd(PFN_DOWN(pa), prot);
return;
}
② 如果sz为PGDIR_SIZE,pa代表的就是下一级页表基地址,就仅填充PGD页表项,不再向下遍历填充下一级页表。先判断pgd_val(pgdp[pgd_index])对应页表项是否填充过,如果已经填充过就直接返回。未填充过就把(pa >> 12) << 10 | prot写到表项中,见3.3.2章节页表项格式,其中pa>>12是计算ppn。
③ 如果sz不等于PGDIR_SIZE,那么除了要填充PGD页表项外,还有向下遍历填充下一级页表。
if (pgd_val(pgdp[pgd_index]) == 0) {
next_phys = alloc_pgd_next(va);
pgdp[pgd_index] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
nextp = get_pgd_next_virt(next_phys);
memset(nextp, 0, PAGE_SIZE);
④ 对应的页表项==0,表示未填充过。先使用alloc_pgd_next分配下一级页表,因为要得到下一级页表的物理地址才能填充。如果是3级页表alloc_pgd_next=alloc_pmd,如果是2级页表alloc_pdg_next=alloc_pte。这里是alloc_pmd,由于memblcok也还未初始化,这里直接就使用的是early_pmd[]数组。
分配到下一级页表后,就将下一级页表的地址转化为PPN再与页表属性相或赋值到pgdp[]中就完成了页表项的填充。这里的页表属性为PAGE_TABLE,其值为V位为1,其他都为0,表示非子叶页表。
} else {
next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_index]));
nextp = get_pgd_next_virt(next_phys);
⑤如果已经填充了页表项,那么从页表项中获取到下一级页表的物理地址。获取到下一级页表的物理地址后,需要调用get_pgd_next_virt将其转化为虚拟地址。因为若是开了MMU之后,使用的是虚拟地址,否则访问不到页表的内存。见下描述。
}
create_pgd_next_mapping(nextp, va, pa, sz, prot);
⑥填充完PGD页表项后,需要接着再往下填充下一级页表。其中nextp为下一级页表的地址,可能是物理地址也有可能是虚拟地址。
}
static phys_addr_t __init alloc_pmd(uintptr_t va)
{
uintptr_t pmd_num;
if (mmu_enabled)
return memblock_phys_alloc(PAGE_SIZE, PAGE_SIZE);
① 如果使能mmu,就调用memblock_phys_alloc分配内存。
pmd_num = (va - PAGE_OFFSET) >> PGDIR_SHIFT;
BUG_ON(pmd_num >= NUM_EARLY_PMDS);
return (uintptr_t)&early_pmd[pmd_num * PTRS_PER_PMD];
② 如果未使能mmu,页表就是使用静态的early_pmd[xxx]。
}
static pmd_t *__init get_pmd_virt(phys_addr_t pa)
{
if (mmu_enabled) {
clear_fixmap(FIX_PMD);
return (pmd_t *)set_fixmap_offset(FIX_PMD, pa);
① 如果使能了mmu,将物理地址反向映射到fixmap的空间,也就是FIX_PMD这个虚拟地址再使能mmu后访问到pa,即FIX_PMD与pa做了虚拟地址到物理地址的映射。
} else {
return (pmd_t *)((uintptr_t)pa);
② 如果没有使能mmu,直接返回物理地址即可。
}
}
PMD页表填充
static void __init create_pmd_mapping(pmd_t *pmdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pte_t *ptep;
phys_addr_t pte_phys;
uintptr_t pmd_index = pmd_index(va);
如果sz=PMD_SIZE,不再填充PTE页表,可能是粗粒度映射,也有可能是fixmap的填充。
if (sz == PMD_SIZE) {
if (pmd_none(pmdp[pmd_index]))
pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pa), prot);
return;
}
if (pmd_none(pmdp[pmd_index])) {
pte_phys = alloc_pte(va);//分配下一级页表PTE
pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pte_phys), PAGE_TABLE);//填充pmd页表项
ptep = get_pte_virt(pte_phys);
//将下一级页表的物理地址转化为虚拟地址,当使能MMU时,使用的是虚拟地址。
memset(ptep, 0, PAGE_SIZE); //将下一级页表的PTE清空。
} else {
pte_phys = PFN_PHYS(_pmd_pfn(pmdp[pmd_index]));
ptep = get_pte_virt(pte_phys);
//获取下一级页表的虚拟地址。
}
create_pte_mapping(ptep, va, pa, sz, prot);
}
pmd页表项的填充与PGD页表项的填充逻辑类似,当sz为PMD_SIZE时,传进来的pa就是下一级页表或者就是映射的物理地址(粗粒度映射的时候)。
是否是粗粒度映射主要看PMD页表项的属性值,当属性值XRW权限为可读可写可执行,那么该页表项指向的就是实际的物理地址而不是PTE页表的基地址。
PTE页表填充
static void __init create_pte_mapping(pte_t *ptep,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
uintptr_t pte_index = pte_index(va);
BUG_ON(sz != PAGE_SIZE);
if (pte_none(ptep[pte_index]))
ptep[pte_index] = pfn_pte(PFN_DOWN(pa), prot);
}
PTE页表是最后一级页表,因此填充的逻辑就比较简单了,计算pa所在的PPN,再左移动10位或上prot,赋值到对应的表项即可。
Fixmap映射
Fixmap是一段固定的虚拟地址空间,使能MMU后通过这段虚拟空间先访问到物理空间,具体有那些模块可以看看下面数据结构。
enum fixed_addresses {
FIX_HOLE,
#define FIX_FDT_SIZE SZ_1M
FIX_FDT_END,
FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,
FIX_PTE,
FIX_PMD,
FIX_EARLYCON_MEM_BASE,
__end_of_fixed_addresses
};
从上可知,fixmap的地址空间主要分类有4种
- FIX_FDT: 用于映射到DBT的,范围为FIX_FDT~FIX_END,通过这段地址访问到设备树。
- FIX_PTE: 用于映射pte页表,范围为FIX_PTE~FIX_PTE+4K,即一个页大小。场景是当使能了MMU后,对于页表的访问也先需要通过虚拟地址访问,因此FIX_PTE就是对应页表所在物理内存的虚拟地址。
- FIX_PMD: 用于映射pmd页表,范围为FIX_PMD~FIX_PMD+4K,即一个页大小。用于读写pmd页表的物理内存。
- FIX_EARLYCON_MEM_BASE: 用于映射到earlycon。
几个地址如何保证落在FIXADDR_START~FIXADDR_TOP了,先来看看在映射DBT时的实现:
/* Create fixed mapping for early FDT parsing */
end_va = __fix_to_virt(FIX_FDT) + FIX_FDT_SIZE;
for (va = __fix_to_virt(FIX_FDT); va < end_va; va += PAGE_SIZE)
create_pte_mapping(fixmap_pte, va,
dtb_pa + (va - __fix_to_virt(FIX_FDT)),
PAGE_SIZE, PAGE_KERNEL);
可以看在映射对FIX_FDT~FIX_FDT+ FIX_FDT_SIZE的范围调用__fix_to_virt函数进行了转化,转化的地址是由FIXADDR_TOP向下相减,因此FIX_FDT转化的地址一定会落在FIXADDR_START~FIXADDR_TOP范围。
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)
Fixmap映射过程主要调用了create_pgd_mapping/create_pmd_mapping/create_pte_mapping 3个函数填充对应的PGD/PMD/PTE页表,由于内存管理还没有准备好,所以页表使用的是全局的静态数组,如下:
pmd_t early_pmd[PTRS_PER_PMD * NUM_EARLY_PMDS] __initdata __aligned(PAGE_SIZE);
pmd_t fixmap_pmd[PTRS_PER_PMD] __page_aligned_bss;
pte_t fixmap_pte[PTRS_PER_PTE] __page_aligned_bss;
上述除了FIX_FDT的范围由FIX_FDT_SIZE决定外,其他的FIX_PTE、FIX_PMD、EARLYCON都是4K范围。
fixmap初始化
asmlinkage void __init setup_vm(uintptr_t dtb_pa)
{
/* Setup early PGD for fixmap */
create_pgd_mapping(early_pg_dir, FIXADDR_START,
(uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);
③填充FIXADDR_START虚拟地址根目录页表项,因为映射大小PGDIR_SIZE,所以只填充根目录页表项。页表项PFN指向下一级页表的物理地址,为fixmap_pgd_next,本章实验是3级页表,所以为fixmap_pmd[512],页表项的属性为PAGE_TABLE(0x1)。最终的填充内容就是上图
early_pg_dir[315]=(0x80a83000 >> 12 ) << 10 | 0x01 = 0x202a0c01,格式见3.3.2章节实际的物理地址需要右移12转化为PPN,然后再左移10
/* Setup fixmap PMD */
create_pmd_mapping(fixmap_pmd, FIXADDR_START,
(uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE);
④填充FIXADDR_START二级页表所在页表项,因为映射的地址大小时PMD_SIZE,所以只填充二级页表。页表项中指向的下一级页表为fixmap_pte[512],页表项属性为PAGE_TABLE。最终的填充内容就是上图
fixmap_pmd[503] = (0x80a85000 >> 12) << 10 | 0x01 = 0x202a1401。
经过③和④步骤,就将FIXADDR_START~FIXADDR_TOP这段虚拟地址空间的PGD/PMD页表填充好了,但是这段虚拟地址空间最终映射到那块物理地址了?最终会映射到DBT存在的那段物理地址,而DBT会在接下来再进行映射,所以了PTE页表项暂时先不填充,待到后续再进行填充。
}
fixmap的映射是对3个全局数组的填充,事先会填充好PGD/PMD,而PTE页表根据是那个模块使用再进行填充,具体的填充过程在上一章节已经描述,这里就不再赘述。
DTB的映射
asmlinkage void __init setup_vm(uintptr_t dtb_pa)
{
........
/* Create fixed mapping for early FDT parsing */
end_va = __fix_to_virt(FIX_FDT) + FIX_FDT_SIZE;
for (va = __fix_to_virt(FIX_FDT); va < end_va; va += PAGE_SIZE)
create_pte_mapping(fixmap_pte, va,
dtb_pa + (va - __fix_to_virt(FIX_FDT)),
PAGE_SIZE, PAGE_KERNEL);
⑦__fix_to_virt(FIX_FDT)~__fix_to_virt(FIX_FDT) + FIX_FDT_SIZE这段虚拟地址空间映射到DBT物理空间,这段虚拟地址空间在FIXMAP范围内,由于FIXMAP的虚拟地址空间已经填充好了PGD/PMD页表项,要映射到DTB只需要填充PTE页表项目即可,所以这里调用的是create_pte_mapping填充PTE页表。
/* Save pointer to DTB for early FDT parsing */
dtb_early_va = (void *)fix_to_virt(FIX_FDT) + (dtb_pa & ~PAGE_MASK);
/* Save physical address for memblock reservation */
dtb_early_pa = dtb_pa;
⑧dtb_early_va保存的就是dtb起始的虚拟地址,dtb_early_pa是dtb的其实物理地址。
........
}
PMD与PTE页表映射
页表也是存储在物理内存中的,分配页表可以是静态的全局数组也也可使用内存分配接口获取到。分配到页表的物理内存后需要进行填充,而使能mmu后访问物理内存必现要使用虚拟地址,填充页表是为了做虚拟地址转化,访问页表又要虚拟地址,那如何解决这个问题了?Fixmap的fix_pte和fix_pmd就排上用场了,在fixmap固定的虚拟地址空间访问,前面已经填充了PGD/PMD,剩下再填充好PTE就可以转化到相应物理地址,因此当新分配的页表要访问时,先将其物理地址填充到fixmap对应的pte表项中,这样就可以通过fixmap访问到页表了。
如上图所示,其中fix_pte和fix_pmd就是专门用来访问页表的,fix_pte专门用于访问pte类型页表,fix_pmd专门用于访问pmd类型页表,页表分配到空间后。
下面来看看实际的应用场景,以create_pmd_mapping示例说明。
static void __init create_pmd_mapping(pmd_t *pmdp,
uintptr_t va, phys_addr_t pa,
phys_addr_t sz, pgprot_t prot)
{
pte_t *ptep;
phys_addr_t pte_phys;
uintptr_t pmd_index = pmd_index(va);
if (sz == PMD_SIZE) {
if (pmd_none(pmdp[pmd_index]))
pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pa), prot);
return;
}
if (pmd_none(pmdp[pmd_index])) {
pte_phys = alloc_pte(va);
① 分配pte页表,这时候使用能mmu,分配pte页表调用memblock_phys_alloc进行分配,返回的是页表的物理地址。
pmdp[pmd_index] = pfn_pmd(PFN_DOWN(pte_phys), PAGE_TABLE);
ptep = get_pte_virt(pte_phys);
②因为接下来要调用create_pte_mapping填充pte页表,即①分配的页表,但是此时已经使能了mmu,如果传递的页表基地址是物理内存,将无法访问到PTE页表,所以要对其转化给虚拟地址。
memset(ptep, 0, PAGE_SIZE);
} else {
pte_phys = PFN_PHYS(_pmd_pfn(pmdp[pmd_index]));
ptep = get_pte_virt(pte_phys);
}
create_pte_mapping(ptep, va, pa, sz, prot);
}
接下来就看看get_pte_virt的实现。
static pte_t *__init get_pte_virt(phys_addr_t pa)
{
if (mmu_enabled) {
clear_fixmap(FIX_PTE);
return (pte_t *)set_fixmap_offset(FIX_PTE, pa);
①使能了MMU,先调用clear_fixmap将对应的FIX_PTE的页表项都清除掉。接着调用set_fixmap_offset获取虚拟地址。
} else {
return (pte_t *)((uintptr_t)pa);
}
}
/* Return a pointer with offset calculated */
#define __set_fixmap_offset(idx, phys, flags) \\\\
({ \\\\
unsigned long ________addr; \\\\
__set_fixmap(idx, phys, flags); \\\\
________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1)); \\\\
________addr; \\\\
})
#define set_fixmap_offset(idx, phys) \\\\
__set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL)
② 调用__set_fixmap将物理地址填充到FIX_PTE对应的PTE页表项目,最终的目的是要让FIX_PTE虚拟地址与当前新分配的页表pa映射起来。对于本章实验示例填充的4.3节图中PTE表项的fixmap_pte[0xff]。
void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot)
{
unsigned long addr = __fix_to_virt(idx);
③ __fix_to_virt 是获取FIX_PTE的虚拟地址,使其落在fixmap的范围内。
pte_t *ptep;
BUG_ON(idx <= FIX_HOLE || idx >= __end_of_fixed_addresses);
ptep = &fixmap_pte[pte_index(addr)];
④ pte_index(addr)计算的是FIX_PTE在fixmap_pte[]数组的偏移,也就是页表项的位置。
if (pgprot_val(prot)) {
set_pte(ptep, pfn_pte(phys >> PAGE_SHIFT, prot));
⑤ 将新分配的页表物理地址phys与prot计算设置到表项中。这样访问FIX_PTE就可以访问到新分配的页表了,就可以进行填充操作了。
} else {
pte_clear(&init_mm, addr, ptep);
local_flush_tlb_page(addr);
}
}
粗粒度内核映射
asmlinkage void __init setup_vm(uintptr_t dtb_pa)
{
uintptr_t va, end_va;
uintptr_t load_pa = (uintptr_t)(&_start);
uintptr_t load_sz = (uintptr_t)(&_end) - load_pa;
uintptr_t map_size = best_map_size(load_pa, MAX_EARLY_MAPPING_SIZE);
①_start和_end分别为内核加载到的物理地址起始和结束位置。
va_pa_offset = PAGE_OFFSET - load_pa;
pfn_base = PFN_DOWN(load_pa);
②va_pa_offset内核虚拟地址相对物理地址的偏移量,pfn_base是内核开始地址对应的pfn,
即叶帧号。
/*
* Enforce boot alignment requirements of RV32 and
* RV64 by only allowing PMD or PGD mappings.
*/
BUG_ON(map_size == PAGE_SIZE);
/* Sanity check alignment and size */
BUG_ON((PAGE_OFFSET
BUG_ON((load_pa
BUG_ON(load_sz > MAX_EARLY_MAPPING_SIZE);
......
/*
* Setup early PGD covering entire kernel which will allows
* us to reach paging_init(). We map all memory banks later
* in setup_vm_final() below.
*/
end_va = PAGE_OFFSET + load_sz;
for (va = PAGE_OFFSET; va < end_va; va += map_size)
create_pgd_mapping(early_pg_dir, va,
load_pa + (va - PAGE_OFFSET),
map_size, PAGE_KERNEL_EXEC);
⑥对虚拟地址PAGE_OFFSET~end_va范围进行映射,使用early_pg_dir[512]作为根目录页表,由于映射的大小时map_size=0x200000;所以在填充根目录页表后,会继续向下遍历下一级PMD页表,填充到PMD页表时map_size=PMD_SIZE,页表项属性为PAGE_KERNEL_EXEC就不再往下遍历填充PTE页表了,这样就完成一个PGD/PMD页表的填充,达成一个PAGE_OFFSET~end_va地址范围的粗粒度映射,对应上图的10M空间。
}