Chinaunix首页 | 论坛 | 博客
  • 博客访问: 771133
  • 博文数量: 370
  • 博客积分: 2334
  • 博客等级: 大尉
  • 技术积分: 3222
  • 用 户 组: 普通用户
  • 注册时间: 2012-05-06 16:56
文章分类

全部博文(370)

文章存档

2013年(2)

2012年(368)

分类:

2012-05-18 11:39:51

------------------------------------------

本文系本站原创,欢迎转载!

转载请注明出处:http://ericxiao.cublog.cn/

------------------------------------------

  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);

     }

     //找到第一个结束地址大于addressVMA

     vma = find_vma(mm, address);

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

if (!vma)

         goto bad_area;

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

     if (vma->vm_start <= address)

         goto good_area;

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

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

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

         goto bad_area;

    

     if (error_code & 4) {

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

         //入栈一次是四个字节

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

         if (address + 32 < regs->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;

 

         //分别取当前pmdinit_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);

 

         //取相应的页面项

         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 < PAGE_SIZE)

         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_areabad_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 < 32)

              tsk->thread.screen_bitmap |= 1 << bit;

     }

     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:地址所对应的PTEPMD

*/

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) << PAGE_SHIFT) >

              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”等错误只知道是内存方面的错误,现在终于知其然亦知其所以然了 ^_^

阅读(669) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~