Linux页面异常处理

Linux页面异常处理是一个复杂的过程.它用来处理内存访问各种异常,因为这部份内容涉及到了系统调用的相关部份,所以,我们暂且忽略这部份信息,只要知道,如果有内存访问异常情况,就会转入到do_page_fault()中处理,关于系统调用这部份,我们之后再给出详细的分析,详情请关注本站更新.

   同以往一样,本文的代码是基于linux-2.6.21.页面异常处理程序的代码如下:


/*


     参数的含义:


regs:里面保存着异常情况时,各CPU寄存器的值.


Error_code:错误代码.根据作者的注释,代码中各字位的含义如下:


           第0位: 0:没有这个页面       1:权限不对


           第1位: 0:读错误             1:写错误


           第2位: 0:内核               1:用户空间


*/


asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)


{


     struct task_struct *tsk;


     struct mm_struct *mm;


     struct vm_area_struct * vma;


     unsigned long address;


     unsigned long page;


     int write;


     siginfo_t info;


//发生异常时,CPU把异常的地址压入CR2中.此段嵌入汇编的含意是将CR2中的值取出放到


//address变量中


     __asm__("movl %%cr2,%0":"=r" (address));


     //事情通知链表


     if (notify_die(DIE_PAGE_FAULT, "page fault", regs, error_code, 14,


                       SIGSEGV) == NOTIFY_STOP)


         return;


     //恢复中断,CR2中的值已经得到了保存


     if (regs->eflags & (X86_EFLAGS_IF|VM_MASK))


         local_irq_enable();


     //取发生异常的处理 task_struct


     tsk = current;


     info.si_code = SEGV_MAPERR;


//条件编译.假设开关末打开


#ifdef CONFIG_X86_4G


     /*


      * On 4/4 all kernels faults are either bugs, vmalloc or prefetch


      */


     /* If it's vm86 fall through */


     if (unlikely(!(regs->eflags & VM_MASK) && ((regs->xcs & 3) == 0))) {


         if (error_code & 3)


              goto bad_area_nosemaphore;


         goto vmalloc_fault;


     }


#else


     if (unlikely(address >= TASK_SIZE)) {   //地址大于TASK_SIZE 说明错误发生在内核空间


         if (!(error_code & 5))   //5=101 -> error_code = 010 || 000 内核空间的写/读地址错误


              goto vmalloc_fault;


         //发生在用户空间的,地址超出TASK_SIZE


         goto bad_area_nosemaphore;


     } 


#endif





     mm = tsk->mm;





     /*


      * If we're in an interrupt, have no user context or are running in an


      * atomic region then we must not take the fault..


      */


     if (in_atomic() || !mm)


         goto bad_area_nosemaphore;





     


     if (!down_read_trylock(&mm->mmap_sem)) {


         if ((error_code & 4) == 0 &&


             !search_exception_tables(regs->eip))


              goto bad_area_nosemaphore;


         down_read(&mm->mmap_sem);


     }


     //找到第一个结束地址大于address的VMA


     vma = find_vma(mm, address);


     //没有这样的VMA.说明异常地址是在进程地址堆栈的上方,非法


if (!vma)


         goto bad_area;


     //地址落在一个VMA区域中


     if (vma->vm_start 


         goto good_area;


     //VM_GROWSDOWN:向下增长,只有栈才有这样的属性.


     //不属于栈而且又落在空洞中,非法


     if (!(vma->vm_flags & VM_GROWSDOWN))


         goto bad_area;


     


     if (error_code & 4) {


         //error_code = 1XX :在用户空间


         //入栈一次是四个字节


         //如果操作不是如入栈引起的,非法


         if (address + 32 esp)


              goto bad_area;


     }


     //只要栈顶没有到达下面的数据段或者MMAP映射区,系统都认为是合法的,扩大栈空间


     //参考内核情景分析>>


     if (expand_stack(vma, address))


         goto bad_area;





          ……


          ……





}


为了方便分析,我们理一下各标号的代码含义:


1: vmalloc_fault:  内核非连续空间的异常处理


我们在vmalloc/vfree的实现中可以看到(参考本站内存管理之非连续物理地址分配(vmalloc)>>一文),vmalloc分配的地址是位于VMALLOC_START,VMALLOC_END区域内的.随后,内核为其做了页面映射的工作,回顾之前的代码,我们在取内核页目录的时候是从init_mm.pgd中取得的.然而.一旦内核初始化完成之后,就不会使用init_mm了,也就是说,init_mm中的映射关系还没有更新到当前内核页目录中去.然以,在访问vmalloc分配的地址的时候,就会产生异常.这也是vmalloc_fault标号的来由.如果是这样的情况,把init_mm中的映射关系更新到当前内核使用页表就可以了.关于这部份的详细信息,我们在系统初始化的时候再介绍,详情请关注本站更新 ^_^.vmalloc_fault标号对应的代码如下:


//内核非连续空间的异常处理(R/W)


vmalloc_fault:


     {


         


         //计算地址所对应页目录偏移值


         int index = pgd_index(address);


         unsigned long pgd_paddr;


         pgd_t *pgd, *pgd_k;


         pmd_t *pmd, *pmd_k;


         pte_t *pte_k;


         


//从CR3中取当前内核页目录


         asm("movl %%cr3,%0":"=r" (pgd_paddr));


         //发生异常的页目录项


         pgd = index + (pgd_t *)__va(pgd_paddr);


         //init_mm中相应的内核页目录项


         pgd_k = init_mm.pgd + index;





         //如果init_mm中没有相关信息,那么它就是一个不折不扣的错误了,转至no_contex处理


         if (!pgd_present(*pgd_k))


              goto no_context;





         //分别取当前pmd与init_mm中的对应pmd


         pmd = pmd_offset(pgd, address);


         pmd_k = pmd_offset(pgd_k, address);


         if (!pmd_present(*pmd_k))


              goto no_context;


         //更新


         set_pmd(pmd, *pmd_k);   //(这里更新的就是全局页目录表pgd中的对应位置的表项值)





         //取相应的页面项


         pte_k = pte_offset_kernel(pmd_k, address);


         if (!pte_present(*pte_k))


              goto no_context;


         //如果到这里没有异常的话,映射信息已经更新好了,返回


         return;


     }


2:no_context:我们可以看到,在vmalloc_fault中如果异常错误的话,就会转入到这个标号中进行.


这个标号首先它判断是否是由一个错误的系统调用参数引起的.如果是.则向相应进程发送SIGSEGV.如果是内核本身的错误,就打印出Oops错误.然后把内核挂起.代码如下:


no_context:


     //地址修正:通常是到异常表里去找相关信息.这个函数的具体实现等到系统调用分析的时候再  


     //进行


     if (fixup_exception(regs))


         return;





     //如果不是系统调用参数的错误,只可能是内核编程的错误了,Oops


     if (is_prefetch(regs, address, error_code))


         return;





/*


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


* terminate things with extreme prejudice.


*/





     bust_spinlocks(1);





#ifdef CONFIG_X86_PAE


     if (error_code & 16) {


         pte_t *pte = lookup_address(address);





         if (pte && pte_present(*pte) && !pte_exec_kernel(*pte))


              printk(KERN_CRIT "kernel tried to execute NX-protected page - exploit attempt? (uid: %d)\n", current->uid);


     }


#endif


     if (address 


         printk(KERN_ALERT "Unable to handle kernel NULL pointer dereference");


     else


         printk(KERN_ALERT "Unable to handle kernel paging request");


     printk(" at virtual address %08lx\n",address);


     printk(KERN_ALERT " printing eip:\n");


     printk("%08lx\n", regs->eip);


     asm("movl %%cr3,%0":"=r" (page));


     page = ((unsigned long *) __va(page))[address >> 22];


     printk(KERN_ALERT "*pde = %08lx\n", page);


     /*


      * We must not directly access the pte in the highpte


      * case, the page table might be allocated in highmem.


      * And lets rather not kmap-atomic the pte, just in case


      * it's allocated already.


      */


#ifndef CONFIG_HIGHPTE


     if (page & 1) {


         page &= PAGE_MASK;


         address &= 0x003ff000;


         page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT];


         printk(KERN_ALERT "*pte = %08lx\n", page);


     }


#endif


     die("Oops", regs, error_code);


     bust_spinlocks(0);


     do_exit(SIGKILL);


3: bad_area标号: 地址空间以外的线性地址异常处理.如果线性地址落在vma区域的空洞中,又不是堆栈空间的扩展,则进程发生了错误.


bad_area:


     up_read(&mm->mmap_sem);





bad_area_nosemaphore:


     //我们可以看到bad_area与bad_area_nosemaphore区别,后者没有锁住信号量


     if (error_code & 4) {


         /* 4 = 100  -> error_code = 1xx 表示在用户空间*/


         //如果是用户空间,则向该进程发送SIGSEGV信号


         if (is_prefetch(regs, address, error_code))


              return;





         tsk->thread.cr2 = address;


         /* Kernel addresses are always protection faults */


         tsk->thread.error_code = error_code | (address >= TASK_SIZE);


         tsk->thread.trap_no = 14;


         info.si_signo = SIGSEGV;


         info.si_errno = 0;


         /* info.si_code has been set above */


         info.si_addr = (void __user *)address;


         force_sig_info(SIGSEGV, &info, tsk);


         return;


     }


4: good_area标号的处理.与上述几个标号的处理相比.good_area的处理相对要复杂一些,它主要是处理一些正常的访问.具体代码如下:


// good_area:处理进程空间内的线性地址


good_area:


     info.si_code = SEGV_ACCERR;


     write = 0;





     // 3 = 011


     switch (error_code & 3) {            //3


         default: /* 3: write, present */


#ifdef TEST_VERIFY_AREA


              if (regs->cs == KERNEL_CS)


                   printk("WP fault at %08lx\n", regs->eip);


#endif


              /* fall through */


         case 2:       /* write, not present */


              // error_code:010 || 110  写一个不存在的页面


              if (!(vma->vm_flags & VM_WRITE)) //区域没有可写的权限


                   goto bad_area;


              write++; //write置为了-1


              break;


         case 1:       /* read, present  error_code = 001 || 101 读操作,但是权限不够*/ 


              goto bad_area;


         case 0:       /* read, not present  error_code = 000|100 读一个不存在的页面*/


              if (!(vma->vm_flags & (VM_READ | VM_EXEC)))  //没有相应的权限


                   goto bad_area;


     }





survive:


          //write = 1 :写操作 write = 0 :读操作


     switch (handle_mm_fault(mm, vma, address, write)) {


         case VM_FAULT_MINOR:


              tsk->min_flt++;


              break;


         case VM_FAULT_MAJOR:


              tsk->maj_flt++;


              break;


         case VM_FAULT_SIGBUS:


              goto do_sigbus;


         case VM_FAULT_OOM:


              goto out_of_memory;


         default:


              BUG();


     }





     /*


      * Did it hit the DOS screen memory VA from vm86 mode?


      */


     if (regs->eflags & VM_MASK) {


         unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT;


         if (bit 


              tsk->thread.screen_bitmap |= 1 


     }


     up_read(&mm->mmap_sem);


     return;


handle_mm_fault是这个处理过程的重点,我们看一下具体的实现:


/*


     参数含义: 


         Mm:进程描述符


         Vma:发生异常所在的vma区


         Address:发生异常的地址


         Write_access:如果为1:表示的是一个写操作.如果是0则为读操作


*/


int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,


     unsigned long address, int write_access)


{


     pgd_t *pgd;


     pmd_t *pmd;





     __set_current_state(TASK_RUNNING);


     //取得进程对应的pgd


     pgd = pgd_offset(mm, address);





     inc_page_state(pgfault);





     if (is_vm_hugetlb_page(vma))


         return VM_FAULT_SIGBUS; /* mapping truncation does this. */


     spin_lock(&mm->page_table_lock);


     //返回或者建立一个pmd


     pmd = pmd_alloc(mm, pgd, address);





     if (pmd) {


         //返回或者创建一个pte


         pte_t * pte = pte_alloc_map(mm, pmd, address);


         if (pte)


              //具体异常的处理


              return handle_pte_fault(mm, vma, address, write_access, pte, pmd);


     }


     spin_unlock(&mm->page_table_lock);


     return VM_FAULT_OOM;


}


疑问:我们在上面看到,异常处理程序会从PGD->PTE建了映射.那是不是在sys_brk在伸展空间的时候,只要使地址区间包含在进程的VMA区域.没必要为其从PGD->PTE建立映射呢? 有待验证 *^_^*





转入handle_pte_fault()


/*


     参数含义:


     Mm:进程的内存描述符


     Vma:异常地址所在的VMA


     Address:发生异常所在的地址


     Write_access:1:写 0:读


     Pte,pmd:地址所对应的PTE与PMD


*/


static inline int handle_pte_fault(struct mm_struct *mm,


     struct vm_area_struct * vma, unsigned long address,


     int write_access, pte_t *pte, pmd_t *pmd)


{


     pte_t entry;


     //取得PTE的值


     entry = *pte;


     if (!pte_present(entry)) {


         //pte所映射的页面不在内存


         


         if (pte_none(entry))


              //PTE没有映射.复习一下前面所讲述的sys_brk在扩展过程的地址区域的时候


              //只分配了一个可以访问的线性地址,没有为其映射页面


              return do_no_page(mm, vma, address, write_access, pte, pmd);


         //pte_file()????


         if (pte_file(entry))


              return do_file_page(mm, vma, address, write_access, pte, pmd);


         //运行到这里的话.说明PTE映射的页面已经被交换到磁盘上去了,把其交换回来


         //具体的过程等分析交换的时候再讲述


         return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);


     }





     //PTE映射的页面在内存中


     if (write_access) {


         //写异常


         if (!pte_write(entry))


              //访问一个没有写权限的页面


              return do_wp_page(mm, vma, address, pte, pmd, entry);





         entry = pte_mkdirty(entry);


     }


//注意:读一个已经映射好的页面,是不会产生异常的


     entry = pte_mkyoung(entry);


     ptep_set_access_flags(vma, address, pte, entry, write_access);


     update_mmu_cache(vma, address, entry);


     pte_unmap(pte);


     spin_unlock(&mm->page_table_lock);


     return VM_FAULT_MINOR;


}


