摘要:本文首先了解了物理内存管理的结构体,接着阅读了物理内存如何初始化,然后分析了物理内存的申请、释放和查询等操作接口的源代码。
本文分享自华为云社区《鸿蒙轻内核A核源码分析系列三 物理内存》,作者: zhushy。
物理内存(Physicalmemory)是指通过物理内存条而获得的内存空间,相对应的概念是虚拟内存(Virtual memory)。虚拟内存使得应用进程认为它拥有一个连续完整的内存地址空间,而通常是通过虚拟内存和物理内存的映射对应着多个物理内存页。本文我们先来熟悉下 OpenHarmony 鸿蒙轻内核提供的物理内存(Physical memory)管理模块。
本文中所涉及的源码,以 OpenHarmonyLiteOS-A 内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_a 获取。如果涉及开发板,则默认以 hispark_taurus
为例。
我们首先了解了物理内存管理的结构体,接着阅读了物理内存如何初始化,然后分析了物理内存的申请、释放和查询等操作接口的源代码。
1、物理内存结构体介绍
1.1、物理内存页 LosVmPage
鸿蒙轻内核 A 核的物理内存采用了段页式管理,每个物理内存段被分割为物理内存页。在头文件 kernel/base/include/los_vm_page.h 中定义了物理内存页结构体,以及内存页数组 g_vmPageArray 及数组大小 g_vmPageArraySize。物理内存页结构体 LosVmPage 可以和物理内存页一一对应,也可以对应多个连续的内存页,此时使用 nPages 指定内存页的数量。
typedef struct VmPage {
LOS_DL_LIST node; /**< 物理内存页节点,挂在VmFreeList空闲内存页链表上 */
PADDR_T physAddr; /**< 物理内存页内存开始地址*/
Atomic refCounts; /**< 物理内存页引用计数 */
UINT32 flags; /**< 物理内存页标记 */
UINT8 order; /**< 物理内存页所在的链表数组的索引,总共有9个链表 */
UINT8 segID; /**< 物理内存页所在的物理内存段的编号 */
UINT16 nPages; /**< 连续物理内存页的数量 */
} LosVmPage;
extern LosVmPage *g_vmPageArray;
extern size_t g_vmPageArraySize;
复制代码
在文件 kernel\base\include\los_vm_common.h 中定义了内存页的大小、掩码和逻辑位移值,可以看出每个内存页的大小为 4KiB。
#ifndef PAGE_SIZE
#define PAGE_SIZE (0x1000U)
#endif
#define PAGE_MASK (~(PAGE_SIZE - 1))
#define PAGE_SHIFT (12)
复制代码
1.2、物理内存段 LosVmPhysSeg
在文件 kernel/base/include/los_vm_phys.h 中定义了物理内存段 LosVmPhysSeg 等几个结构体。该文件的部分代码如下所示。⑴处的宏是物理内存伙伴算法中空闲内存页节点链表数组的大小,VM_PHYS_SEG_MAX 表示系统支持的物理内存段的数量。⑵处的结构体用于伙伴算法中空闲内存页节点链表数组的元素类型,除了记录双向链表,还维护链表上节点数量。⑶就是我们要介绍的物理内存段,包含开始地址,大小,内存页基地址,空闲内存页节点链表数组,LRU 链表数组等成员。
⑴ #define VM_LIST_ORDER_MAX 9
#define VM_PHYS_SEG_MAX 32
⑵ struct VmFreeList {
LOS_DL_LIST node; // 空闲物理内存页节点
UINT32 listCnt; // 空闲物理内存页节点数量
};
⑶ typedef struct VmPhysSeg {
PADDR_T start; /* 物理内存段的开始地址 */
size_t size; /* 物理内存段的大小,bytes */
LosVmPage *pageBase; /* 物理内存段第一个物理内存页结构体地址 */
SPIN_LOCK_S freeListLock; /* 伙伴算法双向链表自旋锁 */
struct VmFreeList freeList[VM_LIST_ORDER_MAX]; /* 空闲物理内存页的伙伴双向链表 */
SPIN_LOCK_S lruLock; /* LRU双向链表自旋锁 */
size_t lruSize[VM_NR_LRU_LISTS]; /* LRU大小 */
LOS_DL_LIST lruList[VM_NR_LRU_LISTS];/* LRU双向链表 */
} LosVmPhysSeg;
struct VmPhysArea {
PADDR_T start; // 物理内存区开始地址
size_t size; // 物理内存区大小
};
复制代码
在 kernel/base/vm/los_vm_phys.c 文件中定义了物理内存区数组 g_physArea[],如下代码所示,其中
SYS_MEM_BASE 为 DDR_MEM_ADDR 的宏名称,DDR_MEM_ADDR 和 SYS_MEM_SIZE_DEFAULT 定义在文件./device/hisilicon/hispark_taurus/sdk_liteos/board/target_config.h 中,表示开发板相关的物理内存地址和大小。
STATIC struct VmPhysArea g_physArea[] = {
{
.start = SYS_MEM_BASE,
.size = SYS_MEM_SIZE_DEFAULT,
},
};
复制代码
看下物理内存区 VmPhysArea 和物理内存段的 LosVmPhysSeg 区别,前者信息教少,主要记录开始地址和大小,为一块物理内存的最简单描述;后者除了物理内存块开始地址和大小,还维护物理页开始地址,空闲物理页伙伴链表,LRU 链表,相应的自旋锁等信息。
上面提到了伙伴算法,先看下伙伴算法的示意图,如下。每个物理内存段都分割为一个一个的内存页,空闲的内存页挂载在空闲内存页节点链表上。共有 9 个空闲内存页节点链表,这些链表组成链表数组。第一个链表上的内存页节点大小为 1 个内存页,第二个链表上的内存页节点大小为 2 个内存页,第三个链表上的内存页节点大小为 4 个内存页,依次下去,第 9 个链表上的内存页节点大小为 2^8 个内存页。申请内存、释放内存时会操作这些空闲内存页节点链表,后文详细分析。
2、物理内存管理模块初始化
本节主要讲解物理内存管理模块是如何初始化的,核心函数是 OsVmPageStartup()。在讲解之前,会先看下物理内存初始化过程中的一些内部函数。
2.1 物理内存管理初始化内部函数
2.1.1 函数 OsVmPhysSegCreate
函数 OsVmPhysSegCreate 用于把指定的一个物理内存区 VmPhysArea 转换为物理内存段 LosVmPhysSeg。传入的 2 个参数分别为物理内存区的开始内存地址和大小。⑴处表示系统支持的物理内存段的数量为 32 个,超过则转换错误。⑵处从物理内存段全局数组 g_vmPhysSeg 中获取一个可用的物理内存段。⑶处如果物理内存段 seg 为数组 g_vmPhysSeg 中的第一个元素,则跳过循环体直接执行⑸设置物理内存段的开始地址和大小。如果不为第一个元素,并且前一个物理内存段的开始地址在要转换的物理内存段的结束地址之后,则执行⑷处代码覆盖前一个物理内存段。在配置物理内存区的时候,需要注意这里的影响。
STATIC INT32 OsVmPhysSegCreate(paddr_t start, size_t size)
{
struct VmPhysSeg *seg = NULL;
⑴ if (g_vmPhysSegNum >= VM_PHYS_SEG_MAX) {
return -1;
}
⑵ seg = &g_vmPhysSeg[g_vmPhysSegNum++];
⑶ for (; (seg > g_vmPhysSeg) && ((seg - 1)->start > (start + size)); seg--) {
⑷ *seg = *(seg - 1);
}
⑸ seg->start = start;
seg->size = size;
return 0;
}
复制代码
函数 OsVmPhysSegAdd 调用上述函数 OsVmPhysSegCreate 依次把配置的多个物理内存区一一进行转换,对于开发板 hispark_taurus 只配置了一块物理内存区域。
VOID OsVmPhysSegAdd(VOID)
{
INT32 i, ret;
LOS_ASSERT(g_vmPhysSegNum < VM_PHYS_SEG_MAX);
for (i = 0; i < (sizeof(g_physArea) / sizeof(g_physArea[0])); i++) {
ret = OsVmPhysSegCreate(g_physArea[i].start, g_physArea[i].size);
if (ret != 0) {
VM_ERR("create phys seg failed");
}
}
}
复制代码
2.1.2 函数 OsVmPhysInit
函数 OsVmPhysInit 继续初始化物理内存段信息。⑴处循环物理内存段数组,这里不是循环 32 次,而是多少个物理段就循环遍历多少次。遍历到每一个物理内存段,然后执行⑵设置当前物理内存段的第一个物理页结构体的地址,每一个物理内存页都有自己的结构体 LosVmPage,这些结构体维护在通过 malloc 内存堆申请的 g_vmPageArray 数组里,后文会详细讲述。⑶处 seg->size>> PAGE_SHIFT 计算当前内存段对于的内存页数量,然后更新 nPages,这是后续物理内存段第一个内存页对应的的物理内存页结构体在数组 g_vmPageArray 中索引。⑷处开始的函数 OsVmPhysFreeListInit 和 OsVmPhysLruInit 初始化伙伴双向链表和 LRU 双向链表,后续分析这 2 个函数。
VOID OsVmPhysInit(VOID)
{
struct VmPhysSeg *seg = NULL;
UINT32 nPages = 0;
int i;
for (i = 0; i < g_vmPhysSegNum; i++) {
⑴ seg = &g_vmPhysSeg[i];
⑵ seg->pageBase = &g_vmPageArray[nPages];
⑶ nPages += seg->size >> PAGE_SHIFT;
⑷ OsVmPhysFreeListInit(seg);
OsVmPhysLruInit(seg);
}
}
复制代码
2.1.3 函数 OsVmPhysFreeListInit
每个物理内存段使用 9 个空闲物理内存页节点链表来维护空闲物理内存页。OsVmPhysFreeListInit 函数用于初始化指定物理内存段的空闲物理内存页节点链表。操作前后需要开启、关闭空闲链表自旋锁。⑴处遍历空闲物理内存页节点链表数组,然后执行⑵初始化每个双向链表。⑶处把每个链表中的空闲物理内存页的数量初始化为 0。
STATIC INLINE VOID OsVmPhysFreeListInit(struct VmPhysSeg *seg)
{
int i;
UINT32 intSave;
struct VmFreeList *list = NULL;
LOS_SpinInit(&seg->freeListLock);
LOS_SpinLockSave(&seg->freeListLock, &intSave);
for (i = 0; i < VM_LIST_ORDER_MAX; i++) {
⑴ list = &seg->freeList[i];
⑵ LOS_ListInit(&list->node);
⑶ list->listCnt = 0;
}
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
}
复制代码
2.1.4 函数 OsVmPhysLruInit
和上个函数类似,函数 OsVmPhysLruInit 初始化指定物理内存段的 LRU 链表数组中的 LRU 链表。LRU 链表分五类,由枚举类型 enumOsLruList 定义。代码较简单,读者自行阅读代码即可。
STATIC VOID OsVmPhysLruInit(struct VmPhysSeg *seg)
{
INT32 i;
UINT32 intSave;
LOS_SpinInit(&seg->lruLock);
LOS_SpinLockSave(&seg->lruLock, &intSave);
for (i = 0; i < VM_NR_LRU_LISTS; i++) {
seg->lruSize[i] = 0;
LOS_ListInit(&seg->lruList[i]);
}
LOS_SpinUnlockRestore(&seg->lruLock, intSave);
}
复制代码
2.1.5 函数 OsVmPageInit
函数 OsVmPageInit 用于初始化物理内存页的初始值,该函数需要 3 个参数,分别是物理内存页结构体地址,物理内存页的开始地址,物理内存段编号。⑴处初始化内存页的链表节点,这个链表节点通常会挂载在伙伴算法的空闲内存页节点链表上。⑵处设置内存页标记为空闲内存页
FILE_PAGE_FREE,该值由枚举类型 enumOsPageFlags 定义。⑶处设置内存页的引用计数为 0。⑷处设置内存页的开始地址。⑸处设置内存页所在的物理内存段的编号。⑹处设置内存页顺序 order
初始值,此时不属于任何空闲内存页节点链表。⑺处设置内存页的 nPages 数值为 0。⑻处的宏 VMPAGEINIT 调用函数 OsVmPageInit 并自动增加内存页结构体 page 地址和内存页 pa 地址。
STATIC VOID OsVmPageInit(LosVmPage *page, paddr_t pa, UINT8 segID)
{
⑴ LOS_ListInit(&page->node);
⑵ page->flags = FILE_PAGE_FREE;
⑶ LOS_AtomicSet(&page->refCounts, 0);
⑷ page->physAddr = pa;
⑸ page->segID = segID;
⑹ page->order = VM_LIST_ORDER_MAX;
⑺ page->nPages = 0;
}
...
#define VMPAGEINIT(page, pa, segID) do { \
⑻ OsVmPageInit(page, pa, segID); \
(page)++; \
(pa) += PAGE_SIZE; \
} while (0)
复制代码
2.2 物理内存页初始化函数 VOIDOsVmPageStartup(VOID)
了解上述几个内部函数后,我们正式开始阅读物理内存页初始化函数 VOIDOsVmPageStartup(VOID)。系统在启动时,该函数用于初始化物理内存,把物理内存段划分割为为物理内存页。该函数被 kernel/base/vm/los_vm_boot.c 中的 UINT32OsSysMemInit(VOID)调用,进一步被文件 platform/los_config.c 中的 INT32OsMain(VOID)函数调用。下面详细分析下函数的代码。
⑴处的 g_vmBootMemBase 初始值为(UINTPTR)&__bss_end,表示系统可用内存在 bss 段之后;ROUNDUP 用于内存向上对齐。函数 OsVmPhysAreaSizeAdjust()用于调整物理区的开始地址和大小。⑵处的 OsVmPhysPageNumGet()计算物理内存段可以划分多少物理内存页,此行代码重新计算物理内存页数目,此时每个物理页对应一个物理页结构体,相应结构体也占用内存空间。⑶处计算物理页结构体数组的大小,数组的每个元素对应每个物理页结构体 LosVmPage。接下来一行调用函数 OsVmBootMemAlloc 为物理页结构体数组 g_vmPageArray 申请内存空间,申请的内存空间从地址 g_vmBootMemBase 截取指定的长度。⑷处再次调用函数 OsVmPhysAreaSizeAdjust()用于调整物理内存区的开始地址和大小,确保基于内存页对齐。⑸处调用函数 OsVmPhysSegAdd()转换为物理内存段,⑹处调用 OsVmPhysInit 函数初始化物理内存段的空闲物理内存页节点链表和 LRU 链表。上文分析过这几个内部函数。⑺处遍历每个物理内存段,获取遍历到的物理内存段的总页数 nPage。⑻处为提升初始化物理内存页的性能,把页数分为 8 份,count 为每份的内存页的数目,left 为等分为 8 份后剩余的内存页数。⑼处循环初始化物理内存页,⑽处初始化剩余的物理内存页。⑾处的函数 OsVmPageOrderListInit 把物理内存页插入到空闲内存页节点链表,该函数进一步调用 OsVmPhysPagesFreeContiguous 函数,后续再分析该函数。初始化完成后,物理内存段上的内存页都挂载到空闲内存页节点链表上了。
VOID OsVmPageStartup(VOID)
{
struct VmPhysSeg *seg = NULL;
LosVmPage *page = NULL;
paddr_t pa;
UINT32 nPage;
INT32 segID;
⑴ OsVmPhysAreaSizeAdjust(ROUNDUP((g_vmBootMemBase - KERNEL_ASPACE_BASE), PAGE_SIZE));
/*
* Pages getting from OsVmPhysPageNumGet() interface here contain the memory
* struct LosVmPage occupied, which satisfies the equation:
* nPage * sizeof(LosVmPage) + nPage * PAGE_SIZE = OsVmPhysPageNumGet() * PAGE_SIZE.
*/
⑵ nPage = OsVmPhysPageNumGet() * PAGE_SIZE / (sizeof(LosVmPage) + PAGE_SIZE);
⑶ g_vmPageArraySize = nPage * sizeof(LosVmPage);
g_vmPageArray = (LosVmPage *)OsVmBootMemAlloc(g_vmPageArraySize);
⑷ OsVmPhysAreaSizeAdjust(ROUNDUP(g_vmPageArraySize, PAGE_SIZE));
⑸ OsVmPhysSegAdd();
⑹ OsVmPhysInit();
for (segID = 0; segID < g_vmPhysSegNum; segID++) {
⑺ seg = &g_vmPhysSeg[segID];
nPage = seg->size >> PAGE_SHIFT;
⑻ UINT32 count = nPage >> 3; /* 3: 2 ^ 3, nPage / 8, cycle count */
UINT32 left = nPage & 0x7; /* 0x7: nPage % 8, left page */
⑼ for (page = seg->pageBase, pa = seg->start; count > 0; count--) {
/* note: process large amount of data, optimize performance */
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
VMPAGEINIT(page, pa, segID);
}
for (; left > 0; left--) {
⑽ VMPAGEINIT(page, pa, segID);
}
⑾ OsVmPageOrderListInit(seg->pageBase, nPage);
}
}
复制代码
3、物理内存管理模块接口
学习过物理内存初始化后,接下来我们会分析物理内存管理模块的接口函数,包含申请、释放、查询等功能接口。
3.1 申请物理内存页接口
3.1.1 申请物理内存页接口介绍
申请物理内存页的接口有 3 个,分别用于满足不同的申请需求。LOS_PhysPagesAllocContiguous 函数的传入参数为要申请物理内存页的数目,返回值为申请到的物理内存页对应的内核虚拟地址空间中的虚拟内存地址。⑴处调用函数 OsVmPhysPagesGet 申请指定数目的物理内存页,然后⑵处调用函数 OsVmPageToVaddr 转换为内核虚拟内存地址。函数 LOS_PhysPageAlloc 申请一个物理内存页,返回值为申请到的物理页对应的物理页结构体地址。代码比较简单,见⑶处,调用函数 OsVmPageToVaddr 传入 ONE_PAGE 参数申请 1 个物理内存页。函数 LOS_PhysPagesAlloc 用于申请 nPages 个物理内存页,并挂在双向链表 list 上,返回值为实际申请到的物理页数目。⑷处循环调用函数 OsVmPhysPagesGet()申请一个物理内存页,如果申请成功不为空,则插入到双向链表,申请成功的物理页的数目加 1;如果申请失败则跳出循环。⑹返回实际申请到的物理页的数目。
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);
}
......
LosVmPage *LOS_PhysPageAlloc(VOID)
{
⑶ return OsVmPhysPagesGet(ONE_PAGE);
}
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;
}
复制代码
3.1.2 申请物理内存页内部接口实现
3 个内存页申请函数都调用了函数 OsVmPhysPagesGet,下文会详细分析申请物理内存页内部接口实现。
3.1.2.1 函数 OsVmPhysPagesGet
函数 OsVmPhysPagesGet 用于申请指定数量的物理内存页,返回值为物理内存页结构体地址。⑴处遍历物理内存段数组,对遍历到的物理内存段执行⑵处代码,调用函数 OsVmPhysPagesAlloc()从指定的内存段中申请指定数目的物理内存页。如果申请成功,则执行⑶把内存页的引用计数初始化为 0,根据注释,如果是连续的内存页,则第一个内存页持有引用计数数值。接下来以后更新内存页的数量,并返回申请到的内存页的结构体地址;如果申请失败则继续循环申请或者返回 NULL。
STATIC LosVmPage *OsVmPhysPagesGet(size_t nPages)
{
UINT32 intSave;
struct VmPhysSeg *seg = NULL;
LosVmPage *page = NULL;
UINT32 segID;
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;
}
复制代码
3.1.2.2 函数 OsVmPhysPagesAlloc
从上文的介绍,我们知道物理内存段包含一个空闲内存页节点链表数组,数组大小为 9。数组中的每个链表上的内存页节点的大小等于 2 的幂次方个内存页,例如:第 0 个链表上挂载的空闲内存节点的大小为 2 的 0 次方个内存页,即 1 个内存页;第 8 个链表上挂载的内存页节点的大小为 2 的 8 次方个内存页,即 256 个内存页。相同大小的内存块挂在同一个链表上进行管理。
分析函数 OsVmPhysPagesAlloc 之前,先看下函数 OsVmPagesToOrder,该函数根据指定的物理页的数目计算属于空闲内存页节点链表数组中的第几个双向链表。当 nPages 为最小 1 时,order 取值为 0;当为 2 时,order 取值 1…等于取底为 2 的对数 Log2(nPages)。
#define VM_ORDER_TO_PAGES(order) (1 << (order))
......
UINT32 OsVmPagesToOrder(size_t nPages)
{
UINT32 order;
for (order = 0; VM_ORDER_TO_PAGES(order) < nPages; order++);
return order;
}
复制代码
继续分析下函数 OsVmPhysPagesAlloc(),该函数基于传入参数从指定的内存段申请指定数目的内存页。⑴处调用的函数上文已经讲述,根据内存页数目计算出链表数组索引值。如果索引值小于链表最大索引值 VM_LIST_ORDER_MAX,则执行⑵从小内存页节点向大内存页节点循环各个双向链表。⑶处获取双向链表,如果空闲链表为空则继续循环;如果不为空,则执行⑷获取链表上的空闲内存页结构体。
如果根据内存页数计算出的数组索引值大于等于链表最大索引值 VM_LIST_ORDER_MAX,说明空闲链表上并没有这么大块的内存页节点,需要从物理内存段上申请,需要执行⑸调用函数 OsVmPhysLargeAlloc()申请大的内存页。如果申请不到内存页则申请失败,返回 NULL;如果申请到合适的内存页,则继续执行后续 DONE 标签代码。这些代码从空闲链表中删除,拆分,多余的空闲内存页插入空闲链表等,后文继续分析调用的这些函数。先看下这些参数的实际传入参数,order
为要申请的内存页对应的链表数组索引,newOrder 为实际申请的内存页对应的链表数组索引。⑹处的 for 循环条件中,&page[nPages]为需要申请的内存页结构体的结束地址,&tmp[1<< newOrder]
表示伙伴算法中空闲内存页节点链表上的内存块的结束地址。这里为啥使用 for 循环呢,上面申请内存时,应该申请了多个内存节点拼接起来了。看下⑺处的函数的传入参数,&page[nPages]为需要申请的内存页结构体的结束地址,往后的部分被拆分放入空闲链表。(1<< min(order, newOrder))表示实际申请的内存页的数目。
STATIC LosVmPage *OsVmPhysPagesAlloc(struct VmPhysSeg *seg, size_t nPages)
{
struct VmFreeList *list = NULL;
LosVmPage *page = NULL;
LosVmPage *tmp = NULL;
UINT32 order;
UINT32 newOrder;
⑴ 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;
}
} else {
newOrder = VM_LIST_ORDER_MAX - 1;
⑸ page = OsVmPhysLargeAlloc(seg, nPages);
if (page != NULL) {
goto DONE;
}
}
return NULL;
DONE:
for (tmp = page; tmp < &page[nPages]; tmp = &tmp[1 << newOrder]) {
⑹ OsVmPhysFreeListDelUnsafe(tmp);
}
OsVmPhysPagesSpiltUnsafe(page, order, newOrder);
⑺ OsVmRecycleExtraPages(&page[nPages], nPages, ROUNDUP(nPages, (1 << min(order, newOrder))));
return page;
}
复制代码
3.1.2.3 函数 OsVmPhysLargeAlloc
当执行到这个函数时,说明空闲链表上的单个内存页节点的大小已经不能满足要求,超过了第 9 个链表上的内存页节点的大小了。⑴处计算需要申请的内存大小。⑵从最大的链表上进行遍历每一个内存页节点。⑶根据每个内存页的开始内存地址,计算需要的内存的结束地址,如果超过内存段的大小,则继续遍历下一个内存页节点。
⑷处此时 paStart 表示当前内存页的结束地址,接下来 paStart>= paEnd 表示当前内存页的大小满足申请的需求;paStart< seg->start 和 paStart >= (seg->start + seg->size)发生溢出错误,内存页结束地址不在内存段的地址范围内。⑸处表示当前内存页的下一个内存页结构体,如果该结构体不在空闲链表上,则 break 跳出循环。如果在空闲链表上,表示连续的空闲内存页会拼接起来,满足大内存申请的需要。⑹表示一个或者多个连续的内存页的大小满足申请需求。
STATIC LosVmPage *OsVmPhysLargeAlloc(struct VmPhysSeg *seg, size_t nPages)
{
struct VmFreeList *list = NULL;
LosVmPage *page = NULL;
LosVmPage *tmp = NULL;
PADDR_T paStart;
PADDR_T paEnd;
⑴ size_t size = nPages << PAGE_SHIFT;
⑵ list = &seg->freeList[VM_LIST_ORDER_MAX - 1];
LOS_DL_LIST_FOR_EACH_ENTRY(page, &list->node, LosVmPage, node) {
⑶ paStart = page->physAddr;
paEnd = paStart + size;
if (paEnd > (seg->start + seg->size)) {
continue;
}
for (;;) {
⑷ paStart += PAGE_SIZE << (VM_LIST_ORDER_MAX - 1);
if ((paStart >= paEnd) || (paStart < seg->start) ||
(paStart >= (seg->start + seg->size))) {
break;
}
⑸ tmp = &seg->pageBase[(paStart - seg->start) >> PAGE_SHIFT];
if (tmp->order != (VM_LIST_ORDER_MAX - 1)) {
break;
}
}
⑹ if (paStart >= paEnd) {
return page;
}
}
return NULL;
}
复制代码
3.1.2.4 函数 OsVmPhysFreeListDelUnsafe 和 OsVmPhysFreeListAddUnsafe
内部函数 OsVmPhysFreeListDelUnsafe 用于从空闲内存页节点链表上删除一个内存页节点,名称中有 Unsafe 字样,是因为函数体内并没有对链表操作加自旋锁,安全性由外部调用函数保证。⑴处进行校验,确保内存段和空闲链表索引符合要求。⑵处获取内存段和空闲链表,⑶处空闲链表上内存页节点数目减 1,并把内存块从空闲链表上删除。⑷处设置内存页的 order 索引值为最大值来标记非空闲内存页。
STATIC VOID OsVmPhysFreeListDelUnsafe(LosVmPage *page)
{
struct VmPhysSeg *seg = NULL;
struct VmFreeList *list = NULL;
⑴ if ((page->segID >= VM_PHYS_SEG_MAX) || (page->order >= VM_LIST_ORDER_MAX)) {
LOS_Panic("The page segment id(%u) or order(%u) is invalid\n", page->segID, page->order);
}
⑵ seg = &g_vmPhysSeg[page->segID];
list = &seg->freeList[page->order];
⑶ list->listCnt--;
LOS_ListDelete(&page->node);
⑷ page->order = VM_LIST_ORDER_MAX;
}
复制代码
和空闲链表上删除对应的函数是空闲链表上插入空闲内存页节点函数 OsVmPhysFreeListAddUnsafe。⑴处更新内存页的要挂载的空闲链表的索引值,然后获取内存页所在的内存段 seg,并获取索引值对应的空闲链表。执行⑵把空闲内存页节点插入到空闲链表并更新节点数目。
STATIC VOID OsVmPhysFreeListAddUnsafe(LosVmPage *page, UINT8 order)
{
struct VmPhysSeg *seg = NULL;
struct VmFreeList *list = NULL;
if (page->segID >= VM_PHYS_SEG_MAX) {
LOS_Panic("The page segment id(%d) is invalid\n", page->segID);
}
⑴ page->order = order;
seg = &g_vmPhysSeg[page->segID];
list = &seg->freeList[order];
⑵ LOS_ListTailInsert(&list->node, &page->node);
list->listCnt++;
}
复制代码
3.1.2.5 函数 OsVmPhysPagesSpiltUnsafe
函数 OsVmPhysPagesSpiltUnsafe 用于分割内存块,参数中 oldOrder 表示需要申请的内存页节点对应的链表索引,newOrder 表示实际申请的内存页节点对应的链表索引。如果索引值相等,则不需要拆分,不会执行 for 循环块的代码。由于伙伴算法中的链表数组中元素的特点,即每个链表中的内存页节点的大小等于 2 的幂次方个内存页。在拆分时,依次从高索引 newOrder 往低索引 oldOrder 遍历,拆分一个内存页节点作为空闲内存页节点挂载到对应的空闲链表上。⑴处开始循环从高索引到低索引,索引值减 1,然后执行⑵获取伙伴内存页节点,可以看出,申请的内存块大于需求时,会把后半部分的高地址部分放入空闲链表,保留前半部分的低地址部分。⑶处的断言确保伙伴内存页节点索引值是最大值,表示属于空闲内存页节点。⑷处调用函数把内存页节点放入空闲链表。
STATIC VOID OsVmPhysPagesSpiltUnsafe(LosVmPage *page, UINT8 oldOrder, UINT8 newOrder)
{
UINT32 order;
LosVmPage *buddyPage = NULL;
for (order = newOrder; order > oldOrder;) {
⑴ order--;
⑵ buddyPage = &page[VM_ORDER_TO_PAGES(order)];
⑶ LOS_ASSERT(buddyPage->order == VM_LIST_ORDER_MAX);
⑷ OsVmPhysFreeListAddUnsafe(buddyPage, order);
}
}
复制代码
这里有必要放这一张图,直观演示一下。假如我们需要申请 8 个内存页大小的内存节点,但是只有 freeList[7]链表上才有空闲节点。申请成功后,超过了应用需要的大小,需要进行拆分。把 2^7 个内存页分为 2 份大小为 2^6 个内存页的节点,第一份继续拆分,第二份挂载到 freeList[6]链表上。然后把第一份 2^6 个内存页拆分为 2 个 2^5 个内存页节点,第一份继续拆分,第二份挂载到 freeList[5]链表上。依次进行下去,最后拆分为 2 份 2^3 个内存页大小的内存页节点,第一份作为实际申请的内存页返回,第二份挂载到 freeList[3]链表上。如下图红色部分所示。
另外,函数 OsVmRecycleExtraPages 会调用 OsVmPhysPagesFreeContiguous 来回收申请的多余的内存页,后文再分析。
3.2 释放物理内存页接口
3.2.1 释放物理内存页接口介绍
和申请物理内存页接口相对应着,释放物理内存页的接口有 3 个,分别用于满足不同的释放内存页需求。函数 LOS_PhysPagesFreeContiguous 的传入参数为要释放物理页对应的内核虚拟地址空间中的虚拟内存地址和内存页数目。⑴处调用函数 OsVmVaddrToPage 把虚拟内存地址转换为物理内存页结构体地址,然后⑵处把内存页的连续内存页数目设置为 0。⑶处调用函数 OsVmPhysPagesFreeContiguous()释放物理内存页。函数 LOS_PhysPageFree 用于释放一个物理内存页,传入参数为要释放的物理页对应的物理页结构体地址。⑷处对引用计数自减,当小于等于 0,表示没有其他引用时才进一步执行释放操作。该函数同样会调用函数 OsVmPhysPagesFreeContiguous()释放物理内存页。函数 LOS_PhysPagesFree 用于释放挂在双向链表上的多个物理内存页,返回值为实际释放的物理页数目。⑸处遍历内存页双向链表,从链表上移除要释放的内存页节点。⑹处代码和释放一个内存页的函数代码相同。⑺处计算遍历的内存页的数目,函数最后会返回该值。
VOID LOS_PhysPagesFreeContiguous(VOID *ptr, size_t nPages)
{
UINT32 intSave;
struct VmPhysSeg *seg = NULL;
LosVmPage *page = NULL;
if (ptr == NULL) {
return;
}
⑴ page = OsVmVaddrToPage(ptr);
if (page == NULL) {
VM_ERR("vm page of ptr(%#x) is null", ptr);
return;
}
⑵ page->nPages = 0;
seg = &g_vmPhysSeg[page->segID];
LOS_SpinLockSave(&seg->freeListLock, &intSave);
⑶ OsVmPhysPagesFreeContiguous(page, nPages);
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
}
......
VOID LOS_PhysPageFree(LosVmPage *page)
{
UINT32 intSave;
struct VmPhysSeg *seg = NULL;
if (page == NULL) {
return;
}
⑷ if (LOS_AtomicDecRet(&page->refCounts) <= 0) {
seg = &g_vmPhysSeg[page->segID];
LOS_SpinLockSave(&seg->freeListLock, &intSave);
OsVmPhysPagesFreeContiguous(page, ONE_PAGE);
LOS_AtomicSet(&page->refCounts, 0);
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
}
}
······
size_t LOS_PhysPagesFree(LOS_DL_LIST *list)
{
UINT32 intSave;
LosVmPage *page = NULL;
LosVmPage *nPage = NULL;
LosVmPhysSeg *seg = NULL;
size_t count = 0;
if (list == NULL) {
return 0;
}
LOS_DL_LIST_FOR_EACH_ENTRY_SAFE(page, nPage, list, LosVmPage, node) {
⑸ LOS_ListDelete(&page->node);
⑹ if (LOS_AtomicDecRet(&page->refCounts) <= 0) {
seg = &g_vmPhysSeg[page->segID];
LOS_SpinLockSave(&seg->freeListLock, &intSave);
OsVmPhysPagesFreeContiguous(page, ONE_PAGE);
LOS_AtomicSet(&page->refCounts, 0);
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
}
⑺ count++;
}
return count;
}
复制代码
3.2.2 释放物理内存页内部接口实现
3.2.2.1 函数 OsVmVaddrToPage
函数 OsVmVaddrToPage 把虚拟内存地址转换为物理页结构体地址。⑴处调用函数 LOS_PaddrQuery()把虚拟地址转为物理地址,该函数在虚实映射部分会详细讲述。⑵处遍历物理内存段,如果物理内存地址处于物理内存段的地址范围,则可以返回该物理地址对应的物理页结构体地址。
⑹处根据内存页数量计算对应的链表索引,根据索引值计算链表上内存页节点的大小。⑺处调用函数 OsVmPhysPagesFree()释放指定链表上的内存页,然后更新内存页数量和内存页结构体地址。
LosVmPage *OsVmVaddrToPage(VOID *ptr)
{
struct VmPhysSeg *seg = NULL;
⑴ PADDR_T pa = LOS_PaddrQuery(ptr);
UINT32 segID;
for (segID = 0; segID < g_vmPhysSegNum; segID++) {
seg = &g_vmPhysSeg[segID];
⑵ if ((pa >= seg->start) && (pa < (seg->start + seg->size))) {
return seg->pageBase + ((pa - seg->start) >> PAGE_SHIFT);
}
}
return NULL;
}
复制代码
3.2.2.2 函数OsVmPhysPagesFreeContiguous
函数OsVmPhysPagesFreeContiguous()
用于释放指定数量的连续物理内存页。⑴处根据物理内存页获取对应的物理内存地址。⑵处根据物理内存地址获取空闲内存页链表数组索引数值(TODO 为什么物理内存地址和索引有对应关系?),⑶处获取索引值对应的链表上的内存页节点的内存页数目。⑷处如果要释放的内存页数nPages
小于当前链表上的内存页节点的数目,则跳出循环执行⑹处代码,去释放到小索引的双向链表上。⑸处调用函数OsVmPhysPagesFree()
释放指定链表上的内存页,然后更新内存页数量和内存页结构体地址。
⑹处根据内存页数量计算对应的链表索引,根据索引值计算链表上内存页节点的大小。⑺处调用函数OsVmPhysPagesFree()
释放指定链表上的内存页,然后更新内存页数量和内存页结构体地址。
VOID OsVmPhysPagesFreeContiguous(LosVmPage *page, size_t nPages)
{
paddr_t pa;
UINT32 order;
size_t n;
while (TRUE) {
⑴ pa = VM_PAGE_TO_PHYS(page);
⑵ order = VM_PHYS_TO_ORDER(pa);
⑶ n = VM_ORDER_TO_PAGES(order);
⑷ if (n > nPages) {
break;
}
⑸ OsVmPhysPagesFree(page, order);
nPages -= n;
page += n;
}
while (nPages > 0) {
⑹ order = LOS_HighBitGet(nPages);
n = VM_ORDER_TO_PAGES(order);
⑺ OsVmPhysPagesFree(page, order);
nPages -= n;
page += n;
}
}
复制代码
3.2.2.3 函数 OsVmPhysPagesFree
函数 OsVmPhysPagesFree()释放内存页到对应的空闲内存页链表。⑴做传入参数校验。⑵处需要至少是倒数第二个链表,这样内存页节点可以和大索引链表上的节点合并。⑶处获取内存页对应的物理内存地址。⑷处的 VM_ORDER_TO_PHYS(order)计算出链表索引值对应的物理地址,然后进行异或运算计算出伙伴页的物理内存地址。⑸处物理地址转换为内存页结构体,进一步判断如果内存页不存在或者不在空闲链表上,则跳出循环 while 循环。否则执行⑹把伙伴页从链表上移除,然后索引值加 1。⑺处更新物理地址及其对齐的内存页(TODO 没有看懂)。当索引 order 为 8,要插入到最后一个链表上时,则直接执行⑻插入内存页到链表上。
VOID OsVmPhysPagesFree(LosVmPage *page, UINT8 order)
{
paddr_t pa;
LosVmPage *buddyPage = NULL;
⑴ if ((page == NULL) || (order >= VM_LIST_ORDER_MAX)) {
return;
}
⑵ if (order < VM_LIST_ORDER_MAX - 1) {
⑶ pa = VM_PAGE_TO_PHYS(page);
do {
⑷ pa ^= VM_ORDER_TO_PHYS(order);
⑸ buddyPage = OsVmPhysToPage(pa, page->segID);
if ((buddyPage == NULL) || (buddyPage->order != order)) {
break;
}
⑹ OsVmPhysFreeListDel(buddyPage);
order++;
⑺ pa &= ~(VM_ORDER_TO_PHYS(order) - 1);
page = OsVmPhysToPage(pa, page->segID);
} while (order < VM_LIST_ORDER_MAX - 1);
}
⑻ OsVmPhysFreeListAdd(page, order);
}
复制代码
3.3 查询物理页地址接口
3.3.1 函数 LOS_VmPageGet()
函数 LOS_VmPageGet 用于根据物理内存地址参数计算对应的物理内存页结构体地址。⑴处遍历物理内存段,调用函数 OsVmPhysToPage 根据物理内存地址和内存段编号计算物理内存页结构体,该函数后文再分析。⑵处如果获取的物理内存页结构体不为空,则跳出循环,返回物理内存页结构体指针。
LosVmPage *LOS_VmPageGet(PADDR_T paddr)
{
INT32 segID;
LosVmPage *page = NULL;
for (segID = 0; segID < g_vmPhysSegNum; segID++) {
⑴ page = OsVmPhysToPage(paddr, segID);
⑵ if (page != NULL) {
break;
}
}
return page;
}
复制代码
继续看下函数 OsVmPhysToPage 的代码。⑴处如果参数传入的物理内存地址不在指定的物理内存段的地址范围之内则返回 NULL。⑵处计算物理内存地址相对内存段开始地址的偏移值。⑶处根据偏移值计算出偏移的内存页的数目,然后返回物理内存地址对应的物理页结构体的地址。
LosVmPage *OsVmPhysToPage(paddr_t pa, UINT8 segID)
{
struct VmPhysSeg *seg = NULL;
paddr_t offset;
if (segID >= VM_PHYS_SEG_MAX) {
LOS_Panic("The page segment id(%d) is invalid\n", segID);
}
seg = &g_vmPhysSeg[segID];
⑴ if ((pa < seg->start) || (pa >= (seg->start + seg->size))) {
return NULL;
}
⑵ offset = pa - seg->start;
⑶ return (seg->pageBase + (offset >> PAGE_SHIFT));
}
复制代码
3.3.2 函数 LOS_PaddrToKVaddr
函数 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);
}
}
return (VADDR_T *)(UINTPTR)(paddr - SYS_MEM_BASE + KERNEL_ASPACE_BASE);
}
复制代码
3.4 其他函数
3.4.1 函数 OsPhysSharePageCopy
函数 OsPhysSharePageCopy 用于复制共享内存页。 ⑴处进行参数校验, ⑵处获取老内存页,⑶处获取内存段。⑷处如果老内存页引用计数为 1,则把老物理内存地址直接赋值给新物理内存地址。⑸处如果内存页有多个引用,则先转化为虚拟内存地址,然后执行⑹进行内存页的内容复制。⑺刷新新老内存页的引用计数。
VOID OsPhysSharePageCopy(PADDR_T oldPaddr, PADDR_T *newPaddr, LosVmPage *newPage)
{
UINT32 intSave;
LosVmPage *oldPage = NULL;
VOID *newMem = NULL;
VOID *oldMem = NULL;
LosVmPhysSeg *seg = NULL;
⑴ if ((newPage == NULL) || (newPaddr == NULL)) {
VM_ERR("new Page invalid");
return;
}
⑵ oldPage = LOS_VmPageGet(oldPaddr);
if (oldPage == NULL) {
VM_ERR("invalid oldPaddr %p", oldPaddr);
return;
}
⑶ seg = &g_vmPhysSeg[oldPage->segID];
LOS_SpinLockSave(&seg->freeListLock, &intSave);
⑷ if (LOS_AtomicRead(&oldPage->refCounts) == 1) {
*newPaddr = oldPaddr;
} else {
⑸ newMem = LOS_PaddrToKVaddr(*newPaddr);
oldMem = LOS_PaddrToKVaddr(oldPaddr);
if ((newMem == NULL) || (oldMem == NULL)) {
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
return;
}
⑹ if (memcpy_s(newMem, PAGE_SIZE, oldMem, PAGE_SIZE) != EOK) {
VM_ERR("memcpy_s failed");
}
⑺ LOS_AtomicInc(&newPage->refCounts);
LOS_AtomicDec(&oldPage->refCounts);
}
LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
return;
}
复制代码
总结
本文首先了解了物理内存管理的结构体,接着阅读了物理内存如何初始化,然后分析了物理内存的申请、释放和查询等操作接口的源代码。后续也会陆续推出更多的分享文章,敬请期待,有任何问题、建议,都可以留言给我。谢谢。
点击关注,第一时间了解华为云新鲜技术~
评论