缺页异常被触发通常有两种情况——
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()
- <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))
- 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);
- }
- </span>