摘要:本文介紹了MMU虛實映射的基本概念,運行機制,分析了映射初始化、映射查詢、映射虛擬內存和物理內存,解除虛實映射,更改映射屬性,重新映射等常用接口的代碼。


本文分享自華為云社區??《使用MRS CDL實現實時數據同步的極致性能》??,作者: zhushy 。


虛實映射是指系統通過內存管理單元(MMU,MemoryManagement Unit)將進程空間的虛擬地址(VA)與實際的物理地址(PA)做映射,并指定相應的訪問權限、緩存屬性等。程序執行時,CPU訪問的是虛擬內存,通過MMU找到映射的物理內存,并做相應的代碼執行或數據讀寫操作。MMU的映射由頁表(Page Table)來描述,其中保存虛擬地址和物理地址的映射關系以及訪問權限等。每個進程在創建的時候都會創建一個頁表,頁表由一個個頁表條目(Page Table Entry, PTE)構成,每個頁表條目描述虛擬地址區間與物理地址區間的映射關系。頁表數據在內存區域存儲位置的開始地址叫做轉換表基地址(translation table base,ttb)。MMU中有一塊頁表緩存,稱為快表(TLB, Translation LookasideBuffers),做地址轉換時,MMU首先在TLB中查找,如果找到對應的頁表條目可直接進行轉換,提高了查詢效率。

本文中所涉及的源碼,以OpenHarmonyLiteOS-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

L1頁表將全部的4GiB地址空間劃分為4096份,每份大小1MiB。每份對應一個32位的頁表項,內容是L2頁表基地址或某個1MiB物理內存的基地址。內存的高12位記錄頁號,用于對頁表項定位,也就是4096個頁面項的索引;低20位記錄頁內偏移值,虛實地址頁內偏移值相等。使用虛擬地址中的虛擬頁號查詢頁表得到對應的物理頁號,然后與虛擬地址中的頁內位移組成物理地址。


對于用戶進程,每個一級頁表條目描述符占用4個字節,可表示1MiB的內存空間的映射關系,即1GiB用戶空間(LiteOS-A內核中用戶空間占用1GiB)的虛擬內存空間需要1024個。系統創建用戶進程時,在內存中申請一塊4KiB大小的內存塊作為一級頁表的存儲區域,系統根據當前進程的需要會動態申請內存作為二級頁表的存儲區域。現在我們就知道,在虛擬內存章節,用戶進程虛擬地址空間初始化函數OsCreateUserVmSpace申請了4KiB的內存作為頁表存儲區域的依據了。每個用戶進程需要申請字節的頁表地址,對于內核進程,頁表存儲區域是固定的,即UINT8g_firstPageTable[0x4000],大小為16KiB。


L1頁表項的低2位用于定義頁表項的類型,頁表描述符類型有如下3種:

  • Invalid 無效頁表項,虛擬地址沒有映射到物理地址,訪問會產生缺頁異常;
  • Page Table 指向L2頁表的頁表項;
  • Section Section頁表項對應1M的節,直接使用頁表項的最高12位替代虛擬地址的高12位即可得到物理地址。


L2頁表把1MiB的地址范圍按4KiB的內存頁大小繼續分成256個小頁。內存的高20位記錄頁號,用于對頁表項定位;低12位記錄頁內偏移值,虛實地址頁內偏移值相等。使用虛擬地址中的虛擬頁號查詢頁表得到對應的物理頁號,然后與虛擬地址中的頁內位移組成物理地址。每個L2頁表項將4K的虛擬內存地址轉換為物理地址。


L2頁表描述符類型有如下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

函數OsGetPte1用于獲取指定虛擬地址對應的L1頁表項地址。L1頁表項地址由頁表項基地址加上頁表項索引組成,其中頁表項索引等于虛擬地址的高12位。


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

函數OsGetPte2用于獲取指定虛擬地址對應的L2頁表項地址。L2頁表項地址由頁表項基地址加上頁表項索引組成,其中頁表項索引等于虛擬地址對1MiB取余后的高20位。(為啥va % MMU_DESCRIPTOR_L1_SMALL_SIZE取余?TODO)。


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)); }

2、虛擬映射初始化

在文件kernel/base/vm/los_vm_boot.c的系統內存初始化函數OsSysMemInit()會調用虛實映射初始化函數OsInitMappingStartUp()。代碼定義在arch/arm/arm/src/los_arch_mmu.c,代碼如下。⑴處函數使TLB失效,涉及些cp15寄存器和匯編,后續再分析。⑵處函數切換到臨時TTV。⑶處設置內核地址空間的映射。下面分別詳細這些函數代碼。


