接上一篇
回到函數do_page_fault,如果不是核心的缺頁異常而是使用者程序的缺頁異常,那麼調用函數__do_page_fault,這個應該是本文的重點,主要讨論的是使用者程序的缺頁異常,結合最前面說的使用者程序産生缺頁異常的四種情況,函數__do_page_fault都會排查到,源碼如下:
__do_page_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,
struct task_struct *tsk)
{
struct vm_area_struct *vma;
int fault;
vma = find_vma(mm, addr);
fault = VM_FAULT_BADMAP;
if (unlikely(!vma))
goto out;
if (unlikely(vma->vm_start > addr))
goto check_stack;
good_area:
if (access_error(fsr, vma)) {
fault = VM_FAULT_BADACCESS;
goto out;
}
fault = handle_mm_fault(mm, vma, addr & PAGE_MASK, (fsr & FSR_WRITE) ? FAULT_FLAG_WRITE : 0);
if (unlikely(fault & VM_FAULT_ERROR))
return fault;
if (fault & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
return fault;
check_stack:
if (vma->vm_flags & VM_GROWSDOWN && !expand_stack(vma, addr))
goto good_area;
out:
return fault;
}
l 首先,檢視缺頁異常的這個虛拟位址addr,找它後面最近的vma,如果真的沒有找到,那麼說明通路的位址是真的錯誤了,因為它根本不在所配置設定的任何一個vma線性區;這是一種嚴重錯誤,将傳回錯誤碼(fault)VM_FAULT_BADMAP,核心會殺掉這個程序;
l 如果addr後面有vma,但addr并未落在這個vma的區間内,這存在一種可能,要知道棧的增長方向和堆是相反的即棧是向下增長,是以也許addr實際上是棧的一個位址,它後面的vma實際上是棧的vma,棧已無法擴充,即通路addr時,這個addr并沒有落在vma中是以更無二級頁表映射,導緻缺頁異常,是以檢視addr後面的vma是否是向下增長并且棧是否無法擴充,以此界定addr是不是棧位址,如果是則進入缺頁異常處理流程,否則同樣傳回錯誤碼(fault)VM_FAULT_BADMAP,核心會殺掉這個程序;
l 權限錯誤也就傳回,比如缺頁報錯(fsr)報的是不可寫,但vma本身就不可寫,那麼就直接傳回,因為問題根本不是缺頁,而是vma就已經有問題;傳回錯誤碼(fault) VM_FAULT_BADACCESS,這也是一種嚴重錯誤,核心會殺掉這個程序;s
l 最後是對确實缺頁異常的情況進行處理,調用函數handle_mm_fault,正常情況下将傳回VM_FAULT_MAJOR或VM_FAULT_MINOR,傳回錯誤碼fault并加一task的maj_flt或min_flt成員;
函數handle_mm_fault,就是為引發缺頁的程序配置設定一個實體頁框,它先确定與引發缺頁的線性位址對應的各級頁目錄項是否存在,如不存在則分進行配置設定。具體如何配置設定這個頁框是通過調用handle_pte_fault()完成的,注意最後一個參數flag,它來源于fsr,辨別寫異常和非寫異常,這是為了達到進一步推後配置設定實體記憶體的一個鋪墊;源碼如下:
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
__set_current_state(TASK_RUNNING);
count_vm_event(PGFAULT);
if (unlikely(is_vm_hugetlb_page(vma)))
return hugetlb_fault(mm, vma, address, flags);
pgd = pgd_offset(mm, address);
pud = pud_alloc(mm, pgd, address);
if (!pud)
return VM_FAULT_OOM;
pmd = pmd_alloc(mm, pud, address);
if (!pmd)
return VM_FAULT_OOM;
pte = pte_alloc_map(mm, pmd, address);
if (!pte)
return VM_FAULT_OOM;
return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}
首先注意下個細節,在二級頁表條目不存在時,會先建立條目;最終會調用函數handle_pte_fault,該函數功能注釋已經描述很清楚,源碼如下:
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl;
entry = *pte;
if (!pte_present(entry)) {
if (pte_none(entry)) {
if (vma->vm_ops) {
if (likely(vma->vm_ops->fault))
return do_linear_fault(mm, vma, address,
pte, pmd, flags, entry);
}
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
}
if (pte_file(entry))
return do_nonlinear_fault(mm, vma, address,
pte, pmd, flags, entry);
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry);
}
ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry)))
goto unlock;
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, entry);
} else {
if (flags & FAULT_FLAG_WRITE)
flush_tlb_page(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}
回過頭看下那四個異常的情況,上面的内容會比較好了解些,首先擷取到二級頁表條目值entry,對于寫時複制的情況,它的異常addr的二級頁表條目還是存在的(就是說起碼存在标志L_PTE_PRESENT),隻是說映射的實體頁不可寫,是以由(!pte_present(entry))可界定這是請求調頁的情況;
在請求調頁情況下,如果這個二級頁表條目的值為0,即什麼都沒有,那麼說明這個位址所在的vma是完完全全沒有做過映射實體頁的操作,那麼根據該vma是否存在vm_ops成員即操作函數,并且vm_ops存在fault成員,這說明是檔案映射而非匿名映射,反之是匿名映射,分别調用函數do_linear_fault、do_anonymous_page;
仍然在請求調頁的情況下,如果二級頁表條目的值含有L_PTE_FILE标志,說明這是個非線性檔案映射,将調用函數do_nonlinear_fault配置設定實體頁;其他情況視為實體頁曾被配置設定過,但後來被linux交換出記憶體,将調用函數do_swap_page再配置設定實體頁;
檔案線性/非線性映射和交換分區的映射除請求調頁方面外,還涉及檔案、交換分區的很多内容,為簡化起見,下面僅以匿名映射為例描述使用者空間缺頁異常的實際處理,而事實上日常使用的malloc都是匿名映射;
匿名映射展現了linux為程序配置設定實體空間的基本态度,不到實在不行的時候不配置設定實體頁,當使用malloc/mmap申請映射一段實體空間時,核心隻是給該程序建立了段線性區vma,但并未映射實體頁,然後如果試圖去讀這段申請的程序空間,由于未建立相應的二級頁表映射條目,MMU會發出缺頁異常,而這時核心依然隻是把一個預設的零頁zero_pfn(這是在初始化時建立的,前面的記憶體頁表的文章描述過)給vma映射過去,當應用程式又試圖寫這段申請的實體空間時,這就是實在不行的時候了,核心才會給vma映射實體頁,源碼如下:
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags)
{
struct page *page;
spinlock_t *ptl;
pte_t entry;
if (!(flags & FAULT_FLAG_WRITE)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
vma->vm_page_prot));
ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (!pte_none(*page_table))
goto unlock;
goto setpte;
}
pte_unmap(page_table);
if (unlikely(anon_vma_prepare(vma)))
goto oom;
page = alloc_zeroed_user_highpage_movable(vma, address);
if (!page)
goto oom;
__SetPageUptodate(page);
if (mem_cgroup_newpage_charge(page, mm, GFP_KERNEL))
goto oom_free_page;
entry = mk_pte(page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!pte_none(*page_table))
goto release;
page_add_new_anon_rmap(page, vma, address);
setpte:
set_pte_at(mm, address, page_table, entry);
update_mmu_cache(vma, address, entry);
unlock:
pte_unmap_unlock(page_table, ptl);
return 0;
release:
mem_cgroup_uncharge_page(page);
page_cache_release(page);
goto unlock;
oom_free_page:
page_cache_release(page);
oom:
return VM_FAULT_OOM;
}
結合上面的描述和源碼注釋應該比較容易能了解請求調頁的原理和流程;