积分1215 / 贡献70

提问2答案被采纳57文章2

作者动态

[经验分享] LiteOS-A内核页表和地址映射 原创 精华

深开鸿_王皓 显示全部楼层 发表于 2023-11-7 14:26:14

OpenHarmony LiteOS-A内核页表和地址映射

​ 本文的目的是介绍下OpenHarmony操作系统采用的LiteOS-A内核的内存奥秘,通过追踪内核启动流程,观察虚拟内存的分布,包括虚拟地址范围以及基本作用,并跟踪物理地址和虚拟地址的映射过程,这个过程是和硬件强相关的,普通应用开发者不是特别关心,但是对于系统开发者内存使用特别是移植内核到新的硬件上会有比较大的帮助。

​ 启动过程抓住了几个关键函数点,按照时间线包括main函数,OsSysMemInit函数等。

1. main函数运行之前

在跳转到main函数之前,页表就做了初始化。详细见arch/arm/arm/src/startup/reset_vector_up.S

注:liteos-a中虚拟地址可能因环境而不同

页表地址:g_firstPageTable,大小16KB,虚拟地址范围为0x40130000-0x40133fff。

地址映射(均为section mapping,每个表项映射到1MB大小的section):

  • 虚拟地址0x40000000-0x7fffffff -> 物理地址0x40000000-0x7fffffff
  • 虚拟地址0x80000000-0xbfffffff -> 物理地址0x40000000-0x7fffffff
  • 虚拟地址0xc8000000-0xf4ffffff -> ... (其他映射,到外设物理低地址)

如下图:

liteos-a-memory-vm-page-table-beforemain.drawio.png

建立页表核心代码如下:

/*
 * r4: page table base address
 * r6: physical address
 * r7: virtual address
 * r8: sizes
 * r10: flags
 * r9 and r12 will be used as variable
 */
#ifdef LOSCFG_KERNEL_MMU
page_table_build:
    mov     r10, r6
    bfc     r10, #20, #12                       /* r9: pa % MB */
    add     r8, r8, r10
    add     r8, r8, #(1 << 20)
    sub     r8, r8, #1
    lsr     r6, #20                             /* r6 = physical address / MB */
    lsr     r7, #20                             /* r7 = virtual address / MB */
    lsr     r8, #20                             /* r8 = roundup(size, MB) */

page_table_build_loop:
    orr     r12, r9, r6, lsl #20                /* r12: flags | physAddr */
    str     r12, [r4, r7, lsl #2]               /* gPgTable[l1Index] = physAddr | flags */
    add     r6, #1                              /* physAddr+ */
    add     r7, #1                              /* l1Index++ */
    subs    r8, #1                              /* sizes-- */
    bne     page_table_build_loop
    bx      lr
#endif

2. OsSysMemInit函数运行后

2.1 初始化

OsSysMemInit:

OsSysMemInit()
|   ...
|-> OsInitMappingStartUp()

OsInitMappingStartUp中设置并启用了新的页表布局,函数分析如下:

OsInitMappingStartUp() 
|-> ...
|
|   // 1. 切换到一个临时页表,其内容和原来的页表g_firstPageTable相同
|   // 因为后面的操作就要对g_firstPageTable进行修改
|-> OsSwitchTmpTTB();
    |   // 1.1 从系统内存池m_aucSysMem0中申请16KB大小的临时页表
    |-> tmpTtbase = LOS_MemAllocAlign(m_aucSysMem0, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS,
    |       MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS);
    |
    |   // 1.2 内核空间mmu页表基地址切换到tmpTtbase,
    |   // 并将原来g_firstPageTable中内容复制到tmpTtbase
    |-> kSpace->archMmu.virtTtb = tmpTtbase;
    |   err = memcpy_s(kSpace->archMmu.virtTtb, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS,
    |        g_firstPageTable, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS);
    |   kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);
    |
    |   // 1.3 切换TTBR0中页表基址到tmpTtbase物理地址
    |-> OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);
