萬字解讀鴻蒙輕內核物理記憶體模組

摘要:本文首先了解了物理記憶體管理的結構體,接著閱讀了物理記憶體如何初始化,然後分析了物理記憶體的申請、釋放和查詢等操作介面的源程式碼。

本文分享自華為雲社區《鴻蒙輕內核A核源碼分析系列三 物理記憶體》,作者: zhushy。

物理記憶體(Physical memory)是指通過物理記憶體條而獲得的記憶體空間,相對應的概念是虛擬記憶體(Virtual memory)。虛擬記憶體使得應用進程認為它擁有一個連續完整的記憶體地址空間,而通常是通過虛擬記憶體和物理記憶體的映射對應著多個物理記憶體頁。本文我們先來熟悉下OpenHarmony鴻蒙輕內核提供的物理記憶體(Physical memory)管理模組。

本文中所涉及的源碼,以OpenHarmony LiteOS-A內核為例,均可以在開源站點//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    32struct 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;
}

總結

本文首先了解了物理記憶體管理的結構體,接著閱讀了物理記憶體如何初始化,然後分析了物理記憶體的申請、釋放和查詢等操作介面的源程式碼。後續也會陸續推出更多的分享文章,敬請期待,有任何問題、建議,都可以留言給我。謝謝。

 

點擊關注,第一時間了解華為雲新鮮技術~