鴻蒙輕內核A核源碼分析系列三 物理內存之二
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,然后執行⑵獲取伙伴內存頁節點,可以看出,申請的內存塊大于需求時,會把后半部分的高地址部分放入空閑鏈表,保留前半部分的低地址部分。⑶處的斷言確?;锇閮却骓摴濣c索引值是最大值,表示屬于空閑內存頁節點。⑷處調用函數把內存頁節點放入空閑鏈表。
- 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]鏈表上才有空閑節點。申請成功后,超過了應用需要的大小,需要進行拆分。把27個內存頁分為2份大小為26個內存頁的節點,第一份繼續拆分,第二份掛載到freeList[6]鏈表上。然后把第一份26個內存頁拆分為2個25個內存頁節點,第一份繼續拆分,第二份掛載到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()用于釋放指定數量的連續物理內存頁。⑴處根據物理內存頁獲取對應的物理內存地址。⑵處根據物理內存地址獲取空閑內存頁鏈表數組索引數值。⑶處獲取索引值對應的鏈表上的內存頁節點的內存頁數目。⑷處如果要釋放的內存頁數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()釋放內存頁到對應的空閑內存頁鏈表。內存頁塊釋放時,會在當前鏈表找地址連續的伙伴內存頁塊進行合并,然后去上一級鏈表上繼續查找是否存在連續的伙伴內存頁塊。⑴做傳入參數校驗。⑵處需要至少是倒數第二個鏈表,這樣內存頁節點可以和大索引鏈表上的節點合并。⑶處獲取內存頁對應的物理內存地址,然后后面會開始do-while循環,查找是否存在連續的內存頁節點。⑷處的VM_ORDER_TO_PHYS(order)計算出鏈表索引值對應的伙伴位圖,然后進行異或運算計算出伙伴內存頁的物理內存地址。⑸處物理地址轉換為內存頁結構體,進一步判斷:如果內存頁不存在或者不在空閑鏈表上,則跳出循環while循環。否則如果伙伴內存節點存在,則執行⑹把伙伴頁從鏈表上移除,然后索引值加1。⑺處鏈表索引加1,然后進行邏輯與計算得到物理內存地址。此時物理內存地址,和合并的兩塊內存頁塊地址連續。該內存地址在高一級的空閑鏈表上不一定存在,存在則繼續合并,不存在則退出循環。當索引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;
- }
總結
本文首先了解了物理內存管理的結構體,接著閱讀了物理內存如何初始化,然后分析了物理內存的申請、釋放和查詢等操作接口的源代碼。