前言
HMOS系统初始化过程主要在OsMain函数中,内存初始化的过程在OsMain函数中OsSysMemInit函数中完成。本文将作为《HarmonyOS内存管理——内存分配》文章的延续,对内存初始化过程进行分析。
正文
内核中Vaddr与Paddr的关系
由于HMOS代码仅开源了C代码,一些启动的汇编代码并没有开源,所以系统初始化时使用汇编对内存的设置操作没有办法通过源码的方式进行讲解。但通过分析一些相关函数以及参考Linux启动代码,可以反向窥探到一些端倪。
首先可以观察内核中Vaddr
与Paddr
之间地址的转换,之间的地址翻译是通过LOS_PaddrToKVaddr
函数完成的,该函数源码如下:
VADDR_T *LOS_PaddrToKVaddr(PADDR_T paddr)
{
struct VmPhysSeg *seg = NULL;
UINT32 segID;
for (segID = 0; segID < g_vmPhysSegNum; segID++) {
seg = &g_vmPhysSeg[segID];
if ((paddr >= seg->start) && (paddr < (seg->start + seg->size))) {
return (VADDR_T *)(UINTPTR)(paddr - SYS_MEM_BASE + KERNEL_ASPACE_BASE);
}
}
/* calculate the pa to va offset */
return (VADDR_T *)(UINTPTR)(paddr - SYS_MEM_BASE + KERNEL_ASPACE_BASE);
}
在函数中可知,vaddr = paddr - SYS_MEM_BASE + KERNEL_ASPACE_BASE
,该计算公式明显是一个线性映射,这与Linux启动时后使用的段式页表管理机制相似。所以可以做这样的推断:SYS_MEM_BASE为烧录时物理内存地址的起始地址,KERNEL_ASPACE_BASE是内核启动时连接即运行时的地址。
如果上述的情况成立,则在启动时需要初始化系统的临时页表,并且将内核相关的内粗暴地址空间对应的页表项设置为段映射模式,这样使用偏移就能够解决内核的Vaddr到Paddr的翻译过程。具体可以看一个例子如下:
假设
(1) 页表基地址为0x0(存放在CP15的c2寄存器上)
(2) 0xc0000000所在段(也就是段序号为0xc00)的页表项地址0x3000
(3) 页表项地址0x3000的值为0x20000000(也就是段序号为0x300)
当虚拟地址为0xc0001000,计算方式如下
(1) 左移20位的得到虚拟地址所在段序号为0xc00,获取低20位得到段内偏移为0x1000
(2) 计算对应页表项地址=页表基地址0x0+段序号0xc00*页表项长度4=0x3000
(3) 0x3000地址上的值为0x20000000,提取高12位得到0x200,所以对应物理段基址为0x20000000
(4) 物理段基址加上段内偏移得到实际的物理地址0x20001000
除此之外,在内核的所有页表完成初始化后,HMOS调用了OsSwitchTmpTTB函数进行了ttb的切换,并且随后调用OsSetKSectionAttr函数对内核的地址进行了冲洗的映射,并释放了原有启动时的临时全局页表。至此我们可以推断,HMOS在汇编代码执行初始化过程中设置了临时全局页表,并且在内核中通过计算偏移的方式完成了Vaddr到Paddr的翻译过程。下面进入到C代码中对内存初始化的分析。
OsSysMemInit
OsSysMemInit函数源码:
UINT32 OsSysMemInit(VOID)
{
STATUS_T ret;
OsKSpaceInit(); // g_kVmSpace 和 g_vMallocSpace初始化
ret = OsKHeapInit(OS_KHEAP_BLOCK_SIZE); // 内核动态内存池初始化
if (ret != LOS_OK) {
VM_ERR("OsKHeapInit fail");
return LOS_NOK;
}
OsVmPageStartup(); // 内存页初始化
OsInitMappingStartUp(); // 映射初始化
ret = ShmInit(); // 共享内存初始化
if (ret < 0) {
VM_ERR("ShmInit fail");
return LOS_NOK;
}
return LOS_OK;
}
如上代码注释所示,OsSysMemInit函数完成了对系统内存初始化的上述操作。下文将会对各个函数进行深入的分析。
OsKSpaceInit
OsKSpaceInit函数源码:
VOID OsKSpaceInit(VOID)
{
OsVmMapInit(); // 对VmSpace列表互斥锁进行初始化
OsKernVmSpaceInit(&g_kVmSpace, OsGFirstTableGet()); // 对g_kVmSpace虚拟内存空间进行初始化
OsVMallocSpaceInit(&g_vMallocSpace, OsGFirstTableGet()); // 对g_vMallocSpace虚拟内存空间进行初始化
}
HMOS在内核中通过维护g_vmSpaceList管理所有的VmSpace结构体。上一篇提及过,VmSpace是虚拟内存空间,除在该函数涉及到的两种g_kVmSpace和g_vMallocSpace两种虚拟内存空间外,创建各个进程的时候都会初始化一个VmSpace并将其加入到g_vmSpaceList链表中。OsVmMampInit函数用于初始化g_vmSpaceListMux互斥锁结构体,保证一次只能有一个任务操作g_vmSpaceList链表。
g_kVmSpace表示内核中的虚拟内存空间,主要供内核中进程使用,内核中的进程共同使用该虚拟内存空间。g_vMallocSpace表示内核中使用LOS_VMalloc函数分配的区域。在OsKernVmSpaceInit函数和OsVMallocSpaceInit函数传参中第二个参数接受的为MMU L1表入口的地址:
VADDR_T *OsGFirstTableGet()
{
return (VADDR_T *)g_firstPageTable;
}
__attribute__((aligned(MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS))) \
__attribute__((section(".bss.prebss.translation_table"))) UINT8 \
g_firstPageTable[MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS];
其中宏定义MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS的值为0x4000U,即表示为最多可接受0x4000个L1入口项,指向L2的地址后section的内存属性设置。
OsKernVmSpaceInit和OsVMallocSpaceInit函数源码如下图所示:
BOOL OsKernVmSpaceInit(LosVmSpace *vmSpace, VADDR_T *virtTtb)
{
vmSpace->base = KERNEL_ASPACE_BASE;
vmSpace->size = KERNEL_ASPACE_SIZE;
vmSpace->mapBase = KERNEL_VMM_BASE;
vmSpace->mapSize = KERNEL_VMM_SIZE;
#ifdef LOSCFG_DRIVERS_TZDRIVER
vmSpace->codeStart = 0;
vmSpace->codeEnd = 0;
#endif
return OsVmSpaceInitCommon(vmSpace, virtTtb);
}
BOOL OsVMallocSpaceInit(LosVmSpace *vmSpace, VADDR_T *virtTtb)
{
vmSpace->base = VMALLOC_START;
vmSpace->size = VMALLOC_SIZE;
vmSpace->mapBase = VMALLOC_START;
vmSpace->mapSize = VMALLOC_SIZE;
#ifdef LOSCFG_DRIVERS_TZDRIVER
vmSpace->codeStart = 0;
vmSpace->codeEnd = 0;
#endif
return OsVmSpaceInitCommon(vmSpace, virtTtb);
}
两个函数的结构相近,不同之处在于对base和size相关参数的设置。这里的base和size指的是虚拟内存的base地址以及size大小。从当前HMOS的内核虚拟内存分布结构分析,其虚拟内存的排布如下所示:
同时两个函数同时调用了OsVmSpaceInitCommon函数,函数源码如下:
STATIC BOOL OsVmSpaceInitCommon(LosVmSpace *vmSpace, VADDR_T *virtTtb)
{
/* 初始化LosVmRegion红黑树 */
LOS_RbInitTree(&vmSpace->regionRbTree, OsRegionRbCmpKeyFn, OsRegionRbFreeFn, OsRegionRbGetKeyFn);
/* 初始化LosVmRegion双向链表 */
LOS_ListInit(&vmSpace->regions);
/* 初始化操作该vmSpace中regions的互斥锁 */
status_t retval = LOS_MuxInit(&vmSpace->regionMux, NULL);
if (retval != LOS_OK) {
VM_ERR("Create mutex for vm space failed, status: %d", retval);
return FALSE;
}
(VOID)LOS_MuxAcquire(&g_vmSpaceListMux);
LOS_ListAdd(&g_vmSpaceList, &vmSpace->node); // 将该vmSpace添加到g_vmSpaceList全局链表中
(VOID)LOS_MuxRelease(&g_vmSpaceListMux);
return OsArchMmuInit(&vmSpace->archMmu, virtTtb); // 初始化vmSpace中MMU对应的ttb
}
如上代码片段中注释所示,该函数主要做了如下5个操作:1、初始化VmSpace中的管理regions的红黑树;2、初始化VmSpace中管理regions的双向链表;3、初始化操作该VmSpace中regions的互斥锁,保证在该VmSpace中一个时刻下只有一方正在操作regions;4、将该VmSpace对象添加到g_vmSpaceList全局链表中;5、初始化VmSpace对应的ttb项,同时包含对virtTtb和physTtb两项的设置。virtTtb和physTtb之间地址的转换公式如下:
其中SYS_MEM_BASE可推断为物理地址的起始地址,KERNEL_ASPACE_BASE可推断为内核虚拟地址的起始地址,virtTtb为g_firstPageTable的地址,该地址为映射后的虚拟地址。
OsKSpaceInit
OsKHeapInit函数源码:
STATUS_T OsKHeapInit(size_t size)
{
STATUS_T ret;
VOID *ptr = NULL;
/*
* roundup to MB aligned in order to set kernel attributes. kernel text/code/data attributes
* should page mapping, remaining region should section mapping. so the boundary should be
* MB aligned.
*/
UINTPTR end = ROUNDUP(g_vmBootMemBase + size, MB); //UINTPTR g_vmBootMemBase = (UINTPTR)&__bss_end;
size = end - g_vmBootMemBase;
ptr = OsVmBootMemAlloc(size); // 基于size调整g_vmBootMemBase的地址,并返回调整前g_vmBootMemBase的数值
if (!ptr) {
PRINT_ERR("vmm_kheap_init boot_alloc_mem failed! %d\n", size);
return -1;
}
m_aucSysMem0 = m_aucSysMem1 = ptr;
ret = LOS_MemInit(m_aucSysMem0, size); // bestfit 算法内存池初始化
if (ret != LOS_OK) {
PRINT_ERR("vmm_kheap_init LOS_MemInit failed!\n");
g_vmBootMemBase -= size;
return ret;
}
LOS_MemExpandEnable(OS_SYS_MEM_ADDR); // 设置为内存池大小可扩展
return LOS_OK;
}
该函数主要的目的时调用LOS_MemInit函数对系统的动态内存池进行初始化。m_aucSysMem0和m_aucSysMem1变量即为内存池的起始地址(虚拟地址)。g_vmBootMemBase的起始值为bss_end的结尾地址,OsKHeapInit函数中传入的参数size大小为512K,在OsVmBootMemAlloc函数接受该size传参。该函数将bss_end的地址传递给ptr,并将bss_end+512K的地址传递给g_vmBootMemBase。然后将bss_end地址和size传递给LOS_MemInit函数,对内核内存池进行初始化。
从上一篇的内存分配分析的内容可得出,当调用LOS_KernelMalloc函数的时候,当分配的空间小于一定数量级时,就会从内存池中分配内存进行使用。内核相关的管理结构体通常不需要很大的地址空间进行存储,所以该内存池中也会有很多和内核对象有关的控制块结构体。内存池中的内存通过LOS_MemAlloc函数进行申请,以及LOS_MemFree进行释放,与原开源LiteOS代码相同。
OsVmPageStartup
OsVmPageStartup函数源码:
VOID OsVmPageStartup(VOID)
{
struct VmPhysSeg *seg = NULL;
LosVmPage *page = NULL;
paddr_t pa;
UINT32 nPage;
INT32 segID;
/* 调整g_physArea[]中各segment的起始地址和size */
OsVmPhysAreaSizeAdjust(ROUNDUP((g_vmBootMemBase - KERNEL_ASPACE_BASE), PAGE_SIZE));
/* 计算各内存segment中可使用的pages数量总和 */
nPage = OsVmPhysPageNumGet();
/* 计算npages个LosVmPage结构体需要的内存大小,并进行分配 */
g_vmPageArraySize = nPage * sizeof(LosVmPage);
g_vmPageArray = (LosVmPage *)OsVmBootMemAlloc(g_vmPageArraySize);
/* 调整g_physArea[]中各segment的起始地址和size */
OsVmPhysAreaSizeAdjust(ROUNDUP(g_vmPageArraySize, PAGE_SIZE));
/* 创建segment并设置相关的变量和参数 */
OsVmPhysSegAdd();
OsVmPhysInit();
/* 设置每个segment中每个页LosVmPage结构体的成员变量 */
for (segID = 0; segID < g_vmPhysSegNum; segID++) {
seg = &g_vmPhysSeg[segID];
nPage = seg->size >> PAGE_SHIFT;
for (page = seg->pageBase, pa = seg->start; page <= seg->pageBase + nPage;
page++, pa += PAGE_SIZE) {
OsVmPageInit(page, pa, segID);
}
OsVmPageOrderListInit(seg->pageBase, nPage);
}
}
在该函数中首先调用OsVmPhysAreaSizeAdjust函数调整g_physArea数组中各段的物理起始地址和size大小,源码如下:
VOID OsVmPhysAreaSizeAdjust(size_t size)
{
INT32 i;
for (i = 0; i < (sizeof(g_physArea) / sizeof(g_physArea[0])); i++) {
g_physArea[i].start += size;
g_physArea[i].size -= size;
}
}
其中g_physArea的内容如下:
/* Physical memory area array */
STATIC struct VmPhysArea g_physArea[] = {
{
.start = SYS_MEM_BASE,
.size = SYS_MEM_SIZE_DEFAULT,
},
};
g_physArea[]全局数组中只定义了一个内存区域,并且该VmPhysArea对象的起始地址被设置为内存起始地址的物理地址。所以这里调用OsVmPhysAreaSizeAdjust函数的目的就是进行校准,使得start的物理地址对应为g_vmBootMemBase的虚拟内存起始地址。因为当调用OsVmBootMemAlloc(size)函数时就是返回当前g_vmBootMemBase的地址,并使得g_vmBootMemBase = g_vmBootMemBase + size。所以在每次调用OsVmBootMemAlloc函数后都会调用OsVmPhysAreaSizeAdjust函数进行g_vmBootMemBase与g_physArea[0].size数值的校准。
在完成内存地址对应的校准之后调用OsVmPhysPageNumGet函数通过g_physArea[0].size的值计算出系统中总共可使用的页面个数,并反回数值给nPage。
随后计算所有页面对应的LosVmPage结构体对应的大小总和,并调用OsVmBootMemAlloc函数进行内存的分配,分配后同样会调用OsVmPhysAreaSizeAdjust函数进行地址的修正。
然后调用OsVmPhysSegAdd函数将g_physArea[]全局数组中的每一个对象初始化为对应的VmPhysSeg结构体对象,进一步调用OsVmPhysInit函数完成所有段的初始化任务(当前HMOS中只有一个段结构体实例),该函数中完成了seg->pageBase = &g_vmPageArray[0]的操作,将段与页面相关联。内存初始化过程至此将g_physArea[]中的全部VmPhysArea对象初始化为VmPhysSeg结构体对象。
在OsVmPageStartup函数的最后一个for循环中完成了对每个VmPhysSeg结构体中对应所有页面LosVmPage对象的初始化。至此段和页面的初始化过程基本结束,在内核内存的排布如下图所示:
OsInitMappingStartUp
OsInitMappingStartUp函数源码:
VOID OsInitMappingStartUp(VOID)
{
OsArmInvalidateTlbBarrier();
/* 切换TTB到临时的TTB,为下一步配置TTB做准备 */
OsSwitchTmpTTB();
/* 重新配置TTB,将内核代码和数据以L2映射的方式进行配置 */
OsSetKSectionAttr();
/* 切换ttbr0 -> ttbr1 */
OsArchMmuInitPerCPU();
}
该函数主要完成了内核临时页表的切换,然后将内核内存以L2的方式进行映射,最后将ttbr0切换至ttbr1。下面针对各函数进行详细的分析。
OsSwitchTmpTTB函数源码:
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;
}
在该函数中首先获得g_kVmSpace对象,当前在该对象中kSpace->archMmu.virtTtb的值为g_firstPageTable。然后从系统的内存池中分配出页表大小的空间,并将地址赋值给tmpTtbase;随后将kSpace->archMmu.virtTtb赋值为tmpTtbase,并将g_firstPageTable的页表项拷贝到tmpTtbase地址空间中,最后调用OsArmWriteTtbr0函数将页表地址写入到ttbr0寄存器中,完成了ttbr0从g_firstPageTable到tmpTtbase的切换,并且此次切换后页表中的内容没有任何的改变。上述的一系列操作主要为下一步调用OsSetKSectionAttr函数做准备。
OsSetKSectionAttr函数源码(函数源码比较长片段截取进行分析):
/* every section should be page aligned */
UINTPTR textStart = (UINTPTR)&__text_start;
UINTPTR textEnd = (UINTPTR)&__text_end;
UINTPTR rodataStart = (UINTPTR)&__rodata_start;
UINTPTR rodataEnd = (UINTPTR)&__rodata_end;
UINTPTR ramDataStart = (UINTPTR)&__ram_data_start;
UINTPTR bssEnd = (UINTPTR)&__bss_end;
UINT32 bssEndBoundary = ROUNDUP(bssEnd, MB);
LosArchMmuInitMapping mmuKernelMappings[] = {
{
.phys = SYS_MEM_BASE + textStart - KERNEL_VMM_BASE,
.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 - KERNEL_VMM_BASE,
.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 - KERNEL_VMM_BASE,
.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"
}
};
对将会使用到的变量进行初始化,值得注意的是mmuKernelMappings[]数组中包含了kernel的代码段、rodata段和数据段的物理地址和虚拟地址,并且各段对齐的大小为Page Size(4K),为后续的重新映射做准备。
LosVmSpace *kSpace = LOS_GetKVmSpace();
status_t status;
UINT32 length;
paddr_t oldTtPhyBase;
int i;
LosArchMmuInitMapping *kernelMap = NULL;
UINT32 kmallocLength;
/* 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, KERNEL_VMM_BASE,
(bssEndBoundary - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT);
if (status != ((bssEndBoundary - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
VM_ERR("unmap failed, status: %d", status);
return;
}
获取到g_kVmSpace;将kSpace->archMmu.virtTtb重新设置回g_firstPageTable,需要注意此时使用的ttb仍是OsSwitchTmpTTB函数中设置的tmpTtbase;然后调用LOS_ArchMmuUnmap函数将kSpace->archMmu页表中全部的页unmap;随后调用LOS_ArchMmuMap函数将以KERNEL_VMM_BASE虚拟内存地址为起始,到内核代码段起始地址为截至的虚拟内存空间映射至g_kVmSpace->archMmu中。由于没有完整的地址映射表,所以推测该部分内容与系统启动相关。
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);
if (status != (kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
VM_ERR("mmap failed, status: %d", status);
return;
}
/* 为各section在g_kVmSpace(kSpace)中分配region */
LOS_VmSpaceReserve(kSpace, kernelMap->size, kernelMap->virt);
}
随后将函数开始时在mmuKernelMappings[]中配置的各段(共三段:代码段、rodata段和ram数据段)映射到g_kVmSpace中,并在每一段映射结束后调用LOS_VmSpaceReserve函数在g_kVmSpace中创建对应的region。
kmallocLength = KERNEL_VMM_BASE + SYS_MEM_SIZE_DEFAULT - bssEndBoundary;
status = LOS_ArchMmuMap(&kSpace->archMmu, bssEndBoundary,
SYS_MEM_BASE + bssEndBoundary - KERNEL_VMM_BASE,
kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE);
if (status != (kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
VM_ERR("unmap failed, status: %d", status);
return;
}
LOS_VmSpaceReserve(kSpace, kmallocLength, bssEndBoundary);
随后计算系统可动态分配的内存空间长度,并进行页面化映射的处理,同样在动态内存区映射结束后调用LOS_VmSpaceReserve函数创建对应的region。
/* 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));
最后一步完成将页表寄存器中tmpTtbase重新切换成设置好的Ttb的操作。并释放原tmpTtbase在系统内存池中占用的内存空间。至此OsInitMappingStartUp函数的分析告一段落。
在OsSysMemInit函数中最后一步调用ShmInit函数对共享内存进行的初始化,此处不做过多的讲解,先做保留后续有机会再深入分析。