圖解內存頁面遷移技術

1. 概述

頁面遷移(page migrate)最早是爲 NUMA 系統提供一種將進程頁面遷移到指定內存節點的能力用來提升訪問性能。後來在內核中廣泛被使用,如內存規整、CMA、內存 hotplug 等。

頁面遷移對上層應用業務來說是不可感知的,因爲其遷移的是物理頁面,而應用只訪問的是虛擬內存。內核遷移完成後,更新修改對應頁表指向遷移後的頁面即可。當然了這裏說的不可感知是指業務不太關注,也不需要做對應修改。實際上有些場景發生頁面遷移是業務性能是有影響的,下面會詳細描述。

2. 典型場景

我們列舉 2 個內核中發生頁面遷移的典型場景。

2.1 NUMA Balancing 引起的頁面遷移

在典型 NUMA 中,存在多個 node, 本地 CPU 訪問本地 node 節點對應的 memory 性能會快一些。

Linux 的 NUMA 自動均衡機制會嘗試將內存遷移到正在訪問它的 CPU 節點所在的 node。如下圖所示, CPU24 ~ CPU47 訪問不是本地 node 對應的 memory,性能會比較慢,系統會將其遷移到本地 node 對應的 memory 以提升訪問性能。

遷移後如下圖:

2.2 內存碎片整理

系統使用一段時候後,由於內存碎片的原因,較難滿足連續內存需求,如果需要分配連續大塊內存,需要進行內存規整以形成大塊連續內存,頁面遷移是內存碎片整理的基礎。

3. 實現分析

3.1 遷移模式

內核中通過接口 migrate_pages 實現頁而遷移, 分爲 3 個模式。

fn44ob

3.2 實現流程

內核文檔有描述這個 API 是怎麼工作的。不過這個描述着實是不太友好, 不容易在腦海形成畫面。

我們通過結合代碼實現,把這個轉化爲流程圖:

總結一下,頁面遷移過程本質就是分配一個 new_page, 解除原有 page 映射,把舊 page 複製到新 page 並建立新 page 的映射。

4. 頁面遷移過程用戶態訪問處理

到這裏可能會有疑問:如果在頁面遷移過程中,應用發生發訪問這個遷移中的頁面,會發生什麼?

下面我們重點分析一下,當舊頁面解除了映射,且新頁面未建立映射這個過程中發生了用戶態訪問,內核的處理流程是怎樣的。

首先我們看一下舊頁面解除了映射的過程:

static bool try_to_unmap_one(struct page *page, struct vm_area_struct *vma,
       unsigned long address, void *arg)
{
...  
  if (PageHWPoison(page) && !(flags & TTU_IGNORE_HWPOISON)) {

...
  } else if (pte_unused(pteval) && !userfaultfd_armed(vma)) {
...
  } else if (IS_ENABLED(CONFIG_MIGRATION) &&
    (flags & (TTU_MIGRATION|TTU_SPLIT_FREEZE))) { 
    // 頁面遷移會設置TTU_MIGRATION標記,走到這個分支來
   swp_entry_t entry;
   pte_t swp_pte;

   if (arch_unmap_one(mm, vma, address, pteval) < 0) {
    set_pte_at(mm, address, pvmw.pte, pteval);
    ret = false;
    page_vma_mapped_walk_done(&pvmw);
    break;
   }

   /*
    * Store the pfn of the page in a special migration
    * pte. do_swap_page() will wait until the migration
    * pte is removed and then restart fault handling.
    */
    // 遷移中的頁面, 生成了一個swap entry, 並寫到PTE頁表項中
    // 當再次發生缺頁時會走進do_swap_page等待直到遷移完成.
   entry = make_migration_entry(subpage, pte_write(pteval));
   swp_pte = swp_entry_to_pte(entry);
   if (pte_soft_dirty(pteval))
    swp_pte = pte_swp_mksoft_dirty(swp_pte);
   if (pte_uffd_wp(pteval))
    swp_pte = pte_swp_mkuffd_wp(swp_pte);
    // 當設置了遷移標記的Swap entry到pte後, 這個舊頁面就不能像原來那樣的順利被訪問了
   set_pte_at(mm, address, pvmw.pte, swp_pte);
   /*
    * No need to invalidate here it will synchronize on
    * against the special swap migration pte.
    */
  } else if (PageAnon(page)) {
   swp_entry_t entry = { .val = page_private(subpage) };
   pte_t swp_pte;
   /*
    * Store the swap location in the pte.
    * See handle_pte_fault() ...
    */
   if (unlikely(PageSwapBacked(page) != PageSwapCache(page))) {
    WARN_ON_ONCE(1);
    ret = false;
    /* We have to invalidate as we cleared the pte */
    mmu_notifier_invalidate_range(mm, address, address + PAGE_SIZE);
    page_vma_mapped_walk_done(&pvmw);
    break;
   }
...
}

解除映射後,再次發生映射就走到 do_swap_page 中了。

vm_fault_t do_swap_page(struct vm_fault *vmf)
{
...
  // 獲取到這是一個在遷移過程的的PTE的標識
 entry = pte_to_swp_entry(vmf->orig_pte);
 if (unlikely(non_swap_entry(entry))) {  // 不是傳統的Swap entry
  if (is_migration_entry(entry)) {      // 是遷移標記進來的
     /* 等待migration的完成。本質是在等待舊page釋放其page lock
      * 最終調用到 wait_on_page_bit_common
      */
   migration_entry_wait(vma->vm_mm, vmf->pmd, vmf->address);
  } 
...
}

總結一下:

頁面遷移前,首先會獲取舊頁面和新頁面的頁面鎖 PG_lock,在解除映射的時候傳入了由於頁面遷移導致的解映射標記 TTU_MIGRATION,設置了此標記會生成一個帶頁面遷移標識的 swap_entry 設置到 pte 中。在設置好的那一刻走,應用進程無法很順利地訪問這個頁面了,需要通過 do_swap_entry 路徑。

假如此時應用進程訪問了這個頁面,會走進到 do_swap_entry,取出帶遷移標識的 swap_entry,識別到這個標識,會等待頁面鎖釋放。頁面鎖只有在頁面遷移完成後纔會被釋放,也就是會發生等待直到頁面遷移完成。

5. 用戶態如何避免發生頁面遷移

上面我們已經知道,如果有頁面遷移過程中發生用戶態訪問,很可能是需要發生等待其遷移完成, 這個過程需要一定耗時。而有時的場景我們是需要避免此種時延抖動,那有什麼辦法呢?

方法就是讓這個頁面短時間內變得不可移動。

int migrate_page_move_mapping(struct address_space *mapping,
  struct page *newpage, struct page *page, int extra_count)
{
...
  if (page_count(page) != expected_count) 
   return -EAGAIN;
...
 return MIGRATEPAGE_SUCCESS;
}

可以看到當發生頁面複製過程中,如果 page 的引用計數不符合預期(期望爲 0)時,這時系統認爲有人在使用,不適用做遷移。那麼,我們只需要增加 page 的引用計數就可以。

可以在不想被遷移的時間段開始前通過 pin_user_pages 這樣的接口,結束時 unpin 就可以了。接口最終會調到 try_grab_page 增加引用計數。

bool __must_check try_grab_page(struct page *page, unsigned int flags)
{
...
   refs = GUP_PIN_COUNTING_BIAS; // #define GUP_PIN_COUNTING_BIAS (1U << 10)
   page_ref_add(page, refs);
  }

  return true;
}

原文地址:https://zhuanlan.zhihu.com/p/610249696

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bX38J4dZyNNAmfiRkfdJWQ