用戶空間的缺頁異常可以分為兩種情況--
1.觸發異常的線性地址處於用戶空間的vma中,但還未分配物理頁,如果訪問權限OK的話內核就給進程分配相應的物理頁了。
2.觸發異常的線性地址不處於用戶空間的vma中,這種情況得判斷是不是因為用戶進程的棧空間消耗完而觸發的缺頁異常,如果是的話則在用戶空間對棧區域進行擴展,並且分配相應的物理頁,如果不是則作為一次非法地址訪問來處理,內核將終結進程。
下面來看do_page_fault()函數對用戶空間缺頁異常的處理。
- dotraplinkage void __kprobes
- do_page_fault(struct pt_regs *regs, unsigned long error_code)
- {
- struct vm_area_struct *vma;
- struct task_struct *tsk;
- unsigned long address;
- struct mm_struct *mm;
- int write;
- int fault;
-
- tsk = current; //獲取當前進程
- mm = tsk->mm; //獲取當前進程的地址空間
-
- /* Get the faulting address: */
- address = read_cr2(); //讀取CR2寄存器獲取觸發異常的訪問地址
-
- ...
- ...
- ...
- ...
-
- vma = find_vma(mm, address);//試圖尋找到一個離address最近的vma,vma包含address或在address之後
-
- /*沒有找到這樣的vma則說明address之後沒有虛擬內存區域,因此該address肯定是無效的,
- 通過bad_area()路徑來處理,bad_area()的主體就是__bad_area()-->bad_area_nosemaphore()*/
- if (unlikely(!vma)) {
- bad_area(regs, error_code, address);
- return;
- }
- /*如果該地址包含在vma之中,則跳轉到good_area處進行處理*/
- if (likely(vma->vm_start <= address))
- goto good_area;
-
- /*不是前面兩種情況的話,則判斷是不是由於用戶堆棧所占的頁框已經使用完,而一個PUSH指令
- 引用了一個尚未和頁框綁定的虛擬內存區域導致的一個異常,屬於堆棧的虛擬內存區,其VM_GROWSDOWN位
- 被置位*/
- if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
- bad_area(regs, error_code, address);//不是堆棧區域,則用bad_area()來處理
- return;
- }
- if (error_code & PF_USER) {//必須處於用戶空間
- /*
- * Accessing the stack below %sp is always a bug.
- * The large cushion allows instructions like enter
- * and pusha to work. ("enter $65535, $31" pushes
- * 32 pointers and then decrements %sp by 65535.)
- */
- /*這裡檢查address,只有該地址足夠高(和堆棧指針的差不大於65536+32*sizeof(unsigned long)),
- 才能允許用戶進程擴展它的堆棧地址空間,否則bad_area()處理*/
- if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
- bad_area(regs, error_code, address);
- return;
- }
- }
- if (unlikely(expand_stack(vma, address))) {//堆棧擴展不成功同樣由bad_area()處理
- bad_area(regs, error_code, address);
- return;
- }
-
- /*
- * Ok, we have a good vm_area for this memory access, so
- * we can handle it..
- */
- good_area:
- write = error_code & PF_WRITE;
-
- /*訪問權限不夠則通過bad_area_access_error()處理,該函數是對__bad_area()的封裝,只不過
- 發送給用戶進程的信號為SEGV_ACCERR*/
- if (unlikely(access_error(error_code, write, vma))) {
- bad_area_access_error(regs, error_code, address);
- return;
- }
-
- /*
- * If for any reason at all we couldn't handle the fault,
- * make sure we exit gracefully rather than endlessly redo
- * the fault:
- */
- /*分配新的頁表和頁框*/
- fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
-
- if (unlikely(fault & VM_FAULT_ERROR)) {
- mm_fault_error(regs, error_code, address, fault);
- return;
- }
-
- if (fault & VM_FAULT_MAJOR) {
- tsk->maj_flt++;
- perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, 0,
- regs, address);
- } else {
- tsk->min_flt++;
- perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, 0,
- regs, address);
- }
-
- check_v8086_mode(regs, address, tsk);
-
- up_read(&mm->mmap_sem);
- }