一文分析RISC-V Linux啟動(dòng)之頁(yè)表創(chuàng)建
上篇分析了RISC-V Linux的匯編啟動(dòng)過程,其中講到了relocate重定向需要開啟MMU,今天分析RISC-V Linux的頁(yè)表創(chuàng)建。
注意:本文基于linux5.10.111內(nèi)核
sv39 mmu
RISC-V Linux支持sv32
、sv39
、sv48
等虛擬地址格式,分別代表32為虛擬地址、38位虛擬地址和48位虛擬地址。RISC-V Linux默認(rèn)也是使用sv39格式,sv39的虛擬地址、物理地址、PTE格式如下:
虛擬地址格式:

物理地址格式:

PTE格式:

虛擬地址使用39位表示,其中低12位代表page offset,高位劃分為了三部分:VPN[0]、VPN[1]和VPN[2],分別代表虛擬地址VA在PTE、PMD和PGD中的索引。
物理地址使用56位表示,低12位代表page offset,高位是物理頁(yè)P(yáng)PN[0]、PPN[1]和PPN[2]
PTE保存了物理頁(yè)P(yáng)PN[0]、PPN[1]和PPN[2],和物理地址中的PPN相對(duì)應(yīng);PTE的低10位代表物理地址的訪問權(quán)限,當(dāng)RWX全為0時(shí),則代表該P(yáng)TE存儲(chǔ)的地址是下一級(jí)頁(yè)表的物理地址,否則代表當(dāng)前頁(yè)表是最后一級(jí)頁(yè)表。
再看看sv39 的頁(yè)表格式,sv39使用的是三級(jí)頁(yè)表,PGD
、PMD
和PTE
,每一個(gè)級(jí)頁(yè)表使用9bit表示,即每一級(jí)頁(yè)表都有512個(gè)頁(yè)表項(xiàng)。
在代碼中,創(chuàng)建一個(gè)有512個(gè)元素的數(shù)組即代表一個(gè)頁(yè)表。一個(gè)PTE有512個(gè)頁(yè)表項(xiàng),每一個(gè)頁(yè)表項(xiàng)占用8字節(jié),512*8=4096字節(jié),所以一個(gè)PTE代表4K。一個(gè)PMD也是512個(gè)頁(yè)表項(xiàng),每一項(xiàng)可代表一個(gè)PTE,512 *4 K=2M,所以一個(gè)PMD就代表2M。以此類推,一個(gè)PGD代表512 * 2M=1G。
重要結(jié)論:PGD代表1G
、PMD代表2M
、PTE代表4K
。sv39默認(rèn)的頁(yè)大小是4K。
三級(jí)頁(yè)表虛擬地址轉(zhuǎn)為物理地址過程示意圖:

sv39三級(jí)頁(yè)表虛擬地址轉(zhuǎn)為物理地址過程:
MMU通過satp寄存器得到PGD的物理地址,結(jié)合PGD index(即VPN[2])找到PMD;找到PMD后,再結(jié)合PMD index(即VPN[1])找到PTE,然后結(jié)合PTE index(即VPN[0])得到VA在PTE索引中的值,從而得到物理地址。
最后在PTE中取出PPN[2]、PPN[1]和PPN[0],再和虛擬地址的低12位offset相加,得到最終的物理地址。
臨時(shí)頁(yè)表分析
MMU開啟前,需要建立好kernel、dtb、trampoline等頁(yè)表。以便MMU開啟后,并且在內(nèi)存管理模塊運(yùn)行之前,kernel可以正常初始化,dtb可以正常地被解析。這部分頁(yè)表都是臨時(shí)頁(yè)表,最終的頁(yè)表在setup_vm_final()建立。
臨時(shí)頁(yè)表創(chuàng)建順序:
首先為fixmap創(chuàng)建早期的PGD、PMD,這時(shí)PGD使用early_pg_dir
。然后對(duì)從kernel開始的前2M內(nèi)存建立二級(jí)頁(yè)表,此時(shí)PGD使用trampoline_pg_dir
,為這2M建立的頁(yè)表也叫作superpage
。再然后,對(duì)整個(gè)kernel創(chuàng)建二級(jí)頁(yè)表,此時(shí)PGD使用early_pg_dir
。最后為dtb預(yù)留4M大小創(chuàng)建二級(jí)頁(yè)表。
頁(yè)表創(chuàng)建函數(shù)
create_pgd_mapping()
pgdp
:PGD頁(yè)表
va
:虛擬地址
pa
:物理地址
sz
:映射大小,PGDIR_SIZE或PMD_SIZE或PTE_SIZE
prot
:PAGE_KERNEL_EXEC/PAGE_KERNEL表示當(dāng)前是最后一級(jí)頁(yè)表,否則pa代表下一級(jí)頁(yè)表的物理地址
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ?


