鴻蒙輕內核A核源碼分析系列三物理內存之一
從本篇開始,我們分析下鴻蒙輕內核A核的內存管理部分,包括物理內存、虛擬內存、虛擬映射等部分。物理內存(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 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個內存頁。申請內存、釋放內存時會操作這些空閑內存頁節點鏈表,后文詳細分析。

1.3、物理內存伙伴位圖
上文提到伙伴算法,還需要了解下伙伴位圖。在伙伴算法中,每個鏈表的索引都對應一個位圖。 位圖的某位對應于兩個伙伴塊,為1就表示其中一塊忙,為0表示兩塊都閑或都在使用 。系統每次分配和回收伙伴塊時都要對它們的伙伴位 跟1進行異或運算 。所謂異或是指剛開始時,兩個伙伴塊都空閑,它們的伙伴位為0,如果其中一塊被使用,異或后得1;如果另一塊也被使用,異或后得0;如果前面一塊回收了異或后得1;如果另一塊也回收了異或后得0。位圖用于在釋放內存頁塊時,判斷兩塊內存是否屬于地址連續的伙伴內存塊。
在文件kernel/base/include/los_vm_phys.h中定義了2個比較重要的和伙伴位圖相關的宏,如下。⑴處的宏VM_ORDER_TO_PHYS(order)表示對應每個空閑鏈表都有一個位來標記伙伴內存塊。⑵處宏VM_PHYS_TO_ORDER(phys)把物理內存地址轉換為空閑鏈表索引。那么問題是,物理內存地址和索引有對應關系?物理地址已基于內存頁大小進行對齊。理論上這個值可大可小,不明白為什么這么設計?TODO。
- ⑴ #define VM_ORDER_TO_PHYS(order) (1 << (PAGE_SHIFT + (order)))
- ⑵ #define VM_PHYS_TO_ORDER(phys) (min(LOS_LowBitGet((phys) >> PAGE_SHIFT), VM_LIST_ORDER_MAX - 1))
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;
- }