精华内容
下载资源
问答
  • 操作系统缺页异常
    2021-05-18 14:13:15

    缺页异常,页缺失

    Page fault,指的是硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。

    通常情况下,用于处理此中断的程序是操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。而如果访问是不被允许的,那么操作系统通常会结束相关的进程。

    虽然其名为“页缺失”错误,但实际上这并不一定是一种错误。而且这一机制对于利用虚拟内存来增加程序可用内存空间的操作系统(比如Microsoft Windows和各种类 Unix 系统)中都是常见且有必要的。

    详解

    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 并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页异常的写时复制;

    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、触发异常的线性地址处于用户空间的vma中,但还未分配物理页,如果访问权限OK的话内核给进程分配相应的物理页了  2、触发异常的线性地址不处于用户空间的vma中,这种...
  • 前面简单的分析了内核处理用户空间缺页异常的流程,进入到了handle_mm_fault()函数,该函数为触发缺页异常的地址address分配各级的页目录,也就是说现在已经拥有了一个和address配对的pte了,但是这个pte如何去映射...

    前面简单的分析了内核处理用户空间缺页异常的流程,进入到了handle_mm_fault()函数,该函数为触发缺页异常的地址address分配各级的页目录,也就是说现在已经拥有了一个和address配对的pte了,但是这个pte如何去映射物理页框,内核又得根据pte的状态进行分类和判断,而这个过程又会牵扯出一些其他的概念……这也是初读linux内核源码的最大障碍吧,在一些复杂的处理中,一个点往往可以延伸出一个面,容易让人迷失方向……因此后面打算分几次将这个函数分析完,自己也没有完全理解透,所以不到位的地方欢迎大家指出,一起交流~

    staticinlineinthandle_pte_fault(structmm_struct *mm,

    structvm_area_struct *vma, unsignedlongaddress,

    pte_t *pte, pmd_t *pmd, unsignedintflags)

    {

    pte_t entry;

    spinlock_t *ptl;

    entry = *pte;

    if(!pte_present(entry)) {//如果页不在主存中

    if(pte_none(entry)) {//页表项内容为0,表明进程未访问过该页

    if(vma->vm_ops) {

    if(likely(vma->vm_ops->fault))

    returndo_linear_fault(mm, vma, address,

    pte, pmd, flags, entry);

    }

    returndo_anonymous_page(mm, vma, address,

    pte, pmd, flags);

    }

    if(pte_file(entry))

    returndo_nonlinear_fault(mm, vma, address,

    pte, pmd, flags, entry);

    returndo_swap_page(mm, vma, address,

    pte, pmd, flags, entry);

    }

    ...

    ...

    }

    首先要确定的一点就是pte对应的页是否驻留在主存中,因为pte有可能之前映射了页,但是该页被换出了。上面的代码给出了pte对应的页没有驻留在主存中的情况。如果pte对应的页没有驻留在主存中,且没有映射任何页,即pte_present()返回0,pte_none()返回0,则要判断要分配一个匿名页还是一个映射页。在Linux虚拟内存中,如果页对应的vma映射的是文件,则称为映射页,如果不是映射的文件,则称为匿名页。两者最大的区别体现在页和vma的组织上,因为在页框回收处理时要通过页来逆向搜索映射了该页的vma。对于匿名页的逆映射,vma都是通过vma结构体中的vma_anon_node(链表节点)和anon_vma(链表头)组织起来,再把该链表头的信息保存在页描述符中;而映射页和vma的组织是通过vma中的优先树节点和页描述符中的mapping->i_mmap优先树树根进行组织的,具体可以参看ULK3。

    来看基于文件的映射的处理:

    staticintdo_linear_fault(structmm_struct *mm,structvm_area_struct *vma,

    unsignedlongaddress, pte_t *page_table, pmd_t *pmd,

    unsignedintflags, pte_t orig_pte)

    {

    pgoff_t pgoff = (((address & PAGE_MASK)

    - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

    pte_unmap(page_table);//如果page_table之前用来建立了临时内核映射,则释放该映射

    return__do_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);

    }

    关键函数__do_fault():

    staticint__do_fault(structmm_struct *mm,structvm_area_struct *vma,

    unsignedlongaddress, pmd_t *pmd,

    pgoff_t pgoff, unsignedintflags, pte_t orig_pte)

    {

    pte_t *page_table;

    spinlock_t *ptl;

    structpage *page;

    pte_t entry;

    intanon = 0;

    intcharged = 0;

    structpage *dirty_page = NULL;

    structvm_fault vmf;

    intret;

    intpage_mkwrite = 0;

    vmf.virtual_address = (void__user *)(address & PAGE_MASK);

    vmf.pgoff = pgoff;

    vmf.flags = flags;

    vmf.page = NULL;

    ret = vma->vm_ops->fault(vma, &vmf);//调用定义好的fault函数,确保将所需的文件数据读入到映射页

    if(unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))

    returnret;

    if(unlikely(PageHWPoison(vmf.page))) {

    if(ret & VM_FAULT_LOCKED)

    unlock_page(vmf.page);

    returnVM_FAULT_HWPOISON;

    }

    if(unlikely(!(ret & VM_FAULT_LOCKED)))

    lock_page(vmf.page);

    else

    VM_BUG_ON(!PageLocked(vmf.page));

    page = vmf.page;

    if(flags & FAULT_FLAG_WRITE) {//写访问

    if(!(vma->vm_flags & VM_SHARED)) {//私有映射,则要创建一个副本进行写时复制

    anon = 1;//标记为一个匿名映射

    if(unlikely(anon_vma_prepare(vma))) {//创建一个anon_vma实例给vma

    ret = VM_FAULT_OOM;

    gotoout;

    }

    page = alloc_page_vma(GFP_HIGHUSER_MOVABLE,//分配一个页

    vma, address);

    if(!page) {

    ret = VM_FAULT_OOM;

    gotoout;

    }

    if(mem_cgroup_newpage_charge(page, mm, GFP_KERNEL)) {

    ret = VM_FAULT_OOM;

    page_cache_release(page);

    gotoout;

    }

    charged = 1;

    if(vma->vm_flags & VM_LOCKED)

    clear_page_mlock(vmf.page);

    copy_user_highpage(page, vmf.page, address, vma);

    __SetPageUptodate(page);

    }else{

    if(vma->vm_ops->page_mkwrite) {

    inttmp;

    unlock_page(page);

    vmf.flags = FAULT_FLAG_WRITE|FAULT_FLAG_MKWRITE;

    tmp = vma->vm_ops->page_mkwrite(vma, &vmf);

    if(unlikely(tmp &

    (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {

    ret = tmp;

    gotounwritable_page;

    }

    if(unlikely(!(tmp & VM_FAULT_LOCKED))) {

    lock_page(page);

    if(!page->mapping) {

    ret = 0;

    unlock_page(page);

    gotounwritable_page;

    }

    }else

    VM_BUG_ON(!PageLocked(page));

    page_mkwrite = 1;

    }

    }

    }

    page_table = pte_offset_map_lock(mm, pmd, address, &ptl);

    if(likely(pte_same(*page_table, orig_pte))) {//确定没有竞争,也就是页表项中的内容和之前是一样的

    flush_icache_page(vma, page);

    entry = mk_pte(page, vma->vm_page_prot);//页表项指向对应的物理页

    if(flags & FAULT_FLAG_WRITE)

    entry = maybe_mkwrite(pte_mkdirty(entry), vma);

    if(anon) {

    inc_mm_counter(mm, anon_rss);

    page_add_new_anon_rmap(page, vma, address);//建立匿名页与第一个vma的逆向映射

    }else{

    inc_mm_counter(mm, file_rss);

    page_add_file_rmap(page);//建立页与vma的普通映射

    if(flags & FAULT_FLAG_WRITE) {

    dirty_page = page;

    get_page(dirty_page);

    }

    }

    set_pte_at(mm, address, page_table, entry);//修改page_table使其指向entry对应的页框

    update_mmu_cache(vma, address, entry);

    }else{

    if(charged)

    mem_cgroup_uncharge_page(page);

    if(anon)

    page_cache_release(page);

    else

    anon = 1;

    }

    pte_unmap_unlock(page_table, ptl);

    out:

    if(dirty_page) {

    structaddress_space *mapping = page->mapping;

    if(set_page_dirty(dirty_page))

    page_mkwrite = 1;

    unlock_page(dirty_page);

    put_page(dirty_page);

    if(page_mkwrite && mapping) {

    balance_dirty_pages_ratelimited(mapping);

    }

    if(vma->vm_file)

    file_update_time(vma->vm_file);

    }else{

    unlock_page(vmf.page);

    if(anon)

    page_cache_release(vmf.page);

    }

    returnret;

    unwritable_page:

    page_cache_release(page);

    returnret;

    }

    首先要做的就是调用vma->vm_ops中定义好的fault()函数,将所需的数据从文件读入到映射页中,该函数还会将vma插入到映射页的mapping->i_mmap优先树中。

    文件一般以共享的方式进行映射,接下来就要判断触发异常的操作是否包含写操作,如果是写操作并且该vma不是以共享的方式映射该页,则要进行写时复制,也就是创建一个新的页来供该vma读写,此时会申请一个匿名页,并将数据拷贝到该匿名页中。

    接下来就要计算出page对应的pte值是多少,并将page_table指向的pte以该值进行填充,这样就完成了页表项到物理页的映射

    再来看分配匿名页的处理

    staticintdo_anonymous_page(structmm_struct *mm,structvm_area_struct *vma,

    unsignedlongaddress, pte_t *page_table, pmd_t *pmd,

    unsignedintflags)

    {

    structpage *page;

    spinlock_t *ptl;

    pte_t entry;

    pte_unmap(page_table);

    if(check_stack_guard_page(vma, address) 

    returnVM_FAULT_SIGBUS;

    if(!(flags & FAULT_FLAG_WRITE)) {

    entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),

    vma->vm_page_prot));

    page_table = pte_offset_map_lock(mm, pmd, address, &ptl);

    if(!pte_none(*page_table))

    gotounlock;

    gotosetpte;

    }

    if(unlikely(anon_vma_prepare(vma)))//分配一个anon_vma实例

    gotooom;

    page = alloc_zeroed_user_highpage_movable(vma, address);

    if(!page)

    gotooom;

    __SetPageUptodate(page);

    if(mem_cgroup_newpage_charge(page, mm, GFP_KERNEL))

    gotooom_free_page;

    entry = mk_pte(page, vma->vm_page_prot);

    if(vma->vm_flags & VM_WRITE)

    entry = pte_mkwrite(pte_mkdirty(entry));

    page_table = pte_offset_map_lock(mm, pmd, address, &ptl);

    if(!pte_none(*page_table))

    gotorelease;

    inc_mm_counter(mm, anon_rss);

    page_add_new_anon_rmap(page, vma, address);//建立线性区和匿名页的反向映射

    setpte:

    set_pte_at(mm, address, page_table, entry);//设置page_table对应的pte

    update_mmu_cache(vma, address, entry);//更新MMU缓存

    unlock:

    pte_unmap_unlock(page_table, ptl);

    return0;

    release:

    mem_cgroup_uncharge_page(page);

    page_cache_release(page);

    gotounlock;

    oom_free_page:

    page_cache_release(page);

    oom:

    returnVM_FAULT_OOM;

    }

    匿名页分配的工作和__do_fault()中分配匿名页差不多,只不过前面多了一个读写的判断,如果是读的话,不会分配匿名页,而是让pte指向一个被0填充的页,这样就进一步推迟了页的分配。也许你会觉得奇怪,既然要读数据怎么可以分配一个事先准备好的全0的页,其实仔细想想就会明白,缺页异常处理进行到这里,一定是第一次访问相应的内存时才会触发,匿名页对应的一般都是堆,栈这些区域,对这些区域的访问一定先是写而不是读,所以对于这种操作本身就不正常,分配一个被0填充的页使用户进程读出来的都是0也许会更安全一些。

    如果不是这两种情况的话,也就是说pte_none()返回的是0,那就说明pte之前映射过页,只是该页已被换出

    如果该页之前是用来进行非线性文件映射的话,其处理的主体函数就是上面介绍过的__do_fault()

    staticintdo_nonlinear_fault(structmm_struct *mm,structvm_area_struct *vma,

    unsignedlongaddress, pte_t *page_table, pmd_t *pmd,

    unsignedintflags, pte_t orig_pte)

    {

    pgoff_t pgoff;

    flags |= FAULT_FLAG_NONLINEAR;

    if(!pte_unmap_same(mm, pmd, page_table, orig_pte))

    return0;

    if(unlikely(!(vma->vm_flags & VM_NONLINEAR))) {//确保vma具有非线性映射属性

    print_bad_pte(vma, address, orig_pte, NULL);

    returnVM_FAULT_SIGBUS;

    }

    pgoff = pte_to_pgoff(orig_pte);//获取映射的文件偏移

    return__do_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);

    }

    pte_to_pgoff()这个函数是和pgoff_to_pte()相对的一组操作。在非线性文件映射的页被换出时,其映射文件的偏移会以PAGE_SIZE为单位进行编码,存储到其pte中,所以当要重新换入该页时,要进行相应的解码计算出pgoff,再由__do_fault()进行处理!

    对于页没有驻留在主存的情况中的最后一种处理方式,do_swap_page(),留在下次再做分析!

    展开全文
  • 缺页异常

    千次阅读 2018-05-25 00:09:56
    页式管理机制通过页面目录,页面表,将每一个线性地址(虚拟地址)转换成物理地址,但并不是每一次CPU都能访问到相应的物理内存单元,因此这样映射便失败了,会产生缺页异常;页错误,又叫页缺失,计算机系统术语,...

     页式管理机制通过页面目录,页面表,将每一个线性地址(虚拟地址)转换成物理地址,但并不是每一次CPU都能访问到相应的物理内存单元,因此这样映射便失败了,会产生缺页异常;


    页错误,又叫页缺失,计算机系统术语,是指在引入分页机制的操作系统中,一个进程的代码和数据被放置在一个虚拟的地址空间中,地址空间按固定长度划分为好多。同时,物理内存也按固定长度划分为好多

    因为 物理内存小而 硬盘空间大,为了在内存里放置更多的 进程操作系统的设计者们决定把页映射到内存帧或硬盘上的 虚拟内存文件中。
    进程的可视范围是它自己的 地址空间,它并不知道某一页映射到内存里还是 硬盘上,进程只管自己运行。当 进程需要访问某一页时, 操作系统通过查看分页表,得知被请求的页在内存里还是 硬盘里。若在内存里,则进行地址翻译;若在 硬盘里,则发生 页缺失操作系统立即阻塞该 进程,将 硬盘里对应的页换入内存,然后使该进程就绪(可以继续运行)。


    首先明确下什么是缺页异常,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是直到实在不行的时候才会分配物理页




    展开全文
  • 缺页异常被触发通常有两种情况——1.程序设计的不当导致访问了非法的地址2.访问的地址是合法的,但是该地址还未分配物理页框下面解释一下第二种情况,这是虚拟内存管理的一个特性。尽管每个进程独立拥有3GB的可访问...

    缺页异常被触发通常有两种情况——

    1.程序设计的不当导致访问了非法的地址

    2.访问的地址是合法的,但是该地址还未分配物理页框

    下面解释一下第二种情况,这是虚拟内存管理的一个特性。尽管每个进程独立拥有3GB的可访问地址空间,但是这些资源都是内核开出的空头支票,也就是说进程手握着和自己相关的一个个虚拟内存区域(vma),但是这些虚拟内存区域并不会在创建的时候就和物理页框挂钩,由于程序的局部性原理,程序在一定时间内所访问的内存往往是有限的,因此内核只会在进程确确实实需要访问物理内存时才会将相应的虚拟内存区域与物理内存进行关联(为相应的地址分配页表项,并将页表项映射到物理内存),也就是说这种缺页异常是正常的,而第一种缺页异常是不正常的,内核要采取各种可行的手段将这种异常带来的破坏减到最小。

    缺页异常的处理函数为do_page_fault(),该函数是和体系结构相关的一个函数,缺页异常的来源可分为两种,一种是内核空间(访问了线性地址空间的第4个GB),一种是用户空间(访问了线性地址空间的0~3GB),以X86架构为例,先来看内核空间异常的处理。

    dotraplinkage void __kprobes

    do_page_fault(struct pt_regs *regs, unsigned long error_code)

    {

    struct vm_area_struct *vma;

    struct task_struct *tsk;

    unsigned long address;

    struct mm_struct *mm;

    int write;

    int fault;

    tsk = current; //获取当前进程

    mm = tsk->mm;  //获取当前进程的地址空间

    /* Get the faulting address: */

    address = read_cr2(); //读取CR2寄存器获取触发异常的访问地址

    ...

    ...

    if (unlikely(fault_in_kernel_space(address))) { //判断address是否处于内核线性地址空间

    if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) {//判断是否处于内核态

    if (vmalloc_fault(address) >= 0)//处理vmalloc异常

    return;

    if (kmemcheck_fault(regs, address, error_code))

    return;

    }

    /* Can handle a stale RO->RW TLB: */

    /*异常发生在内核地址空间但不属于上面的情况或上面的方式无法修正,

    则检查相应的页表项是否存在,权限是否足够*/

    if (spurious_fault(error_code, address))

    return;

    /* kprobes don't want to hook the spurious faults: */

    if (notify_page_fault(regs))

    return;

    /*

    * Don't take the mm semaphore here. If we fixup a prefetch

    * fault we could otherwise deadlock:

    */

    bad_area_nosemaphore(regs, error_code, address);

    return;

    }

    ...

    ...

    }

    该函数传递进来的两个参数--

    regs包含了各个寄存器的值

    error_code是触发异常的错误类型,它的含义如下

    /*

    * Page fault error code bits:

    *

    *   bit 0 ==    0: no page found   1: protection fault

    *   bit 1 ==    0: read access     1: write access

    *   bit 2 ==    0: kernel-mode access  1: user-mode access

    *   bit 3 ==               1: use of reserved bit detected

    *   bit 4 ==               1: fault was an instruction fetch

    */

    enum x86_pf_error_code {

    PF_PROT     =       1 <

    PF_WRITE    =       1 <

    PF_USER     =       1 <

    PF_RSVD     =       1 <

    PF_INSTR    =       1 <

    };

    首先要检查该异常的触发地址是不是位于内核地址空间 也就是address>=TASK_SIZE_MAX,一般为3GB。然后要检查触发异常时是否处于内核态,满足这两个条件就尝试通过vmalloc_fault()来解决这个异常。由于使用vmalloc申请内存时,内核只会更新主内核页表,所以当前使用的进程页表就有可能因为未与主内核页表同步导致这次异常的触发,因此该函数试图将address对应的页表项与主内核页表进行同步

    static noinline int vmalloc_fault(unsigned long address)

    {

    unsigned long pgd_paddr;

    pmd_t *pmd_k;

    pte_t *pte_k;

    /* 确定触发异常的地址是否处于VMALLOC区域*/

    if (!(address >= VMALLOC_START && address 

    return -1;

    /*

    * Synchronize this task's top level page-table

    * with the 'reference' page table.

    *

    * Do _not_ use "current" here. We might be inside

    * an interrupt in the middle of a task switch..

    */

    pgd_paddr = read_cr3();//获取当前的PGD地址

    pmd_k = vmalloc_sync_one(__va(pgd_paddr), address);//将当前使用的页表和内核页表同步

    if (!pmd_k)

    return -1;

    /*到这里已经获取了内核页表对应于address的pmd,并且将该值设置给了当前使用页表的pmd,

    最后一步就是判断pmd对应的pte项是否存在*/

    pte_k = pte_offset_kernel(pmd_k, address);//获取pmd对应address的pte项

    if (!pte_present(*pte_k))//判断pte项是否存在,不存在则失败

    return -1;

    return 0;

    }

    同步处理:

    static inline pmd_t *vmalloc_sync_one(pgd_t *pgd, unsigned long address)

    {

    unsigned index = pgd_index(address);

    pgd_t *pgd_k;

    pud_t *pud, *pud_k;

    pmd_t *pmd, *pmd_k;

    pgd += index; //记录当前页表pgd对应address的偏移

    pgd_k = init_mm.pgd + index;//记录内核页表对应address的偏移

    if (!pgd_present(*pgd_k))//内核PGD页表对应的项不存在,则无法进行下一步,返回NULL

    return NULL;

    /*

    * set_pgd(pgd, *pgd_k); here would be useless on PAE

    * and redundant with the set_pmd() on non-PAE. As would

    * set_pud.

    */

    /*获取当前页表对应address的PUD地址和内核页表对应address的地址,并判断pud_k对应的项是否存在*/

    pud = pud_offset(pgd, address);

    pud_k = pud_offset(pgd_k, address);

    if (!pud_present(*pud_k))

    return NULL;

    /*对pmd进行和上面类似的操作*/

    pmd = pmd_offset(pud, address);

    pmd_k = pmd_offset(pud_k, address);

    if (!pmd_present(*pmd_k))

    return NULL;

    if (!pmd_present(*pmd))//当前使用页表对应的pmd项不存在,则修正pmd项使其和内核页表的pmd_k项相同

    set_pmd(pmd, *pmd_k);

    else

    BUG_ON(pmd_page(*pmd) != pmd_page(*pmd_k));

    return pmd_k;

    }

    如果do_page_fault()函数执行到了bad_area_nosemaphore(),那么就表明这次异常是由于对非法的地址访问造成的。在内核中产生这样的结果的情况一般有两种:

    1.内核通过用户空间传递的系统调用参数,访问了无效的地址

    2.内核的程序设计缺陷

    第一种情况内核尚且能通过异常修正机制来进行修复,而第二种情况就会导致OOPS错误了,内核将强制用SIGKILL结束当前进程。

    内核态的bad_area_nosemaphore()的实际处理函数为bad_area_nosemaphore()-->__bad_area_nosemaphore()-->no_context()

    static noinline void

    no_context(struct pt_regs *regs, unsigned long error_code,

    unsigned long address)

    {

    struct task_struct *tsk = current;

    unsigned long *stackend;

    unsigned long flags;

    int sig;

    /* Are we prepared to handle this kernel fault? */

    /*fixup_exception()用于搜索异常表,并试图找到一个对应该异常的例程来进行修正,

    这个例程在fixup_exception()返回后执行*/

    if (fixup_exception(regs))

    return;

    /*

    * 32-bit:

    *

    *   Valid to do another page fault here, because if this fault

    *   had been triggered by is_prefetch fixup_exception would have

    *   handled it.

    *

    * 64-bit:

    *

    *   Hall of shame of CPU/BIOS bugs.

    */

    if (is_prefetch(regs, error_code, address))

    return;

    if (is_errata93(regs, address))

    return;

    /*

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

    * terminate things with extreme prejudice:

    */

    /* 走到这里就说明异常确实是由于内核的程序设计缺陷导致的了,内核将

    产生一个oops,下面的工作就是打印CPU寄存器和内核态堆栈的信息到控制台并

    终结当前的进程*/

    flags = oops_begin();

    show_fault_oops(regs, error_code, address);

    stackend = end_of_stack(tsk);

    if (*stackend != STACK_END_MAGIC)

    printk(KERN_ALERT "Thread overran stack, or stack corrupted

    ");

    tsk->thread.cr2      = address;

    tsk->thread.trap_no  = 14;

    tsk->thread.error_code   = error_code;

    sig = SIGKILL;

    if (__die("Oops", regs, error_code))

    sig = 0;

    /* Executive summary in case the body of the oops scrolled away */

    printk(KERN_EMERG "CR2: %016lx

    ", address);

    oops_end(flags, regs, sig);

    }

    展开全文
  • 用户空间的缺页异常可以分为两种情况--1、触发异常的线性地址处于用户空间的vma中,但还未分配物理页,如果访问权限OK的话内核就给进程分配相应的物理页了2、触发异常的线性地址不处于用户空间的vma中,这种情况得...
  • 缺页异常被触发通常有两种情况——1.程序设计的不当导致访问了非法的地址2.访问的地址是合法的,但是该地址还未分配物理页框下面解释一下第二种情况,这是虚拟内存管理的一个特性。尽管每个进程独立拥有3GB的可访问...
  • Windows内存管理学习笔记(三)—— 无处不在的缺页异常前言缺页异常实验一:设置虚拟内存 前言 一、学习自滴水编程达人中级班课程,官网:https://bcdaren.com 二、海东老师牛逼! 缺页异常 描述: 当CPU访问一...
  • 缺页中断与缺页异常

    千次阅读 2020-07-04 21:40:57
    缺页异常的情况: 1线性地址不在虚拟地址空间中 2线性地址在虚拟地址空间中,但没有访问权限 3接上一条,没有与物理地址建立映射关系 4fork等系统调用时并没有映射物理页,写数据->缺页异常->写时拷贝 5映射...
  • 如果进程访问的虚拟地址空间部分尚未与页帧关联,处理器自动地引发一个缺页异常,内核必须处理此异常。 缺页处理的实现因处理器的不同而有所不同。由于CPU采用了不同的内存管理概念,生成缺页异常的细节也不太相同。...
  • 操作系统第五章课后题 缺页异常处理过程 在页式虚存管理系统中,设页面大小为26,页表内容见下表。 现访问虚地址:(233)8和(345)8。 问是否会发生缺页(页故障)异常? 若会则简述缺页异常处理过程。 否则将虚...
  • 深入Linux内核架构笔记(缺页异常

    千次阅读 2022-01-28 10:36:11
    如果进程访问 的虚拟地址空间部分尚未与页帧关联,处理器自动地引发一个缺页异常,内核必须处理此异常。这是 内存管理中最重要、最复杂的方面之一,因为必须考虑到无数的细节。例如,内核必须确定以下情况。  ...
  • 缺页异常详解

    千次阅读 2018-09-04 20:22:20
    首先明确下什么是缺页异常,CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、IO设备等等,但从CPU发出的访问地址并非是这些外设在地址总线上的物理地址,而是一个虚拟地址,由MMU将虚拟地址转换...
  • 在程序的执行过程中,因为遇到某种障碍而使 CPU 无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射的时候,CPU会产生一次缺页异常,从而进行相应的缺页异常处理。基于 CPU 的这一特性,Linux ...
  • //该分支表明发生缺页时是发生在访问内核空间时 if(unlikely(address >= TASK_SIZE)) { //该分支表示发生缺页异常时,代码是在内核态访问内核态不存在 //的地址,转到vmalloc_fault处理分支,可能是访问了不连续...
  • linux缺页异常详解

    2013-08-28 10:05:51
    全面描述linux arm缺页异常的处理原理和处理方式,结合原理的解释描述以及全部相关的源码注释,深刻理解缺页异常
  • (41)缺页异常简介

    2020-10-22 23:45:26
    一、什么情况下会引起缺页异常 很简单,PTE的P=0时,就会引发缺页异常缺页异常有4种情形,见下图: 不常使用的线性地址可能是没有挂物理页的,当我们访问这个地址,由于PTE P=0,会触发缺页异常,程序跳转到异常...
  • 缺页异常
  • 参考:
  • 缺页异常处理

    2018-03-08 10:49:00
    1 缺页异常在linux内核处理中占有非常重要的位置,很多linux特性,如写时复制,页框延迟分配,内存回收中的磁盘和内存交换,都需要借助缺页异常来进行,缺页异常处理程序主要处理以下四种情形:1请求调页: ...
  • 浅谈操作系统内核的缺页异常(Page Fault) 缺页中断就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。在这个时候,被内存映射的文件实际上成了一个分页交换文件。 页缺失(英语:Page fault,又名硬...
  • 缺页异常是很常见的现象,但是其来源有两种,一种是真实的异常,这是由于内存访问的地址未分配并未映射而产生的访问了非法地址的情况;另外一种是虚拟内存已经分配出去了,但是实际上的物理内存并未映射分配而产生的...
  • Linux缺页异常处理

    千次阅读 2018-11-01 14:32:47
    1.如果访问的虚拟地址在进程空间没有对应的VMA(mmap和malloc可以分配vma),则缺页处理失败,程序出现段错误. 2.Linux把没有映射到文件的映射叫做匿名映射(malloc和mmap的匿名映射)。 3.remap_pfn_range把内核...
  • 缺页异常(WHO I AM?)

    2020-09-22 17:43:56
    当进程需要访问某一页时, 操作系统通过查看页表核对此项是否在物理内存中,若不在物理内存中,就触发了缺页异常( Page Fault)。此时,由于CPU没有数据就无法进行计算,CPU罢工了用户进程也就出现了缺页中断,...
  • 缺页异常理解

    2021-06-30 17:15:57
    缺页异常并不可怕,只要CPU要的虚拟地址经过MMU的一番寻址之后没有找到或者找到后无权限,就会出现缺页异常,因此触发异常后的处理流程将是重点内容。 5.2 缺页错误的分类处理 缺页中断会交给PageFaultHandler处理...
  • linux 内存 缺页异常

    2022-03-14 13:52:04
    产生缺页异常中断的几种情况 当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程 当MMU中确实没有创建...
  • 缺页中断和缺页异常

    千次阅读 2018-02-02 12:33:00
    缺页异常 会出现缺页异常的情况: 线性地址不在虚拟地址空间中 线性地址在虚拟地址空间中,但没有访问权限 接上一条,没有与物理地址建立映射关系 fork等系统调用时并没有映射物理页,写数据->缺页...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,506
精华内容 7,402
关键字:

缺页异常

友情链接: ILI9225.zip