VOID OsInitMappingStartUp(VOID) { ⑴   OsArmInvalidateTlbBarrier();  ⑵   OsSwitchTmpTTB();  ⑶  OsSetKSectionAttr(KERNEL_VMM_BASE, FALSE);     OsSetKSectionAttr(UNCACHED_VMM_BASE, TRUE);     OsKSectionNewAttrEnable(); }

2.1 函數OsSwitchTmpTTB

⑴處獲取內核地址空間。L1頁表項由4096個頁表項組成,每個4Kib,共需要16Kib大小。所以⑵處代碼按16Kib對齊申請16Kib大小的內存存放L1頁表項。⑶處設置內核虛擬內存地址空間的轉換表基地址(translation table base,ttb)。⑷處把g_firstPageTable數據復制到內核地址空間的轉換表。如果復制失敗,則直接使用g_firstPageTable。⑸處設置內核虛擬地址空間的物理內存基地址,然后寫入MMU寄存器。


STATIC VOID OsSwitchTmpTTB(VOID) {     PTE_T *tmpTtbase = NULL;     errno_t err; ⑴   LosVmSpace *kSpace = LOS_GetKVmSpace();      /* ttbr address should be 16KByte align */ ⑵   tmpTtbase = LOS_MemAllocAlign(m_aucSysMem0, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS,                                   MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS);     if (tmpTtbase == NULL) {         VM_ERR("memory alloc failed");         return;     }  ⑶  kSpace->archMmu.virtTtb = tmpTtbase; ⑷  err = memcpy_s(kSpace->archMmu.virtTtb, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS,                    g_firstPageTable, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS);     if (err != EOK) {         (VOID)LOS_MemFree(m_aucSysMem0, tmpTtbase);         kSpace->archMmu.virtTtb = (VADDR_T *)g_firstPageTable;         VM_ERR("memcpy failed, errno: %d", err);         return;     } ⑸  kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);     OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);     ISB; }

2.2 函數OsSetKSectionAttr

內部函數OsSetKSectionAttr用與設置內核虛擬地址空間段的屬性,分別針對[KERNEL_ASPACE_BASE,KERNEL_ASPACE_BASE+KERNEL_ASPACE_SIZE]和[UNCACHED_VMM_BASE,UNCACHED_VMM_BASE+UNCACHED_VMM_SIZE]進行設置。內核虛擬地址空間是固定映射到物理內存的。


⑴處計算相對內核虛擬地址空間基地址的偏移。⑵處先計算相對偏移值的text、rodata、data_bss段的虛擬內存地址,然后創建這些段的虛實映射關系。⑶處設置內核虛擬地址區間的虛擬轉換基地址和物理轉換基地址。然后解除虛擬地址的虛實映射。⑷處按指定的標簽對text段之前的內存區間進行虛實映射。⑸處映射text、rodata、data_bss段的內存區間,并調用函數LOS_VmSpaceReserve在進程空間中保留一段地址區間(為啥保留 TODO?)。⑹是BSS段后面的heap區,映射虛擬地址空間的內存堆區間。


