上篇分析了RISC-V Linux的彙編啟動過程,其中講到了relocate重定向需要開啟MMU,今天分析RISC-V Linux的頁表建立。
注意:本文基於linux5.10.111核心
、
sv48虛擬位址格式:#######實體位址格式:###############PTE格式:################虛擬位址使用39位表示,其中低12位元代表page offset,高位為了三個部分劃分:VP N[0]、VP N[1]和VP N[2],分別代表虛擬位址VA在PTE、PMD和PGD中的索引。 ###物理位址使用56位元表示,低12位元代表page offset,高位元是物理頁PPN[0]、PPN[1]和PPN[2]
PTE保存了物理頁PPN[0] 、PPN[1]和PPN[2],和物理位址中的PPN相對應;PTE的低10位元代表物理位址的存取權限,當RWX全為0時,則代表該PTE儲存的位址是下一級頁表的實體位址,否則代表目前頁表是最後一級頁表。
再看看sv39 的頁表格式,sv39使用的是三級頁表,PGD
、PMD
和PTE
,每一個級頁表使用9bit表示,即每一級頁表都有512個頁表項。
在程式碼中,建立一個有512個元素的陣列即代表一個頁表。一個PTE有512個頁表項,每個頁表項佔用8字節,512*8=4096字節,所以一個PTE代表4K。一個PMD也是512個頁表項,每一項可代表一個PTE,512 *4 K=2M,所以一個PMD就代表2M。以此類推,一個PGD代表512 * 2M=1G。
重要結論:PGD代表1G
、PMD代表2M
、PTE代表4K
。 sv39預設的頁大小是4K。
三級頁表虛擬位址轉為實體位址流程示意圖:
sv39三級頁表虛擬位址轉為實體位址過程:
MMU透過satp暫存器得到PGD的實體位址,結合PGD index(即V PN[2])找到PMD;找到PMD後,再結合PMD index(即V PN[1])找到PTE,然後結合PTE index(即V PN[0 ])得到VA在PTE索引中的值,從而得到實體位址。
最後在PTE中取出PPN[2]、PPN[1]和PPN[0],再和虛擬位址的低12位元offset相加,得到最終的實體位址。
MMU開啟前,需要建立好kernel、dtb、trampoline等頁表。以便MMU開啟後,並且在記憶體管理模組運行之前,kernel可以正常初始化,dtb可以正常地被解析。這部分頁表都是臨時頁表,最終的頁表在setup_vm_final()建立。
臨時頁表建立順序:
首先為fixmap建立早期的PGD、PMD,這時PGD使用early_pg_dir
。接著對從kernel開始的前2M記憶體建立二級頁表,此時PGD使用trampoline_pg_dir
,為這2M建立的頁表也叫作superpage
。再然後,對整個kernel建立二級頁表,此時PGD使用early_pg_dir
。最後為dtb預留4M大小建立二級頁表。
void __init create_pgd_mapping(pgd_t *pgdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
pgdp
:PGD頁表
#va
:虛擬位址
##pa:實體位址
sz
:映射大小,PGDIR_SIZE或PMD_SIZE或PTE_SIZE
prot
:PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址
static void __init create_pmd_mapping(pmd_t *pmdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
pmdp
:PMD页表
va
:虚拟地址
pa
:物理地址
sz
:映射大小,PMD_SIZE或PAGE_SIZE
prot
:权限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址
static void __init create_pte_mapping(pte_t *ptep, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
ptep
:PTE页表
va
:虚拟地址
pa
:物理地址
sz
:映射大小,PAGE_SIZE
prot
:权限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址
例如,将虚拟地址PAGE_OFFSET映射到物理地址pa,映射大小为4K,创建三级页表PGD、PMD和PTE:
create_pgd_mapping(early_pg_dir,PAGE_OFFSET, (uintptr_t)early_pmd,PGDIR_SIZE,PAGE_TABLE); create_pmd_mapping(early_pmd,PAGE_OFFSET, (uintptr_t)early_pte,PGDIR_SIZE,PAGE_TABLE); create_pte_mapping(early_pte,PAGE_OFFSET, (uintptr_t)pa,PAGE_SIZE,PAGE_KERNEL_EXEC);
这样创建后,MMU就会根据PAGE_OFFSET在PGD中找到PMD,然后再PMD中找到PTE,最后取出物理地址。
RISC-V Linux启动,经历了两次页表创建过程,第一次使用C函数setup_vm()
创建临时页表,第二次使用C函数setup_vm_final()
创建最终页表。
具体细节参考代码中的注释,下面的代码省略了一些不重要的部分。
asmlinkage void __init setup_vm(uintptr_t dtb_pa) { uintptr_t va, pa, end_va; uintptr_t load_pa = (uintptr_t)(&_start); uintptr_t load_sz = (uintptr_t)(&_end) - load_pa; uintptr_t map_size; //load_pa就是kernel加载的其实物理地址 //load_sz就是kernel的实际大小 //page_offset就是kernel的起始物理地址对应的虚拟地址,va_pa_offset是他们的偏移量 va_pa_offset = PAGE_OFFSET - load_pa; //计算得到kernel起始物理地址的物理页,PFN_DOWN是将物理地址右移12位,因为sv39的物理地址的低12位是pa_offset,所以右移12位,得到pfn pfn_base = PFN_DOWN(load_pa); map_size = PMD_SIZE;//PMD_SIZE为2M,在当前,map_size只能为PGDIR_SIZE或PMD_SIZE。这时kernel默认不允许建立PTE。 //检查PAGE_OFFSET是否1G对齐,以及kernel入口地址是否2M对齐 BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0); BUG_ON((load_pa % map_size) != 0); //allc_pte_early里面是BUG(),对于临时页表,kernel不允许我们建立PTE pt_ops.alloc_pte = alloc_pte_early; pt_ops.get_pte_virt = get_pte_virt_early; #ifndef __PAGETABLE_PMD_FOLDED pt_ops.alloc_pmd = alloc_pmd_early; pt_ops.get_pmd_virt = get_pmd_virt_early; #endif /* 设置 early PGD for fixmap */ create_pgd_mapping(early_pg_dir, FIXADDR_START, (uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE); /* 设置 fixmap PMD */ create_pmd_mapping(fixmap_pmd, FIXADDR_START, (uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE); /* 设置 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); /* * 设置覆盖整个内核的早期PGD,这将使我们能够达到paging_init()。 * 稍后在下面的 setup_vm_final() 中映射所有内存。 */ 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); /* 为dtb创建早期的PMD */ create_pgd_mapping(early_pg_dir, DTB_EARLY_BASE_VA, (uintptr_t)early_dtb_pmd, PGDIR_SIZE, PAGE_TABLE); /* 为 FDT 早期扫描创建两个连续的 PMD 映射 */ pa = dtb_pa & ~(PMD_SIZE - 1); create_pmd_mapping(early_dtb_pmd, DTB_EARLY_BASE_VA, pa, PMD_SIZE, PAGE_KERNEL); create_pmd_mapping(early_dtb_pmd, DTB_EARLY_BASE_VA + PMD_SIZE, pa + PMD_SIZE, PMD_SIZE, PAGE_KERNEL); dtb_early_va = (void *)DTB_EARLY_BASE_VA + (dtb_pa & (PMD_SIZE - 1)); ...... }
setup_vm()在最开始就进行了kernel入口地址的对齐检查,要求入口地址2M对齐。假设内存起始地址为0x80000000,那么kernel只能放在0x80000000、0x80200000等2M对齐处。为什么会有这种对齐要求呢?
我猜测单纯是为给opensbi预留了2M空间,因为kernel之前还有opensbi,而opensbi运行完之后,默认跳转地址就是偏移2M,kernel只是为了跟opensbi对应,所以设置了2M对齐。
那opensbi需要占用2M这么大?实际上只需要几百KB,因此opensbi和kernel中间有一段内存是空闲的,没有人使用。这个问题我们下篇再讲。
在该函数中开始为整个物理内存做内存映射,通过swapper
页表来管理,并且清除掉汇编阶段的页表。
static void __init setup_vm_final(void) { uintptr_t va, map_size; phys_addr_t pa, start, end; u64 i; /** * 此时MMU已经开启,但是页表还没完全建立。 */ pt_ops.alloc_pte = alloc_pte_fixmap; pt_ops.get_pte_virt = get_pte_virt_fixmap; #ifndef __PAGETABLE_PMD_FOLDED pt_ops.alloc_pmd = alloc_pmd_fixmap; pt_ops.get_pmd_virt = get_pmd_virt_fixmap; #endif /* Setup swapper PGD for fixmap */ create_pgd_mapping(swapper_pg_dir, FIXADDR_START, __pa_symbol(fixmap_pgd_next), PGDIR_SIZE, PAGE_TABLE); /* 为整个物理内存创建页表 */ for_each_mem_range(i, &start, &end) { if (start >= end) break; if (start <= __pa(PAGE_OFFSET) && __pa(PAGE_OFFSET) < end) start = __pa(PAGE_OFFSET); //best_map_size是选择合适的映射大小,kernel入口地址2M对齐或者kernel大小能被2M整除时,map_size就是2M,否则就是4K。 map_size = best_map_size(start, end - start); for (pa = start; pa < end; pa += map_size) { va = (uintptr_t)__va(pa); create_pgd_mapping(swapper_pg_dir, va, pa, map_size, PAGE_KERNEL_EXEC); } } /* 清除fixmap的PMD和PTE */ clear_fixmap(FIX_PTE); clear_fixmap(FIX_PMD); /* 切换到swapper页表,这个是最终的页表,汇编阶段relocate开启MMU的操作,跟下面这句是一样的。 */ csr_write(CSR_SATP, PFN_DOWN(__pa_symbol(swapper_pg_dir)) | SATP_MODE); local_flush_tlb_all();//刷新TLB ...... }
说明:
在setup_vm_final()函数中,通过swapper_pg_dir
页表来管理整个物理内存的访问。并且清除汇编阶段的页表fixmap_pte和early_pg_dir。(本质上就是把该页表项的内容清0,即赋值为0)
最终把swapper_pg_dir
页表的物理地址赋值给SATP
寄存器。这样CPU就可以通过该页表访问整个物理内存。
切换页表通过如下实现:
csr_write(CSR_SATP,PFN_DOWN(_pa(swapper_pg_dir))|SATP_MODE);
在swapper_pg_dir管理的kernel space中,其虚拟地址与物理地址空间的偏移是固定的,为va_pa_offset
(定义在arch/riscv/mm/init.c中的一个全局变量)
注意:swapper_pg_dir管理的是kernel space的页表,即它把物理内存映射到的虚拟地址空间是只能kernel访问的。user space不能访问,用户空间如果访问,必须自行建立页表,把物理地址映射到user space的虚拟地址空间。kernel线程共享这个swapper_pg_dir页表。
RISC-V Linux啟動時的頁表建立相對來說還是比較容易理解的,都是C語言創建的,程式碼也比較少。主要就是setup_vm()和setup_vm_final()兩個頁表建立函式。理解了sv39的一些位址格式後,再去分析原始碼就比較容易。不過不同kernel版本程式碼都不一樣,需要具體情況具體分析。
本篇提到了setup_vm()會檢查kernel入口位址是否2M對齊,如果不對齊kernel無法啟動,但其實我們可以解除這個2M對齊限制,將這部分空間利用起來,下篇教大家優化這部分記憶體。
以上是RISC-V Linux啟動之頁表建立分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!