上篇分析了RISC-V Linux的匯編啟動過程,其中講到了relocate重定向需要開啟MMU,今天分析RISC-V Linux的頁表創(chuàng)建。 注意:本文基于linux5.10.111內(nèi)核 RISC-V Linux支持 虛擬地址格式: 物理地址格式: 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位代表物理地址的訪問權(quán)限,當RWX全為0時,則代表該PTE存儲的地址是下一級頁表的物理地址,否則代表當前頁表是最后一級頁表。 再看看sv39 的頁表格式,sv39使用的是三級頁表, 在代碼中,創(chuàng)建一個有512個元素的數(shù)組即代表一個頁表。一個PTE有512個頁表項,每一個頁表項占用8字節(jié),512*8=4096字節(jié),所以一個PTE代表4K。一個PMD也是512個頁表項,每一項可代表一個PTE,512 *4 K=2M,所以一個PMD就代表2M。以此類推,一個PGD代表512 * 2M=1G。 重要結(jié)論:PGD代表 三級頁表虛擬地址轉(zhuǎn)為物理地址過程示意圖: sv39三級頁表虛擬地址轉(zhuǎn)為物理地址過程: MMU通過satp寄存器得到PGD的物理地址,結(jié)合PGD index(即V PN[2])找到PMD;找到PMD后,再結(jié)合PMD index(即V PN[1])找到PTE,然后結(jié)合PTE index(即V PN[0])得到VA在PTE索引中的值,從而得到物理地址。 最后在PTE中取出PPN[2]、PPN[1]和PPN[0],再和虛擬地址的低12位offset相加,得到最終的物理地址。 MMU開啟前,需要建立好kernel、dtb、trampoline等頁表。以便MMU開啟后,并且在內(nèi)存管理模塊運行之前,kernel可以正常初始化,dtb可以正常地被解析。這部分頁表都是臨時頁表,最終的頁表在setup_vm_final()建立。 臨時頁表創(chuàng)建順序: 首先為fixmap創(chuàng)建早期的PGD、PMD,這時PGD使用 登錄后復制 登錄后復制 登錄后復制 例如,將虛擬地址PAGE_OFFSET映射到物理地址pa,映射大小為4K,創(chuàng)建三級頁表PGD、PMD和PTE: 登錄后復制 這樣創(chuàng)建后,MMU就會根據(jù)PAGE_OFFSET在PGD中找到PMD,然后再PMD中找到PTE,最后取出物理地址。 RISC-V Linux啟動,經(jīng)歷了兩次頁表創(chuàng)建過程,第一次使用C函數(shù) 具體細節(jié)參考代碼中的注釋,下面的代碼省略了一些不重要的部分。 登錄后復制 setup_vm()在最開始就進行了kernel入口地址的對齊檢查,要求入口地址2M對齊。假設內(nèi)存起始地址為0x80000000,那么kernel只能放在0x80000000、0x80200000等2M對齊處。為什么會有這種對齊要求呢? 我猜測單純是為給opensbi預留了2M空間,因為kernel之前還有opensbi,而opensbi運行完之后,默認跳轉(zhuǎn)地址就是偏移2M,kernel只是為了跟opensbi對應,所以設置了2M對齊。 那opensbi需要占用2M這么大?實際上只需要幾百KB,因此opensbi和kernel中間有一段內(nèi)存是空閑的,沒有人使用。這個問題我們下篇再講。 在該函數(shù)中開始為整個物理內(nèi)存做內(nèi)存映射,通過 登錄后復制 說明: 在setup_vm_final()函數(shù)中,通過 最終把 切換頁表通過如下實現(xiàn): 在swapper_pg_dir管理的kernel space中,其虛擬地址與物理地址空間的偏移是固定的,為 注意:swapper_pg_dir管理的是kernel space的頁表,即它把物理內(nèi)存映射到的虛擬地址空間是只能kernel訪問的。user space不能訪問,用戶空間如果訪問,必須自行建立頁表,把物理地址映射到user space的虛擬地址空間。kernel線程共享這個swapper_pg_dir頁表。 RISC-V Linux啟動時的頁表創(chuàng)建相對來說還是比較容易理解的,都是C語言創(chuàng)建的,代碼也比較少。主要就是setup_vm()和setup_vm_final()兩個頁表創(chuàng)建函數(shù)。理解了sv39的一些地址格式后,再去分析源碼就比較容易。不過不同kernel版本代碼都不一樣,需要具體情況具體分析。 本篇提到了setup_vm()會檢查kernel入口地址是否2M對齊,如果不對齊kernel無法啟動,但其實我們可以解除這個2M對齊限制,將這部分空間利用起來,下篇教大家優(yōu)化這部分內(nèi)存。
sv39 mmu
sv32、sv39、sv48等虛擬地址格式,分別代表32為虛擬地址、38位虛擬地址和48位虛擬地址。RISC-V Linux默認也是使用sv39格式,sv39的虛擬地址、物理地址、PTE格式如下:PGD、PMD和PTE,每一個級頁表使用9bit表示,即每一級頁表都有512個頁表項。1G、PMD代表2M、PTE代表4K。sv39默認的頁大小是4K。臨時頁表分析
early_pg_dir。然后對從kernel開始的前2M內(nèi)存建立二級頁表,此時PGD使用trampoline_pg_dir,為這2M建立的頁表也叫作superpage。再然后,對整個kernel創(chuàng)建二級頁表,此時PGD使用early_pg_dir。最后為dtb預留4M大小創(chuàng)建二級頁表。頁表創(chuàng)建函數(shù)
create_pgd_mapping()
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_SIZEprot:PAGE_KERNEL_EXEC/PAGE_KERNEL表示當前是最后一級頁表,否則pa代表下一級頁表的物理地址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)
pmdp:PMD頁表va:虛擬地址pa:物理地址sz:映射大小,PMD_SIZE或PAGE_SIZEprot:權(quán)限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示當前是最后一級頁表,否則pa代表下一級頁表的物理地址create_pte_mapping()
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_SIZEprot:權(quán)限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示當前是最后一級頁表,否則pa代表下一級頁表的物理地址使用舉例
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);
頁表創(chuàng)建源碼分析
setup_vm()創(chuàng)建臨時頁表,第二次使用C函數(shù)setup_vm_final()創(chuàng)建最終頁表。setup_vm()
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);
/*
* 設置覆蓋整個內(nèi)核的早期PGD,這將使我們能夠達到paging_init()。
* 稍后在下面的 setup_vm_final() 中映射所有內(nèi)存。
*/
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創(chuàng)建早期的PMD */
create_pgd_mapping(early_pg_dir, DTB_EARLY_BASE_VA,
(uintptr_t)early_dtb_pmd, PGDIR_SIZE, PAGE_TABLE);
/* 為 FDT 早期掃描創(chuàng)建兩個連續(xù)的 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_final()
swapper頁表來管理,并且清除掉匯編階段的頁表。static void __init setup_vm_final(void)
{
uintptr_t va, map_size;
phys_addr_t pa, start, end;
u64 i;
/**
* 此時MMU已經(jīng)開啟,但是頁表還沒完全建立。
*/
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);
/* 為整個物理內(nèi)存創(chuàng)建頁表 */
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
......
}
swapper_pg_dir頁表來管理整個物理內(nèi)存的訪問。并且清除匯編階段的頁表fixmap_pte和early_pg_dir。(本質(zhì)上就是把該頁表項的內(nèi)容清0,即賦值為0)swapper_pg_dir頁表的物理地址賦值給SATP寄存器。這樣CPU就可以通過該頁表訪問整個物理內(nèi)存。csr_write(CSR_SATP,PFN_DOWN(_pa(swapper_pg_dir))|SATP_MODE);va_pa_offset(定義在arch/riscv/mm/init.c中的一個全局變量)
總結(jié)
以上就是RISC-V Linux啟動之頁表創(chuàng)建分析的詳細內(nèi)容,更多請關注www.92cms.cn其它相關文章!






