页式存储管理机制通过页面目录和页面表将每个线性地址(也可以称为虚拟地址)转换成物理地址。如果在这个过程中遇到某种阻碍而使CPU无法最终访问到相应的物理内存单元,映射就失败了。而当前的指令也就不能执行完成。此时CPU会产生一次页面出错(page fault)异常(也称缺页异常中断),进而执行预定的页面异常处理程序,使应用程序得以因映射失败而暂停的指令处开始恢复执行,或进行一些善后处理。这里所说的阻碍可以由以下几种情况:
- 相应的页面目录项或页面表项为空,也就是该线性地址与物理地址的映射关系尚未建立,或者已经撤销
- 相应的物理页面不在内存中
- 指令中规定的访问方式与页面的权限不符,例如企图写一个只读的页面
在这个情景中,我们假定一段用户程序曾经将一个已打开文件通过mmap系统调用映射到内存,然后又已经将映射撤销(通过munmap系统调用)。在撤销一个映射区间时,常常会在虚存地址空间中留下一个孤立的空洞,而相应的地址则不应该继续使用了。但是,在用户程序中往往会有错误,以致在程序中某个地方还再次访问这个已经撤销的区域(程序员们一定会同意,这是不足为奇的)。这时候,一次因越界访问一个无效地址(invalid address)而引起映射失败,从而就产生了一次页面出错异常。中断请求以及异常的响应机制将在中断和异常的博客中集中介绍,我们在那里可以找到从发生异常到进入内核相应服务程序的全过程。这里假定CPU的运行已经到达了页面异常服务程序放的主图do_page_fault的入口处。
函数do_page_fault的代码比较长,我们将随着情景的进展按需要来展示去有关的片段。这里先看开头几行代码:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
struct task_struct *tsk;
struct mm_struct *mm;
struct vm_area_struct * vma;
unsigned long address;
unsigned long page;
unsigned long fixup;
int write;
siginfo_t info;
__asm__("movl %%cr2,%0":"=r" (address));
tsk = current;
if (address >= TASK_SIZE)
goto vmalloc_fault;
mm = tsk->mm;
info.si_code = SEGV_MAPERR;
if (in_interrupt() || !mm)
goto no_context;
down(&mm->mmap_sem);
vma = find_vma(mm, address);
if (!vma)
goto bad_area;
if (vma->vm_start <= address)
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
首先是一行汇编代码。为什么要用汇编代码呢?当i386 CPU产生页面出错异常时,CPU将导致映射失败的线性地址放在控制寄存器CR2中,而这显然是相应的服务程序所必需的信息。可是,在c语言中并没有相应的语法可以用来读取CR2的内容,所以只能用汇编代码。这行汇编代码只有输出部分而没有输入部分,它将%0与变量address相结合,并说明该变量应该被分配在一个寄存器中。
同时,内核的中断、异常响应机制还传过来两个参数。一个是pt_regs结构指针regs。它指向例外发生前夕CPU中各寄存器内容的一份副本,这是由内核的中断响应机制保存下来的现场。而error_code则进一步指明映射失败的具体原因。
然后是获取当前进程的task_struct数据结构。在内核中,可以通过一个宏操作current取得当前进程(当前正在运行的进程)的task_struct结构的地址。在每个进程的task_struct结构中有一个指针,指向其mm_struct结构,而跟虚存管理和映射有关的信息都在那个结构中。这里要指出,CPU实际进行的映射并不涉及mm_struct结构,而是向之前讲的那样通过页面目录和页面表进行,但是mm_struct结构反映了,或者说描述了这种映射。
接下来,需要检测两个特殊的 情况。一个特殊情况是in_interrupt返回非0,说明映射的失败发生在某个中断服务程序中,因而与当前进程毫无关系。另一个特殊情况是当前进程的mm指针为空,也就是说该进程的映射尚未建立,当然也就不可能与当前进程有关。可是,不跟当前进程有关,in_interrupt又返回0.那这次异常发生在什么地方呢?其实还是在某个中断、异常服务程序中,只不过不在in_interrupt能检测到的范围中而已。如果发生这些特殊情况,控制就通过goto语句转到标号no_context处,不过那与我们这个情景无关,所以我们略去对那段代码的讨论。
以下的操作有互斥的要求,也就是不容许别的进程来打扰,所以要由对信号量的PV操作,即down()/up()操作来保证。为了这个目的,在mm_struct结构中还设置了所需的信号量mmap_sem。这样,从down返回后,就不会有别的进程来打扰了。
可以想象,在知道了发生映射失败的地址以及所属的进程以后,接下来应该要搞清楚的是这个地址是否落在某个已经建立映射的区间,或者进一步具体指出在哪个区间。事实正是如此,这就是find_vma所要做的事情。以前讲过,find_vma试图在一个虚存空间中找出结束地址大于给定地址的第一个区间。如果找不到的话,那本次页面异常就必定是因越界访问而引起。那么,在什么情况下会找不到?回忆一下内核对用户虚存空间的使用,堆栈在用户去的顶部,从上向下伸展,而进程的代码和数据都是自底向上分配空间。如果没有一个区间的结束地址高于给定的地址,那就说明这个地址是在堆栈之上,也就是3G字节以上了。要从用户空间访问属于系统的空间,那当然是越界了,然后就转向bad_area,不过我们这个情景所说的不是这个情况。
如果找到了这么一个区间,而且其起始地址又高于给定的地址(见148行),那就说明给定的地址恰好落在这个区间。这样,映射
。。。。。。



