RISC-V Linux啟動之頁表創建分析

上篇分析了RISC-V linux的匯編啟動過程,其中講到了relocate重定向需要開啟MMU,今天分析RISC-V Linux的頁表創建。

注意:本文基于linux5.10.111內核

sv39 mmu

RISC-V Linux支持sv32、sv39、sv48等虛擬地址格式,分別代表32為虛擬地址、38位虛擬地址和48位虛擬地址。RISC-V Linux默認也是使用sv39格式,sv39的虛擬地址、物理地址、PTE格式如下:

虛擬地址格式:

RISC-V Linux啟動之頁表創建分析

物理地址格式:

RISC-V Linux啟動之頁表創建分析

PTE格式:

RISC-V Linux啟動之頁表創建分析

虛擬地址使用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

三級頁表虛擬地址轉為物理地址過程示意圖:RISC-V Linux啟動之頁表創建分析

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大小創建二級頁表。

頁表創建函數

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_SIZE

prot: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_SIZE

prot:權限,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_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()創建最終頁表。

具體細節參考代碼中的注釋,下面的代碼省略了一些不重要的部分。

setup_vm()

asmlinkage?void?__init?setup_vm(uintptr_t?dtb_pa) { ?uintptr_t?va,?pa,?end_va; ?uintptr_t?load_pa?=?(uintptr_t)(&amp;_start); ?uintptr_t?load_sz?=?(uintptr_t)(&amp;_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?<blockquote data-tool="mdnice編輯器" style="border-top: none;border-right: none;border-bottom: none;font-size: 0.9em;overflow: auto;color: rgb(106, 115, 125);padding: 10px 10px 10px 20px;margin-bottom: 20px;margin-top: 20px;border-left-color: rgb(239, 112, 96);background: rgb(255, 249, 249);"> <p style="font-size: 16px;padding-top: 8px;padding-bottom: 8px;color: black;line-height: 26px;">setup_vm()在最開始就進行了kernel入口地址的對齊檢查,要求入口地址2M對齊。假設內存起始地址為0x80000000,那么kernel只能放在0x80000000、0x80200000等2M對齊處。為什么會有這種對齊要求呢?</p> <p style="font-size: 16px;padding-top: 8px;padding-bottom: 8px;color: black;line-height: 26px;">我猜測單純是為給opensbi預留了2M空間,因為kernel之前還有opensbi,而opensbi運行完之后,默認跳轉地址就是偏移2M,kernel只是為了跟opensbi對應,所以設置了2M對齊。</p> <p style="font-size: 16px;padding-top: 8px;padding-bottom: 8px;color: black;line-height: 26px;">那opensbi需要占用2M這么大?實際上只需要幾百KB,因此opensbi和kernel中間有一段內存是空閑的,沒有人使用。這個問題我們下篇再講。</p> </blockquote><h3 data-tool="mdnice編輯器" style="margin-top: 30px;margin-bottom: 15px;font-weight: bold;font-size: 20px;"> <span style="display: none;"></span>setup_vm_final()<span style="display: none;"></span> </h3><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">在該函數中開始為整個物理內存做內存映射,通過swapper頁表來管理,并且清除掉匯編階段的頁表。</p><pre class="brush:php;toolbar:false;">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,?&amp;start,?&amp;end)?{ ??if?(start?&gt;=?end) ???break; ??if?(start?<p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">說明:</p><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">在setup_vm_final()函數中,通過swapper_pg_dir頁表來管理整個物理內存的訪問。并且清除匯編階段的頁表fixmap_pte和early_pg_dir。(本質上就是把該頁表項的內容清0,即賦值為0)</p><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">最終把swapper_pg_dir頁表的物理地址賦值給SATP寄存器。這樣CPU就可以通過該頁表訪問整個物理內存。</p><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">切換頁表通過如下實現:</p><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">csr_write(CSR_SATP,PFN_DOWN(_pa(swapper_pg_dir))|SATP_MODE);</p><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">在swapper_pg_dir管理的kernel space中,其虛擬地址與物理地址空間的偏移是固定的,為va_pa_offset(定義在arch/risc-v/mm/init.c中的一個全局變量)</p><blockquote data-tool="mdnice編輯器" style="border-top: none;border-right: none;border-bottom: none;font-size: 0.9em;overflow: auto;color: rgb(106, 115, 125);padding: 10px 10px 10px 20px;margin-bottom: 20px;margin-top: 20px;border-left-color: rgb(239, 112, 96);background: rgb(255, 249, 249);"><p style="font-size: 16px;padding-top: 8px;padding-bottom: 8px;color: black;line-height: 26px;">注意:swapper_pg_dir管理的是kernel space的頁表,即它把物理內存映射到的虛擬地址空間是只能kernel訪問的。user space不能訪問,用戶空間如果訪問,必須自行建立頁表,把物理地址映射到user space的虛擬地址空間。kernel線程共享這個swapper_pg_dir頁表。</p></blockquote><h2 data-tool="mdnice編輯器" style="margin-top: 30px;margin-bottom: 15px;font-weight: bold;border-bottom: 1px solid rgb(239, 112, 96);font-size: 1.3em;"> <span style="display: none;"></span><span style="display: inline-block;background: rgb(239, 112, 96);color: rgb(255, 255, 255);padding: 2px 10px 1px;border-top-right-radius: 3px;border-top-left-radius: 3px;margin-right: 3px;">總結</span><span style="display: inline-block;vertical-align: bottom;border-bottom: 36px solid #efebe9;border-right: 20px solid transparent;"> </span> </h2><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">RISC-V Linux啟動時的頁表創建相對來說還是比較容易理解的,都是c語言創建的,代碼也比較少。主要就是setup_vm()和setup_vm_final()兩個頁表創建函數。理解了sv39的一些地址格式后,再去分析源碼就比較容易。不過不同kernel版本代碼都不一樣,需要具體情況具體分析。</p><p data-tool="mdnice編輯器" style="padding-top: 8px;padding-bottom: 8px;line-height: 26px;">本篇提到了setup_vm()會檢查kernel入口地址是否2M對齊,如果不對齊kernel無法啟動,但其實我們可以解除這個2M對齊限制,將這部分空間利用起來,下篇教大家優化這部分內存。</p>

? 版權聲明
THE END
喜歡就支持一下吧
點贊9 分享