|
|   // 2. 修改g_firstPageTable中的映射
|-> OsSetKSectionAttr(KERNEL_VMM_BASE, FALSE);
|   OsSetKSectionAttr(UNCACHED_VMM_BASE, FALSE);
    |   // 2.1 根据目标虚拟地址与KERNEL_VMM_BASE的偏移,
    |   // 获取各个段偏移后的起始和结束的虚拟地址
    |-> 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);
    |
    |   // 2.2 将内核空间的页表基址设为g_firstPageTable
    |   // 并将virtAddr到bssEndBoundary的映射清除
    |-> 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);
    |
    |   // 2.3 将virtAddr到textStart,映射到SYS_MEM_BASE起始的物理地址
    |-> status = LOS_ArchMmuMap(&kSpace->archMmu, virtAddr, SYS_MEM_BASE,
    |       (textStart - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
    |        flags);
    |
    |   // 2.4 映射mmuKernelMappings
    |-> length = sizeof(mmuKernelMappings) / sizeof(LosArchMmuInitMapping);
    |   for (i = 0; i < length; i++) {
    |       kernelMap = &mmuKernelMappings[i];
    |       status = LOS_ArchMmuMap(&kSpace->archMmu, kernelMap->virt, kernelMap->phys,
    |           kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT, kernelMap->flags);
    |       LOS_VmSpaceReserve(kSpace, kernelMap->size, kernelMap->virt);
    |   }
    |
    |   // 2.5 将bssEndBoundary到virtAddr+SYS_MEM_SIZE_DEFAULT,
    |   // 映射到SYS_MEM_SIZE_DEFAULT + bssEndBoundary - virtAddr起始的物理地址
    |-> 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);
|
|   // 3. 重新启用g_firstPageTable
|-> OsKSectionNewAttrEnable();
    |   // 3.1 将内核空间的页表基址设为g_firstPageTable
    |-> kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;
    |   kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);
    |
    |   // 3.2 释放原来的临时页表,并设置页表基址为g_firstPageTable
    |-> oldTtPhyBase = OsArmReadTtbr0();
    |   oldTtPhyBase = oldTtPhyBase & MMU_DESCRIPTOR_L2_SMALL_FRAME;
    |   OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);
    |   ISB;
    |   LOS_MemFree(m_aucSysMem0, (VOID *)(UINTPTR)(oldTtPhyBase - SYS_MEM_BASE + KERNEL_VMM_BASE));
    |
    |   // 3.3 清除TLB
    |-> OsCleanTLB();

LOS_ArchMmuMap的功能是设置指定页表映射,代码如下:

LOS_ArchMmuMap(LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T paddr, 
|   size_t count, UINT32 flags)
|-> while (count > 0) {
    |   // 如果映射超过1MB,则选择section mapping
    |-> if (MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(*mmuMapInfo.vaddr) &&
    |       MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(*mmuMapInfo.paddr) &&
    |       count >= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1) {
    |       // 将vaddr映射到paddr,映射一个section
    |       saveCounts = OsMapSection(&mmuMapInfo, &count);
            |   // 获取mmu flags
            |-> mmuFlags |= OsCvtSecFlagsToAttrs(*mmuMapInfo->flags);
            |
            |   // 获取虚拟地址对应的1级页表entry的物理地址
            |-> pte1Paddr = OsGetPte1Paddr(mmuMapInfo->archMmu->physTtb, *mmuMapInfo->vaddr);
            |
            |   // 将物理地址对应section地址,写入到虚拟地址对应的1级页表entry
            |-> OsSavePte1(OsGetPte1Ptr(mmuMapInfo->archMmu->virtTtb, *mmuMapInfo->vaddr),
            |       OsTruncPte1(*mmuMapInfo->paddr) | mmuFlags | MMU_DESCRIPTOR_L1_TYPE_SECTION);
            |
            |   // 更新参数以便下一次迭代
            |-> *count -= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1;
            |   *mmuMapInfo->vaddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;
            |   *mmuMapInfo->paddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;
            |
            |   // 返回0x100
            |-> return MMU_DESCRIPTOR_L2_NUMBERS_PER_L1;
    |   } else {
    |   // 如果不超过1MB,则选择两级页表映射
    |
        |   // 获取虚拟地址对应的1级页表entry的虚拟地址
        |-> l1Entry = OsGetPte1Ptr(archMmu->virtTtb, *mmuMapInfo.vaddr);
        |
        |   // 将vaddr映射到paddr,映射count个页,两级页表映射
        |   // 如果OsIsPte1Invalid,则l1Entry未设置,则需设置l1Entry
        |   // 否则直接OsMapL2PageContinous,映射l2页
        |-> if (OsIsPte1Invalid(*l1Entry)) {
        |       saveCounts = OsMapL1PTE(&mmuMapInfo, l1Entry, &count);
                |-> ...
                |
                |   // 获取l2页表entry,如果不存在则分配
                |-> OsGetL2Table(mmuMapInfo->archMmu, OsGetPte1Index(*mmuMapInfo->vaddr), &pte2Base)
                |
                |   // 将l1Entry设为映射到pte2Base 
                |-> *l1Entry = pte2Base | MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE;
                |   if (*mmuMapInfo->flags & VM_MAP_REGION_FLAG_NS) {
                |       *l1Entry |= MMU_DESCRIPTOR_L1_PAGETABLE_NON_SECURE;
                |   }
                |   *l1Entry &= MMU_DESCRIPTOR_L1_SMALL_DOMAIN_MASK;
                |   *l1Entry |= MMU_DESCRIPTOR_L1_SMALL_DOMAIN_CLIENT; // use client AP
                |   OsSavePte1(OsGetPte1Ptr(mmuMapInfo->archMmu->virtTtb, *mmuMapInfo->vaddr), *l1Entry);
                |
                |-> ...
        |   } else if (OsIsPte1PageTable(*l1Entry)) {
        |       saveCounts = OsMapL2PageContinous(&mmuMapInfo, l1Entry, &count);
        |   } else {
        |       LOS_Panic("%s %d, unimplemented tt_entry %x\n", __FUNCTION__, __LINE__, l1Entry);
        |   }
    |   }
    |
|   }
|   return mapped;

