18 請求調頁

上一篇博文引出了“請求調頁”技術,術語“請求調頁”指的是一種動態內存分配技術,它把頁框的分配推遲到不能再推遲爲止,也就是說,一直推遲到進程要訪問的頁不在物理RAM中時爲止,由此引起一個缺頁異常。

 

請求調頁技術背後的動機是:進程開始運行的時候並不訪問其線性地址空間中的全部地址。

 

事實上,有一部分地址也許永遠不被進程使用。此外,程序的局部性原理保證了在程序執行的每個階段,真正引用的進程頁只有一小部分,因此臨時用不着的頁所在的頁框可以由其他進程來使用。因此,對於全局分配(一開始就給進程分配所需要的全部頁框,直到程序結束才釋放這些頁框)來說,請求調頁是首選的,因爲它增加了系統中的空閒頁框的平均數,從而更好地利用空閒內存。從另一個觀點來看,在RAM總數保持不變的情況下,請求調頁從總體上能使系統有更大的吞吐量。

 

爲這一切優點付出的代價是系統額外的開銷:由請求調頁所引發的每個“缺頁”異常必須由內核處理,這將浪費CPU的時鐘週期。幸運的是,局部性原理保證了一旦進程開始在一組頁上運行,在接下來相當長的一段時間內它會一直停留在這些頁上而不去訪問其他的頁,這樣我們就可以認爲“缺頁”異常是一種稀有事件。

 

稀有歸稀有,下面,我們就來仔細研究一下缺頁異常的最後一步,也就是推遲得不能再推遲得那一步,請求調頁。

 

接着上一篇博文handle_mm_fault中調用handle_pte_fault函數:
handle_pte_fault(mm, vma, address, pte, pmd, write_access);


參數mm是發生缺頁的進程的mm_struct數據結構,address是發生缺頁異常的那個線性地址,vma是address所在的線性區,pte和pmd是對應的頁表和頁中間目錄,write_access是寫標記,在handle_mm_fault表示該頁是否可寫,是則爲1,不是則爲0。

//mm/Memory.c
static inline int handle_pte_fault(struct mm_struct *mm,
  struct vm_area_struct *vma, unsigned long address,
  pte_t *pte, pmd_t *pmd, int write_access)
{
 pte_t entry;
 pte_t old_entry;
 spinlock_t *ptl;

 old_entry = entry = *pte;
 if (!pte_present(entry)) {
  if (pte_none(entry)) {
   if (vma->vm_ops) {
    if (vma->vm_ops->nopage)
     return do_no_page(mm, vma, address,
         pte, pmd,
         write_access);
    if (unlikely(vma->vm_ops->nopfn))
     return do_no_pfn(mm, vma, address, pte,
        pmd, write_access);
   }
   return do_anonymous_page(mm, vma, address,
       pte, pmd, write_access);
  }
  if (pte_file(entry))
   return do_file_page(mm, vma, address,
     pte, pmd, write_access, entry);
  return do_swap_page(mm, vma, address,
     pte, pmd, write_access, entry);
 }

 ptl = pte_lockptr(mm, pmd);
 spin_lock(ptl);
 if (unlikely(!pte_same(*pte, entry)))
  goto unlock;
 if (write_access) {
  if (!pte_write(entry))
   return do_wp_page(mm, vma, address,
     pte, pmd, ptl, entry);
  entry = pte_mkdirty(entry);
 }
 entry = pte_mkyoung(entry);
 if (!pte_same(old_entry, entry)) {
  ptep_set_access_flags(vma, address, pte, entry, write_access);
  update_mmu_cache(vma, address, entry);
  lazy_mmu_prot_update(entry);
 } else {
  /*
   * This is needed only for protection faults but the arch code
   * is not yet telling us if this is a protection fault or not.
   * This still avoids useless tlb flushes for .text page faults
   * with threads.
   */
  if (write_access)
   flush_tlb_page(vma, address);
 }
unlock:
 pte_unmap_unlock(pte, ptl);
 return VM_FAULT_MINOR;
}


我們說發生缺頁異常的時機是被訪問的頁可能不在主存中,其原因或者是進程從沒訪問過該頁,或者是內核已經回收了相應的頁框。在這兩種情況下,缺頁處理程序必須爲進程分配新的頁框。

 

