一文透彻了解缺页异常

缺页异常处理流程图解

首先明确下什么是缺页异常,CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU将虚拟地址转换成物理地址再从地址总线上发出,MMU上的这种虚拟地址和物理地址的转换关系是需要创建的,并且MMU还可以设置这个物理页是否可以进行写操作,当没有创建一个虚拟地址到物理地址的映射,或者创建了这样的映射,但那个物理页不可写的时候,MMU将会通知CPU产生了一个缺页异常。

下面总结下缺页异常的几种情况:

1、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区vma的时候,可以肯定这是一个编码错误,这将杀掉该进程;

2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页异常,并且可能是栈溢出导致的缺页异常;

3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将如上面的2的情况产生缺页异常,若先进行读操作虽也会产生缺页异常,将被映射给默认的零页(zero_pfn),等再进行写操作时,仍会产生缺页异常,这次必须分配物理页了,进入写时复制的流程;

4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,“它的”vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页异常的写时复制 ;

目前来看,应该就是这四种情况,还是比较清晰的,可发现一个重要规律就是,linux是直到实在不行的时候才会分配物理页,把握这个原则理解的会好一些,下面详细的看缺页处理:

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

arm的缺页处理函数为arch/arm/mm/fault.c文件中的do_page_fault函数,关于缺页异常是怎么一步步调到这个函数的,同上一篇位置进程地址空间创建说的一样,后面会有专题文章描述这个问题,现在只关心缺页异常的处理,下面是函数do_page_fault :

static int __kprobes

do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)

{

         struct task_struct *tsk;

         struct mm_struct *mm;

         int fault, sig, code;

    /*空函数*/

         if (notify_page_fault(regs, fsr))

                   return 0;

    /*获取到缺页异常的进程描述符和其内存描述符*/

         tsk = current;

         mm  = tsk->mm;

         /*

          * If we're in an interrupt or have no user

          * context, we must not take the fault..

          */

         /*1、判断当前是否是在原子操作中(中断、可延迟函数、临界区)发生的异常

      2、通过mm是否存在判断是否是内核线程,对于内核线程,进程描述符的mm总为NULL

      一旦成立,说明是在内核态中发生的异常,跳到标号no_context*/

         if (in_atomic() || !mm)

                   goto no_context;

         /*

          * As per x86, we may deadlock here.  However, since the kernel only

          * validly references user space from well defined areas of the code,

          * we can bug out early if this is from code which shouldn't.

          */

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

                   if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))

                            goto no_context;

                   down_read(&mm->mmap_sem);

         } else {

                   /*

                    * The above down_read_trylock() might have succeeded in

                    * which case, we'll have missed the might_sleep() from

                    * down_read()

                    */

                   might_sleep();

#ifdef CONFIG_DEBUG_VM

                   if (!user_mode(regs) &&

                       !search_exception_tables(regs->ARM_pc))

                            goto no_context;

#endif

         }

         fault = __do_page_fault(mm, addr, fsr, tsk);

         up_read(&mm->mmap_sem);

         /*

          * Handle the "normal" case first - VM_FAULT_MAJOR / VM_FAULT_MINOR

          */

         /*如果返回值fault不是这里面的值,那么应该会是VM_FAULT_MAJOR或VM_FAULT_MINOR,说明问题解决了,返回,一般正常情况下,__do_page_fault的返回值fault会是0(VM_FAULT_MINOR)或者其他一些值,都不是下面之后会看到的这些*/

         if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS))))

                   return 0;

         /*如果fault是VM_FAULT_OOM这个级别的错误,那么这要杀掉进程*/

         if (fault & VM_FAULT_OOM) {

                   /*

                    * We ran out of memory, call the OOM killer, and return to

                    * userspace (which will retry the fault, or kill us if we

                    * got oom-killed)

                    */

                   pagefault_out_of_memory();

                   return 0;

         }

         /*

          * If we are in kernel mode at this point, we

          * have no context to handle this fault with.

          */

         /*再次判断是否是内核空间出现了页异常,并且通过__do_page_fault没有没有解决,跳到到no_context*/

         if (!user_mode(regs))

                   goto no_context;

         /*下面两个情况,通过英文注释可以理解,

           一个是无法修复,另一个是访问非法地址,都是要杀掉进程的错误*/

         if (fault & VM_FAULT_SIGBUS) {

                   /*

                    * We had some memory, but were unable to

                    * successfully fix up this page fault.

                    */

                   sig = SIGBUS;

                   code = BUS_ADRERR;

         } else {

                   /*

                    * Something tried to access memory that

                    * isn't in our memory map..

                    */

                   sig = SIGSEGV;

                   code = fault == VM_FAULT_BADACCESS ?

                            SEGV_ACCERR : SEGV_MAPERR;

         }

         /*给用户进程发送相应的信号,杀掉进程*/

         __do_user_fault(tsk, addr, fsr, sig, code, regs);

         return 0;

no_context:

    /*内核引发的异常处理,如修复不畅,内核也要杀掉*/

         __do_kernel_fault(mm, addr, fsr, regs);

         return 0;

}

首先看第一个重点,源码片段如下 :

/*1、判断当前是否是在原子操作中(中断、可延迟函数、临界区)发生的异常

2、通过mm是否存在判断是否是内核线程,对于内核线程,进程描述符的mm总为NULL,一旦成立,说明是在内核态中发生的异常,跳到标号no_context*/

if (in_atomic() || !mm)

                   goto no_context;

如果当前执行流程在内核态,不论是在临界区(中断/推后执行/临界区)还是内核进程本身(内核的mm为NULL),说明在内核态出了问题,跳到标号no_context进入内核态异常处理,由函数__do_kernel_fault完成,这个函数首先尽可能的设法解决这个异常,通过查找异常表中和目前的异常对应的解决办法并调用执行,这个部分的细节一直没有找到在哪里,如果找到的话留言告我一下吧!如果无法通过异常表解决,那么内核就要在打印其页表等内容后退出了!其源码如下 :

static void

__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,

                     struct pt_regs *
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值