LOS_ArchMmuUnmap的功能是清除指定页表映射,代码如下:

LOS_ArchMmuUnmap(LosArchMmu *archMmu, VADDR_T vaddr, size_t count)
|-> while (count > 0) {
    |-> l1Entry = OsGetPte1Ptr(archMmu->virtTtb, vaddr);
    |
    |   // 根据l1Entry类型,将l1Entry中的映射清除
    |   // l1Entry为两级页表映射时还需将对应的l2Entry清除
    |-> 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, l1Entry, &vaddr, &count);
    |       } else {
    |           LOS_Panic("%s %d, unimplemented\n", __FUNCTION__, __LINE__);
    |       }
    |   } else if (OsIsPte1PageTable(*l1Entry)) {
    |       unmapCount = OsUnmapL2PTE(archMmu, l1Entry, vaddr, &count);
    |       OsTryUnmapL1PTE(archMmu, l1Entry, vaddr, OsGetPte2Index(vaddr) + unmapCount,
    |                       MMU_DESCRIPTOR_L2_NUMBERS_PER_L1);
    |       vaddr += unmapCount << MMU_DESCRIPTOR_L2_SMALL_SHIFT;
    |   } else {
    |       LOS_Panic("%s %d, unimplemented\n", __FUNCTION__, __LINE_        _);
    |   }
    |-> ...
|   }
|   return unmapped;

2.2 内存布局

特殊地址:

  • 用户空间起始虚拟地址和大小:

    • USER_ASPACE_BASE=0x01000000
    • USER_ASPACE_SIZE=KERNEL_ASPACE_BASE - USER_ASPACE_BASE - 0x01000000,即0x3e000000
  • 内核空间起始虚拟地址和大小:

    • KERNEL_ASPACE_BASE=KERNEL_VMM_BASE=KERNEL_VADDR_BASE=0x40000000
    • KERNEL_ASPACE_SIZE=KERNEL_VMM_SIZE=KERNEL_VADDR_SIZE=DDR_MEM_SIZE=0x40000000
  • uncached vmm空间起始虚拟地址和大小:

    • UNCACHED_VMM_BASE=KERNEL_VMM_BASE + KERNEL_VMM_SIZE=0x80000000
    • UNCACHED_VMM_SIZE=DDR_MEM_SIZE=0x40000000
  • vmalloc空间起始虚拟地址和大小:

    • VMALLOC_START=UNCACHED_VMM_BASE + UNCACHED_VMM_SIZE=0xc0000000
    • VMALLOC_SIZE=0x08000000
  • 外设相关空间起始虚拟地址和大小:

    • PERIPH_DEVICE:

      • PERIPH_DEVICE_BASE=VMALLOC_START + VMALLOC_SIZE=0xc8000000
      • PERIPH_DEVICE_SIZE=PERIPH_PMM_SIZE=0x0f000000
    • PERIPH_CACHED:

      • PERIPH_CACHED_BASE=PERIPH_DEVICE_BASE + PERIPH_DEVICE_SIZE=0xd7000000
      • PERIPH_CACHED_SIZE=PERIPH_PMM_SIZE=0x0f000000
    • PERIPH_UNCACHED:

      • PERIPH_UNCACHED_BASE=PERIPH_CACHED_BASE + PERIPH_CACHED_SIZE=0xe6000000
      • PERIPH_UNCACHED_SIZE=PERIPH_PMM_SIZE=0x0f000000

