【轉載】巨頁的原理分析

巨頁的原理分析

巨頁的原理,概括起來,就是在內核頁面大小一定的情況下,分配物理地址連續的多個頁框,模擬出一個大頁面供用戶態程序訪問,從而減少用戶程序缺頁次數,提高性能。

  爲了讓內核將這連續的多個頁框視爲一個整體,各個CPU架構分別做了手腳。

首先,hugetlbfs並不支持read/write。

  實際上,巨頁的使用,是通過用戶態mmap一個hugetlbfs文件,然後第一次訪問時缺頁來完成。

  例如,對於x86來說,有一個CR3頁表基址寄存器。

  當發生普通頁面缺頁時(其實準確的講應該叫tlbmiss),CPU根據CR3的內容,找到本進程的頁表基址,再依次遍歷頁表,最終定位到具體的頁表項(即pte entry)。

  如果是hugetlb所在的虛擬空間缺頁,則do_page_fault會給倒數第二級的pmd entry加一個標誌,

  相當於告訴CPU說,pmd entry就是最後一級頁表。那麼當page fault返回,CPU再次訪問缺頁地址時,CPU遍歷頁表,當找到pmd entry就不會往下尋址了,

  相當於提高了尋址範圍(此原理在上述ibm文檔中有詳細說明)

  對於mips來說,由於頁表的遍歷和TLB的重填是軟件來完成的(x86通過硬件完成),因此這裏需要對頁表遍歷和TLB重填流程做一定修改。

  所以,考察hugetlbfs的mmap驅動,以及hugetlbfs的page fault 。

  mmap: hugetlbfs_file_mmap

  hugetlbfs_file_mmap主要是對映射到的vma區間設置VM_HUGETLB和VM_RESERVED屬性,前者用來在pagefault流程裏,識別巨頁

  區間引發的缺頁,後者用來防止巨頁vma區間所包含的頁被換出。

  page fault: hugetlb_fault

  hugetlb_fault函數的作用,主要是分配出連續物理頁框,並把頁框地址按一定方式寫到頁表裏。這樣,再次發生tlb重填時,就可以根據頁表裏的

  映射寫到tlb裏。

  但是hugetlb的缺頁,和普通缺頁的區別又是在哪裏?根據上面的描述,我們提出兩個問題:

  1)頁框地址按什麼方式寫到頁表裏

  2)tlb重填時,如何根據頁表內容寫入tlb

  我們繼續回到hugetlb_fault流程。

  [cpp]

  int hugetlb_fault(struct mm_struct *mm,struct vm_area_struct *vma,

  unsigned long address, unsigned int flags)

  { pte_t *ptep;

  ptep = huge_pte_alloc(mm, address,huge_page_size(h));

  entry = huge_ptep_get(ptep);

  if (huge_pte_none(entry)) {

  ret = hugetlb_no_page(mm, vma, address, ptep,flags);

  goto out_mutex;

  }

  }

  我們知道,對於普通的缺頁,需生成從pgd到pte的映射樹,

  即,以發生缺頁的虛擬地址爲key,依次搜索進程頁表,如果沒有對應的pte表,

  就分配一個新的pte表,將pte表地址填入上一級的pmd entry,最後返回pte表裏,addr對應的pte entry地址(見pte_alloc_map函數)

  然而對於巨頁來說,假設一個巨頁由n個連續頁框組成,則需要把這n個頁框可能的頁表路徑都先分配好(pgd->pud->pmd->pte),

  也就是比普通缺頁,需要多“確認” (n-1)次頁表路徑的“暢通”。

  所以,需要調用huge_pte_alloc函數,給addr --> (addr+n*PAGE_SIZE)的虛擬空間,按照PGAE_SIZE爲步長,依次分配pgd到pte的路徑

  當然,這裏最後一步,分配具體頁框還沒有執行。

  [cpp]

  pte_t *huge_pte_alloc(struct mm_struct *mm,unsigned long addr,

  unsigned long sz)

  {

  pte_t *pte = NULL;

  unsigned long i = 0;

  unsigned long htlb_entries = 1 <<HUGETLB_PAGE_ORDER;

  addr &= HPAGE_MASK;

  for (i = 0; i < htlb_entries; i++) {

  pte = huge_pte_alloc_single(mm, addr);

  if (!pte)

  return NULL;

  addr += PAGE_SIZE;

  }

  return pte;

  }

  其中,htlb_entries是一個巨頁包含的頁框個數。

  再接着往下看,如果是第一次訪問巨頁空間,那麼走的是hugetlb_no_page,這是個相對較大的函數。

  [cpp]

  static int hugetlb_no_page(struct mm_struct*mm, struct vm_area_struct *vma,

  unsigned long address, pte_t *ptep, unsignedint flags)

  {

  idx = vma_hugecache_offset(h, vma, address);

  page = find_lock_page(mapping, idx);

  if (!page) {

  size = i_size_read(mapping->host) >>huge_page_shift(h);

  page = alloc_huge_page(vma, address, 0);

  err = add_to_page_cache(page, mapping, idx,GFP_KERNEL);

  }

  new_pte = make_huge_pte(vma, page,((vma->vm_flags & VM_WRITE)

  && (vma->vm_flags &VM_SHARED)));

  set_huge_pte_at(mm, address, ptep, new_pte);

  }

  hugetlb_no_page負責分配一塊連續的頁框,根據一個巨頁所包含的虛擬空間,將所有涉及到的頁表項pte entry,都指向這塊連續頁框的起始地址。這樣,以後無論進程發生在這塊巨頁空間裏任何一處的tlb異常,都會將tlb的pfn指向這塊物理空間,保證完成最終的tlb映射。

  vma_hugecache_offset根據傳入的缺頁地址address,計算出此address相對於整個地址空間的index(以巨頁大小爲單位)

  接着,根據idx在mapping空間裏找出對應的page, 即巨頁的第一個頁。如果找到不對應的頁,則說明還沒有分配巨頁,於是調用

  alloc_huge_page,要麼從hstates高速緩衝,要麼從夥伴系統獲取物理連續的頁框,之後通過

  add_to_page_cache加到pagecache裏,可以看出,巨頁文件對應的mapping,都是由巨頁的index構成的radix tree。

  緊接着,根據找到的page,生成一個pteentry,並給pte entry設置上_PAGE_HUGE標記,以便在缺頁返回後,再次訪址引起的tlb load異常時,可以

  辨識出這是一個巨頁pte entry

  最後,set_huge_pte_at(mm,address,ptep,new_pte)是比較關鍵的地方。

  [cpp]

  void set_huge_pte_at(struct mm_struct *mm,unsigned long addr, pte_t *ptep,

  pte_t entry)

  {

  unsigned long i;

  unsigned long htlb_entries = 1 <<HUGETLB_PAGE_ORDER;

  pte_t entry2;

  entry2 = __pte(pte_val(entry) + (HPAGE_SIZE>> 1));

  addr &= HPAGE_MASK;

  for (i = 0; i < htlb_entries; i += 2) {

  ptep = huge_pte_offset(mm, addr);

  set_pte_at(mm, addr, ptep, entry);

  addr += PAGE_SIZE;

  ptep = huge_pte_offset(mm, addr);

  set_pte_at(mm, addr, ptep, entry2);

  addr += PAGE_SIZE;

  }

  }

  這個函數,首先計算出一個巨頁需要htlb_entries個連續頁框,接着,根據hugetlb奇數頁,加上一個巨頁一半大小,算出偶數頁所在的tlb entry2。

  接着,對addr到addr+HPAGE_SIZE的空間內,凡是奇數頁地址,都設置爲entry,而偶數頁則都設置爲entry2,示意圖如下:


