鴻蒙輕內核A核源碼分析系列之虛實映射(1)基礎概念
虛實映射是指系統通過內存管理單元(Memory Management Unit,MMU)將進程空間的虛擬地址(VA)與實際的物理地址(PA)做映射,并指定相應的訪問權限、緩存屬性等。程序執行時,CPU訪問的是虛擬內存,通過MMU找到映射的物理內存,并做相應的代碼執行或數據讀寫操作。MMU的映射由頁表(Page Table)來描述,頁表保存虛擬地址和物理地址的映射關系以及訪問權限等。每個進程在創建的時候都會創建一個頁表,頁表由一個個頁表條目(Page Table Entry, PTE)構成,每個頁表條目描述虛擬地址區間與物理地址區間的映射關系。頁表數據在內存區域存儲位置的開始地址叫做轉換表基地址/頁表基地址(Translation Table Base,TTB)。MMU中有一塊頁表緩存,稱為快表(Translation Lookaside Buffers, TLB),它緩存最近查找過的VA對應的頁表項。做地址轉換時,MMU首先在TLB中查找,如果找到對應的頁表項,則可直接進行轉換,否則就要去物理內存中讀取頁表項。TLB緩存可以減少訪問物理內存的次數,提升查詢效率。
本文中所涉及的源碼,以OpenHarmony LiteOS-A內核為例,均可以在開源站點https://gitee.com/openharmony/kernel_liteos_a 獲取。如果涉及開發板,則默認以hispark_taurus為例。MMU相關的操作函數主要在文件arch/arm/arm/src/los_arch_mmu.c中定義。
虛實映射其實就是一個建立頁表的過程。MMU支持多級頁表,LiteOS-A內核采用二級頁表描述進程空間。首先介紹下一級頁表和二級頁表。
1、一級頁表L1和二級頁表L2
1.1 頁表項基礎概念
L1頁表將全部的4GiB虛擬內存地址空間劃分為4096份,每份大小1MiB。每份對應一個32位的頁表項,內容是L2頁表基地址TTB或某個1MiB大小的物理內存的地址。其中高12位記錄頁號,用于對頁表項定位,也就是4096個頁表項的索引;低20位記錄頁內偏移值,虛實地址頁內偏移值相等。使用虛擬地址中的虛擬頁號查詢頁表得到對應的物理頁號,然后與虛擬地址中的頁內位移組成物理地址。每個L1頁表項將1MiB的虛擬內存地址轉換為物理地址。如下圖所示:

對于用戶進程,每個一級頁表條目描述符占用4個字節(即32位的L1頁表項),可表示1MiB的內存空間的映射關系,即1GiB用戶空間(LiteOS-A內核中用戶空間占用1GiB)的虛擬內存空間需要1024個L1頁表項。系統創建用戶進程時,在內存中申請一塊4KiB大小(=4byte*1024)的內存塊作為一級頁表項的存儲區域,系統根據當前進程的需要會動態申請內存作為二級頁表的存儲區域?,F在我們就知道,在虛擬內存章節,用戶進程虛擬地址空間初始化函數OsCreateUserVmSpace()申請了4KiB的內存作為頁表存儲區域的依據了:VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1);,這段內存的開始地址就是TTB頁表基地址。每個用戶進程需要申請自己的頁表項存儲區域,對于內核進程,頁表項存儲區域是固定的,即UINT8 g_firstPageTable[0x4000],大小為16KiB。
L1頁表項的低2位用于定義頁表項的類型,頁表項類型有如下3種:
- Invalid 無效頁表項,虛擬地址沒有映射到物理地址,訪問會產生缺頁異常;
- Page Table 指向L2頁表的頁表項;
- Section Section 頁表項對應1MiB大小的內存塊,直接使用頁表項的最高12位替代虛擬地址的高12位即可得到物理地址。
L2頁表把1MiB的地址范圍按4KiB的內存頁大小繼續分成256個小頁。內存的高20位記錄頁號,用于對頁表項定位;低12位記錄頁內偏移值,虛實地址頁內偏移值相等。使用虛擬地址中的虛擬頁號查詢頁表得到對應的物理頁號,然后與虛擬地址中的頁內位移組成物理地址。每個L2頁表項將4KiB的虛擬內存地址轉換為物理地址。如下圖所示:

