Page Fault缺页中断
缺页中断的整体流程
缺页中断要处理的场景有:
1) 栈扩展的时候要进行缺页中断,特征是 address在vma->start下面(想想APUE上面那张内存布局的图)。
2)正常malloc出来的内存,address在一个vma中间。
3 ) 中断代码执行的时候遇到了缺页。
这个操作是CPU架构相关的,代码在arch/i386mm/fault.c
伪代码:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) { /* * 当发生页面异常的时候,CPU将导致映射失败的线性地址放入控制寄存器CR2中. * 同时, 内核的中断机制会把现场(各个寄存器的值)保存下来(参数regs), error_code进一步指明失败的具体原因. */ __asm__("movl %%cr2,%0":"=r" (address)); //取出CR2到address变量中 /* * 取出当前进程的task_struct结构, current是一个宏: * _asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL)); */ tsk = current; mm = tsk->mm; /* * 检查当前的pagefault是否于一个进程关联了. * in_interrupt返回1说明正在一个中断服务程序中发生了pagefault,而于特定的进程无关联. * 我们主要看和进程有关联的pagefault,也就是用户层分配内存导致了pagefault. */ if (in_interrupt() || !mm) goto no_context; // 接下来的操作需要互斥 down(&mm->mmap_sem); /* * 找到异常地址所属于的vma * 如果找不到,说明用户程序越界了. */ vma = find_vma(mm, address); if (vma->vm_start <= address) // 找到了address对应的vma,虚拟内存的映射已经建立,仍然有pagefault说明还没有进行物理内存的映射 goto good_area; if (!(vma->vm_flags & VM_GROWSDOWN)) // vma_end一直向上找,直到最上面的栈区,说明要寻找的vma是mmap区域的一个vma,不过已经被unmmap掉了。 goto bad_area; // 再看bad_area bad_area: up(&mm->mmap_sem); // 释放锁 bad_area_nosemaphore: /* * 通过error_code检查本次的异常是由内核引发的还是用户态的程序引发的。 * 设置进程的状态,然后强制向进程发送一个SIGSEGV的信号。然后结束了。 */ if (error_code & 4) { tsk->thread.cr2 = address; tsk->thread.error_code = error_code; tsk->thread.trap_no = 14; info.si_signo = SIGSEGV; info.si_errno = 0; /* info.si_code has been set above */ info.si_addr = (void *)address; force_sig_info(SIGSEGV, &info, tsk); return; } }
每次从中断/异常返回之前,都要检查当前是否由悬而未决的信号需要处理。
缺页中断场景1 - 进程中栈的扩展
分析当发生函数调用进行push参数的时候,栈的空间页不够用的情形,也就是最普通的pagefault情形。 代码在do_page_fault:151开始。
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) { if (error_code & 4) { /* * 正常情况下push指令会往栈里塞4个字节的数据,检查越界的条件是 address<%esp-4 * 次处多给address留了32字节,原因是i386的pusha指令会把8个32位寄存器32个字节的内容压入栈,所以有可能缺少32字节。 */ if (address + 32 < regs->esp) goto bad_area; } if (expand_stack(vma, address)) goto bad_area; good_area: info.si_code = SEGV_ACCERR; write = 0; /* * 根据error_code 进一步判断。次处的bit0是0, bit1是1(允许写入) 所以进入case 2 */ switch (error_code & 3) { default: /* 3: write, present */ case 2: /* write, not present */ if (!(vma->vm_flags & VM_WRITE)) goto bad_area; write++; break; case 1: /* read, present */ goto bad_area; case 0: /* read, not present */ if (!(vma->vm_flags & (VM_READ | VM_EXEC))) goto bad_area; } /* * 开始真正的pagefault了!!! */ switch (handle_mm_fault(mm, vma, address, write)) { case 1: tsk->min_flt++; break; case 2: tsk->maj_flt++; break; case 0: goto do_sigbus; default: goto out_of_memory; } }
1)先来看一下栈对应vma的扩展
2)栈的扩展代码在include/linux/mm.h 可见这是一个通用的操作和架构无关。
static inline int expand_stack(struct vm_area_struct * vma, unsigned long address) { unsigned long grow; address &= PAGE_MASK; // 把地址按页对齐,address是所属页的下面的边界 grow = (vma->vm_start - address) >> PAGE_SHIFT; // 所需要扩展的页面数 /* * 检查限制。每个进程的task_struct结构里都有一个rlim数组,里面有各种对资源的限制 */ if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur || ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur) return -ENOMEM; // 更新vma的start为address所在页面的低边界 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; return 0; }
1)缺页中断的任务是建立从虚拟内存到物理内存的映射,映射过程中的页表可能也没有分配。所以要逐级的检查页表是否为空。
2) 页面的分配也是一次分配一个页面。
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, int write_access) { int ret = -1; pgd_t *pgd; pmd_t *pmd; // 根据mm中的pgd和address的高10位取出pgd的值 // 页目录之占1页,pgd永远是可以取出来的,就是mm->pgd + address<<PGD_SHIT 永远合法地址 pgd = pgd_offset(mm, address); // i386的页式是分两层的,pmd和pgd的值一样,此处直接返回 pmd = pmd_alloc(pgd, address); // 缺页中断发生,pmd和pgd内容一样的,指向pgd页目录的一项,其内容是0,因为此时pte没有分配。 if (pmd) { pte_t * pte = pte_alloc(pmd, address); if (pte) ret = handle_pte_fault(mm, vma, address, write_access, pte); } return ret; }
1)缺页中断过程中分配pte,一次分配一个页面(下次落在这个pte上的地址就不为空了)。
2) set_pmd 把新的pte地址反向填入上一层的pmd。
3)返回page+address,这个address是在pte这一层的偏移。
extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address) { address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1); unsigned long page = (unsigned long) get_pte_fast(); if (!page) return get_pte_slow(pmd, address); // 分配成功一个pte页后,将新页的地址反向添入上一层pmd里 set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page))); // 返回address在新分配的pte偏移量 return (pte_t *)page + address; }
1)因为内存还没分配出来,所以由pte_alloc分配返回的address在pte的偏移上内容是0(指向0).
2) do_swap_page 和mmap相关,暂时跳过。
static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, int write_access, pte_t * pte) { pte_t entry; spin_lock(&mm->page_table_lock); entry = *pte; if (!pte_present(entry)) { spin_unlock(&mm->page_table_lock); if (pte_none(entry)) return do_no_page(mm, vma, address, write_access, pte); return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access); } }
1)do_no_page 处理缺少的页的处理回调。
2)如果是通过mmap的方式要调用文件系统的函数建立映射
3)此处是正常的内存分配,也就是匿名页
static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma, unsigned long address, int write_access, pte_t *page_table) { struct page * new_page; pte_t entry; if (!vma->vm_ops || !vma->vm_ops->nopage) return do_anonymous_page(mm, vma, page_table, write_access, address); }
1)这一层分配一个页面,反向填入上一层pte的位子上。
static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table, int write_access, unsigned long addr) { struct page *page = NULL; // 先假设申请的页是只读的,映射到zeropage。内核中分配了一个1024的long数组,并初始化为0,所有的只读页都会映射到这个。写的时候发生COW pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot)); if (write_access) { // 如果写操作发生,则真真的申请一个物理页 page = alloc_page(GFP_HIGHUSER); if (!page) return -1; clear_user_highpage(page, addr); entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot))); mm->rss++; flush_page_to_ram(page); } // 回填pte的指针 set_pte(page_table, entry); update_mmu_cache(vma, addr, entry); return 1; }