鸿蒙轻内核A核源码分析系列五(1) 虚实映射

举报
zhushy 发表于 2021/11/25 16:55:57 2021/11/25
【摘要】 虚实映射是指系统通过内存管理单元(MMU,Memory Management Unit)将进程空间的虚拟地址(VA)与实际的物理地址(PA)做映射,并指定相应的访问权限、缓存属性等。程序执行时,CPU访问的是虚拟内存,通过MMU找到映射的物理内存,并做相应的代码执行或数据读写操作。MMU的映射由页表(Page Table)来描述,其中保存虚拟地址和物理地址的映射关系以及访问权限等。每个进程在...

虚实映射是指系统通过内存管理单元(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);
}

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。