鴻蒙輕核心原始碼分析:虛實對映

華為雲開發者社群發表於2021-11-26
摘要:本文介紹了MMU虛實對映的基本概念,執行機制,分析了對映初始化、對映查詢、對映虛擬記憶體和實體記憶體,解除虛實對映,更改對映屬性,重新對映等常用介面的程式碼。

本文分享自華為雲社群《使用MRS CDL實現實時資料同步的極致效能》,作者: zhushy 。

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

本文中所涉及的原始碼,以OpenHarmony 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的記憶體作為頁表儲存區域的依據了。每個使用者程式需要申請位元組的頁表地址,對於核心程式,頁表儲存區域是固定的,即UINT8 g_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 page table型別頁表項並儲存,然後執行函式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生成L1 section型別頁表項並儲存。⑴處轉換為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用於生成L1 page 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虛實對映的基本概念,執行機制,分析了對映初始化、對映查詢、對映虛擬記憶體和實體記憶體,解除虛實對映,更改對映屬性,重新對映等常用介面的程式碼。感謝閱讀,有什麼問題,請留言。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章