Chinaunix

标题: do_page_fault函数处理流程 [打印本页]

作者: cluter    时间: 2011-05-28 00:58
标题: do_page_fault函数处理流程
本帖最后由 cluter 于 2011-05-28 01:01 编辑

只解释下关键代码的流程,附件里是关于缺页处理思路更清晰的一个整理,无代码的。
  1. //*********************************缺页异常处理函数*******************************************
  2. do_page_fault(struct pt_regs *regs, unsigned long error_code)
  3. {

  4.        
  5.         //获取当前cpu正在运行的进程的进程描述符
  6.         //然后获取该进程的内存描述符
  7.         tsk = current;
  8.         mm = tsk->mm;

  9.         /* Get the faulting address: */
  10.         //获取出错的地址
  11.         address = read_cr2();


  12.         /*
  13.          * We fault-in kernel-space virtual memory on-demand. The
  14.          * 'reference' page table is init_mm.pgd.
  15.          *
  16.          * NOTE! We MUST NOT take any locks for this case. We may
  17.          * be in an interrupt or a critical region, and should
  18.          * only copy the information from the master page table,
  19.          * nothing more.
  20.          *
  21.          * This verifies that the fault happens in kernel space
  22.          * (error_code & 4) == 0, and that the fault was not a
  23.          * protection error (error_code & 9) == 0.
  24.          */
  25.          //页访问出错地址address在内核空间
  26.         if (unlikely(fault_in_kernel_space(address))) {
  27.                 //检查标志位确定访问发生在"内核态"
  28.                 if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {
  29.                         //如果是内核空间"非连续内存"的访问,
  30.                         //则直接拷贝"内核页表项"到"用户页表项"
  31.                         //如果"内核页表项"为null,说明内核有BUG,返回-1
  32.                         if (vmalloc_fault(address) >= 0)
  33.                                 return;
  34.                 }

  35.                 //如果在"用户态"则直接进入"非法访问"处理函数
  36.                 //如果vmalloc_fault返回-1,则表示内核BUG
  37.                 bad_area_nosemaphore(regs, error_code, address);
  38.                 //错误处理函数
  39.                 // 1 "用户态"错误-->直接终止进程
  40.                 // 2 "内核态"错误
  41.                 //                       系统调用参数错误 ---->终止进程/返回系统调用错误码
  42.                 //                       内核BUG                            ---->内核panic
  43.                 return;
  44.         }


  45.         /*
  46.          * If we're in an interrupt, have no user context or are running
  47.          * in an atomic region then we must not take the fault:
  48.          */
  49.          // 1 在中断中,此时没有进程上下文
  50.          // 2 在原子操作流程中
  51.          // 都不允许处理缺页异常
  52.         if (unlikely(in_atomic() || !mm)) {
  53.                 bad_area_nosemaphore(regs, error_code, address);
  54.                 return;
  55.         }

  56.         /*
  57.          * When running in the kernel we expect faults to occur only to
  58.          * addresses in user space.  All other faults represent errors in
  59.          * the kernel and should generate an OOPS.  Unfortunately, in the
  60.          * case of an erroneous fault occurring in a code path which already
  61.          * holds mmap_sem we will deadlock attempting to validate the fault
  62.          * against the address space.  Luckily the kernel only validly
  63.          * references user space from well defined areas of code, which are
  64.          * listed in the exceptions table.
  65.          *
  66.          * As the vast majority of faults will be valid we will only perform
  67.          * the source reference check when there is a possibility of a
  68.          * deadlock. Attempt to lock the address space, if we cannot we then
  69.          * validate the source. If this is invalid we can skip the address
  70.          * space check, thus avoiding the deadlock:
  71.          */
  72.          //此时可以确定缺页地址address在"用户空间"了
  73.         if (unlikely(!down_read_trylock(&mm->mmap_sem))) {
  74.                 //错误发生在"内核态",查看异常表
  75.                 //如果在内核态引起缺页,则引起缺页的"指令地址"一定在"异常表"中
  76.                 //如果"异常表"中返回指令地址,则说明可能是"请求调页",也可能是"非法访问"
  77.                 //如果"异常表"中无地址,则肯定是内核错误
  78.                 if ((error_code & PF_USER) == 0 &&
  79.                     !search_exception_tables(regs->ip)) {
  80.                        //内核panic
  81.                         bad_area_nosemaphore(regs, error_code, address);
  82.                         return;
  83.                 }
  84.                 down_read(&mm->mmap_sem);
  85.         } else {
  86.                 /*
  87.                  * The above down_read_trylock() might have succeeded in
  88.                  * which case we'll have missed the might_sleep() from
  89.                  * down_read():
  90.                  */
  91.                 might_sleep();
  92.         }
  93.         //寻找address所在的vma
  94.         vma = find_vma(mm, address);
  95.         //如果address之后无vma,则肯定是非法访问
  96.         if (unlikely(!vma)) {
  97.                 bad_area(regs, error_code, address);
  98.                 return;
  99.         }
  100.         // 1 如果vma->start_address<=address,则直接跳到 "合法访问"阶段
  101.         // 2 如果vma->start_address>address,则也有可能是用户的"入栈行为"导致缺页
  102.         if (likely(vma->vm_start <= address))
  103.                 goto good_area;
  104.         // "入栈"操作,则该vma的标志为 "向下增长"
  105.         if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
  106.                 bad_area(regs, error_code, address);
  107.                 return;
  108.         }
  109.         // 确定缺页发生在"用户态"
  110.         if (error_code & PF_USER) {
  111.                 /*
  112.                  * Accessing the stack below %sp is always a bug.
  113.                  * The large cushion allows instructions like enter
  114.                  * and pusha to work. ("enter $65535, $31" pushes
  115.                  * 32 pointers and then decrements %sp by 65535.)
  116.                  */
  117.                  //验证缺页address和栈顶sp的关系
  118.                 if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
  119.                         bad_area(regs, error_code, address);
  120.                         return;
  121.                 }
  122.         }
  123.         //扩展栈
  124.         if (unlikely(expand_stack(vma, address))) {
  125.                 bad_area(regs, error_code, address);
  126.                 return;
  127.         }

  128.         /*
  129.          * Ok, we have a good vm_area for this memory access, so
  130.          * we can handle it..
  131.          */
  132. good_area:
  133.         write = error_code & PF_WRITE;
  134.         // 再次验证"权限"
  135.         if (unlikely(access_error(error_code, write, vma))) {
  136.                 bad_area_access_error(regs, error_code, address);
  137.                 return;
  138.         }

  139.         /*
  140.          * If for any reason at all we couldn't handle the fault,
  141.          * make sure we exit gracefully rather than endlessly redo
  142.          * the fault:
  143.          */
  144.          //分配新"页框"
  145.         fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);

  146.         up_read(&mm->mmap_sem);
  147. }


  148. //*******************************访问权限验证函数********************************************
  149. access_error(unsigned long error_code, int write, struct vm_area_struct *vma)
  150. {       
  151.         //如果是"写操作"引起的缺页,则该vma必须可写
  152.         if (write) {
  153.                 /* write, present and write, not present: */
  154.                 if (unlikely(!(vma->vm_flags & VM_WRITE)))
  155.                         return 1;
  156.                 return 0;
  157.         }

  158.         /* read, present: */
  159.         //检查该页是否已经在RAM中,如果"特权位"置位表示页框在RAM中
  160.         //表示进程访问"有特权" 页框
  161.         if (unlikely(error_code & PF_PROT))
  162.                 return 1;

  163.         /* read, not present: */
  164.         //如果该页不在内存中,该线性区必须可"读"
  165.         if (unlikely(!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE))))
  166.                 return 1;

  167.         return 0;
  168. }
