万字解读鸿蒙轻内核物理内存模块

Posted 华为云开发者社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了万字解读鸿蒙轻内核物理内存模块相关的知识,希望对你有一定的参考价值。

摘要:本文首先了解了物理内存管理的结构体,接着阅读了物理内存如何初始化,然后分析了物理内存的申请、释放和查询等操作接口的源代码。

本文分享自华为云社区《鸿蒙轻内核A核源码分析系列三 物理内存》,作者: zhushy。

物理内存(Physical memory)是指通过物理内存条而获得的内存空间,相对应的概念是虚拟内存(Virtual memory)。虚拟内存使得应用进程认为它拥有一个连续完整的内存地址空间,而通常是通过虚拟内存和物理内存的映射对应着多个物理内存页。本文我们先来熟悉下OpenHarmony鸿蒙轻内核提供的物理内存(Physical memory)管理模块。

本文中所涉及的源码,以OpenHarmony LiteOS-A内核为例,均可以在开源站点kernel_liteos_a: LiteOS kernel for embedded devices with rich resources | 适用于资源较丰富嵌入式设备的LiteOS内核 获取。如果涉及开发板,则默认以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链表分五类,由枚举类型enum OsLruList定义。代码较简单,读者自行阅读代码即可。

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,该值由枚举类型enum OsPageFlags定义。⑶处设置内存页的引用计数为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 物理内存页初始化函数VOID OsVmPageStartup(VOID)

了解上述几个内部函数后,我们正式开始阅读物理内存页初始化函数VOID OsVmPageStartup(VOID)。系统在启动时,该函数用于初始化物理内存,把物理内存段划分割为为物理内存页。该函数被kernel/base/vm/los_vm_boot.c中的UINT32 OsSysMemInit(VOID)调用,进一步被文件platform/los_config.c中的INT32 OsMain(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()把虚拟地址转为物理地址,该函数在虚实映射部分会详细讲述。⑵处遍历物理内存段,如果物理内存地址处于物理内存段的地址范围,则可以返回该物理地址对应的物理页结构体地址。

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;
}

总结

本文首先了解了物理内存管理的结构体,接着阅读了物理内存如何初始化,然后分析了物理内存的申请、释放和查询等操作接口的源代码。后续也会陆续推出更多的分享文章,敬请期待,有任何问题、建议,都可以留言给我。谢谢。

点击关注,第一时间了解华为云新鲜技术~

以上是关于万字解读鸿蒙轻内核物理内存模块的主要内容,如果未能解决你的问题,请参考以下文章

从结构体内存池初始化到申请释放,详细解读鸿蒙轻内核的动态内存管理

深层剖析鸿蒙轻内核M核的动态内存如何支持多段非连续性内存

解读鸿蒙轻内核的监控器:异常钩子函数

从五大结构体,带你掌握鸿蒙轻内核动态内存Dynamic Memory

鸿蒙轻内核A核源码分析系列五 虚实映射基础概念

鸿蒙轻内核的得力助手:带你掌握4种内存调试方法