摘要:本文介绍了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内核为例,均可以在开源站点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的内存作为页表存储区域的依据了。每个用户进程需要申请字节的页表地址,对于内核进程,页表存储区域是固定的,即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虚实映射的基本概念,运行机制,分析了映射初始化、映射查询、映射虚拟内存和物理内存,解除虚实映射,更改映射属性,重新映射等常用接口的代码。感谢阅读,有什么问题,请留言。

点击关注,第一时间了解华为云新鲜技术~

鸿蒙轻内核源码分析:虚实映射相关推荐

  1. 鸿蒙轻内核源码分析:虚拟内存

    摘要:本文以代码+文字的形式,介绍虚拟内存管理的结构体.相关宏定义,分析内核虚拟地址空间和用户进程虚拟地址空间如何初始化等内容. 本文分享自华为云社区<鸿蒙轻内核A核源码分析系列四(2) 虚拟内 ...

  2. 鸿蒙轻内核源码分析:虚拟文件系统 VFS

    本文分享自华为云社区<鸿蒙轻内核M核源码分析系列二一 01 虚拟文件系统VFS>,作者:zhushy . VFS(Virtual File System)是文件系统的虚拟层,它不是一个实际 ...

  3. 鸿蒙轻内核源码分析:MMU协处理器

    摘要:本系列首先了解下ARM CP15协处理器的知识,接着介绍下协处理器相关的汇编指令,最后分析下MMU相关汇编代码. 本文分享自华为云社区<鸿蒙轻内核A核源码分析系列六 MMU协处理器> ...

  4. 鸿蒙轻内核源码分析:异常钩子模块系统中断异常,如何转储异常信息

    摘要:本篇介绍下鸿蒙轻内核中异常钩子模块发生系统中断异常时如何转储异常信息. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列十七(3) 异常信息ExcInfo>,作者: zhushy. ...

  5. 鸿蒙轻内核源码分析:掌握信号量使用差异

    摘要:本文带领大家一起剖析鸿蒙轻内核的信号量模块的源代码,包含信号量的结构体.信号量池初始化.信号量创建删除.申请释放等. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列十一 信号量Semap ...

  6. 鸿蒙轻内核源码分析:文件系统LittleFS

    摘要:本文先介绍下LFS文件系统结构体的结构体和全局变量,然后分析下LFS文件操作接口. 本文分享自华为云社区<# 鸿蒙轻内核M核源码分析系列二一 02 文件系统LittleFS>,作者: ...

  7. v15.03 鸿蒙内核源码分析(内存映射) | 映射真是个好东西 | 百篇博客分析HarmonyOS源码

    子曰:"德不孤,必有邻." <论语>:里仁篇 百篇博客系列篇.本篇为: v15.xx 鸿蒙内核源码分析(内存映射篇) | 映射真是个好东西 内存管理相关篇为: v11. ...

  8. 鸿蒙内核源码分析系列 | 读懂HarmonyOS内核源代码!

    本系列从HarmonyOS架构层视角整理成文, 并用生活场景及讲故事的方式试图去解构内核,一窥究竟.帮助你读懂并快速理解鸿蒙操作系统源码. 1.鸿蒙内核源码分析(调度机制篇) 2.鸿蒙内核源码分析(进 ...

  9. v11.03 鸿蒙内核源码分析(内存分配) | 内存有哪些分配方式 | 百篇博客分析HarmonyOS源码

    子曰:"君子周而不比,小人比而不周."<论语>:为政篇 百篇博客系列篇.本篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪些分配方式 内存管理相关篇为 ...

最新文章

  1. WebService客户端添加SOAPHeader信息
  2. 一种清除windows通知区域“僵尸”图标的方案——XP系统解决方案
  3. Mybatis工作流程,附带mybatis的mapper文件和config配置文件模板。mapper文件和dao接口的关系——xml中的namespace和sql标签id命名要求。
  4. K-近邻算法实现简单filmClassify
  5. tar打包和解压命令
  6. php tongjiapi 使用_Kayako REST API使用详解一
  7. 使用混合多云每个人都应避免的3个陷阱(第4部分)
  8. MOXy作为您的JAX-RS JSON提供程序–客户端
  9. 模板实现栈队列以及链表
  10. CTex + Texmaker
  11. 设计模式- 创建型模式, 建造者模式(2)
  12. java 创建文件夹的方法_java中创建文件夹的方法
  13. buck电路pscad仿真_十二脉波整流器谐波抑制控制策略仿真
  14. GitHub超过2600星的TensorFlow教程,简洁清晰还不太难丨资源
  15. windows库的创建和使用:静态库+动态库
  16. 【Django 2021年最新版教程25】模板语言 前端for循环怎么用 实例
  17. delphi打印机编程
  18. java二重积分_用java实现二重积分的计算
  19. linux无线网卡模拟ap,在 openSUSE 上使用 create_ap 创建虚拟 WiFi 热点来方便使用 KDE Connect 连接手机...
  20. js 金额格式化 和 转成人民币大写金额形式

热门文章

  1. 实战 | Java 流之Stream,Lambda以及日期
  2. Bootstrap列表组的情景类
  3. es6 async函数的异步迭代器
  4. 2016年广东省电子设计大赛健康电子专题——健康养殖远程监控系统(环境监控系统V1.0版本)
  5. PCL点云CSV转PCD文件
  6. date time 分开存储如何合并_如何将多个日期跨度合并/拆分为一个时间轴(Oracle 11g)?...
  7. linux下gsl怎么运行,linux下gsl安装问题与解决
  8. 前端协商缓存强缓存如何使用_http协商缓存与强缓存
  9. 支付宝手机网站支付(基于Java实现支付宝手机网站支付)
  10. (转) intellij idea部署web项目时的位置(Tomcat)