缺頁異常被觸發通常有兩種情況——
1.程序設計的不當導致訪問了非法的地址
2.訪問的地址是合法的,但是該地址還未分配物理頁框
下面解釋一下第二種情況,這是虛擬內存管理的一個特性。盡管每個進程獨立擁有3GB的可訪問地址空間,但是這些資源都是內核開出的空頭支票,也就是說進程手握着和自己相關的一個個虛擬內存區域(vma),但是這些虛擬內存區域並不會在創建的時候就和物理頁框掛鉤,由於程序的局部性原理,程序在一定時間內所訪問的內存往往是有限的,因此內核只會在進程確確實實需要訪問物理內存時才會將相應的虛擬內存區域與物理內存進行關聯(为相應的地址分配頁表項,並將頁表項映射到物理內存),也就是說這種缺頁異常是正常的,而第一種缺頁異常是不正常的,內核要采取各種可行的手段將這種異常帶來的破壞減到最小。
缺頁異常的處理函數为do_page_fault(),該函數是和體系結構相關的一個函數,缺頁異常的來源可分为兩種,一種是內核空間(訪問了線性地址空間的第4個GB),一種是用戶空間(訪問了線性地址空間的0~3GB),以X86架構为例,先來看內核空間異常的處理。
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))
return;
}
/* 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))
return;
/*
* 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對應的頁表項與主內核頁表進行同步
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)
return -1;
/*到這裏已經獲取了內核頁表對應於address的pmd,並且將該值設置给了當前使用頁表的pmd,
最後一步就是判斷pmd對應的pte項是否存在*/
pte_k = pte_offset_kernel(pmd_k, address);//獲取pmd對應address的pte項
if (!pte_present(*pte_k))//判斷pte項是否存在,不存在則失敗
return -1;
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))
return NULL;
/*對pmd進行和上面類似的操作*/
pmd = pmd_offset(pud, address);
pmd_k = pmd_offset(pud_k, address);
if (!pmd_present(*pmd_k))
return NULL;
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()
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))
return;
/*
* 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))
return;
if (is_errata93(regs, address))
return;
/*
* 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);
}