如果addr地址属于进程的地址空间,则do_page_fault()转到good_area标记处的语句执行:
/*
* Ok, we have a good vm_area for this memory access, so
* we can handle it..
*/
good_area:
si_code = SEGV_ACCERR;
write = 0;
switch (error_code & 3) {
default: /* 3: write, present */
#ifdef TEST_VERIFY_AREA
if (regs->cs == KERNEL_CS)
printk("WP fault at %08lx/n", regs->eip);
#endif
/* fall through */
case 2: /* write, not present */
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;
write++;
break;
case 1: /* read, present */
goto bad_area;
case 0: /* read, not present */
if (!(vma->vm_flags & (VM_READ | VM_EXEC | VM_WRITE)))
goto bad_area;
}
首先,缺页异常中可能在用户态,即error_code & 3 = 2,如果异常由写访问引起,函数检查这个线性区是否可写(!(vma->vm_flags & VM_WRITE))。如果不可写,跳到bad_area代码处;如果可写,把write局部变量置为1。
如果异常由读或执行访问引起,函数检查这一页是否已经存在于RAM中。在存在的情况下,异常发生是由于进程试图访问用户态下的一个有特权的页框(页框的User/Supervisor标志被清除),因此函数跳到bad_area代码处(然而,这种情况从不会发生,因为内核不会把具有特权的页框贼给进程。)。在不存在的情况下(error_code & 3 = 0),函数还将检查这个线性区是否可读或可执行。
如果这个线性区的访问权限与引起异常的访问类型相匹配,则调用handle_mm_fault()函数分配一个新的页框:
survive:
/*
* If for any reason at all we couldn't handle the fault,
* make sure we exit gracefully rather than endlessly redo
* the fault.
*/
switch (handle_mm_fault(mm, vma, address, write)) {
case VM_FAULT_MINOR:
tsk->min_flt++;
break;
case VM_FAULT_MAJOR:
tsk->maj_flt++;
break;
case VM_FAULT_SIGBUS:
goto do_sigbus;
case VM_FAULT_OOM:
goto out_of_memory;
default:
BUG();
}
/*
* Did it hit the DOS screen memory VA from vm86 mode?
*/
if (regs->eflags & VM_MASK) {
unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT;
if (bit < 32)
tsk->thread.screen_bitmap |= 1 << bit;
}
up_read(&mm->mmap_sem);
return;
如果handle_mm_fault()函数成功地给进程分配一个页框,则返回VM_FAULT_MINOR或VM_FAULT_MAJOR。值VM_FAULT_MINOR表示在没有阻塞当前进程的情况下处理了缺页;这种缺页叫做次缺页(minor fault)。值VM_FAULT_MAJOR表示缺页迫使当前进程睡眠(很可能是由于当用磁盘上的数据填充所分配的页框时花费时间);阻塞当前进程的缺页就叫做主缺页(major fault)。函数也返回VM_FAULT_OOM(没有足够的内存)或VM_FAULT_STGBOS(其他任何错误)。
如果handle_mm_fault()返回值VM_FAULT_SIGBUS,则向进程发送SIGBUS信号:
do_sigbus:
up_read(&mm->mmap_sem);
/* Kernel mode? Handle exceptions or die */
if (!(error_code & 4))
goto no_context;
/* User space => ok to do another page fault */
if (is_prefetch(regs, address, error_code))
return;
tsk->thread.cr2 = address;
tsk->thread.error_code = error_code;
tsk->thread.trap_no = 14;
force_sig_info_fault(SIGBUS, BUS_ADRERR, address, tsk);
}
如果handle_mm_fault()不分配新的页框,就返回值VM_FAULT_OOM,此时内核通常杀死当前进程,不过,如果当前进程是init进程(tsk->pid == 1),则只是把它放在运行队列的末尾并调用调度程序;一旦init恢复执行,则又去执行handle_mm_fault():
out_of_memory:
up_read(&mm->mmap_sem);
if (tsk->pid == 1) {
yield();
down_read(&mm->mmap_sem);
goto survive;
}
printk("VM: killing process %s/n", tsk->comm);
if (error_code & 4)
do_exit(SIGKILL);
goto no_context;
下面我们就来详细分析handle_mm_fault()函数,这个函数是重中之重,其作用于4个参数:
mm:指向异常发生时正在CPU上运行的进程的内存描述符。
vma:指向引起异常的线性地址所在线性区的描述符。
address:引起异常的线性地址。
write_access:如果tsk试图向address写,则置为1(我们这里的情况);如果tsk试图在address读或执行,则置为0。
static inline int handle_mm_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
int write_access)
{
return __handle_mm_fault(mm, vma, address, write_access) &
(~VM_FAULT_WRITE);
}
/*
* By the time we get here, we already hold the mm semaphore
*/
int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, int write_access)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
__set_current_state(TASK_RUNNING);
count_vm_event(PGFAULT);
if (unlikely(is_vm_hugetlb_page(vma)))
return hugetlb_fault(mm, vma, address, write_access);
pgd = pgd_offset(mm, address);
pud = pud_alloc(mm, pgd, address);
if (!pud)
return VM_FAULT_OOM;
pmd = pmd_alloc(mm, pud, address);
if (!pmd)
return VM_FAULT_OOM;
pte = pte_alloc_map(mm, pmd, address);
if (!pte)
return VM_FAULT_OOM;
return handle_pte_fault(mm, vma, address, pte, pmd, write_access);
}
这个函数会检查用来映射address的页中间目录和页表是否存在:
if (!pud)
if (!pmd)
if (!pte)
但是pgd是肯定存在的,通过:
pgd = pgd_offset(mm, address)
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
嗯,很简单,就是找到address对应的那个页全局目录项,赋给pgd局部变量。
即使address属于进程的地址空间,相应的页表也可能还没有被分配,因此,在做别的事情之前首先执行分配页目录和页表的任务:
pud = pud_alloc(mm, pgd, address);
pmd = pmd_alloc(mm, pud, address);
pte = pte_alloc_map(mm, pmd, address);
如果是i386就用不到pud,所以pud_alloc分配一个空的pud,handle_pte_fault也用不到,我们挑pte_alloc_map来看一下吧,pmd_alloc跟他差不多:
#define pte_alloc_map(mm, pmd, address) /
((unlikely(!pmd_present(*(pmd))) && __pte_alloc(mm, pmd, address))? /
NULL: pte_offset_map(pmd, address))
int __pte_alloc(struct mm_struct *mm, pmd_t *pmd, unsigned long address)
{
struct page *new = pte_alloc_one(mm, address);
if (!new)
return -ENOMEM;
pte_lock_init(new);
spin_lock(&mm->page_table_lock);
if (pmd_present(*pmd)) { /* Another has populated it */
pte_lock_deinit(new);
pte_free(new);
} else {
mm->nr_ptes++;
inc_zone_page_state(new, NR_PAGETABLE);
pmd_populate(mm, pmd, new);
}
spin_unlock(&mm->page_table_lock);
return 0;
}
struct page *pte_alloc_one(struct mm_struct *mm, unsigned long address)
{
struct page *pte;
#ifdef CONFIG_HIGHPTE
pte = alloc_pages(GFP_KERNEL|__GFP_HIGHMEM|__GFP_REPEAT|__GFP_ZERO, 0);
#else
pte = alloc_pages(GFP_KERNEL|__GFP_REPEAT|__GFP_ZERO, 0);
#endif
return pte;
}
看见没有,如果不是高端映射,分配一个页表,其大小就是一个页面。
回到__handle_mm_fault中,pgd局部变量包含引用address的页全局目录项。如果需要的话,调用pud_alloc()和pmd_alloc()函数分别分配一个新的页上级目录和页中间目录(在80x86微处理器中,这种分配永远不会发生,因为页上级目录总是包含在页全局目录中,并且页中间目录或者包含在页上级目录中(PAE未激活),或者与页上级目录一块被分配(PAE被激活))。但是!pud_alloc()和pmd_alloc()函数还是会执行成功的,其返回的pud和pmd临时变量的值就是address对应的那个页全局目录项的值。然后,如果需要的话调用的pte_alloc_map()函数会分配一个新的页表。我们在__pte_alloc函数中看到,(pmd_present(*pmd))说明页全局目录指向的那个页表已经存在了,就pte_free(new),即不用分配一个新的页表了。
如果以上两步都成功,pte局部变量所指向的页表项就是引用address的表项。然后调用handle_pte_fault()函数检查address地址所对应的页表项,并决定如何为进程分配一个新页框:
1、如果被访问的页不存在,也就是说,这个页还没有被存放在任何一个页框中,那么,内核分配一个新的页框并适当地初始化。这种技术称为请求调页(demand paging)。
2、如果被访问的页存在但是标记为只读,也就是说,它已经被存放在一个页框中,那么,内核分配一个新的页框,并把旧页框的数据拷贝到新页框来初始化它的内容。这种技术称为写时复制(Copy On Write,COW)。
这两个技术都是非常非常非常重要的知识要点,后面两篇博文将会重点讨论这两个技术,我们在希望大家重视!