对有效的虚拟地址,如果是缺页错误的话,Linux 必须区分页所在的位置,即判断页是在交换文件中,还是在可执行映象中。为此,Linux 通过页表项中的信息区分页所在的位置。如果该页的页表项是无效的,但非空,则说明该页处于交换文件中,操作系统要从交换文件装入页。对于有效的虚拟地址address,do_page_fault( )转到good_area标号处的语句执行:
good_area:
write = 0;
if (error_code & 2) { /* 写访问 */
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;
write++;
} else
/* 读访问 */
if (error_code & 1 ||
!(vma->vm_flags & (VM_READ |
VM_EXEC)))
goto bad_area;
如果错误由写访问引起,函数检查这个虚拟区是否可写。如果不可写,跳到bad_area代码处;如果可写,把write局部变量置为1。
如果错误由读或执行访问引起,函数检查这一页是否已经存在于物理内存中。如果在,错误的发生就是由于进程试图访问用户态下的一个有特权的页面(页面的User/Supervisor标志被清除),因此函数跳到bad_area代码处(实际上这种情况从不发生,因为内核根本不会给用户进程分配有特权的页面)。如果不存在物理内存,函数还将检查这个虚拟区是否可读或可执行。
如果这个虚拟区的访问权限与引起错误的访问类型相匹配,则调用handle_mm_fault( )函数:
if (!handle_mm_fault(tsk, vma, address, write)) {
tsk->tss.cr2 = address;
tsk->tss.error_code = error_code;
tsk->tss.trap_no = 14;
force_sig(SIGBUS, tsk);
if (!(error_code & 4)) /* 内核态 */
goto no_context;
}
如果handle_mm_fault( )函数成功地给进程分配一个页面,则返回1;否则返回一个适当的错误码,以便do_page_fault( )函数可以给进程发送SIGBUS信号。handle_mm_fault( )函数有4个参数:tsk指向错误发生时正在CPU上运行的进程;vma 指向引起错误的虚拟地址所在虚拟区;address 为引起错误的虚拟地址;write_access:如果tsk试图向address写,则置为1,如果tsk试图读或执行address,则置为0
handle_mm_fault()函数首先检查用来映射address的页中间目录和页表是否存在。即使address属于进程的地址空间,但相应的页表可能还没有分配,因此,在做别的事情之前首先执行分配页目录和页表的任务:
pgd = pgd_offset(vma->vm_mm, address);
pmd = pmd_alloc(pgd, address);
if (!pmd)
return
-1;
pte = pte_alloc(pmd, address);
if (!pte)
return -1;
pgd_offset()宏计算出address所在页在页目录中的目录项指针;如果有中间目录(i386不起作用),调用pmd_alloc( )函数分配一个新的中间目录。然后,如果需要的话,调用pte_alloc( )函数分配一个新的页表。如果这两步都成功,pte局部变量所指向的页表表项就是引用address的表项。然后调用handle_pte_fault( )函数检查address地址所对应的页表表项:
return handle_pte_fault(tsk, vma, address, write_access,
pte);
handle_pte_fault( )函数决定怎样给进程分配一个新的页面:
如果被访问的页不存在,也就是说,这个页还没有被存放在任何一个页面中,那么,内核分配一个新的页面并适当地初始化;这种技术称为请求调页。
如果被访问的页存在但是被标为只读,也就是说,它已经被存放在一个页面中,那么,内核分配一个新的页面,并把旧页面的数据拷贝到新页面来初始化它的内容;这种技术称为写时复制。