页式存储管理机制通过页面目录和页面表将每个线性地址(也可以理解为虚拟地址)转换成物理地址。如果在这个过程中遇到某种阻碍而是CPU无法最终访问到相应的物理内存单元,映射便失败了,而当前的指令也就不能执行完成。此时CPU会产生一个页面错误(page fault)异常(exception)(也称缺页中断),进而执行预定的页面异常处理程序,使应用程序得意从映射失败而暂停的指令处开始恢复执行,或进行一些善后处理。这里所说的阻碍可以有以下几种情况:
1) 相应的页面目录或页面表项为空,也就是该线性地址与物理地址的映射尚未建立,或者已撤销。
2)相应的物理页面不在内存中。
3)指令中规定的访问方式与页面权限不符,例如企图写一个“只读”页面。
当CPU的运行已经到达了页面异常服务程序的主体do_page_fault()的入口处。
fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
/*pt_regs结构指针regs,它指向CPU发生前夕的各寄存器内容的一个副本;这是由内核的中断响应机制保存下来的"现场"
error_code则进一步指明映射失败的具体原因。*/
struct task_struct *tsk;
struct mm_struct *mm;
struct vm_area_struct * vma;
unsigned long address;
unsigned long page;
int write;
siginfo_t info;
/* get the address */
/**
* 读取引起异常的线性地址。CPU控制单元把这个值存放在cr2控制寄存器中。
*/
__asm__("movl %%cr2,%0":"=r" (address));
if (notify_die(DIE_PAGE_FAULT, "page fault", regs, error_code, 14,SIGSEGV) == NOTIFY_STOP)
return;
/* It's safe to allow irq's after cr2 has been saved */
/**
* 只在保存了cr2就可以打开中断了。
* 如果中断发生前是允许中断的,或者运行在虚拟8086模式,就打开中断。
*/
if (regs->eflags & (X86_EFLAGS_IF|VM_MASK))
local_irq_enable();
tsk = current; //取得当前进程的task_struct结构的地址
info.si_code = SEGV_MAPERR;
if (in_atomic() || !mm)
goto bad_area_nosemaphore;
vma = find_vma(mm, address);
/**
* 如果vma为空,说明在出错地址后面没有线性区了,说明错误的地址肯定是无效的。
*/
if (!vma)
goto bad_area;
/**
* vma在address后面,并且它的起始地址在address前面,说明线性区包含了这个地址。
* 谢天谢地,这很可能不是真的错误,可能是COW机制起作用了,也可能是需要调页了。
*/
if (vma->vm_start <= address)//表示映射已经建立
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN)) 当VM_GROWSDOWN为0的话。表示此区间不是堆栈区。
goto bad_area;
* 运行到此,说明address地址后面的vma有VM_GROWSDOWN标志,表示它是一个堆栈区
* 请注意,如果是内核态访问用户态的堆栈空间,就应该直接扩展堆栈,而不判断if (address + 32 < regs->esp)
*/
if (error_code & 4) {
/*
* accessing the stack below %esp is always a bug.
* The "+ 32" is there due to some instructions (like
* pusha) doing post-decrement on the stack and that
* doesn't show up until later..
*/
/**
* 虽然下一个线性区是堆栈,可是离非法地址太远了,不可能是操作堆栈引起的错误
* xie.baoyou注:32而不是4是考虑到pusha的存在。
*/
if (address + 32 < regs->esp)
goto bad_area;
}
/**
* 线程堆栈空间不足,就扩展一下,一般会成功的,不会运行到bad_area.
* 注意:如果异常发生在内核态,说明内核正在访问用户态的栈,就直接扩展用户栈。
*/
if (expand_stack(vma, address))
goto bad_area;
当运行到expand_stack()时,表示属于正常的堆栈扩展请求,那就从缺页的地方开始分配若干页面并建立映射,并将其并入堆栈区间,使其得以扩展。
expand_stack()中的操作(不粘源码了):
将地址按页面边界对齐,并计算需要增长几个页面才能把给定的地址包括进去(通常不是一个)。
在rlim结构数组,规定对各种资源分配使用的限制,所以需要进行检查;如果扩展后以后的空间大小超过了可用于堆栈的资源,或者使动态分配的页面总量超过了可用于该进程的资源限制,那就不能扩展,就会返回一个负的出错代码-ENOMEME,在do_page_fault()中也会返回bad_area;
在正常的请求下(改变了堆栈的结构数据),将转交给good_area完成(完成对新扩展的页面对物理内存的映射)。
good_area:先检查权限问题(堆栈可写),然后采用相应函数进行页面的分配。再分配页面表的时候,先看缓冲池(内核将释放的页面表会先保存在内存池中)中是否为空;
空了通过handle_pte_fault()来分配。-》do_no_page()-》do_anonymous_page()-》alloc_pages()为其分配一个的物理内存页面,通过set_pte()将分配到物理页面连同所有状态及标志位设置进page_table所指的页面表项。至此映射成功。
(中间有一部分代码因为在此情景不涉及没有说明)
特别指出,当CPU从一次页面错误异常处理返回到用户空间时,将会先重新执行因映射失败而中途夭折的那条指令,然后才继续往下执行。(不同于中断)
查看失败的具体原因。(bad_area)
bad_area_nosemaphore:
/* User mode accesses just cause a SIGSEGV */
/**
* 发生在用户态的错误地址。
* 就发生一个SIGSEGV信号给current进程,并结束函数。
*/
if (error_code & 4) {
/*
* Valid to do another page fault here because this one came
* from user space.
*/
if (is_prefetch(regs, address, error_code))
return;
tsk->thread.cr2 = address;
/* Kernel addresses are always protection faults */
tsk->thread.error_code = error_code | (address >= TASK_SIZE);
tsk->thread.trap_no = 14;
info.si_signo = SIGSEGV;
info.si_errno = 0;
/* info.si_code has been set above */
info.si_addr = (void __user *)address;
/**
* force_sig_info确信进程不忽略或阻塞SIGSEGV信号
* SEGV_MAPERR或SEGV_ACCERR已经被设置在info.si_code中。
*/
force_sig_info(SIGSEGV, &info, tsk);
return;
}
当error_code的bit0为0,表示没有物理页面;bit1为1表示写操作;bit2为1时,表示失败是当CPU处于用户模式发生的。(VM_GROWSDOWN为1属于特殊情况)
对当前进程的task_struct结构内的一些成分进行设置后,就向该进程发出一个强制的信号“SIGSEGV”,(显示屏显示"Segment Fault"),然后使进程撤销。本次例外任务结束。
(在每次从中断/异常返回之前,都要检查当前进程是否有悬而未决的信号需要处理。)