复制代码


顺便提出几个问题:
                                在"内核态"访问非连续内存时,为何要拷贝“内核页表项”到用“户页表项”,为何不直接使用“内核页表”?
                                对于一个4G的机器,用户进程的页框大部分分布在物理内存的哪个范围?
                                为什么当物理内存大于1G的时候,就开始划分出”高端内存“?
                                64bit的机器上不需要”高端内存“,这是为什么?
作者: amarant    时间: 2011-05-28 07:24
高端内存的作用是访问内核空间1g不能直接寻址之外的内存
折衷的办法就是
把1G划为直接映射的一部分和高端内存

64位机的内核空间能够足够大到寻址所有内存(虚拟地址空间不止4G)、
作者: cluter    时间: 2011-05-28 07:54
回复 2# amarant


    LS起这么早。。。关于为何使用高端内存,阁下理解的很透彻。。,第一个问题LS怎么看?
作者: amarant    时间: 2011-05-28 09:27
回复 3# cluter


    这个问题我说不准,就不敢胡说了。。
作者: tempname2    时间: 2011-05-28 12:51
第一个问题我还真没看懂是什么意思

第二个问题,我没有仔细研究过代码,这应该取决于内核向Buddy System索要内存时传入的flag参数。新版的代码已经改的乱七八糟了,我在ULK上看过的代码片断里参数好像包括了GFP_HIGHUSER,也就是说,优先在大于896M的物理内存里拿。

第三第四个问题,内核使用高端内存是因为内核拥有的虚拟地址大于物理内存。访问物理内存必需先将其映射于某一块虚拟地址段之上,而大多体系结构中,内核是与进程共用地址空间的。Linux下内核一般只占顶1G,资源有些紧张,能映射到的物理也就紧张。因此在其1G的虚拟地址空间之中,开辟一个小窗口动态映射“够不着”的那些物理内存。所以说,高端内存的出现是因为“内核虚拟地址空间 <  物理内存大小”。