由于这个函数涉及到的过程较多.我们依次据情况分析


1):请求调页的情况


内核总是把用户空间的内存分配推迟到不能再延迟为止,直到要访问线性地址对应的物理地址时才会为它分配内存,这种情况对应上面代码中的do_no_page()的情况.





static int


do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,


unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)


{


struct page * new_page;


struct address_space *mapping = NULL;


pte_t entry;


int sequence = 0;


int ret = VM_FAULT_MINOR;


int anon = 0;





//并不是一个磁盘高速缓存的页面


if (!vma->vm_ops || !vma->vm_ops->nopage)


     return do_anonymous_page(mm, vma, page_table,


                   pmd, write_access, address);


//对于磁盘高速缓存这部份,等到文件系统的时候再给出分析


……


……;


}





//终于转入到正题了


static int


do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,


     pte_t *page_table, pmd_t *pmd, int write_access,


     unsigned long addr)


{


pte_t entry;


struct page * page = ZERO_PAGE(addr);





//零页.对于一个没有为PTE分配页面的读操作,通常是将它映射到零页.这个页面是只读的


//如果其后,对这个页面进行访问的话,再为其分配一个真正的物理页面


entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));





//如果是一个写操作,则为其映射内存


if (write_access) {


     //在x86平台,此函数为空


     pte_unmap(page_table);


     spin_unlock(&mm->page_table_lock);





     if (unlikely(anon_vma_prepare(vma)))


          goto no_mem;


     //alloc_page_vma à alloc_page().为其分配一个物理内存


     page = alloc_page_vma(GFP_HIGHUSER, vma, addr);


     if (!page)


          goto no_mem;


     clear_user_highpage(page, addr);





     spin_lock(&mm->page_table_lock);


     page_table = pte_offset_map(pmd, addr);





     if (!pte_none(*page_table)) {


          pte_unmap(page_table);


          page_cache_release(page);


          spin_unlock(&mm->page_table_lock);


          goto out;


     }


     


              mm->rss++;


//为刚分得的page建立页表项


     entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,


                              vma->vm_page_prot)),


                     vma);


     lru_cache_add_active(page);


     mark_page_accessed(page);


     page_add_anon_rmap(page, vma, addr);


}


