萬字解讀鴻蒙輕核心實體記憶體模組

華為雲開發者社群發表於2021-11-05
摘要:本文首先了解了實體記憶體管理的結構體,接著閱讀了實體記憶體如何初始化,然後分析了實體記憶體的申請、釋放和查詢等操作介面的原始碼。

本文分享自華為雲社群《鴻蒙輕核心A核原始碼分析系列三 實體記憶體》,作者: zhushy。

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

本文中所涉及的原始碼,以OpenHarmony LiteOS-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    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;
}

總結

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

 

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

相關文章