如果“内核虚拟地址空间 >  物理内存大小”,就不会出现高端内存了。这时还可以分为两种情况,一为“内核虚拟地址空间太大”,比如64位操作系统;一为“物理内存太小”,比如32位操作系统时物理内存小于896M。
作者: cluter    时间: 2011-05-28 14:44
回复 5# tempname2


    呵呵,ls 2 3 问题都理解的很深刻。给进程分配的页框和进程的页表都优先在高端内存索取!!

   第一个问题其实就是:当进程系统调用进入内核态,为何不使用内核页表,而是把内核页表项拷贝到进程页表,然后继续访问。
作者: cluter    时间: 2011-05-29 21:27
回复 4# amarant


    其实,只需要逆向思考下就可以了,如果切换页表,系统必须做其他什么事情?
作者: hauto    时间: 2011-05-29 22:29
对第一个问题我的理解是:
每个进程都有一个页表,页表既有内核地址空间的,也有用户地址空间的。
但是,有些内核空间的只在 init_mm的页表中有,而当前进程没有,当访问这些地址的时候,就出现页中断,
这里把 init_mm的页表中的相关项拷贝到当前进程的页表中。这样再访问的时候,就不会页中断了。
作者: amarant    时间: 2011-05-30 10:53
我不了解X86页表在内存中的储存结构,如果X86中的页全局表项必须是以数组的方式顺序存储。那么就很好理解为什么要多此一举复制内核页表过来了。
作者: futex    时间: 2011-05-30 13:52
看代码是从init_mm.pgd上拷贝过去的,如果不拷贝而直接切换到init_mm_pgd上,那至少存在这个问题,内核中使用copy_from_user等访问进程用户地址空间的时候必然出错。另外执行signal处理函数时要利用程序在用户空间的栈来保存被冲掉的内核栈保存的内容,也必然出错。其原因在于init_mm.gpd上没有当前进程在用户地址空间的映射表。呵呵,不知俺说的对不对。
作者: futex    时间: 2011-05-30 14:44
"操作"是指执行内存映射吗? 如果是的话这个过程应该由mmu来进行吧? 不切换pgd怎么操作呢?
作者: futex    时间: 2011-05-30 15:58
另外,楼主的问题好像不是很正确,似乎只是复制到pmd_t中的内容,而没有继续复制pte_t, 也就是说页表其实是和init_mm.pgd共用的。每复制一次有4M地址空间的map,不算少了吧?
作者: cluter    时间: 2011-06-04 10:06
回复 8# hauto


    页表不管在物理内存的什么地址,都应该由内核管理,所以我们认为这种是内核态。所以不会出现切换时造成“缺页”。
    就是 肯定COPY了pte_t。要不然进程进入“内核态”,访问不存在的地址,肯定出错。
作者: cluter    时间: 2011-06-04 10:22
回复 10# futex


    阁下认识的还是满深刻的,如果不拷贝的话,使用内核页表,当再次访问进程空间时,映射就会丢失,因为内核没有进程空间的地址“映射表”。而系统调用经常需要用到进程空间的数据。
作者: unbutun    时间: 2011-08-18 00:31
只解释下关键代码的流程,附件里是关于缺页处理思路更清晰的一个整理,无代码的。

顺便提出几个问题:
...
cluter 发表于 2011-05-28 00:58



    hi, 这个附件失效了,能否再传一个,多谢了
作者: allen303allen    时间: 2011-08-21 17:10
对第一个问题,我的理解是:
内核维护的是一个“主内核页表”,所有的用户进程都是在这个主内核页表的基础上通过修改用户地址空间部分的页表来为自己所用,所以用户进程陷入内核时不需要切换页表,因为在内核态使用的页表其内容跟主内核页表是一样的(非缺页的情况)。
而内核在使用非连续物理内存时,就需要修改内核页表,此时内核修改的只是主内核页表,而不会修改其他用户进程的页表。此时当其他用户进程陷入内核后访问内核地址空间发生缺页中断时,就会判断如果不是其他的异常情况,就把主内核页表的内容拷贝到自己的页表中来(使自己页表的内核页表部分与主内核页表保持一致)。
这样,所有的用户进程在陷入内核态访问内核地址空间发生缺页时,都要这么做,因为他们自己页表的内核部分不是最新的,这就是为什么使用非连续物理内存的效率会比较低了。
作者: embeddedlwp    时间: 2011-09-06 17:33
回复 16# allen303allen


    allen303allen兄对内存管理的理解很透彻阿
作者: sky345824266    时间: 2015-08-07 10:44
附件访问不了, 无法打开




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2