当前位置: 首页 > 科技观察

鸿蒙之光内核虚拟实景映射A核心源码解析系列(一)基本概念

时间:2023-03-22 13:31:47 科技观察

更多内容请访问:鸿蒙技术社区https://harmonyos.51cto.com,与华为官方联合打造表示即系统通过内存管理单元(MMU)将进程空间的虚拟地址(VA)映射到实际物理地址(PA),并指定相应的访问权限、缓存属性等。程序执行时,CPU访问虚拟内存,通过MMU找到映射的物理内存,进行相应的代码执行或数据读写操作。MMU的映射是用页表(PageTable)来描述的,它存储了虚拟地址和物理地址的映射关系,以及访问权限。每个进程在创建时都会创建一个页表。页表由页表项(PageTableEntry,PTE)组成。每个页表项描述了虚拟地址范围和物理地址范围之间的映射关系。页表数据在内存区的存储位置的起始地址称为转换表基地址/页表基地址(TranslationTableBase,TTB)。MMU中有一个页表缓存,称为快表(TranslationLookasideBuffers,TLB),它缓存了最近搜索到的VA对应的页表项。MMU在进行地址转换时,首先在TLB中查找。如果找到对应的页表项,就可以直接转换。否则,需要读取物理内存中的页表项。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和二级页表L21.1页表项的基本概念L1页表将整个4GiB的虚拟内存地址空间划分为4096个部分,每个部分大小为1MiB。每一个副本对应一个32位的页表项,内容为二级页表的基地址TTB或者一块大小为1MiB的物理内存的地址。其中,高12位记录页号,用于定位页表项,即4096个页表项的索引;低20位记录页中的偏移值,虚地址页和实地址页中的偏移值相等。利用虚拟地址中的虚拟页号查询页表得到对应的物理页号,然后用虚拟地址中的页位移构成物理地址。每个L1页表条目将1MiB的虚拟内存地址转换为物理地址。如下图所示:对于一个用户进程,每个一级页表项描述符占用4个字节(即一个32位的L1页表项),可以表示1MiB内存空间的映射关系,即,1GiB用户空间(LiteOS-A用户空间在内核中占用1GiB)虚拟内存空间需要1024个L1页表项。系统在创建用户进程时,会在内存中申请一块4KiB(=4byte*1024)的内存块作为一级页表项的存储区域。系统根据当前进程的需要,动态申请内存作为二级页表的存储区域。.现在我们知道在虚拟内存章节中,用户进程虚拟地址空间初始化函数OsCreateUserVmSpace()已经申请了4KiB内存作为页表存储区的基础:VADDR_T*ttb=LOS_PhysPagesAllocContiguous(1);,起始地址这块内存的是TTB页表的基地址。每个用户进程都需要申请自己的页表项存储区。对于内核进程,页表项存储区域是固定的,即UINT8g_firstPageTable[0x4000],大小为16KiB。L1页表项的低2位用于定义页表项的类型。页表项分为三种:Invalid无效页表项,虚拟地址没有映射到物理地址,访问会导致pagefault异常;PageTable指向二级页表的页表项;Sectionsection页表项对应一个1MiB大小的内存块,直接用页表项的高12位代替虚拟地址的高12位就可以得到物理地址。L2页表将1MiB的地址范围按照4KiB的内存页大小划分为256个小页。内存的高20位记录页号,用于定位页表项;低12位记录页中的偏移值,虚地址页和实地址页中的偏移值相等。利用虚拟地址中的虚拟页号查询页表得到对应的物理页号,然后用虚拟地址中的页位移构成物理地址。每个L2页表条目将4KiB的虚拟内存地址转换为物理地址。如下图所示:L2页表项的低2位用于标识页表项的类型。有四种:Invalid无效页表项,虚拟地址没有映射到物理地址,访问会导致pagefault异常;LargePage大页条目支持64KiB大页,目前不支持;SmallPage小页表项支持4KiB小页的二级页表映射;SmallPageXN小页面条目扩展。页表项类型在文件arch/arm/arm/include/los_mmu_descriptor_v6.h中定义,代码如下:/*L1descriptortype*/#defineMMU_DESCRIPTOR_L1_TYPE_INVALID(0x0<<0)#defineMMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE(0x1<<0)#defineMMU_DESCRIPTOR_<0)#defineMMU_DESCRIPTOR_L1_TYPE_MASK(0x3<<0)/*L2descriptortype*/#defineMMU_DESCRIPTOR_L2_TYPE_INVALID(0x0<<0)#defineMMU_DESCRIPTOR_L2_TYPE_LARGE_PAGE(0x1<<0)#defineMMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE(0x2<<0)#defineMMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE_XN(0x3<<0)#defineMMU_DESCRIPTOR_L2_TYPE_MASK(0x3<<0)1.2页表项操作页表项相关的操作定义在文件arch/arm/arm/include/los_pte_ops.h中。1.2.1函数OsGetPte1获取虚拟地址的L1页表项。(1)处的OsGetPte1Index()内联函数获取虚拟地址的高12位作为页表号。(2)(2)处的OsGetPte1Ptr()内联函数根据页表项的基地址和虚地址获取对应的L1页表项地址。(3)中的函数OsGetPte1()用于获取指定虚拟地址对应的L1页表项数据。一级页表项地址由页表项基地址pte1BasePtr加上虚拟地址va对应的页表项索引(页表号)组成,其中页表项索引等于虚拟地址的高12位地址。需要注意函数OsGetPte1Index()和OsGetPte1()的区别。前者是页表项的内存地址,后者是页表项地址上保存的页表项数据。STATICINLINEUINT32OsGetPte1Index(vaddr_tva){⑴returnva>>MMU_DESCRIPTOR_L1_SMALL_SHIFT;}STATICINLINEPTE_T*OsGetPte1Ptr(PTE_T*pte1BasePtr,vaddr_tva){⑵return(pte1BasePtr+OsGetPte1Index(va));}STATICINLINEPTE_TOsGetPte1(PTE_T*pte1BasePtr,vaddr_tva){⑶return*OsGetPte1Ptr(pte1BasePtr,va);}1.2.2函数OsGetPte2获取虚拟地址的二级页表项(1)OsGetPte2Index()函数根据虚拟地址获取对应页表项的页表号。.因为L2页表项的细分是1MiB的内存块,所以这里的虚拟地址是从1MiB中减去的。(2)处的函数OsGetPte2()用于获取指定虚拟地址对应的L2页表项的地址。L2页表项地址由页表项基址pte2BasePtr地址加上页表项索引组成,其中页表项索引等于虚拟地址的高20位取1MiB的余数。STATICINLINEUINT32OsGetPte2Index(vaddr_tva){⑴return(va%MMU_DESCRIPTOR_L1_SMALL_SIZE)>>MMU_DESCRIPTOR_L2_SMALL_SHIFT;}STATICINLINEPTE_TOsGetPte2(PTE_T*pte2BasePtr,vaddr_tva){⑵return*(pte2BasePtr+OsGetPte2Index(va));}1.2.3页表项类型判断函数从上文已经可以看出,每个L1页表项的低2位标记了页表项的类型,OsIsPte1PageTable、OsIsPte1Invalid、OsIsPte1Section等函数决定了L1页表项是页表、无效还是Section类型。STATICINLINEBOOLOsIsPte1PageTable(PTE_Tpte1){return(pte1&MMU_DESCRIPTOR_L1_TYPE_MASK)==MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE;}STATICINLINEBOOLOsIsPte1Invalid(PTE_Tpte1){return(pte1&MMU_DESCRIPTOR_L1_TYPE_MASK)==MMU_DESCRIPTOR_L1_TYPE_INVALID;}STATICINLINEBOOLOsIsPte1Section(PTE_Tpte1){return(pte1&MMU_DESCRIPTOR_L1_TYPE_MASK)==MMU_DESCRIPTOR_L1_TYPE_SECTION;}同样,下面4个函数用于确定L2页表条目的类型。STATICINLINEBOOLOsIsPte2SmallPage(PTE_Tpte2){return(pte2&MMU_DESCRIPTOR_L2_TYPE_MASK)==MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE;}STATICINLINEBOOLOsIsPte2SmallPageXN(PTE_Tpte2){return(pte2&MMU_DESCRIPTOR_L2_TYPE_MASK)==MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE_XN;}STATICINLINEBOOLOsIsPte2LargePage(PTE_Tpte2){return(pte2&MMU_DESCRIPTOR_L2_TYPE_MASK)==MMU_DESCRIPTOR_L2_TYPE_LARGE_PAGE;}STATICINLINEBOOLOsIsPte2Invalid(PTE_Tpte2){return(pte2&MMU_DESCRIPTOR_L2_TYPE_MASK)==MMU_DESCRIPTOR_L2_TYPE_INVALID;}1.2.4OsTruncPte1函数截取物理地址的高12位。以下代码的宏定义定义在文件arch\arm\arm\include\los_mmu_descriptor_v6.h中。其中MMU_DESCRIPTOR_L1_SMALL_FRAME等于~(0x100000-1)=0xFFF00000,即取高12位。所以函数OsTruncPte1截取了物理内存地址的高12位。#defineMMU_DESCRIPTOR_L1_SMALL_SIZE0x100000#defineMMU_DESCRIPTOR_L1_SMALL_MASK(MMU_DESCRIPTOR_L1_SMALL_SIZE-1)#defineMMU_DESCRIPTOR_L1_SMALL_FRAME(~MMU_DESCRIPTOR_L1_SMALL_MASK)#defineMMU_DESCRIPTOR_L1_SMALL_SHIFT20#defineMMU_DESCRIPTOR_L1_SECTION_ADDR(x)((x)&MMU_DESCRIPTOR_L1_SMALL_FRAME)......STATICINLINEADDR_TOsTruncPte1(ADDR_Taddr){returnMMU_DESCRIPTOR_L1_SECTION_ADDR(addr);}1.2.5L2页表项连续运行函数(1)中的函数OsSavePte2设置L2页表项数据,将页表项指针地址pte2Ptr指向的内存中保存的数据写入页表项数据pte2。OsSavePte2Continuous函数用于连续设置L2页表项数据。需要的参数是pte2BasePtr页表基地址、作为起始索引的索引虚拟地址对应的页号、页表项地址pte2和连续页表项数count。(2)设置页表项的基地址,则页表号加1,页表项数减1。(3)更新页表项的地址,增加的大小为MMU_DESCRIPTOR_L2_SMALL_SIZE,也就是4KiB,然后统计保存成功的个数加1。(4)处while循环条件中的MMU_DESCRIPTOR_L2_NUMBERS_PER_L1等于256(即每1MiB对应的L2页表项数)。函数OsClearPte2Continuous用于清除页表项的基地址。如果做{⑵pte2BasePtr[index++]=pte2;count--;⑶pte2+=MMU_DESCRIPTOR_L2_SMALL_SIZE;saveCounts++;⑷}while((count!=0)&&(index!=MMU_DESCRIPTOR_L2_NUMBERS_PER_L1));DSB;returnsaveCounts;}STATICINLINEVOIDOsClearPte2Tru,pte2Trcontinuous(UINT32count){UINT32index=0;DMB;while(count>0){pte2Ptr[index++]=0;count--;}DSB;}更多信息请访问:与华为官方共建的鸿蒙技术社区//harmonyos.51cto.com