create_pmd_mapping()
pmdp
:PMD頁(yè)表
va
:虛擬地址
pa
:物理地址
sz
:映射大小,PMD_SIZE或PAGE_SIZE
prot
:權(quán)限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示當(dāng)前是最后一級(jí)頁(yè)表,否則pa代表下一級(jí)頁(yè)表的物理地址
create_pte_mapping()
ptep
:PTE頁(yè)表
va
:虛擬地址
pa
:物理地址
sz
:映射大小,PAGE_SIZE
prot
:權(quán)限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示當(dāng)前是最后一級(jí)頁(yè)表,否則pa代表下一級(jí)頁(yè)表的物理地址
使用舉例
例如,將虛擬地址PAGE_OFFSET映射到物理地址pa,映射大小為4K,創(chuàng)建三級(jí)頁(yè)表PGD、PMD和PTE:
這樣創(chuàng)建后,MMU就會(huì)根據(jù)PAGE_OFFSET在PGD中找到PMD,然后再PMD中找到PTE,最后取出物理地址。
頁(yè)表創(chuàng)建源碼分析
RISC-V Linux啟動(dòng),經(jīng)歷了兩次頁(yè)表創(chuàng)建過程,第一次使用C函數(shù)setup_vm()
創(chuàng)建臨時(shí)頁(yè)表,第二次使用C函數(shù)setup_vm_final()
創(chuàng)建最終頁(yè)表。
具體細(xì)節(jié)參考代碼中的注釋,下面的代碼省略了一些不重要的部分。
setup_vm()
setup_vm()在最開始就進(jìn)行了kernel入口地址的對(duì)齊檢查,要求入口地址2M對(duì)齊。假設(shè)內(nèi)存起始地址為0x80000000,那么kernel只能放在0x80000000、0x80200000等2M對(duì)齊處。為什么會(huì)有這種對(duì)齊要求呢?
我猜測(cè)單純是為給opensbi預(yù)留了2M空間,因?yàn)閗ernel之前還有opensbi,而opensbi運(yùn)行完之后,默認(rèn)跳轉(zhuǎn)地址就是偏移2M,kernel只是為了跟opensbi對(duì)應(yīng),所以設(shè)置了2M對(duì)齊。
那opensbi需要占用2M這么大?實(shí)際上只需要幾百KB,因此opensbi和kernel中間有一段內(nèi)存是空閑的,沒有人使用。這個(gè)問題我們下篇再講。
setup_vm_final()
在該函數(shù)中開始為整個(gè)物理內(nèi)存做內(nèi)存映射,通過swapper
頁(yè)表來管理,并且清除掉匯編階段的頁(yè)表。
說明:
在setup_vm_final()函數(shù)中,通過swapper_pg_dir
頁(yè)表來管理整個(gè)物理內(nèi)存的訪問。并且清除匯編階段的頁(yè)表fixmap_pte和early_pg_dir。(本質(zhì)上就是把該頁(yè)表項(xiàng)的內(nèi)容清0,即賦值為0)
最終把swapper_pg_dir
頁(yè)表的物理地址賦值給SATP
寄存器。這樣CPU就可以通過該頁(yè)表訪問整個(gè)物理內(nèi)存。
切換頁(yè)表通過如下實(shí)現(xiàn):
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中的一個(gè)全局變量)
注意:swapper_pg_dir管理的是kernel space的頁(yè)表,即它把物理內(nèi)存映射到的虛擬地址空間是只能kernel訪問的。user space不能訪問,用戶空間如果訪問,必須自行建立頁(yè)表,把物理地址映射到user space的虛擬地址空間。kernel線程共享這個(gè)swapper_pg_dir頁(yè)表。
總結(jié)
RISC-V Linux啟動(dòng)時(shí)的頁(yè)表創(chuàng)建相對(duì)來說還是比較容易理解的,都是C語言創(chuàng)建的,代碼也比較少。主要就是setup_vm()和setup_vm_final()兩個(gè)頁(yè)表創(chuàng)建函數(shù)。理解了sv39的一些地址格式后,再去分析源碼就比較容易。不過不同kernel版本代碼都不一樣,需要具體情況具體分析。
本篇提到了setup_vm()會(huì)檢查kernel入口地址是否2M對(duì)齊,如果不對(duì)齊kernel無法啟動(dòng),但其實(shí)我們可以解除這個(gè)2M對(duì)齊限制,將這部分空間利用起來,下篇教大家優(yōu)化這部分內(nèi)存。
原文作者:嵌入式Linux充電站