不過,如何初始化這個頁框取決於是哪一種頁以及頁以前是否被進程訪問過。特殊情況下:
1.這個頁從未被進程訪問到且沒有映射磁盤文件,或者頁映射了磁盤文件。內核能夠識別這些情況,它根據頁表相應的表項被填充爲0,也就是說,pte_none宏返回1。
2.頁屬於非線性磁盤文件的映射。內核能夠識別這種情況,因爲Present標誌被清0而且Dirty標誌被置1,也就是說,pte_file宏返回1。
3.進程已經訪問過這個頁,但是其內容被臨時保存在磁盤上。內核能夠識別這種情況,這是因爲相應表項沒被填充爲0,但是Present和Dirty標誌被清0。

 

因此,handle_pte_fault()函數通過檢查address對應的頁表項能夠區分這三種情況:
 pte_t old_entry = entry = *pte;
 if (!pte_present(entry)) { /* 頁表項pte的Present標誌被請0,說明缺頁 */
  if (pte_none(entry)) {  /* pte_none宏返回1說明:情況1 */
   if (vma->vm_ops) {
    if (vma->vm_ops->nopage) /* 還記得我們在“線性區數據結構”博文中提到的那些線性區處理方法吧 */
     return do_no_page(mm, vma, address,
         pte, pmd,
         write_access);
    if (unlikely(vma->vm_ops->nopfn))/* 首選nopage方法如果沒有,就選nopfn方法 */
     return do_no_pfn(mm, vma, address, pte,
        pmd, write_access);
   }/* 如果沒有vma->vm_ops方法,就只想匿名調頁函數 */
   return do_anonymous_page(mm, vma, address,
       pte, pmd, write_access);
  }
  if (pte_file(entry)) /* pte_file宏返回1說明:情況2 */
   return do_file_page(mm, vma, address,
     pte, pmd, write_access, entry);
  /* 情況3 */
  return do_swap_page(mm, vma, address,
     pte, pmd, write_access, entry);
 }

 

我們將在內存映射和頁框回收博文分別討論第2和第3種情況。

 

下面詳細說說第1種情況。我們先回憶一下關於頁表項的東西,頁表項由數據結構pte_t,在沒有激活PAE的情況下它是:
typedef struct { unsigned long pte_low; } pte_t;

 

對,32位無符號整數,高20位當然就是頁表中存在的頁號,低12位則是相關的標誌,如最低的一位是Present標誌,博文中講得很清楚了。那麼:
#define pte_none(x)  (!(x).pte_low)

 

如果pte_none返回1,則說明pte_t完全就沒有內容,則就是我們說的第一種情況,即這個頁從未被進程訪問到且沒有映射磁盤文件,或者頁映射了磁盤文件。糊塗了吧,別急,接着往下看。

 

當頁從未被訪問或頁線性地映射磁盤文件時則調用do_no_page()函數。有兩種方法裝入所缺的頁,這取決於這個頁是否被映射到一個磁盤文件。該函數通過檢查vma線性區描述符操作的vm_operations_struct結構中的nopage字段來確定這一點,如果頁被映射到一個文件,nopage字段就指向一個函數,該函數把所缺的頁從磁盤裝人到RAM。因此,可能的情況是:

(1)vma->vm_ops->nopage字段不爲NULL。在這種情況下,線性區映射了一個磁盤文件,nopage字段指向裝入頁的函數。這種情況將在“內存映射的請求調頁”一博中進行闡述。這裏只簡單地提一下:
new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, &ret);

(2)或者vma->vm_ops字段爲NULL,或者vma->vm_ops->nopage字段爲NULL。在這種情況下,線性區沒有映射磁盤文件,也就是說,它是一個匿名映射(anonymous mapping)。因此,do_no_page()調用do_anonymous_page()函數獲得一個新的頁框:
return do_anonymous_page(mm, vma, address, pte, pmd, write_access);

 