虚拟地址范围:

  • 用户空间:USER_ASPACE_BASE-USER_ASPACE_SIZE-1,即0x01000000-0x3effffff
  • 内核空间:KERNEL_ASPACE_BASE-KERNEL_ASPACE_SIZE-1,即0x40000000-0x7fffffff
  • uncached vmm空间:UNCACHED_VMM_BASE-UNCACHED_VMM_SIZE-1,即0x80000000-0xbfffffff
  • vmalloc空间:VMALLOC_START-VMALLOC_SIZE-1,即0xc0000000-0xc7ffffff
  • 外设相关空间:包括PERIPH_DEVICE、PERIPH_CACHED、PERIPH_UNCACHED,0xc8000000-0xf4ffffff

如图:

liteos-a-memory-vm-page-table-afterOsSysMemInit.drawio.png

对上图说明:

  • 内核空间、uncached vmm空间、外设相关空间为固定映射,从固定的虚拟地址区域映射到固定的物理地址区域。uncached vmm空间可用于dma
  • 内核空间中,有各个段的映射,包括kernel_text、kernel_rodata、kernel_data_bss。还有kmalloc区域,这片区域又包含了多个小的区域,都用于内存分配相关,包括内核堆空间、用于物理页分配的区域。
  • 用户空间为动态映射,创建用户进程时,从kmalloc物理区域中的页,映射到用户空间中分配的虚拟地址空间。这里比较奇怪,相当于所有的用户进程的ttb是从内核空间里面分配出来的,实际打印测试也验证了这一行为。而物理空间还有还有一大片区域未使用。
  • vmalloc空间为动态映射,从kmalloc物理区域中的页,映射到vmalloc中分配的虚拟地址空间

2.3 地址映射相关函数

LOS_PaddrQuery根据虚拟地址查询对应物理地址:

LOS_PaddrQuery(VOID *vaddr)
|   // 1. 根据虚拟地址类型寻找mmu
|-> if (LOS_IsKernelAddress((VADDR_T)(UINTPTR)vaddr)) {
|       archMmu = &g_kVmSpace.archMmu;
|   } else if (LOS_IsUserAddress((VADDR_T)(UINTPTR)vaddr)) {
|       space = OsCurrProcessGet()->vmSpace;
|       archMmu = &space->archMmu;
|   } else if (LOS_IsVmallocAddress((VADDR_T)(UINTPTR)vaddr)) {
|       archMmu = &g_vMallocSpace.archMmu;
|   } else {
|       VM_ERR("vaddr is beyond range");
|       return 0;
|   }
|
|   // 2. 根据mmu查找物理地址
|-> status = LOS_ArchMmuQuery(archMmu, (VADDR_T)(UINTPTR)vaddr, &paddr, 0);
|   if (status == LOS_OK) {
|       return paddr;
|   } else {
|       return 0;
|   }

LOS_ArchMmuQuery:

LOS_ArchMmuQuery(const LosArchMmu *archMmu, VADDR_T vaddr,
|    PADDR_T *paddr, UINT32 *flags)
|   // 1. 获取1级(顶级)页表entry
|-> PTE_T l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);
|
|   // 2. 检查l1Entry是否有效
|-> if (OsIsPte1Invalid(l1Entry)) {
|       return LOS_ERRNO_VM_NOT_FOUND;
|   } else if (OsIsPte1Section(l1Entry)) {
|   // 3. 如果l1Entry为section,即映射到1MB的section空间
|
    |   // 3.1 设置返回物理地址
    |-> if (paddr != NULL) {
    |       // 等于(l1Entry地址前12位<<20) + vaddr后20位偏移
    |       *paddr = MMU_DESCRIPTOR_L1_SECTION_ADDR(l1Entry) 
    |            + (vaddr & (MMU_DESCRIPTOR_L1_SMALL_SIZE - 1));
    |   }
    |
    |   // 3.2 设置标志位
    |-> if (flags != NULL) {
    |       OsCvtSecAttsToFlags(l1Entry, flags);
    |   }
|   } else if (OsIsPte1PageTable(l1Entry)) {
|   // 4. 如果l1Entry为页表,即映射到两级页表
|
    |   // 4.1 查询2级页表entry
    |-> l2Base = OsGetPte2BasePtr(l1Entry);
    |   if (l2Base == NULL) {
    |       return LOS_ERRNO_VM_NOT_FOUND;
    |   }
    |   l2Entry = OsGetPte2(l2Base, vaddr);
    |
    |   // 4.2 根据l2Entry类型,返回物理地址和设置标志位
    |-> 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;
    |   }
|   }

​ 本文的跟踪代码都是在ARM体系架构下,注意汇编文件reset_vector_up.S不同硬件体系下是不一样的,因为这些代码和硬件是强绑定的。当然目的是一样的,都是通过操作mmu类似的硬件来实现页表功能。读者如果想要把内核移植到新的硬件上,这些硬件体系结构差异化的代码必须实现。

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部