STATIC VOID OsSetKSectionAttr(UINTPTR virtAddr, BOOL uncached) { ⑴  UINT32 offset = virtAddr - KERNEL_VMM_BASE;     /* every section should be page aligned */ ⑵  UINTPTR textStart = (UINTPTR)&__text_start + offset;     UINTPTR textEnd = (UINTPTR)&__text_end + offset;     UINTPTR rodataStart = (UINTPTR)&__rodata_start + offset;     UINTPTR rodataEnd = (UINTPTR)&__rodata_end + offset;     UINTPTR ramDataStart = (UINTPTR)&__ram_data_start + offset;     UINTPTR bssEnd = (UINTPTR)&__bss_end + offset;     UINT32 bssEndBoundary = ROUNDUP(bssEnd, MB);     LosArchMmuInitMapping mmuKernelMappings[] = {         {             .phys = SYS_MEM_BASE + textStart - virtAddr,             .virt = textStart,             .size = ROUNDUP(textEnd - textStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),             .flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_EXECUTE,             .name = "kernel_text"         },         {             .phys = SYS_MEM_BASE + rodataStart - virtAddr,             .virt = rodataStart,             .size = ROUNDUP(rodataEnd - rodataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),             .flags = VM_MAP_REGION_FLAG_PERM_READ,             .name = "kernel_rodata"         },         {             .phys = SYS_MEM_BASE + ramDataStart - virtAddr,             .virt = ramDataStart,             .size = ROUNDUP(bssEndBoundary - ramDataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),             .flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE,             .name = "kernel_data_bss"         }     };     LosVmSpace *kSpace = LOS_GetKVmSpace();     status_t status;     UINT32 length;     int i;     LosArchMmuInitMapping *kernelMap = NULL;     UINT32 kmallocLength;     UINT32 flags;      /* use second-level mapping of default READ and WRITE */ ⑶  kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;     kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);     status = LOS_ArchMmuUnmap(&kSpace->archMmu, virtAddr,                               (bssEndBoundary - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT);     if (status != ((bssEndBoundary - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {         VM_ERR("unmap failed, status: %d", status);         return;     }      flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE | VM_MAP_REGION_FLAG_PERM_EXECUTE;     if (uncached) {         flags |= VM_MAP_REGION_FLAG_UNCACHED;     } ⑷  status = LOS_ArchMmuMap(&kSpace->archMmu, virtAddr, SYS_MEM_BASE,                             (textStart - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,                             flags);     if (status != ((textStart - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {         VM_ERR("mmap failed, status: %d", status);         return;     }  ⑸  length = sizeof(mmuKernelMappings) / sizeof(LosArchMmuInitMapping);     for (i = 0; i < length; i++) {         kernelMap = &mmuKernelMappings[i];         if (uncached) {             kernelMap->flags |= VM_MAP_REGION_FLAG_UNCACHED;         }         status = LOS_ArchMmuMap(&kSpace->archMmu, kernelMap->virt, kernelMap->phys,                                  kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT, kernelMap->flags);         if (status != (kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {             VM_ERR("mmap failed, status: %d", status);             return;         }         LOS_VmSpaceReserve(kSpace, kernelMap->size, kernelMap->virt);     }  ⑹   kmallocLength = virtAddr + SYS_MEM_SIZE_DEFAULT - bssEndBoundary;     flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE;     if (uncached) {         flags |= VM_MAP_REGION_FLAG_UNCACHED;     }     status = LOS_ArchMmuMap(&kSpace->archMmu, bssEndBoundary,                             SYS_MEM_BASE + bssEndBoundary - virtAddr,                             kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,                             flags);     if (status != (kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {         VM_ERR("mmap failed, status: %d", status);         return;     }     LOS_VmSpaceReserve(kSpace, kmallocLength, bssEndBoundary); }

2.3 函數OsKSectionNewAttrEnable

函數OsKSectionNewAttrEnable釋放臨時TTB。代碼看不懂TODO 以后慢慢看。⑴處獲取內核虛擬進程空間,⑵處設置進程空間MMU的虛擬地址轉化表基地址TTB,設置物理內存地址轉換表基地址。⑶處從CP15 C2寄存器讀取TTB地址,取高20位。⑷處將內核頁表基地址(邏輯與的什么?TODO)寫入CP15 c2 TTB寄存器。⑸處清空TLB緩沖區,然后釋放內存。


STATIC VOID OsKSectionNewAttrEnable(VOID) { ⑴  LosVmSpace *kSpace = LOS_GetKVmSpace();     paddr_t oldTtPhyBase;  ⑵  kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;     kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);      /* we need free tmp ttbase */ ⑶  oldTtPhyBase = OsArmReadTtbr0();     oldTtPhyBase = oldTtPhyBase & MMU_DESCRIPTOR_L2_SMALL_FRAME; ⑷  OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);     ISB;      /* we changed page table entry, so we need to clean TLB here */ ⑸  OsCleanTLB();      (VOID)LOS_MemFree(m_aucSysMem0, (VOID *)(UINTPTR)(oldTtPhyBase - SYS_MEM_BASE + KERNEL_VMM_BASE)); }

3、虛實映射函數LOS_ArchMmuMap

虛實映射的知識點TODO

3.1 函數LOS_ArchMmuMap

函數LOS_ArchMmuMap用于映射進程空間虛擬地址區間與物理地址區間,其中輸入參數archMmu為MMU配置結構體,vaddr和paddr分別是虛擬內存和物理內存的開始地址;count為虛擬地址和物理地址映射的數量;flags為映射標簽。⑴處進行函數參數校驗,不支持NON-SECURE的標記,虛擬地址和物理地址需要內存頁4KiB對齊。⑵處當虛擬地址、物理地址基于1MiB對齊,并且數量count大于256時,使用Section頁表項格式。⑶處生成L1 section類型頁表項并保存,下文詳細分析該函數。如果不滿足⑵處條件,需要使用L2映射。首先執行⑷處獲取虛擬地址對應的L1頁表項,接著執行⑸處判斷是否映射,如果沒有對應的映射,則執行⑹處的函數OsMapL1PTE生成L1 pagetable類型頁表項并保存,然后執行函數OsMapL2PageContinous生成L2 頁表項目并保存。如果已經映射為L1 page table頁表項類型,則重新映射。如果不是支持的頁表項類型,則執行LOS_Panic()觸發異常。⑺處統計生成映射的調試,最終會返回映射成功的數量。


status_t LOS_ArchMmuMap(LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T paddr, size_t count, UINT32 flags) {     PTE_T l1Entry;     UINT32 saveCounts = 0;     INT32 mapped = 0;     INT32 checkRst;  ⑴  checkRst = OsMapParamCheck(flags, vaddr, paddr);     if (checkRst < 0) {         return checkRst;     }      /* see what kind of mapping we can use */     while (count > 0) { ⑵      if (MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(vaddr) &&             MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(paddr) &&             count >= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1) {             /* compute the arch flags for L1 sections cache, r ,w ,x, domain and type */ ⑶          saveCounts = OsMapSection(archMmu, flags, &vaddr, &paddr, &count);         } else {             /* have to use a L2 mapping, we only allocate 4KB for L1, support 0 ~ 1GB */ ⑷          l1Entry = OsGetPte1(archMmu->virtTtb, vaddr); ⑸          if (OsIsPte1Invalid(l1Entry)) { ⑹              OsMapL1PTE(archMmu, &l1Entry, vaddr, flags);                 saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);             } else if (OsIsPte1PageTable(l1Entry)) {                 saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);             } else {                 LOS_Panic("%s %d, unimplemented tt_entry %x/n", __FUNCTION__, __LINE__, l1Entry);             }         } ⑺      mapped += saveCounts;     }      return mapped; }

3.2 函數OsMapSection

函數OsMapSection生成L1section類型頁表項并保存。⑴處轉換為MMU標簽。 ⑵處內聯函數OsGetPte1Ptr(archMmu->virtTtb,*vaddr)用于獲取虛擬地址對應的頁表項索引地址,等于頁表項基地址加上虛擬地址的高20位;OsTruncPte1(*paddr) | mmuFlags |MMU_DESCRIPTOR_L1_TYPE_SECTION)為虛擬地址的高12位+MMU標簽+頁表項Section類型值。該行語句的作用是把虛擬地址和物理地理映射,映射關系維護在頁表項。⑶處把虛擬地址和物理地址增加1MiB的大小,映射數量減去256。


STATIC UINT32 OsMapSection(const LosArchMmu *archMmu, UINT32 flags, VADDR_T *vaddr,                            PADDR_T *paddr, UINT32 *count) {     UINT32 mmuFlags = 0;  ⑴  mmuFlags |= OsCvtSecFlagsToAttrs(flags); ⑵  OsSavePte1(OsGetPte1Ptr(archMmu->virtTtb, *vaddr),         OsTruncPte1(*paddr) | mmuFlags | MMU_DESCRIPTOR_L1_TYPE_SECTION); ⑶  *count -= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1;     *vaddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;     *paddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;      return MMU_DESCRIPTOR_L2_NUMBERS_PER_L1; }

3.3 函數OsGetL2Table

函數OsGetL2Table用于生成L2頁表,函數參數中archMmu是MMU,l1Index是L1頁表項,ppa屬于輸出參數,保存L2頁表項基地址。⑴處計算L2頁表項偏移值(為啥這么計算 看不懂 TODO)。⑵處查詢遍歷是否存在L2頁表,⑶處獲取頁表項基地址,然后判斷是否頁表類型,如果是則返回L2頁表項基地址。

如果沒有存在的頁表,則為L2頁表申請內存,如果支持虛擬地址,執行⑷使用LOS_PhysPageAlloc申請內存頁;如果不支持虛擬地址,執行⑸使用LOS_MemAlloc申請內存。⑹處轉換為物理地址,然后返回L2頁表項基地址。


STATIC STATUS_T OsGetL2Table(LosArchMmu *archMmu, UINT32 l1Index, paddr_t *ppa) {     UINT32 index;     PTE_T ttEntry;     VADDR_T *kvaddr = NULL; ⑴  UINT32 l2Offset = (MMU_DESCRIPTOR_L2_SMALL_SIZE / MMU_DESCRIPTOR_L1_SMALL_L2_TABLES_PER_PAGE) *         (l1Index & (MMU_DESCRIPTOR_L1_SMALL_L2_TABLES_PER_PAGE - 1));     /* lookup an existing l2 page table */ ⑵   for (index = 0; index < MMU_DESCRIPTOR_L1_SMALL_L2_TABLES_PER_PAGE; index++) { ⑶      ttEntry = archMmu->virtTtb[ROUNDDOWN(l1Index, MMU_DESCRIPTOR_L1_SMALL_L2_TABLES_PER_PAGE) + index];         if ((ttEntry & MMU_DESCRIPTOR_L1_TYPE_MASK) == MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE) {             *ppa = (PADDR_T)ROUNDDOWN(MMU_DESCRIPTOR_L1_PAGE_TABLE_ADDR(ttEntry), MMU_DESCRIPTOR_L2_SMALL_SIZE) +                 l2Offset;             return LOS_OK;         }     }  #ifdef LOSCFG_KERNEL_VM     /* not found: allocate one (paddr) */ ⑷  LosVmPage *vmPage = LOS_PhysPageAlloc();     if (vmPage == NULL) {         VM_ERR("have no memory to save l2 page");         return LOS_ERRNO_VM_NO_MEMORY;     }     LOS_ListAdd(&archMmu->ptList, &vmPage->node);     kvaddr = OsVmPageToVaddr(vmPage); #else ⑸  kvaddr = LOS_MemAlloc(OS_SYS_MEM_ADDR, MMU_DESCRIPTOR_L2_SMALL_SIZE);     if (kvaddr == NULL) {         VM_ERR("have no memory to save l2 page");         return LOS_ERRNO_VM_NO_MEMORY;     } #endif     (VOID)memset_s(kvaddr, MMU_DESCRIPTOR_L2_SMALL_SIZE, 0, MMU_DESCRIPTOR_L2_SMALL_SIZE);      /* get physical address */ ⑹  *ppa = LOS_PaddrQuery(kvaddr) + l2Offset;     return LOS_OK; }

3.4 函數OsMapL1PTE

函數OsMapL1PTE用于生成L1page table類型頁表項并保存,其中函數參數pte1Ptr是L1頁表項基地址。⑴處獲取L2頁表項基地址, ⑵處把L2頁表項基地址加上描述符類型賦值給L1頁表項基地址。⑶設置標簽,⑷處保存頁表項基地址。


STATIC VOID OsMapL1PTE(LosArchMmu *archMmu, PTE_T *pte1Ptr, vaddr_t vaddr, UINT32 flags) {     paddr_t pte2Base = 0;  ⑴  if (OsGetL2Table(archMmu, OsGetPte1Index(vaddr), &pte2Base) != LOS_OK) {         LOS_Panic("%s %d, failed to allocate pagetable/n", __FUNCTION__, __LINE__);     }  ⑵  *pte1Ptr = pte2Base | MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE; ⑶  if (flags & VM_MAP_REGION_FLAG_NS) {         *pte1Ptr |= MMU_DESCRIPTOR_L1_PAGETABLE_NON_SECURE;     }     *pte1Ptr &= MMU_DESCRIPTOR_L1_SMALL_DOMAIN_MASK;     *pte1Ptr |= MMU_DESCRIPTOR_L1_SMALL_DOMAIN_CLIENT; // use client AP ⑷   OsSavePte1(OsGetPte1Ptr(archMmu->virtTtb, vaddr), *pte1Ptr); }

4、虛實映射查詢函數LOS_ArchMmuQuery

4.1 函數LOS_ArchMmuQuery

函數LOS_ArchMmuQuery用于獲取進程空間虛擬地址對應的物理地址以及映射屬性,其中輸入參數為虛擬地址vaddr,輸出參數為物理地址*paddr和標簽*flags。⑴處獲取虛擬地址對應的頁表項。⑵處如果虛擬地址對應的頁表項描述符類型無效,返回錯誤碼。⑶處如果頁表項描述符類型為Section,則執行⑷獲取映射的物理地址,其MMU_DESCRIPTOR_L1_SECTION_ADDR(l1Entry)為頁表項的高12位,(vaddr& (MMU_DESCRIPTOR_L1_SMALL_SIZE - 1))為虛擬地址的低20位,即頁內偏移值。⑸處獲取映射的標簽值。


虛擬地址對應的頁表項描述符類型為頁表Page Table,則執行⑹調用內聯函數OsGetPte2BasePtr()計算L2頁表項基地址,計算方法為:取頁表項的高22位,低10位置0,轉化為虛擬地址。⑺處計算虛擬地址對應的L2頁表項數值。如果L2頁表項描述符類型為小頁,則執行⑻計算物理地址,然后計算相應的標簽值。⑼處表示當前輕內核還不支持大頁類型。


STATUS_T LOS_ArchMmuQuery(const LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T *paddr, UINT32 *flags) { ⑴  PTE_T l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);     PTE_T l2Entry;     PTE_T* l2Base = NULL;  ⑵  if (OsIsPte1Invalid(l1Entry)) {         return LOS_ERRNO_VM_NOT_FOUND; ⑶  } else if (OsIsPte1Section(l1Entry)) {         if (paddr != NULL) { ⑷          *paddr = MMU_DESCRIPTOR_L1_SECTION_ADDR(l1Entry) + (vaddr & (MMU_DESCRIPTOR_L1_SMALL_SIZE - 1));         }          if (flags != NULL) { ⑸          OsCvtSecAttsToFlags(l1Entry, flags);         }     } else if (OsIsPte1PageTable(l1Entry)) { ⑹      l2Base = OsGetPte2BasePtr(l1Entry);         if (l2Base == NULL) {             return LOS_ERRNO_VM_NOT_FOUND;         } ⑺      l2Entry = OsGetPte2(l2Base, vaddr);         if (OsIsPte2SmallPage(l2Entry) || OsIsPte2SmallPageXN(l2Entry)) {             if (paddr != NULL) { ⑻               *paddr = MMU_DESCRIPTOR_L2_SMALL_PAGE_ADDR(l2Entry) + (vaddr & (MMU_DESCRIPTOR_L2_SMALL_SIZE - 1));             }              if (flags != NULL) {                 OsCvtPte2AttsToFlags(l1Entry, l2Entry, flags);             } ⑼      } else if (OsIsPte2LargePage(l2Entry)) {             LOS_Panic("%s %d, large page unimplemented/n", __FUNCTION__, __LINE__);         } else {             return LOS_ERRNO_VM_NOT_FOUND;         }     }      return LOS_OK; }

5、虛實映射解除函LOS_ArchMmuUnmap

虛實映射解除函數LOS_ArchMmuUnmap解除進程空間虛擬地址區間與物理地址區間的映射關系。 ⑴處函數OsGetPte1用于獲取指定虛擬地址對應的L1頁表項地址。⑵處計算需要解除的無效映射的數量。如果頁表描述符映射類型為Section,并且映射的數量超過256,則執行⑶解除映射Section。如果頁表描述符映射類型為Page Table,則執行⑷先解除二級頁表映射,然后解除一級頁表映射,涉及的2個函數后文詳細分析。⑹處函數使TLB失效,涉及些cp15寄存器和匯編,后續再分析。


STATUS_T LOS_ArchMmuUnmap(LosArchMmu *archMmu, VADDR_T vaddr, size_t count) {     PTE_T l1Entry;     INT32 unmapped = 0;     UINT32 unmapCount = 0;      while (count > 0) { ⑴      l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);         if (OsIsPte1Invalid(l1Entry)) { ⑵          unmapCount = OsUnmapL1Invalid(&vaddr, &count);         } else if (OsIsPte1Section(l1Entry)) {             if (MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(vaddr) && count >= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1) { ⑶              unmapCount = OsUnmapSection(archMmu, &vaddr, &count);             } else {                 LOS_Panic("%s %d, unimplemented/n", __FUNCTION__, __LINE__);             }         } else if (OsIsPte1PageTable(l1Entry)) { ⑷          unmapCount = OsUnmapL2PTE(archMmu, vaddr, &count);             OsTryUnmapL1PTE(archMmu, vaddr, OsGetPte2Index(vaddr) + unmapCount,                             MMU_DESCRIPTOR_L2_NUMBERS_PER_L1 - unmapCount); ⑸          vaddr += unmapCount << MMU_DESCRIPTOR_L2_SMALL_SHIFT;         } else {             LOS_Panic("%s %d, unimplemented/n", __FUNCTION__, __LINE__);         }         unmapped += unmapCount;     } ⑹  OsArmInvalidateTlbBarrier();     return unmapped; }

5.1 函數OsUnmapL1Invalid

函數OsUnmapL1Invalid用于解除無效的映射,會把虛擬地址增加,映射的數量減少。⑴處的MMU_DESCRIPTOR_L1_SMALL_SIZE表示1MiB大小,*vaddr% MMU_DESCRIPTOR_L1_SMALL_SIZE對1MiB取余,向右偏移12位>>MMU_DESCRIPTOR_L2_SMALL_SHIFT表示大小轉換為內存頁數量。(為啥相減TODO?)。⑵處把解除映射的內存頁數量左移12位轉換為地址長度,然后更新虛擬地址。⑶處減去已經解除映射的數量。


STATIC INLINE UINT32 OsUnmapL1Invalid(vaddr_t *vaddr, UINT32 *count) {     UINT32 unmapCount;  ⑴  unmapCount = MIN2((MMU_DESCRIPTOR_L1_SMALL_SIZE - (*vaddr % MMU_DESCRIPTOR_L1_SMALL_SIZE)) >>         MMU_DESCRIPTOR_L2_SMALL_SHIFT, *count); ⑵  *vaddr += unmapCount << MMU_DESCRIPTOR_L2_SMALL_SHIFT; ⑶  *count -= unmapCount;      return unmapCount; }

5.2 函數OsUnmapSection

函數OsUnmapSection用于接觸一級頁表的Section映射。⑴處把虛擬地址對應的頁表項基地址設置為0。⑵處使TLB寄存器失效,⑶更新虛擬地址和映射數量。


STATIC UINT32 OsUnmapSection(LosArchMmu *archMmu, vaddr_t *vaddr, UINT32 *count) { ⑴  OsClearPte1(OsGetPte1Ptr((PTE_T *)archMmu->virtTtb, *vaddr)); ⑵  OsArmInvalidateTlbMvaNoBarrier(*vaddr);  ⑶  *vaddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;     *count -= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1;      return MMU_DESCRIPTOR_L2_NUMBERS_PER_L1; }

5.3 函數OsUnmapL2PTE

函數OsUnmapL2PTE用于。⑴處先調用函數OsGetPte1計算虛擬地址對應頁表項,然后調用函數OsGetPte2BasePtr計算二級頁表基地址。⑵處獲取虛擬地址的二級頁表項索引。⑶計算需要解除映射的數量(為啥取最小值 TODO)。⑷處依次解除各個二級頁表的映射。⑸使TLB失效。


STATIC UINT32 OsUnmapL2PTE(const LosArchMmu *archMmu, vaddr_t vaddr, UINT32 *count) {     UINT32 unmapCount;     UINT32 pte2Index;     PTE_T *pte2BasePtr = NULL;  ⑴  pte2BasePtr = OsGetPte2BasePtr(OsGetPte1((PTE_T *)archMmu->virtTtb, vaddr));     if (pte2BasePtr == NULL) {         LOS_Panic("%s %d, pte2 base ptr is NULL/n", __FUNCTION__, __LINE__);     }  ⑵  pte2Index = OsGetPte2Index(vaddr); ⑶  unmapCount = MIN2(MMU_DESCRIPTOR_L2_NUMBERS_PER_L1 - pte2Index, *count);      /* unmap page run */ ⑷  OsClearPte2Continuous(&pte2BasePtr[pte2Index], unmapCount);      /* invalidate tlb */ ⑸  OsArmInvalidateTlbMvaRangeNoBarrier(vaddr, unmapCount);      *count -= unmapCount;     return unmapCount; }

6、其他函數

6.1 映射屬性修改函數LOS_ArchMmuChangeProt

函數LOS_ArchMmuChangeProt用于修改進程空間虛擬地址區間的映射屬性,其中參數archMmu為進程空間的MMU信息,vaddr為虛擬地址,count為映射的頁數,flags為映射使用的新標簽屬性信息。⑴處對參數進行校驗,⑵處查詢虛擬地址映射的物理地址,如果沒有映射則執行⑶把虛擬地址增加1個內存頁大小繼續修改下一個內存頁的屬性。⑷處先解除當前內存頁的映射,然后執行⑸使用新的映射屬性重新映射,⑹處虛擬地址增加1個內存頁大小繼續修改下一個內存頁的屬性。


STATUS_T LOS_ArchMmuChangeProt(LosArchMmu *archMmu, VADDR_T vaddr, size_t count, UINT32 flags) {     STATUS_T status;     PADDR_T paddr = 0;  ⑴  if ((archMmu == NULL) || (vaddr == 0) || (count == 0)) {         VM_ERR("invalid args: archMmu %p, vaddr %p, count %d", archMmu, vaddr, count);         return LOS_NOK;     }      while (count > 0) { ⑵      count--;         status = LOS_ArchMmuQuery(archMmu, vaddr, &paddr, NULL);         if (status != LOS_OK) { ⑶          vaddr += MMU_DESCRIPTOR_L2_SMALL_SIZE;             continue;         }  ⑷      status = LOS_ArchMmuUnmap(archMmu, vaddr, 1);         if (status < 0) {             VM_ERR("invalid args:aspace %p, vaddr %p, count %d", archMmu, vaddr, count);             return LOS_NOK;         }  ⑸      status = LOS_ArchMmuMap(archMmu, vaddr, paddr, 1, flags);         if (status < 0) {             VM_ERR("invalid args:aspace %p, vaddr %p, count %d",                    archMmu, vaddr, count);             return LOS_NOK;         } ⑹      vaddr += MMU_DESCRIPTOR_L2_SMALL_SIZE;     }     return LOS_OK; }

6.2 映射轉移函數LOS_ArchMmuMove

函數LOS_ArchMmuMove用于將進程空間一個虛擬地址區間的映射關系轉移至另一塊未使用的虛擬地址區間重新做映射,其中參數oldVaddr為老的虛擬地址,newVaddr為新的虛擬內存地址,flags在重新映射時可以更改映射屬性信息。⑴處先查詢老的虛擬地址映射的物理內存。如果沒有映射關系,把新舊虛擬內存都增加一個內存頁的大小,⑵處取消老的虛擬地址的映射,⑶處使用新的虛擬內存重新映射到查詢到的物理內存地址。⑷把新舊虛擬內存都增加一個內存頁的大小,繼續處理下一個內存頁。


STATUS_T LOS_ArchMmuMove(LosArchMmu *archMmu, VADDR_T oldVaddr, VADDR_T newVaddr, size_t count, UINT32 flags) {     STATUS_T status;     PADDR_T paddr = 0;      if ((archMmu == NULL) || (oldVaddr == 0) || (newVaddr == 0) || (count == 0)) {         VM_ERR("invalid args: archMmu %p, oldVaddr %p, newVddr %p, count %d",                archMmu, oldVaddr, newVaddr, count);         return LOS_NOK;     }      while (count > 0) {         count--; ⑴      status = LOS_ArchMmuQuery(archMmu, oldVaddr, &paddr, NULL);         if (status != LOS_OK) {             oldVaddr += MMU_DESCRIPTOR_L2_SMALL_SIZE;             newVaddr += MMU_DESCRIPTOR_L2_SMALL_SIZE;             continue;         }         // we need to clear the mapping here and remain the phy page. ⑵      status = LOS_ArchMmuUnmap(archMmu, oldVaddr, 1);         if (status < 0) {             VM_ERR("invalid args: archMmu %p, vaddr %p, count %d",                    archMmu, oldVaddr, count);             return LOS_NOK;         }  ⑶      status = LOS_ArchMmuMap(archMmu, newVaddr, paddr, 1, flags);         if (status < 0) {             VM_ERR("invalid args:archMmu %p, old_vaddr %p, new_addr %p, count %d",                    archMmu, oldVaddr, newVaddr, count);             return LOS_NOK;         } ⑷      oldVaddr += MMU_DESCRIPTOR_L2_SMALL_SIZE;         newVaddr += MMU_DESCRIPTOR_L2_SMALL_SIZE;     }      return LOS_OK; }

小結

本文介紹了MMU虛實映射的基本概念,運行機制,分析了映射初始化、映射查詢、映射虛擬內存和物理內存,解除虛實映射,更改映射屬性,重新映射等常用接口的代碼。感謝閱讀,有什么問題,請留言。


??點擊關注,第一時間了解華為云新鮮技術~??