L2頁表項的低2位用于識別頁表項的類型,類型有如下4種:
Invalid 無效頁表項,虛擬地址沒有映射到物理地址,訪問會產生缺頁異常;
Large Page 大頁表項,支持64KiB大頁,暫不支持;
Small Page 小頁表項,支持4KiB小頁的二級頁表映射;
Small Page XN 小頁表項擴展。
在文件arch/arm/arm/include/los_mmu_descriptor_v6.h中定義了頁表項類型,代碼如下:
- /* L1 descriptor type */
- #define MMU_DESCRIPTOR_L1_TYPE_INVALID (0x0 << 0)
- #define MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE (0x1 << 0)
- #define MMU_DESCRIPTOR_L1_TYPE_SECTION (0x2 << 0)
- #define MMU_DESCRIPTOR_L1_TYPE_MASK (0x3 << 0)
- /* L2 descriptor type */
- #define MMU_DESCRIPTOR_L2_TYPE_INVALID (0x0 << 0)
- #define MMU_DESCRIPTOR_L2_TYPE_LARGE_PAGE (0x1 << 0)
- #define MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE (0x2 << 0)
- #define MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE_XN (0x3 << 0)
- #define MMU_DESCRIPTOR_L2_TYPE_MASK (0x3 << 0)
1.2 頁表項操作
在文件arch/arm/arm/include/los_pte_ops.h定義了頁表項相關的操作。
1.2.1 函數OsGetPte1獲取虛擬地址的L1頁表項
⑴處的OsGetPte1Index()內聯函數獲取虛擬地址的高12位作為頁表號。
⑵處的OsGetPte1Ptr()內聯函數根據頁表項基地址和虛擬地址獲取對應的L1頁表項地址。
⑶處的函數OsGetPte1()用于獲取指定虛擬地址對應的L1頁表項數據。該L1頁表項地址由頁表項基地址pte1BasePtr加上虛擬地址va對應的頁表項索引(頁表號)組成,其中頁表項索引等于虛擬地址的高12位。
需要注意函數OsGetPte1Index()和OsGetPte1()的區別,前者是頁表項內存地址,后者是頁表項地址上保持的頁表項數據。
- STATIC INLINE UINT32 OsGetPte1Index(vaddr_t va)
- {
- ⑴ return va >> MMU_DESCRIPTOR_L1_SMALL_SHIFT;
- }
- STATIC INLINE PTE_T *OsGetPte1Ptr(PTE_T *pte1BasePtr, vaddr_t va)
- {
- ⑵ return (pte1BasePtr + OsGetPte1Index(va));
- }
- STATIC INLINE PTE_T OsGetPte1(PTE_T *pte1BasePtr, vaddr_t va)
- {
- ⑶ return *OsGetPte1Ptr(pte1BasePtr, va);
- }
1.2.2 函數OsGetPte2獲取虛擬地址的L2頁表項
⑴處OsGetPte2Index()函數根據虛擬地址獲取對應頁表項的頁表號,計算方式是把虛擬地址對1MiB取余,然后取高20位。因為L2頁表項細分的是1MiB內存塊,這里把虛擬地址對1MiB取余。
⑵處的函數OsGetPte2()用于獲取指定虛擬地址對應的L2頁表項地址。L2頁表項地址由頁表項基地pte2BasePtr址加上頁表項索引組成,其中頁表項索引等于虛擬地址對1MiB取余后的高20位。
- STATIC INLINE UINT32 OsGetPte2Index(vaddr_t va)
- {
- ⑴ return (va % MMU_DESCRIPTOR_L1_SMALL_SIZE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT;
- }
- STATIC INLINE PTE_T OsGetPte2(PTE_T *pte2BasePtr, vaddr_t va)
- {
- ⑵ return *(pte2BasePtr + OsGetPte2Index(va));
- }
1.2.3 頁表項類型判斷函數
從上文已經可知,每一個L1頁表項的低2位標記頁表項的類型,OsIsPte1PageTable、OsIsPte1Invalid、OsIsPte1Section等函數分別判斷L1頁表項是否是頁表、無效、Section段類型。
- STATIC INLINE BOOL OsIsPte1PageTable(PTE_T pte1)
- {
- return (pte1 & MMU_DESCRIPTOR_L1_TYPE_MASK) == MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE;
- }
- STATIC INLINE BOOL OsIsPte1Invalid(PTE_T pte1)
- {
- return (pte1 & MMU_DESCRIPTOR_L1_TYPE_MASK) == MMU_DESCRIPTOR_L1_TYPE_INVALID;
- }
- STATIC INLINE BOOL OsIsPte1Section(PTE_T pte1)
- {
- return (pte1 & MMU_DESCRIPTOR_L1_TYPE_MASK) == MMU_DESCRIPTOR_L1_TYPE_SECTION;
- }
同樣,下面4個函數用于判斷L2頁表項的類型。
- STATIC INLINE BOOL OsIsPte2SmallPage(PTE_T pte2)
- {
- return (pte2 & MMU_DESCRIPTOR_L2_TYPE_MASK) == MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE;
- }
- STATIC INLINE BOOL OsIsPte2SmallPageXN(PTE_T pte2)
- {
- return (pte2 & MMU_DESCRIPTOR_L2_TYPE_MASK) == MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE_XN;
- }
- STATIC INLINE BOOL OsIsPte2LargePage(PTE_T pte2)
- {
- return (pte2 & MMU_DESCRIPTOR_L2_TYPE_MASK) == MMU_DESCRIPTOR_L2_TYPE_LARGE_PAGE;
- }
- STATIC INLINE BOOL OsIsPte2Invalid(PTE_T pte2)
- {
- return (pte2 & MMU_DESCRIPTOR_L2_TYPE_MASK) == MMU_DESCRIPTOR_L2_TYPE_INVALID;
- }
1.2.4 OsTruncPte1函數截取物理地址的高12位
下面代碼的宏定義在文件arch\arm\arm\include\los_mmu_descriptor_v6.h中定義。其中MMU_DESCRIPTOR_L1_SMALL_FRAME等于~(0x100000-1)=0xFFF00000即取高12位。所以函數OsTruncPte1截取物理內存地址的高12位。
- #define MMU_DESCRIPTOR_L1_SMALL_SIZE 0x100000
- #define MMU_DESCRIPTOR_L1_SMALL_MASK (MMU_DESCRIPTOR_L1_SMALL_SIZE - 1)
- #define MMU_DESCRIPTOR_L1_SMALL_FRAME (~MMU_DESCRIPTOR_L1_SMALL_MASK)
- #define MMU_DESCRIPTOR_L1_SMALL_SHIFT 20
- #define MMU_DESCRIPTOR_L1_SECTION_ADDR(x) ((x) & MMU_DESCRIPTOR_L1_SMALL_FRAME)
- ......
- STATIC INLINE ADDR_T OsTruncPte1(ADDR_T addr)
- {
- return MMU_DESCRIPTOR_L1_SECTION_ADDR(addr);
- }
1.2.5 L2頁表項連續操作函數
⑴處的函數OsSavePte2設置L2頁表項數據,頁表項指針地址pte2Ptr指向的內存保存的數據寫入頁表項數據pte2。OsSavePte2Continuous函數用于連續設置L2頁表項數據,需要的參數分別有pte2BasePtr頁表基地址,index虛擬地址對應的頁號作為開始索引,頁表項地址pte2和連續的頁表項數量count。⑵處設置頁表項基地址,然后頁表號增加1,頁表項數量減1。⑶處更新頁表項地址,增加的大小為MMU_DESCRIPTOR_L2_SMALL_SIZE,即4KiB大小,然后統計保存成功的數量加1。⑷處的while循環的條件中的MMU_DESCRIPTOR_L2_NUMBERS_PER_L1等于256(即每1MiB對應的L2頁表項的數量)。
函數OsClearPte2Continuous用于清理頁表項基地址。
- STATIC INLINE VOID OsSavePte2(PTE_T *pte2Ptr, PTE_T pte2)
- {
- DMB;
- ⑴ *pte2Ptr = pte2;
- DSB;
- }
- STATIC INLINE UINT32 OsSavePte2Continuous(PTE_T *pte2BasePtr, UINT32 index, PTE_T pte2, UINT32 count)
- {
- UINT32 saveCounts = 0;
- if (count == 0) {
- return 0;
- }
- DMB;
- do {
- ⑵ pte2BasePtr[index++] = pte2;
- count--;
- ⑶ pte2 += MMU_DESCRIPTOR_L2_SMALL_SIZE;
- saveCounts++;
- ⑷ } while ((count != 0) && (index != MMU_DESCRIPTOR_L2_NUMBERS_PER_L1));
- DSB;
- return saveCounts;
- }
- STATIC INLINE VOID OsClearPte2Continuous(PTE_T *pte2Ptr, UINT32 count)
- {
- UINT32 index = 0;
- DMB;
- while (count > 0) {
- pte2Ptr[index++] = 0;
- count--;
- }
- DSB;
- }