天天看點

linux缺頁異常處理--核心空間

 缺頁異常被觸發通常有兩種情況——

1.程式設計的不當導緻通路了非法的位址

2.通路的位址是合法的,但是該位址還未配置設定實體頁框

下面解釋一下第二種情況,這是虛拟記憶體管理的一個特性。盡管每個程序獨立擁有3GB的可通路位址空間,但是這些資源都是核心開出的空頭支票,也就是說程序手握着和自己相關的一個個虛拟記憶體區域(vma),但是這些虛拟記憶體區域并不會在建立的時候就和實體頁框挂鈎,由于程式的局部性原理,程式在一定時間内所通路的記憶體往往是有限的,是以核心隻會在程序确确實實需要通路實體記憶體時才會将相應的虛拟記憶體區域與實體記憶體進行關聯(為相應的位址配置設定頁表項,并将頁表項映射到實體記憶體),也就是說這種缺頁異常是正常的,而第一種缺頁異常是不正常的,核心要采取各種可行的手段将這種異常帶來的破壞減到最小。

       缺頁異常的處理函數為do_page_fault(),該函數是和體系結構相關的一個函數,缺頁異常的來源可分為兩種,一種是核心空間(通路了線性位址空間的第4個GB),一種是使用者空間(通路了線性位址空間的0~3GB),以X86架構為例,先來看核心空間異常的處理。

[cpp]

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寄存器擷取觸發異常的通路位址  

    ...  

         ...  

    if (unlikely(fault_in_kernel_space(address))) { //判斷address是否處于核心線性位址空間  

        if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {//判斷是否處于核心态  

            if (vmalloc_fault(address) >= 0)//處理vmalloc異常  

                return;  

            if (kmemcheck_fault(regs, address, error_code))  

        }  

        /* Can handle a stale RO->RW TLB: */  

        /*異常發生在核心位址空間但不屬于上面的情況或上面的方式無法修正, 

          則檢查相應的頁表項是否存在,權限是否足夠*/  

        if (spurious_fault(error_code, address))  

            return;  

        /* kprobes don't want to hook the spurious faults: */  

        if (notify_page_fault(regs))  

        /* 

         * Don't take the mm semaphore here. If we fixup a prefetch 

         * fault we could otherwise deadlock: 

         */  

        bad_area_nosemaphore(regs, error_code, address);  

        return;  

    }  

}  

該函數傳遞進來的兩個參數--

regs包含了各個寄存器的值

error_code是觸發異常的錯誤類型,它的含義如下

/* 

 * Page fault error code bits: 

 * 

 *   bit 0 ==    0: no page found   1: protection fault 

 *   bit 1 ==    0: read access     1: write access 

 *   bit 2 ==    0: kernel-mode access  1: user-mode access 

 *   bit 3 ==               1: use of reserved bit detected 

 *   bit 4 ==               1: fault was an instruction fetch 

 */  

enum x86_pf_error_code {  

    PF_PROT     =       1 << 0,  

    PF_WRITE    =       1 << 1,  

    PF_USER     =       1 << 2,  

    PF_RSVD     =       1 << 3,  

    PF_INSTR    =       1 << 4,  

};  

首先要檢查該異常的觸發位址是不是位于核心位址空間 也就是address>=TASK_SIZE_MAX,一般為3GB。然後要檢查觸發異常時是否處于核心态,滿足這兩個條件就嘗試通過vmalloc_fault()來解決這個異常。由于使用vmalloc申請記憶體時,核心隻會更新主核心頁表,是以目前使用的程序頁表就有可能因為未與主核心頁表同步導緻這次異常的觸發,是以該函數試圖将address對應的頁表項與主核心頁表進行同步

[cpp]​

static noinline int vmalloc_fault(unsigned long address)  

    unsigned long pgd_paddr;  

    pmd_t *pmd_k;  

    pte_t *pte_k;  

    /* 确定觸發異常的位址是否處于VMALLOC區域*/  

    if (!(address >= VMALLOC_START && address < VMALLOC_END))  

        return -1;  

    /* 

     * Synchronize this task's top level page-table 

     * with the 'reference' page table. 

     * 

     * Do _not_ use "current" here. We might be inside 

     * an interrupt in the middle of a task switch.. 

     */  

    pgd_paddr = read_cr3();//擷取目前的PGD位址  

    pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);//将目前使用的頁表和核心頁表同步  

    if (!pmd_k)  

    /*到這裡已經擷取了核心頁表對應于address的pmd,并且将該值設定給了目前使用頁表的pmd, 

      最後一步就是判斷pmd對應的pte項是否存在*/  

    pte_k = pte_offset_kernel(pmd_k, address);//擷取pmd對應address的pte項  

    if (!pte_present(*pte_k))//判斷pte項是否存在,不存在則失敗  

    return 0;  