在 page fault返回後,再次訪址將產生tlbload/store異常。在build_r4000_tlbchange_handler_head函數裏,

  經過一系列頁表尋址,找到pte entry條目,由於這個條目之前在hugetlb_no_page已經被設置了_PAGE_HUGE,

  因此會走tlb_huge_update-->build_huge_handler_tail-->build_huge_update_entries將獲取到的奇偶頁表項,

  寫入entrylo1 和 entrylo 2。

  [cpp]

  static __cpuinit voidbuild_huge_update_entries(u32 **p,

  unsigned int pte,

  unsigned int tmp)

  {

  build_convert_pte_to_entrylo(p,pte);

  UASM_i_MTC0(p, pte,C0_ENTRYLO0); /* load it */

  uasm_i_ld(p, pte,sizeof(pte_t), tmp);

  build_convert_pte_to_entrylo(p,pte);

  UASM_i_MTC0(p, pte,C0_ENTRYLO1); /* load it */

  uasm_i_ehb(p);

  }

  其中,uasm_i_ld(p,pte,sizeof(pte_t),tmp)將傳入的奇數頁pte entry指針,加上一個pte_t的長度,取地址內容,得到偶數頁pte entry的值接着通過build_huge_tlb_write_entry(p,l, r, pte, tlb_random);將奇偶數頁都寫入TLB條目。(因爲此時系統中沒有匹配虛擬地址的tlb條目,所以probe失敗,index小於0,會跳過build_huge_tlb_write_entry(p, l, r, pte, tlb_indexed);

  直接走r4000_write_huge_probe_fail分支。

  [cpp]

  static __cpuinit voidbuild_huge_tlb_write_entry(u32 **p,

  struct uasm_label **l,

  struct uasm_reloc **r,

  unsigned int tmp,

  enum tlb_write_entry wmode)

  {

  /* Set huge page tlb entry size*/

  uasm_i_lui(p, tmp, PM_HUGE_MASK>> 16);

  uasm_i_ori(p, tmp, tmp,PM_HUGE_MASK & 0xffff);

  uasm_i_mtc0(p, tmp,C0_PAGEMASK);

  build_tlb_write_entry(p, l, r,wmode);

  build_restore_pagemask(p, r,tmp, label_leave);

  }

  首先是根據巨頁大小設置pagemask,接着通過tlbw將pte entry寫入tlb

  最後恢復pagemask跳轉至label_leave,tlb load退出。此時系統已經有了可用的巨頁tlb entry,以後訪問此巨頁空間不會再出現tlb miss了。

  總結一下,tlb巨頁缺頁流程如下:

  tlb miss-->tlb refill填入invalid pte entry-->返回用戶態再次訪址發生tlb load異常-->tlb load異常發現頁表項不在內存中,引發page fault

  -->調用hugetlb_fault填充巨頁頁表項-->返回用戶態再次訪址發生tlb load異常(因爲tlb裏仍然是invalid pte entry)-->

  根據巨頁頁表項填充TLB條目-->返回用戶態再次訪址,不會出現tlb miss,程序正常運行。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章