do_anonymous_page()函數(爲了簡化對這個函數的說明,我們略過處理反映射的語句,有關反映射的內容見“反向映射”博文。)分別處理寫請求和讀請求:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
  unsigned long address, pte_t *page_table, pmd_t *pmd,
  int write_access)
{
 struct page *page;
 spinlock_t *ptl;
 pte_t entry;

 if (write_access) {
  /* Allocate our own private page. */
  pte_unmap(page_table);  /* 釋放pte一種臨時內核映射 */

  if (unlikely(anon_vma_prepare(vma)))
   goto oom;
  page = alloc_zeroed_user_highpage(vma, address);
  if (!page)
   goto oom;

  entry = mk_pte(page, vma->vm_page_prot);
  entry = maybe_mkwrite(pte_mkdirty(entry), vma);

  page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
  if (!pte_none(*page_table))
   goto release;
  inc_mm_counter(mm, anon_rss);
  lru_cache_add_active(page);
  page_add_new_anon_rmap(page, vma, address);
 } else {
  /* Map the ZERO_PAGE - vm_page_prot is readonly */
  page = ZERO_PAGE(address);
  page_cache_get(page);
  entry = mk_pte(page, vma->vm_page_prot);

  ptl = pte_lockptr(mm, pmd);
  spin_lock(ptl);
  if (!pte_none(*page_table))
   goto release;
  inc_mm_counter(mm, file_rss);
  page_add_file_rmap(page);
 }

 set_pte_at(mm, address, page_table, entry);

 /* No need to invalidate - it was non-present before */
 update_mmu_cache(vma, address, entry);
 lazy_mmu_prot_update(entry);
unlock:
 pte_unmap_unlock(page_table, ptl);
 trace_mm_anon_fault(mm, address, page);
 return VM_FAULT_MINOR;
release:
 page_cache_release(page);
 goto unlock;
oom:
 return VM_FAULT_OOM;
}

 

函數根據write_access指向不同的程序段,首選,如果write_access爲1,則:

pte_unmap宏的第一次執行釋放一種臨時內核映射,這種映射了在調用handle_pte_fault()函數之前由pte_offset_map宏所建立頁表項的高端內存物理地址(參見“高端內存映射”一博中的表)。pte_offset_map和pte_unmap這對宏獲取和釋放同一個臨時內核映射。臨時內核映射必須在調用alloc_zeroed_user_highpage,本質上也就是alloc_page()之前釋放,因爲這個函數可能會阻塞當前進程。

 

分配一個新的頁面:
page = alloc_zeroed_user_highpage(vma, address);
#define alloc_zeroed_user_highpage(vma, vaddr) alloc_page_vma(GFP_HIGHUSER | __GFP_ZERO, vma, vaddr)
#define alloc_page_vma(gfp_mask, vma, addr) alloc_pages(gfp_mask, 0)

 

相應的頁表項設置爲頁框的物理地址,除了根據vma->vm_page_prot設置相應頁面的權限外,頁表框被標記爲既髒又可寫的(pte_mkdirty(entry)。如果調試程序試圖往被跟蹤進程只讀線性區中的頁中寫數據,內核就不設置Read/Write標誌。函數maybe_mkwrite()處理這種特殊情況。):
entry = mk_pte(page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
#define __pte(x) ((pte_t) { (x) } )

 

函數遞增內存描述符的rss字段以記錄分配給進程的頁框總數:
inc_mm_counter(mm, anon_rss);

 

lru_cache_add_active()函數把新頁框插入與交換相關的數據結構中,我們在後面博文對它進行說明。

 

相反,當處理讀訪問時,即write_access爲0,頁的內容是無關緊要的,因爲進程第一次對它訪問。給進程一個填充爲0的頁要比給它一個由其他進程填充了信息的舊頁更爲安全。Linux在請求調頁方面做得更深入一些。沒有必要立即給進程分配一個填充爲0的新頁框,由於我們也可以給它一個現有的稱爲零頁(zero page)的頁,這樣可以進一步推遲頁框的分配。零頁在內核初始化期間被靜態分配,並存放在empty_zero_page變量中(長爲4096字節的數組,並用0填充):
#define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page))
extern unsigned long empty_zero_page[1024];
#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
#define __pa(x)   ((unsigned long)(x)-PAGE_OFFSET)


再提醒一下,舊知識,給我記牢咯!:
#define PAGE_OFFSET  0xc0000000
#define PAGE_SHIFT 12
struct page *pfn_to_page(unsigned long pfn)
{
 return __pfn_to_page(pfn);
}
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))

 

由於這個頁被標記爲不可寫的,因此如果進程試圖寫這個頁,則寫時複製機制被激活。當且僅當在這個時候,進程才獲得一個屬於自己的頁並對它進行寫操作。究竟寫時複製是個啥東東,且聽下回分解。


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