缺页异常,页缺失
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;
}