//如果是一个读操作,则将对应PTE映射到零页





set_pte(page_table, entry);


pte_unmap(page_table);





update_mmu_cache(vma, addr, entry);


spin_unlock(&mm->page_table_lock);


out:


return VM_FAULT_MINOR;


no_mem:


return VM_FAULT_OOM;


}





2):写时复制


从上面的过程可以看出.如果是在用户空间发生的读异常,只会指其映射到零页面.在fork()进程的时候,开始的时候子进程怀父进程是共享地址空间的.这些页面通常是只读的,如果在这些只读的页面执行写操作的时候,就会产生一个异常,内核如何处理呢?继续看代码:


上面说的这种情况对应是do_wp_page().





static int do_wp_page(struct mm_struct *mm, struct vm_area_struct * vma,


unsigned long address, pte_t *page_table, pmd_t *pmd, pte_t pte)


{


struct page *old_page, *new_page;





//将pte的值转换成物理页面号


unsigned long pfn = pte_pfn(pte);


pte_t entry;





//物理页面号不合法.出错.退出


if (unlikely(!pfn_valid(pfn))) {


     pte_unmap(page_table);


     printk(KERN_ERR "do_wp_page: bogus page at address %08lx\n",


               address);


     spin_unlock(&mm->page_table_lock);


     return VM_FAULT_OOM;


}


//将页面序号转换成page


old_page = pfn_to_page(pfn);





//判断旧页面是否被锁定


if (!TestSetPageLocked(old_page)) {


     //判断old_page是否只有一个进程在使用


     int reuse = can_share_swap_page(old_page);


     unlock_page(old_page);


     if (reuse) {


               //如果只有一个进程在使用,没必要重新分配页框,直接使用这个页框就行了


          flush_cache_page(vma, address);


          entry = maybe_mkwrite(pte_mkyoung(pte_mkdirty(pte)),


                         vma);


          ptep_set_access_flags(vma, address, page_table, entry, 1);


          update_mmu_cache(vma, address, entry);


          pte_unmap(page_table);


          spin_unlock(&mm->page_table_lock);


          return VM_FAULT_MINOR;


     }


}


pte_unmap(page_table);





if (!PageReserved(old_page))


     page_cache_get(old_page);


spin_unlock(&mm->page_table_lock);





if (unlikely(anon_vma_prepare(vma)))


     goto no_new_page;


//分配一个新的页面


new_page = alloc_page_vma(GFP_HIGHUSER, vma, address);


if (!new_page)


     goto no_new_page;


//将异常的页面拷贝到新页面


copy_cow_page(old_page,new_page,address);


spin_lock(&mm->page_table_lock);


//取得地址对应的PTE


page_table = pte_offset_map(pmd, address);


if (likely(pte_same(*page_table, pte))) {


     if (PageReserved(old_page))


          ++mm->rss;


     else


          page_remove_rmap(old_page);


     //break_cow:将page_table的映射指向刚才分得的新页面


     break_cow(vma, new_page, address, page_table);


     lru_cache_add_active(new_page);


     page_add_anon_rmap(new_page, vma, address);





     /* Free the old page.. */


     new_page = old_page;


}





// 释放new_page old_page


pte_unmap(page_table);


page_cache_release(new_page);


page_cache_release(old_page);


spin_unlock(&mm->page_table_lock);


return VM_FAULT_MINOR;





no_new_page:


page_cache_release(old_page);


return VM_FAULT_OOM;


}





