摘要:本文介绍了 MMU 虚实映射的基本概念,运行机制,分析了映射初始化、映射查询、映射虚拟内存和物理内存,解除虚实映射,更改映射属性,重新映射等常用接口的代码。
本文分享自华为云社区《使用MRS CDL实现实时数据同步的极致性能》,作者: zhushy 。
虚实映射是指系统通过内存管理单元(MMU,MemoryManagement Unit)将进程空间的虚拟地址(VA)与实际的物理地址(PA)做映射,并指定相应的访问权限、缓存属性等。程序执行时,CPU 访问的是虚拟内存,通过 MMU 找到映射的物理内存,并做相应的代码执行或数据读写操作。MMU 的映射由页表(Page Table)来描述,其中保存虚拟地址和物理地址的映射关系以及访问权限等。每个进程在创建的时候都会创建一个页表,页表由一个个页表条目(Page Table Entry, PTE)构成,每个页表条目描述虚拟地址区间与物理地址区间的映射关系。页表数据在内存区域存储位置的开始地址叫做转换表基地址(translation table base,ttb)。MMU 中有一块页表缓存,称为快表(TLB, Translation LookasideBuffers),做地址转换时,MMU 首先在 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 和二级页表 L2
L1 页表将全部的 4GiB 地址空间划分为 4096 份,每份大小 1MiB。每份对应一个 32 位的页表项,内容是 L2 页表基地址或某个 1MiB 物理内存的基地址。内存的高 12 位记录页号,用于对页表项定位,也就是 4096 个页面项的索引;低 20 位记录页内偏移值,虚实地址页内偏移值相等。使用虚拟地址中的虚拟页号查询页表得到对应的物理页号,然后与虚拟地址中的页内位移组成物理地址。
对于用户进程,每个一级页表条目描述符占用 4 个字节,可表示 1MiB 的内存空间的映射关系,即 1GiB 用户空间(LiteOS-A 内核中用户空间占用 1GiB)的虚拟内存空间需要 1024 个。系统创建用户进程时,在内存中申请一块 4KiB 大小的内存块作为一级页表的存储区域,系统根据当前进程的需要会动态申请内存作为二级页表的存储区域。现在我们就知道,在虚拟内存章节,用户进程虚拟地址空间初始化函数 OsCreateUserVmSpace 申请了 4KiB 的内存作为页表存储区域的依据了。每个用户进程需要申请字节的页表地址,对于内核进程,页表存储区域是固定的,即 UINT8g_firstPageTable[0x4000],大小为 16KiB。
L1 页表项的低 2 位用于定义页表项的类型,页表描述符类型有如下 3 种:
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 pagetable 类型页表项并保存,然后执行函数 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 生成 L1section 类型页表项并保存。⑴处转换为 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 用于生成 L1page 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 虚实映射的基本概念,运行机制,分析了映射初始化、映射查询、映射虚拟内存和物理内存,解除虚实映射,更改映射属性,重新映射等常用接口的代码。感谢阅读,有什么问题,请留言。
点击关注,第一时间了解华为云新鲜技术~
评论