在i386 CPU将一个线性地址映射成物理地址的过程中,如果该地址的映射已经建立,但是发现相应页面表项或目录项中的P(present)标志位为0,则表明相应的物理页面不在内存中,从而无法完成本次内存访问。从理论上说,也许应该把这种情况称为受阻而不是失败,因为映射的关系毕竟已经建立,理应与尚未建立映射的情况有所区别,所以我们称为断开。但是,CPU的MMU硬件并不区分这两种不同的情况,只要P标志位为0就都认为是页面映射失败,CPU就会产生一次页面异常(page fault)。事实上,CPU在映射过程中首先看的就是页面表项或目录项中的P标志位。只要P标志位为0,其余各个位段的值就无意义了。至于当一个页面不在内存中时,还是因为映射尚未建立,乃是软件,也就是页面异常处理程序的事情。在越界访问的情景中,我们曾看到在函数handle_pte_fault中的开头几行:
do_page_fault=>handle_mm_fault=>handle_pte_fault
static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, int write_access, pte_t * pte) { pte_t entry; /* * We need the page table lock to synchronize with kswapd * and the SMP-safe atomic PTE updates. */ spin_lock(&mm->page_table_lock); entry = *pte; if (!pte_present(entry)) { /* * If it truly wasn't present, we know that kswapd * and the PTE updates will not touch it later. So * drop the lock. */ spin_unlock(&mm->page_table_lock); if (pte_none(entry)) return do_no_page(mm, vma, address, write_access, pte); return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access); } ......
这里,首先区分的是pte_present,也就是检查表项中的P标志位,看看物理页面是否在内存中。如果不在,则进而通过pte_none检查表项是否为空,即全0.如果为空就说明映射尚未建立,所以要调用do_no_page。这在以前的情景中已经看到过了。反之,如果非空,就说明映射已经建立,只是物理页面不在内存中,所以要通过do_swap_page,从交换设备上换入这个页面。本情景在handle_pte_fault之前的处理以及执行路线都与越界访问的情景相同,所以我们直接进入do_swap_page。这个函数的代码如下:
do_page_fault=>handle_mm_fault=>handle_pte_fault=>do_swap_page
static int do_swap_page(struct mm_struct * mm, struct vm_area_struct * vma, unsigned long address, pte_t * page_table, swp_entry_t entry, int write_access) { struct page *page = lookup_swap_cache(entry); pte_t pte; if (!page) { lock_kernel(); swapin_readahead(entry); page = read_swap_cache(entry); unlock_kernel(); if (!page) return -1; flush_page_to_ram(page); flush_icache_page(vma, page); } mm->rss++; pte = mk_pte(page, vma->vm_page_prot); /* * Freeze the "shared"ness of the page, ie page_count + swap_count. * Must lock page before transferring our swap count to already * obtained page count. */ lock_page(page); swap_free(entry); if (write_access && !is_page_shared(page)) pte = pte_mkwrite(pte_mkdirty(pte)); UnlockPage(page); set_pte(page_table, pte); /* No need to invalidate - it was non-present before */ update_mmu_cache(vma, address, pte); return 1; /* Minor fault */ }
先看看调用时传过来的参数是些什么。建议读者先回到前面通过越界访问扩充堆栈的情景中,顺着CPU的执行路线走一遍,搞清楚这些参数的来龙去脉。参数表中的mm、vma还有address是一目了然的,分别是指向当前进程的mm_struct结构的指针、所属虚存区间的vm_area_struct结构的指针以及映射失败的线性地址。
参数page_table指向映射失败的页面表项,而entry则为该表项的内容。我们以前说过,当物理页面在内存中时,页面表项是一个pte_t结构,指向一个内存页面;而当物理页面不在内存中时,则是一个swp_entry_t结构,指向一个盘上页面。二者实际上都是32位无符号整数。这里要指出,所谓不在内存中是逻辑意义上的,是对CPU的页面映射硬件而言,实际上这个页面很可能在不活跃页面队列中,甚至在活跃页面队列中。
还有一个参数write_access,表示当映射失败时所进行的访问种类(读写),这是在do_page_fault的switch语句中根据CPU产生的出错代码error_code的bit1决定的(注意,在那个switch语句中,default与case 2:之间没有break语句)。此后便逐层传了下来。
由于物理页面不在内存中,所以entry是指向一个盘上页面的类型类似于指针的索引项(加上若干标志位)。
。。。。。。