6.5.3 进程地址空间中的缺页异常处理

对有效的虚拟地址,如果是缺页错误的话,Linux 必须区分页所在的位置,即判断页是在交换文件中,还是在可执行映象中。为此,Linux 通过页表项中的信息区分页所在的位置。如果该页的页表项是无效的,但非空,则说明该页处于交换文件中,操作系统要从交换文件装入页。对于有效的虚拟地址addressdo_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(  )函数决定怎样给进程分配一个新的页面:

如果被访问的页不存在,也就是说,这个页还没有被存放在任何一个页面中,那么,内核分配一个新的页面并适当地初始化;这种技术称为请求调页

如果被访问的页存在但是被标为只读,也就是说,它已经被存放在一个页面中,那么,内核分配一个新的页面,并把旧页面的数据拷贝到新页面来初始化它的内容;这种技术称为写时复制