前言
当前HarmonyOS开源的源码适用于小型IOT设备,从开源仓代码分析,内核使用的是LiteOS,但与现有的 开源LiteOS有着较大的不同。其中与开源版本LiteOS最大的不同之处当数内存管理模块。HarmonyOS支持Arm Cortex-A7 并且可以通过使能MMU对虚拟内存进行管理。该系列文章将对HarmonyOS内存模块进行详细的代码剖析讲解, 该篇文章的分析内容主要侧重与内存模块中分配过程所涉及的函数,后续将会对内存初始化,ELF加载等过程进行 详细的解读。
正文
该文分析的内容类比与Linux,从kmalloc和vmalloc入手,分析内核中内存分析的流程。 Linux中kmalloc可类比于HarmonyOS中的LOS_KernelMalloc,vmalloc可类比于HarmonyOS中的LOS_VMalloc函数。 本文将从两个函数入手,分析页面的分配管理方法以及涉及到的相关结构体。
LOS_KernelMalloc
函数原型: VOID *LOS_KernelMalloc(UINT32 size)
LOS_KernelMalloc可类比于Linux内核的kmalloc申请连续的物理地址内存空间,但对比kmalloc的接受传参,LOS_KernelMalloc函数缺少了flags入参,该入参在Linux中表示分配出的页内存空间的不同用途。如GFP_KERNEL表示分配出的内核内存页,用于内核进程;GFP_USER表示分配出的页内存将用于用户态进程。下文将从LOS_KernelMalloc入手进行逐层分析HMOS的内存分配策略和实现方式。
内核主要使用LOS_KernelMalloc接口申请内核态内存空间。LOS_KernelMalloc的分配策略大体与Linux的策略相似:分配大块内存时(超过4K)调用LOS_PhysPagesAllocContiguous函数以页为单位分配出一个连续的内存空间;否则调用LOS_MemAlloc函数(与开源LiteOS函数接口相同),该函数的内存分配策略为bestfit,从内存池中找到合适的大小进行分配。
函数源码:
VOID *LOS_KernelMalloc(UINT32 size)
{
VOID *ptr = NULL;
if (OsMemLargeAlloc(size)) {
ptr = LOS_PhysPagesAllocContiguous(ROUNDUP(size, PAGE_SIZE) >> PAGE_SHIFT);
} else {
ptr = LOS_MemAlloc(OS_SYS_MEM_ADDR, size);
}
return ptr;
}
接下来主要分析页面分配的流程,LOS_KernelMalloc获取page的关键调用路径:
LOS_KernelMalloc
->LOS_PhysPagesAllocContiguous
-> OsVmPhysPagesGet
->OsVmPhysPagesAlloc
接下来将对涉及的函数进行逐个分析。
LOS_PhysPagesAllocContiguous
VOID *LOS_PhysPagesAllocContiguous(size_t nPages)
{
LosVmPage *page = NULL;
if (nPages == 0) {
return NULL;
}
page = OsVmPhysPagesGet(nPages);
if (page == NULL) {
return NULL;
}
return OsVmPageToVaddr(page);
}
该函数主要干了两件事情,首先调用OsVmPhysPagesGet函数获取指定数量的页内存,再通过调用OsVmPageToVaddr函数将页内存转换成虚拟地址。
OsVmPhysPagesGet
STATIC LosVmPage *OsVmPhysPagesGet(size_t nPages)
{
UINT32 intSave;
struct VmPhysSeg *seg = NULL;
LosVmPage *page = NULL;
UINT32 segID;
if (nPages == 0) {
return NULL;
}
for (segID = 0; segID < g_vmPhysSegNum; segID++) {
seg = &g_vmPhysSeg[segID];
LOS_SpinLockSave(&seg->freeListLock, &intSave);
page = OsVmPhysPagesAlloc(seg, nPages);
if (page != NULL) {
/* the first page of continuous physical addresses holds refCounts */
LOS_AtomicSet(&page->refCounts, 0);
page->nPages = nPages;
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
return page;
}
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
}
return NULL;
}
在HMOS的内存管理机制中引入了VmPhysSeg结构体,该结构体用于对系统段内存的管理,VmPhysSeg的结构体如所示:
typedef struct VmPhysSeg {
PADDR_T start; /* The start of physical memory area */
size_t size; /* The size of physical memory area */
LosVmPage *pageBase; /* The first page address of this area */
SPIN_LOCK_S freeListLock; /* The buddy list spinlock */
struct VmFreeList freeList[VM_LIST_ORDER_MAX]; /* The free pages in the buddy list */
SPIN_LOCK_S lruLock;
size_t lruSize[VM_NR_LRU_LISTS];
LOS_DL_LIST lruList[VM_NR_LRU_LISTS];
} LosVmPhysSeg;
HMOS的seg结构体用于段页式内存管理中对段的描述。支持将系统所有可用的内存地址空间分割成多个段,再将段分为不同数量的页挂载到freeList列表中。
start成员变量表示该seg结构体管理的物理内存起始地址;size表示该区域的大小;pageBase指向该块内存区域起始的页内存结构体;freeList是结构体数组,使用伙伴算法管理内存页。VM_LIST_ORDER_MAX最大值为9,基于伙伴内存的算法可判断出freeList[0]中应该存放的是2^0数量大小的连续也空间,最大2^8表示支持最大256个连续页空间的内存,即管理256*4k大小的空间。
从页分配算法来说,OsVmPhysPagesGet函数遍历了所有的seg,然后调用OsVmPhysPagesAlloc函数从能够满足分配npage数量大小的连续页内存的seg结构体中获取到page结构体指针。
OsVmPhysPagesAlloc
LosVmPage *OsVmPhysPagesAlloc(struct VmPhysSeg *seg, size_t nPages)
{
struct VmFreeList *list = NULL;
LosVmPage *page = NULL;
UINT32 order;
UINT32 newOrder;
if ((seg == NULL) || (nPages == 0)) {
return NULL;
}
order = OsVmPagesToOrder(nPages);
if (order < VM_LIST_ORDER_MAX) {
for (newOrder = order; newOrder < VM_LIST_ORDER_MAX; newOrder++) {
list = &seg->freeList[newOrder];
if (LOS_ListEmpty(&list->node)) {
continue;
}
page = LOS_DL_LIST_ENTRY(LOS_DL_LIST_FIRST(&list->node), LosVmPage, node);
goto DONE;
}
}
return NULL;
DONE:
OsVmPhysFreeListDelUnsafe(page);
OsVmPhysPagesSpiltUnsafe(page, order, newOrder);
return page;
}
首先通过OsVmPagesToOrder函数计算出nPages对应所需最小的order值,然后以order值为初始值向上便利当前seg结构体中freeList[order]上是否有空闲的连续order大小的连续page内存空间。最后通过OsVmPhysFreeListDelUnsafe函数将该page从该seg的freeList[order]数组中删除;再通过OsVmPagesSplitUnsafe函数将当前order的连续page内存页做适当的split操作,并将未使用的page挂在到对应order大小的freeList[order]数组上,这样就完成了一次page的分配过程。
LOS_VMalloc
LOS_VMalloc可类比于Linux的vmalloc函数,用于申请连续的虚拟内存地址空间。但物理页面是不连续的。一般情况下,只有硬件设备才需要物理地址连续的内存,因为硬件设备往往存在于MMU之外,根本不了解虚拟地址;但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用vmalloc(),例如当模块被动态加载到内核当中时,就把模块装载到由vmalloc()分配 的内存上。
Kmalloc与Vmalloc的却别:kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续;kmalloc分配的物理地址与虚拟地址只有一个PAGE—OFFSET偏移,不需要为地址段修改页表,Vmalloc类函数地址完全虚拟,每次分配都需要对页表进行设置,效率相较于Kmalloc低。
在详细解析Vmalloc之前,首先要介绍space(虚拟内存空间)和region(线性区描述符)的概念。一个虚拟内存空间(space)中存在多个先行区描述符(region),Linux内核中对这些虚拟存储区域的组织方式有两种,一种是采用双循环链表(regions),还有一种是采用红黑树的结构。两个结构体的定义如下:
VmSpace
typedef struct VmSpace {
LOS_DL_LIST node; /**< vm space dl list */
LOS_DL_LIST regions; /**< region dl list */
LosRbTree regionRbTree; /**< region red-black tree root */
LosMux regionMux; /**< region list mutex lock */
VADDR_T base; /**< vm space base addr */
UINT32 size; /**< vm space size */
VADDR_T heapBase; /**< vm space heap base address */
VADDR_T heapNow; /**< vm space heap base now */
LosVmMapRegion *heap; /**< heap region */
VADDR_T mapBase; /**< vm space mapping area base */
UINT32 mapSize; /**< vm space mapping area size */
LosArchMmu archMmu; /**< vm mapping physical memory */
VADDR_T codeStart; /**< user process code area start */
VADDR_T codeEnd; /**< user process code area end */
} LosVmSpace;
结构体中成员变量node
用于连接多个虚拟内存空间;regions
即为连接space中regions的双向链表,regionRbTree为采用红黑树结构表示的space中的regions,regionRbTree与regions使用两种方式表实一个相同的regions集合
HMOS一共定义了三类虚拟内存空间:g_kVmSpace,g_vMallocSpace和各进程的VmSpace。在创建每一个进程的时候都会创建ProcessCB->vmSpace结构体记录虚拟内存空间;g_kVmSpace和g_vMallocSpace是在系统初始化时调用OsKSpaceInit(VOID)
函数完成内核相关内存初始化。
LosVmMapRegion
struct VmMapRegion {
LosRbNode rbNode; /**< region red-black tree node */
LosVmSpace *space;
LOS_DL_LIST node; /**< region dl list */
LosVmMapRange range; /**< region address range */
VM_OFFSET_T pgOff; /**< region page offset to file */
UINT32 regionFlags; /**< region flags: cow, user_wired */
UINT32 shmid; /**< shmid about shared region */
UINT8 protectFlags; /**< vm region protect flags: PROT_READ, PROT_WRITE, */
UINT8 forkFlags; /**< vm space fork flags: COPY, ZERO, */
UINT8 regionType; /**< vm region type: ANON, FILE, DEV */
union {
struct VmRegionFile {
unsigned int fileMagic;
struct file *file;
const LosVmFileOps *vmFOps;
} rf;
struct VmRegionAnon {
LOS_DL_LIST node; /**< region LosVmPage list */
} ra;
struct VmRegionDev {
LOS_DL_LIST node; /**< region LosVmPage list */
const LosVmFileOps *vmFOps;
} rd;
} unTypeData;
};
HMOS中的VmMapRegion与Linux中的vm_area_struct结构体相似,称为线性区描述符,它标识了一个线性地址区间,每一个线性区由多个page组成。
函数原型: VOID *LOS_VMalloc(size_t size)
函数源码:
VOID *LOS_VMalloc(size_t size)
{
LosVmSpace *space = &g_vMallocSpace;
LosVmMapRegion *region = NULL;
size_t sizeCount;
size_t count;
LosVmPage *vmPage = NULL;
VADDR_T va;
PADDR_T pa;
STATUS_T ret;
size = LOS_Align(size, PAGE_SIZE);
if ((size == 0) || (size > space->size)) {
return NULL;
}
sizeCount = size >> PAGE_SHIFT;
LOS_DL_LIST_HEAD(pageList);
(VOID)LOS_MuxAcquire(&space->regionMux);
count = LOS_PhysPagesAlloc(sizeCount, &pageList);
if (count < sizeCount) {
VM_ERR("failed to allocate enough pages (ask %zu, got %zu)", sizeCount, count);
goto ERROR;
}
/* allocate a region and put it in the aspace list */
region = LOS_RegionAlloc(space, 0, size, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE, 0);
if (region == NULL) {
VM_ERR("alloc region failed, size = %x", size);
goto ERROR;
}
va = region->range.base;
while ((vmPage = LOS_ListRemoveHeadType(&pageList, LosVmPage, node))) {
pa = vmPage->physAddr;
LOS_AtomicInc(&vmPage->refCounts);
ret = LOS_ArchMmuMap(&space->archMmu, va, pa, 1, region->regionFlags);
if (ret != 1) {
VM_ERR("LOS_ArchMmuMap failed!, err;%d", ret);
}
va += PAGE_SIZE;
}
(VOID)LOS_MuxRelease(&space->regionMux);
return (VOID *)(UINTPTR)region->range.base;
ERROR:
(VOID)LOS_PhysPagesFree(&pageList);
(VOID)LOS_MuxRelease(&space->regionMux);
return NULL;
}
首先调用LOS_PhysPagesAlloc函数分配出sizeCount数量的页面内存,并将其加入到pageList列表中;然后调用LOS_RegionAlloc函数分配/创建一个region结构体;最后在while循环中从pageList中逐个将page取下来并通过LOS_ArchMmuMap函数将list上的全部页面的物理地址(pa)映射到虚拟地址(va)上,每次循环映射一个页面地址空间,va以PAGE_SIZE大小进行增长。下面会对上述涉及到的函数进行逐步解析。
LOS_PhysPagesAlloc
函数源码:
size_t LOS_PhysPagesAlloc(size_t nPages, LOS_DL_LIST *list)
{
LosVmPage *page = NULL;
size_t count = 0;
if ((list == NULL) || (nPages == 0)) {
return 0;
}
while (nPages--) {
page = OsVmPhysPagesGet(ONE_PAGE);
if (page == NULL) {
break;
}
LOS_ListTailInsert(list, &page->node);
count++;
}
return count;
}
nPages决定while循环次数,在每次循环中都会通过调用OsVmPhysPagesGet函数获取到一个页面内存,该函数在上文中已有分析。在获取到一个单独页之后将其添加到list列表中,从而实现了在list列表上添加了nPages个物理地址不连续的页面。
LOS_RegionAlloc
函数源码:
LosVmMapRegion *LOS_RegionAlloc(LosVmSpace *vmSpace, VADDR_T vaddr, size_t len, UINT32 regionFlags, VM_OFFSET_T pgoff)
{
VADDR_T rstVaddr;
LosVmMapRegion *newRegion = NULL;
BOOL isInsertSucceed = FALSE;
/**
* If addr is NULL, then the kernel chooses the address at which to create the mapping;
* this is the most portable method of creating a new mapping. If addr is not NULL,
* then the kernel takes it as where to place the mapping;
*/
(VOID)LOS_MuxAcquire(&vmSpace->regionMux);
if (vaddr == 0) {
rstVaddr = OsAllocRange(vmSpace, len);
} else {
/* if it is already mmapped here, we unmmap it */
rstVaddr = OsAllocSpecificRange(vmSpace, vaddr, len);
if (rstVaddr == 0) {
VM_ERR("alloc specific range va: %#x, len: %#x failed", vaddr, len);
goto OUT;
}
}
if (rstVaddr == 0) {
goto OUT;
}
newRegion = OsCreateRegion(rstVaddr, len, regionFlags, pgoff);
if (newRegion == NULL) {
goto OUT;
}
newRegion->space = vmSpace;
isInsertSucceed = OsInsertRegion(&vmSpace->regionRbTree, newRegion);
if (isInsertSucceed == FALSE) {
(VOID)LOS_MemFree(m_aucSysMem0, newRegion);
newRegion = NULL;
}
OUT:
(VOID)LOS_MuxRelease(&vmSpace->regionMux);
return newRegion;
}
首先判断vaddr是否为0,为0表示没有指定的虚拟内存地址,调用OsAllocRange函数初始化虚拟地址。在成功获取到正确的rstVaddr地址后调用OsCreateRegion创建LosVmMapRegion结构体,并通过OsInsertRegion函数将创建的region结构体作为树节点添加到LosVmSpace结构体的RegionRbTree红黑树上。
LOS_RegionAlloc函数传参中由LOS_VMalloc函数传入的vmSpace指针指向g_vMallocSpace全局变量,表示LOS_VMalloc函数获取到的虚拟内存空间全部映射到g_vMallocSpace的space空间中。
LOS_ArchMmuMap
函数源码:
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;
}
这段代码涉及ARM Cortex-A MMU底层工作逻辑,所以首先需要了解经典的ARM MMU工作结构,再进一步对源码进行解读,MMU工作原理示意图如下:
HMOS使用的是MMU二级映射,第一级为PGD使用4k空间存放第二级PTE的地址。一级的PGD地址是1M对齐,二级PTE为4K地址对齐。在一级表中有两种的地址有两种形式:一种是直接映射1MB的地址空间(称为section模式),另一种是映射到对应二级页表的起始地址(称为table模式)。
回过头来看LOS_ArchMmuMap函数源码逻辑,在while循环中有两个分支,在进入第一个if分支前做了如下三个判断:vaddr是否为0x100000地址对齐,paddr是否为0x100000地址对齐,以及count是否大于256。count表实4k页面的数量,在PGD中每一个PGD最多能够映射256个页面,所以第一个分支的含义是,如果映射的起始地址是1M对齐,且映射的内存大小超过1M,则执行第一个分支,进行section模式的直接地址映射。
在第一个分支中调用了函数OsMapSection()
函数,源码逻辑如下:
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;
}
其中最关键的一步是OsSavePte1函数。该函数首先通过OsGetPtePtr函数获取到第一级4k内存空间中va对应的table ptr地址。获取到地址后将paddr赋值到该地址对应的列表内存中,即完成了1M section的地址映射过程,即这里使用的是第一种的直接映射1MB内存空间的方式。
但是在LOS_VMalloc函数中第一个分支是不会执行到的,LOS_ArchMmuMap函数在LOS_VMalloc中被调用时传参count值始终为1,所以在该场景下始终执行的时else分支。源码如下所示:
} 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);
}
}
该分支中首先调用OsGetPte1()函数获得L1中的页表项,然后进行了两个判断:首先判断该页表项中的内容是否是合法的,然后判断该页表项中的内容是否是page模式(上文提及到与page模式相对的是section模式)。判断页表项是否合法可理解为该页表项是否已被初始化,若没有初始化,则首先调用OsMapL1PTE函数对页表项内容进行初始化:
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);
}
总体的流程是获取二级页表的base地址pte2Base,然后基于flags变量设置pte2Base值,最后将设置好的页表项值填入到pte1Ptr指向的内存。
在完成页表项内容初始化后调用OsMapL2PageContinous()函数完成二级页表页表项的配置:
STATIC UINT32 OsMapL2PageContinous(PTE_T pte1, UINT32 flags, VADDR_T *vaddr, PADDR_T *paddr, UINT32 *count)
{
PTE_T *pte2BasePtr = NULL;
UINT32 archFlags;
UINT32 saveCounts;
pte2BasePtr = OsGetPte2BasePtr(pte1);
if (pte2BasePtr == NULL) {
LOS_Panic("%s %d, pte1 %#x error\n", __FUNCTION__, __LINE__, pte1);
}
/* compute the arch flags for L2 4K pages */
archFlags = OsCvtPte2FlagsToAttrs(flags);
saveCounts = OsSavePte2Continuous(pte2BasePtr, OsGetPte2Index(*vaddr), *paddr | archFlags, *count);
*paddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
*vaddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
*count -= saveCounts;
return saveCounts;
}
首先调用OsGetPte2BasePtr()函数获取pte2Base基址指针,其次调用OsCvtPte2FlagsToAttrs()函数对页面属性进行配置,最后调用OsSavePte2Continuous()函数进行二级页表项的填充:
STATIC INLINE UINT32 OsSavePte2Continuous(PTE_T *pte2BasePtr, UINT32 index, PTE_T pte2, UINT32 count)
{
UINT32 saveCounts = 0;
if (count == 0) {
return 0;
}
DMB;
do {
pte2BasePtr[index++] = pte2;
count--;
pte2 += MMU_DESCRIPTOR_L2_SMALL_SIZE;
saveCounts++;
} while ((count != 0) && (index != MMU_DESCRIPTOR_L2_NUMBERS_PER_L1));
DSB;
return saveCounts;
}
至此LOS_ArchMmuMap()函数完成了对页面的映射过程,同样LOS_VMalloc也完成了内存的申请。