到这为止,各种异常的处理差不多了.但忽略了堆栈空间的扩展.我们接着分析.这是我们这次分析的最后一个函数了^_^


int expand_stack(struct vm_area_struct *vma, unsigned long address)


{


     unsigned long grow;





     if (unlikely(anon_vma_prepare(vma)))


         return -ENOMEM;


     anon_vma_lock(vma);





      //将address按照PAGE_SIZE对齐


     address &= PAGE_MASK;


     //计数要增长的页面大小


     grow = (vma->vm_start - address) >> PAGE_SHIFT;





     //判断系统中内存是否足够


     if (security_vm_enough_memory(grow)) {


         anon_vma_unlock(vma);


         return -ENOMEM;


     }





     //是否超过了限制


     if (over_stack_limit(vma->vm_end - address) ||


              ((vma->vm_mm->total_vm + grow) 


              current->rlim[RLIMIT_AS].rlim_cur) {


         anon_vma_unlock(vma);


         vm_unacct_memory(grow);


         return -ENOMEM;


     }





     //更改vma 的映射范围


     vma->vm_start = address;


     vma->vm_pgoff -= grow;


     vma->vm_mm->total_vm += grow;


     if (vma->vm_flags & VM_LOCKED)


         vma->vm_mm->locked_vm += grow;


     __vm_stat_account(vma->vm_mm, vma->vm_flags, vma->vm_file, grow);


     anon_vma_unlock(vma);


     return 0;


}


总结:

由于do_page_fault代码中采用了大量的goto处理,使整个代码的可读性不太好,不过,先把标号的含义处理清楚,代码的逻辑流程是十分清晰的.以前经常在开发板上看到“SIGSEGV””do_page_fault”等错误只知道是内存方面的错误,现在终于知其然亦知其所以然了 ^_^



来源:http://blog.chinaunix.net/uid-20561320-id-3032220.html