Chinaunix首页 | 论坛 | 博客
  • 博客访问: 325064
  • 博文数量: 78
  • 博客积分: 1322
  • 博客等级: 中尉
  • 技术积分: 680
  • 用 户 组: 普通用户
  • 注册时间: 2010-04-14 13:24
文章分类
文章存档

2012年(20)

2011年(55)

2010年(3)

分类: LINUX

2011-12-01 22:39:40

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

         //取相应的页面项
         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”等错误只知道是内存方面的错误,现在终于知其然亦知其所以然了 ^_^
阅读(5843) | 评论(0) | 转发(0) |
0

上一篇:kgdb

下一篇:ext2文件系统磁盘数据分布

给主人留下些什么吧!~~