鸿蒙轻内核A核源码分析系列四(2) 虚拟内存 原创 精华
鸿蒙轻内核A核源码分析系列四 虚拟内存
【本文正在参与优质创作者激励】
上一篇了解了物理内存,本文我们来熟悉下OpenHarmony
鸿蒙轻内核提供的虚拟内存(Virtual memory)管理模块。
本文中所涉及的源码,以OpenHarmony LiteOS-A
内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_a 获取。如果涉及开发板,则默认以hispark_taurus
为例。
我们首先了解了虚拟内存管理的结构体、相关宏定义,接着会分析内核虚拟地址空间和用户进程虚拟地址空间如何初始化,然后分析虚拟内存区间常用操作包含查找、申请和释放等,最后分析动态内存堆的申请、释放接口的源代码,并简单介绍下内存区间预留接口源代码。
1、 虚拟内存管理相关的结构体
在文件kernel/base/include/los_vm_map.h
中定义了进程地址空间结构体LosVmSpace
,进程地址区间结构体LosVmMapRegion
和进程地址区间范围结构体LosVmMapRange
。每个用户态进程会创建自己的进程空间,内核态会创建2个进程空间,分别g_kVmSpace
和g_vMallocSpace
。从进程空间申请的虚拟内存块使用进程区间LosVmMapRegion
来表示。每个进程空间维护一个红黑树来链接各个进程区间。
1.1 虚拟内存地址空间结构体LosVmSpace
typedef struct VmSpace {
LOS_DL_LIST node; /**< 地址空间双向链表 */
LosRbTree regionRbTree; /**< 地址区间的红黑树根节点 */
LosMux regionMux; /**< 地址区间的红黑树的互斥锁 */
VADDR_T base; /**< 地址空间开始地址 */
UINT32 size; /**< 地址空间大小 */
VADDR_T heapBase; /**< 地址空间的堆开始地址heapBase */
VADDR_T heapNow; /**< 地址空间的堆开始地址heapNow */
LosVmMapRegion *heap; /**< 地址空间的地址区间 */
VADDR_T mapBase; /**< 地址空间的映射区开始地址 */
UINT32 mapSize; /**< 地址空间的映射区大小 */
LosArchMmu archMmu; /**< 地址空间的MMU结构体 */
#ifdef LOSCFG_DRIVERS_TZDRIVER
VADDR_T codeStart; /**< 用户进程代码区开始地址 */
VADDR_T codeEnd; /**< 用户进程代码区结束地址 */
#endif
} LosVmSpace;
1.2 虚拟内存地址区间LosVmMapRegion
typedef struct VmMapRange {
VADDR_T base; /**< 虚拟内存地址区间开始地址 */
UINT32 size; /**< 虚拟内存地址区间大小 */
} LosVmMapRange;
......
struct VmMapRegion;
typedef struct VmMapRegion LosVmMapRegion;
......
struct VmMapRegion {
LosRbNode rbNode; /**< 地址区间红黑树节点 */
LosVmSpace *space; /**< 地址区间所在的地址空间 */
LOS_DL_LIST node; /**< 地址区间双向链表 */
LosVmMapRange range; /**< 地址区间地址范围 */
VM_OFFSET_T pgOff; /**< 地址区间页偏移 */
UINT32 regionFlags; /**< 地址区间标记: cow, user_wired */
UINT32 shmid; /**< 共享地址区间编号 */
UINT8 forkFlags; /**< 地址区间fork标记: COPY, ZERO, */
UINT8 regionType; /**< 地址区间类型: ANON, FILE, DEV */
union {
struct VmRegionFile {
unsigned int fileMagic;
struct file *file;
const LosVmFileOps *vmFOps;
} rf;
struct VmRegionAnon {
LOS_DL_LIST node; /**< 地址区间类型的双向链表 */
} ra;
struct VmRegionDev {
LOS_DL_LIST node; /**< 地址区间类型的双向链表 */
const LosVmFileOps *vmFOps;
} rd;
} unTypeData;
};
2、 虚拟内存相关的宏定义
文件kernel/base/include/los_vm_common.h
和kernel/base/include/los_vm_zone.h
定义了虚拟内存相关的宏。对于32位系统,虚拟进程空间大小为4GiB,OpenHarmony
鸿蒙轻内核当前支持32位系统。⑴和⑵定义了用户进程虚拟地址空间的开始地址和大小,⑶是用户虚拟进程空间的结束地址,接着定义的是用户虚拟进程空间的堆区、映射区的开始地址和大小。
/* user address space, defaults to below kernel space with a 16MB guard gap on either side */
#ifndef USER_ASPACE_BASE
⑴ #define USER_ASPACE_BASE ((vaddr_t)0x01000000UL)
#endif
#ifndef USER_ASPACE_SIZE
⑵ #define USER_ASPACE_SIZE ((vaddr_t)KERNEL_ASPACE_BASE - USER_ASPACE_BASE - 0x01000000UL)
#endif
⑶ #define USER_ASPACE_TOP_MAX ((vaddr_t)(USER_ASPACE_BASE + USER_ASPACE_SIZE))
#define USER_HEAP_BASE ((vaddr_t)(USER_ASPACE_TOP_MAX >> 2))
#define USER_MAP_BASE ((vaddr_t)(USER_ASPACE_TOP_MAX >> 1))
#define USER_MAP_SIZE ((vaddr_t)(USER_ASPACE_SIZE >> 3))
内核虚拟进程空间的宏定义如下,⑴处定义内核进程地址空间开始地址和大小,⑵处定义内核非缓存虚拟地址空间开始地址和大小,⑶处定义虚拟动态分配地址空间开始地址和大小,⑷处定义外设开始地址和大小,⑸处定义外设缓存区开始地址和大小,⑹处定义外设非缓存区开始地址和大小。
#ifdef LOSCFG_KERNEL_MMU
#ifdef LOSCFG_TEE_ENABLE
#define KERNEL_VADDR_BASE 0x41000000
#else
#define KERNEL_VADDR_BASE 0x40000000
#endif
#else
#define KERNEL_VADDR_BASE DDR_MEM_ADDR
#endif
#define KERNEL_VADDR_SIZE DDR_MEM_SIZE
#define SYS_MEM_BASE DDR_MEM_ADDR
#define SYS_MEM_END (SYS_MEM_BASE + SYS_MEM_SIZE_DEFAULT)
#define _U32_C(X) X##U
#define U32_C(X) _U32_C(X)
#define KERNEL_VMM_BASE U32_C(KERNEL_VADDR_BASE)
#define KERNEL_VMM_SIZE U32_C(KERNEL_VADDR_SIZE)
⑴ #define KERNEL_ASPACE_BASE KERNEL_VMM_BASE
#define KERNEL_ASPACE_SIZE KERNEL_VMM_SIZE
/* Uncached vmm aspace */
⑵ #define UNCACHED_VMM_BASE (KERNEL_VMM_BASE + KERNEL_VMM_SIZE)
#define UNCACHED_VMM_SIZE DDR_MEM_SIZE
⑶ #define VMALLOC_START (UNCACHED_VMM_BASE + UNCACHED_VMM_SIZE)
#define VMALLOC_SIZE 0x08000000
#ifdef LOSCFG_KERNEL_MMU
⑷ #define PERIPH_DEVICE_BASE (VMALLOC_START + VMALLOC_SIZE)
#define PERIPH_DEVICE_SIZE U32_C(PERIPH_PMM_SIZE)
⑸ #define PERIPH_CACHED_BASE (PERIPH_DEVICE_BASE + PERIPH_DEVICE_SIZE)
#define PERIPH_CACHED_SIZE U32_C(PERIPH_PMM_SIZE)
⑹ #define PERIPH_UNCACHED_BASE (PERIPH_CACHED_BASE + PERIPH_CACHED_SIZE)
#define PERIPH_UNCACHED_SIZE U32_C(PERIPH_PMM_SIZE)
#else
#define PERIPH_DEVICE_BASE PERIPH_PMM_BASE
#define PERIPH_DEVICE_SIZE U32_C(PERIPH_PMM_SIZE)
#define PERIPH_CACHED_BASE PERIPH_PMM_BASE
#define PERIPH_CACHED_SIZE U32_C(PERIPH_PMM_SIZE)
#define PERIPH_UNCACHED_BASE PERIPH_PMM_BASE
#define PERIPH_UNCACHED_SIZE U32_C(PERIPH_PMM_SIZE)
#endif
虚拟地址空间分布示意图如下:
3、进程地址空间初始化
虚拟进程空间分用户虚拟进程空间和内核虚拟进程空间,每个用户进程都会创建属于自己的进程空间。内核会初始化2个进程空间。下文详细介绍。
3.1 内核虚拟地址空间初始化
3.1.1 函数OsKSpaceInit
函数OsKSpaceInit()
初始化内核进程虚拟地址空间,⑴处的函数初始化虚拟空间链表互斥锁g_vmSpaceListMux
,在操作内核进程空间时需要持有该互斥锁。⑵处开始的函数2个函数OsKernVmSpaceInit
和OsVMallocSpaceInit
分别初始化内核进程虚拟空间g_kVmSpace
和内核动态分配进程空间g_vMallocSpace
。传入的第2个参数由函数OsGFirstTableGet()
获取,即g_firstPageTable
,这是内核的2个进程空间使用的一级页表基地址,大小为0x4000字节,后文在设置转化表基地址MMU virtTtb
时会使用。下文会详细分析这2个函数。
VOID OsKSpaceInit(VOID)
{
⑴ OsVmMapInit();
⑵ OsKernVmSpaceInit(&g_kVmSpace, OsGFirstTableGet());
OsVMallocSpaceInit(&g_vMallocSpace, OsGFirstTableGet());
}
3.1.2 函数OsKernVmSpaceInit
函数OsKernVmSpaceInit()
初始化内核进程虚拟地址空间,⑴处设置地址空间的开始地址和大小,⑵处设置地址空间映射区的开始地址和大小,对于内核虚拟地址空间g_kVmSpace
,这2个开始地址和大小是一样的。⑶处调用通用的地址空间初始化函数,后文分析此函数。
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);
}
3.1.3 函数OsVMallocSpaceInit
函数OsVMallocSpaceInit()
初始化内核堆虚拟空间,设置的虚拟地址空间和映射区地址空间的开始地址和大小也是一样的,代码和函数OsKernVmSpaceInit()
类似,不再赘述。
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);
}
3.2 用户进程虚拟地址空间初始化
3.2.1 函数OsCreateUserVmSpace
在创建进程时,会调用函数OsCreateUserVmSpace()
创建用户进程的虚拟地址空间。⑴为虚拟地址空间结构体申请内存。⑵申请一个内存页,并调用memset_s()
初始化为0,这个内存页虚拟地址会作为页表转换基地址TTB(translation table base,ttb),虚实映射的页表会保存在这个内存区域。在虚实映射章节,会讲述为什么申请4KiB大小内存。⑶处调用函数OsUserVmSpaceInit
初始化用户进程虚拟地址空间。⑷处获取虚拟地址对应的物理页结构体地址。如果初始化失败,则释放申请的内存。⑸处把物理页加入虚拟空间中的MMU的页表链表中,这个链表维护该进程空间映射的内存页。
LosVmSpace *OsCreateUserVmSpace(VOID)
{
BOOL retVal = FALSE;
⑴ LosVmSpace *space = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmSpace));
if (space == NULL) {
return NULL;
}
⑵ VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1);
if (ttb == NULL) {
(VOID)LOS_MemFree(m_aucSysMem0, space);
return NULL;
}
(VOID)memset_s(ttb, PAGE_SIZE, 0, PAGE_SIZE);
⑶ retVal = OsUserVmSpaceInit(space, ttb);
⑷ LosVmPage *vmPage = OsVmVaddrToPage(ttb);
if ((retVal == FALSE) || (vmPage == NULL)) {
(VOID)LOS_MemFree(m_aucSysMem0, space);
LOS_PhysPagesFreeContiguous(ttb, 1);
return NULL;
}
⑸ LOS_ListAdd(&space->archMmu.ptList, &(vmPage->node));
return space;
}
3.2.2 函数OsUserVmSpaceInit
函数OsUserVmSpaceInit
初始化用户进程虚拟地址空间,⑴处设置虚拟地址空间的开始地址和大小。⑵处设置虚拟空间的映射区的开始地址和大小,开始地址在虚拟空间开始地址的1/2处,大小为用户虚拟空间大小的1/8。⑶处设置虚拟空间的堆区,开始地址为虚拟空间开始地址的1/4处。
BOOL OsUserVmSpaceInit(LosVmSpace *vmSpace, VADDR_T *virtTtb)
{
⑴ vmSpace->base = USER_ASPACE_BASE;
vmSpace->size = USER_ASPACE_SIZE;
⑵ vmSpace->mapBase = USER_MAP_BASE;
vmSpace->mapSize = USER_MAP_SIZE;
⑶ vmSpace->heapBase = USER_HEAP_BASE;
vmSpace->heapNow = USER_HEAP_BASE;
vmSpace->heap = NULL;
#ifdef LOSCFG_DRIVERS_TZDRIVER
vmSpace->codeStart = 0;
vmSpace->codeEnd = 0;
#endif
return OsVmSpaceInitCommon(vmSpace, virtTtb);
}
3.3 虚拟地址空间初始化的通用函数
3.3.1 函数OsVmSpaceInitCommon
函数OsVmSpaceInitCommon
用于进程虚拟地址空间的通用部分的初始化,⑴处初始化地址空间的红黑树根节点。⑵处初始化地址空间的地址区间操作互斥锁。⑶处把新创建的地址空间挂在虚拟地址空间双向链表g_vmSpaceList
上。⑷处继续调用函数OsArchMmuInit()
完成地址空间MMU部分的初始化。
STATIC BOOL OsVmSpaceInitCommon(LosVmSpace *vmSpace, VADDR_T *virtTtb)
{
⑴ LOS_RbInitTree(&vmSpace->regionRbTree, OsRegionRbCmpKeyFn, OsRegionRbFreeFn, OsRegionRbGetKeyFn);
⑵ 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);
(VOID)LOS_MuxRelease(&g_vmSpaceListMux);
⑷ return OsArchMmuInit(&vmSpace->archMmu, virtTtb);
}
3.3.2 函数OsArchMmuInit
函数OsArchMmuInit()
用于初始化虚拟地址空间的MMU,MMU在后续系列会详细分析,此处快速了解一下即可。⑴处获取地址空间编号,如果获取失败则返回FALSE
。⑵初始化MMU互斥锁,如果初始化失败则返回FALSE
。⑶处初始化内存页双向链表。⑷处设置MMU的TTB虚拟地址。⑸处设置MMU的TTB物理地址,TTB虚拟地址基于内核虚拟地址空间开始地址的偏移(UINTPTR)virtTtb - KERNEL_ASPACE_BASE
加上物理地址就等于TTB物理地址。
BOOL OsArchMmuInit(LosArchMmu *archMmu, VADDR_T *virtTtb)
{
#ifdef LOSCFG_KERNEL_VM
⑴ if (OsAllocAsid(&archMmu->asid) != LOS_OK) {
VM_ERR("alloc arch mmu asid failed");
return FALSE;
}
#endif
⑵ status_t retval = LOS_MuxInit(&archMmu->mtx, NULL);
if (retval != LOS_OK) {
VM_ERR("Create mutex for arch mmu failed, status: %d", retval);
return FALSE;
}
⑶ LOS_ListInit(&archMmu->ptList);
⑷ archMmu->virtTtb = virtTtb;
⑸ archMmu->physTtb = (VADDR_T)(UINTPTR)virtTtb - KERNEL_ASPACE_BASE + SYS_MEM_BASE;
return TRUE;
}
4、虚拟地址区间常用操作
虚拟地址区间操作分为查找、申请、释放等操作。
4.1 函数LOS_RegionFind
⑴处的函数LOS_RegionFind
用于在进程虚拟地址空间内查找并返回指定虚拟地址对应的虚拟地址区间,两个传入参数分别是虚拟地址空间和虚拟内存地址。该函数有个兄弟函数LOS_RegionRangeFind()
,见⑶处代码,可以用于在进程空间内查找并返回指定地址范围对应的虚拟地址区间,三个传入参数分别指定指定进程空间、虚拟内存开始地址和地址长度(长度单位字节)。这2个函数都调用函数OsFindRegion()
实现地址区间的查找,⑵处的第3个参数为1的原因是地址区间是左闭右开区间,区间的结束地址会减1。下文会分析该函数的代码。
⑴ LosVmMapRegion *LOS_RegionFind(LosVmSpace *vmSpace, VADDR_T addr)
{
LosVmMapRegion *region = NULL;
(VOID)LOS_MuxAcquire(&vmSpace->regionMux);
⑵ region = OsFindRegion(&vmSpace->regionRbTree, addr, 1);
(VOID)LOS_MuxRelease(&vmSpace->regionMux);
return region;
}
⑶ LosVmMapRegion *LOS_RegionRangeFind(LosVmSpace *vmSpace, VADDR_T addr, size_t len)
{
LosVmMapRegion *region = NULL;
(VOID)LOS_MuxAcquire(&vmSpace->regionMux);
region = OsFindRegion(&vmSpace->regionRbTree, addr, len);
(VOID)LOS_MuxRelease(&vmSpace->regionMux);
return region;
}
【本文正在参与优质创作者激励】
liteos_a内存分配图太实用了,感谢分享
我们链接脚本中的text段等在KERNEL_ASPACE_BASE区间吗?
可以参考https://harmonyos.51cto.com/posts/9436中的 2.2 函数OsSetKSectionAtt 部分