同步處理:

static inline pmd_t *vmalloc_sync_one(pgd_t *pgd, unsigned long address)  

    unsigned index = pgd_index(address);  

    pgd_t *pgd_k;  

    pud_t *pud, *pud_k;  

    pmd_t *pmd, *pmd_k;  

    pgd += index; //記錄目前頁表pgd對應address的偏移  

    pgd_k = init_mm.pgd + index;//記錄核心頁表對應address的偏移  

    if (!pgd_present(*pgd_k))//核心PGD頁表對應的項不存在,則無法進行下一步,傳回NULL  

        return NULL;  

     * set_pgd(pgd, *pgd_k); here would be useless on PAE 

     * and redundant with the set_pmd() on non-PAE. As would 

     * set_pud. 

    /*擷取目前頁表對應address的PUD位址和核心頁表對應address的位址,并判斷pud_k對應的項是否存在*/  

    pud = pud_offset(pgd, address);  

    pud_k = pud_offset(pgd_k, address);  

    if (!pud_present(*pud_k))  

    /*對pmd進行和上面類似的操作*/  

    pmd = pmd_offset(pud, address);  

    pmd_k = pmd_offset(pud_k, address);  

    if (!pmd_present(*pmd_k))  

    if (!pmd_present(*pmd))//目前使用頁表對應的pmd項不存在,則修正pmd項使其和核心頁表的pmd_k項相同  

        set_pmd(pmd, *pmd_k);  

    else  

        BUG_ON(pmd_page(*pmd) != pmd_page(*pmd_k));  

    return pmd_k;  

 如果do_page_fault()函數執行到了bad_area_nosemaphore(),那麼就表明這次異常是由于對非法的位址通路造成的。在核心中産生這樣的結果的情況一般有兩種:

1.核心通過使用者空間傳遞的系統調用參數,通路了無效的位址

2.核心的程式設計缺陷

第一種情況核心尚且能通過異常修正機制來進行修複,而第二種情況就會導緻OOPS錯誤了,核心将強制用SIGKILL結束目前程序。

核心态的bad_area_nosemaphore()的實際處理函數為bad_area_nosemaphore()-->__bad_area_nosemaphore()-->no_context()

[cpp] 

<span style="font-size:12px;">static noinline void  

no_context(struct pt_regs *regs, unsigned long error_code,  

       unsigned long address)  

    struct task_struct *tsk = current;  

    unsigned long *stackend;  

    unsigned long flags;  

    int sig;  

    /* Are we prepared to handle this kernel fault? */  

    /*fixup_exception()用于搜尋異常表,并試圖找到一個對應該異常的例程來進行修正, 

      這個例程在fixup_exception()傳回後執行*/  

    if (fixup_exception(regs))  

     * 32-bit: 

     *   Valid to do another page fault here, because if this fault 

     *   had been triggered by is_prefetch fixup_exception would have 

     *   handled it. 

     * 64-bit: 

     *   Hall of shame of CPU/BIOS bugs. 

    if (is_prefetch(regs, error_code, address))  

    if (is_errata93(regs, address))  

     * Oops. The kernel tried to access some bad page. We'll have to 

     * terminate things with extreme prejudice: 

    /* 走到這裡就說明異常确實是由于核心的程式設計缺陷導緻的了,核心将 

       産生一個oops,下面的工作就是列印CPU寄存器和核心态堆棧的資訊到控制台并 

       終結目前的程序*/  

    flags = oops_begin();  

    show_fault_oops(regs, error_code, address);  

    stackend = end_of_stack(tsk);  

    if (*stackend != STACK_END_MAGIC)  

        printk(KERN_ALERT "Thread overran stack, or stack corrupted\n");  

    tsk->thread.cr2      = address;  

    tsk->thread.trap_no  = 14;  

    tsk->thread.error_code   = error_code;  

    sig = SIGKILL;  

    if (__die("Oops", regs, error_code))  

        sig = 0;  

    /* Executive summary in case the body of the oops scrolled away */  

    printk(KERN_EMERG "CR2: %016lx\n", address);  

    oops_end(flags, regs, sig);  

</span>  

------------------越是喧嚣的世界,越需要甯靜的思考------------------

合抱之木,生于毫末;九層之台,起于壘土;千裡之行,始于足下。

積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千裡;不積小流,無以成江海。骐骥一躍,不能十步;驽馬十駕,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鳝之穴無可寄托者,用心躁也。

繼續閱讀