arch/i386/fault.c
     页面异常, swap, address space, shmem, filemap 
     

    从文件开始,而不拘泥于文件。   
    
    此文件内只有两个函数。
    
1.  __verify_write  int __verify_write(const void * addr, unsigned long size)

    此函数主要应用于检查用户传递的内存块是否"可写". 可写意味着: 
    a)内核容许其写,即vma的记录; 
    b)相应的物理页面已经分配,即虚存已经有了对应的映射. 主要使用者是
      access_ok (include/asm-i386/uaccess.h). 
   
   access_ok 大量应用于和用户空间进行数据交换的时候检查用户传递的内
存是否合法.以避免在内核中产生页面访问异常,被别有用心的程序破坏了"大
好形势".

    下面是此函数,以及注释.
/*
 * Ugly, ugly, but the goto's result in better assembly..
 * 在操作用户传递的内存时检查其有效性,如果容许则预先扩展其堆栈
 * 或者为虚拟内存分配物理页面
 */
int __verify_write(const void * addr, unsigned long size)
{
	struct vm_area_struct * vma;
	unsigned long start = (unsigned long) addr;

	if (!size)
		return 1;

	vma = find_vma(current->mm, start);//寻找vma满足start<vma->end
	if (!vma)
		goto bad_area; //访问非法地址
	if (vma->vm_start > start) //访问堆栈空洞,需要扩展用户堆栈
		goto check_stack;

good_area:
	if (!(vma->vm_flags & VM_WRITE))
		goto bad_area;
	size--;
	size += start & ~PAGE_MASK;
	size >>= PAGE_SHIFT;
	start &= PAGE_MASK;

	for (;;) {
		if (handle_mm_fault(current->mm, vma, start, 1) <= 0)//检查page table->
			goto bad_area;  //page dir->page本身, 有任何没有分配的页面,即刻分配物理
		if (!size)          //页面
			break;
		size--;
		start += PAGE_SIZE;
		if (start < vma->vm_end)
			continue;
		vma = vma->vm_next;
		if (!vma || vma->vm_start != start)
			goto bad_area;
		if (!(vma->vm_flags & VM_WRITE))
			goto bad_area;;
	}
	return 1;

check_stack:
	if (!(vma->vm_flags & VM_GROWSDOWN)) //必须是堆栈(或者有VM_GROWSDOWN属性)
		goto bad_area;
	if (expand_stack(vma, start) == 0) //只是扩展其vma中的地址范围
		goto good_area;  //还要给扩展的堆栈分配物理页面

bad_area:
	return 0;
}

    就这个函数本身,其中的注释已经足以理解其逻辑. 需要着重强调的是:
   a) vma 
      vma是内核管理虚拟内存的手段,其中记录了进程所拥有的虚拟地址的
      范围和属性,是处理内存异常的依据之一. 如果进程所有的虚存较多,
      vma还可以组织成平衡树,以加快查找速度,不过这是vma资源的管理算法,
      而不是内核的逻辑. 逻辑和具体资源管理算法应该区别对待,以免陷入
      万劫不复的细节中去.所以, find_vma 本身的细节不用去追究,否则分
      析之粒度就过于详细,反而迷失于内核之中了.

   b) handle_mm_fault (mm/memory.c)
      此函数其使用在这里是"不务正业", 使用在本文件的下一个函数
      do_page_fault才是正道.其逻辑是比较简单的. 内核假定有三级页面映
      射,此函数顺着指定地址摸下去将涉及到的page table entry, page dir
      entry, 逐个检查一遍如有未分配之page dir , page, 就分配一个并设
      置好相应的entry.其复杂之处在于
      handle_mm_fault->handle_pte_fault(也在memory.c之中):
      先来注释一把:

/* 此段不翻译了,我们不是在翻译内核源码.
 * These routines also need to handle stuff like marking pages dirty
 * and/or accessed for architectures that don't do it in hardware (most
 * RISC architectures).  The early dirtying is also good on the i386.
 *
 * There is also a hook called "update_mmu_cache()" that architectures
 * with external mmu caches can use to update those (ie the Sparc or
 * PowerPC hashed page tables that act as extended TLBs).
 *
 * Note the "page_table_lock". It is to protect against kswapd removing
 * pages from under us. Note that kswapd only ever _removes_ pages, never
 * adds them. As such, once we have noticed that the page is not present,
 * we can drop the lock early.
 *
 * The adding of pages is protected by the MM semaphore (which we hold),
 * so we don't need to worry about a page being suddenly been added into
 * our VM.
 */
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)) { //pte不存在,映射有问题,又分为两种情况
		/*
		 * 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, //页面不存在, 就是没有,或者被try_to_swap_out
			                   address,  // 断开, 这种页可以属于page cache
			                   write_access, 
			                   pte       // 参见filemap_nopage 了解page cache 的换入
			                   );
		
                return do_swap_page(mm, vma,  //第二种情况: not present, 那么就属于swapper_space管理
			                  address, pte, 
			                  pte_to_swp_entry(entry), 
			                  write_access
			                 );
	}

    //如果是read 时产生陷入或者时pte 的映射问题
    //或者是一个非法的操作, 已经在do_page_fault 过滤掉了
	if (write_access) { //由于页面写保护产生的陷入, 而OS 却容许用户写入
		if (!pte_write(entry))
			return do_wp_page( mm, //就是处理COW 的第二步
			                    vma, //Copy on Write
			                    address, //第一步是建立一个不容许写的页
			                    pte, entry //却在vma 中赋予用户写的权限
			                   );

		entry = pte_mkdirty(entry);
	}
	entry = pte_mkyoung(entry);
	establish_pte(vma, address, pte, entry);
	spin_unlock(&mm->page_table_lock);
	return 1;
}

        这里要强调的是页面的换入. 有以下途经分配/换入页面,从而建立页面
     映射.
     I)第一种情况:根本没有建立,或者已经被断开. 
        换入函数 do_no_page(mm/memory.c), 首先尝试 vma->vm_ops->nopage,
       如果vma没有提供nopage操作就使用do_anonymous_page分配一个空闲页面.
       (本次分析细节到此结束)
     II)第二种情况是被交换到了swap space
        则通过 do_swap_page从swap 文件读入内存(或者从swap cache 找回).
        
       此函数还涉及计COW, 这里不再讨论.
       
       页面的释放和换出这里页简单提提,首先定义匿名页面是
page->mapping(address mapping)为NULL的页面.页面换出当然是try_to_swap_out
所为 .try_to_swap_out换出非匿名页的时候是直接断开页面映射.匿名的clean页
面的映射也是直接断开,因为不需要写入交换文件. 
       如果是dirty的匿名页,则分配一个swap页(swap file中的一个页面)然后将
swap 页的entry写入pte,并置pte为页面不在内存.
        
    try_to_swap_out区别对待这三种页面,和 handle_pte_fault换入页面的不同
操作相对应.
        
   总结一下页面的周转:从try_to_swap_out和handle_pte_fault看过去, 换
出要不就是swap space,要不就是page->mapping->a_ops->writepage(见mm/vmscan.c
函数page_launder,应该注意到无论是swap cache还是page cahche都使用writepage
换出页面).换入要不就是swap space,要不就是vma->vm_ops->nopage(分配干净页面
就不算进页面周转了).
       
        
   下面的问题就是 page->mapping->a_ops->writepage 和 vma->vm_ops->nopage 
到底是什么函数,怎样赋值?
        
先看page->mapping->a_ops->writepage,搜索page->mapping,在函数
do_generic_file_read(mm/filemap.c)中发现page->mapping来源于
inode->i_mapping. do_generic_file_read->__add_to_page_cache读入一个页面的
文件后,将页面加入page cache,同时赋予page->mapping以addr space.  另外在做搜
索的过程中应该注意函数add_to_page_cache_locked(mm/filemap.c)此函数建立page 
cache内页面的page->mapping映射.搜索调用add_to_page_cache_locked的函数得知,
shmem_nopage(mm/shmem.c)建立的page->mapping来源于inode;add_to_swap_cache
(mm/swap_state.c)建立的的page->mapping映射是swapper_space(也在swap_state.c).
顺着这些线索,可以找到,其实写页面最终和一个inode相关联,这也于理相符. 我们推
断换入用的vma->vm_ops->nopage最终也和inode相关.
   看看vma->vm_ops->nopage的情况. 直接搜索nopage,发现有两种vm_ops,
filemap_nopage(* area,address, no_share)(mm/filemap.c), shmem_nopage( * vma,
address, no_share)(mm/shmem.c). 
    现在看来,vma可以获取文件映射(mmap,参考函数generic_file_mmap),shemem,
或者匿名页,来建立其终物理页面的映射.从这些线索分析,filemap_nopage利用
page-mapping的readpage从文件读入页面内容,呼应了刚才的猜想.shmem_nopage
只是通过vma相关联的文的inode之mapping在swap space寻找被交换出去的shemem
页面,并不用来从文件读取页面内容.寻找到的页面(或者新分配的)就加入page cache
(如果是从swap cache找到的则需要从swap chache移出).到这里再看page cache和
swap cache的界定,page->maping的值如果是swapper_space, 则页面处于swap cache,
如果page->maping的值是其他add space,比如filemap, shemem设置的mapping,则称
页面在page cache 中. 
        
   希望还没有迷路.


2. asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)

   不是想在这里详细介绍i386 cpu的处理机制,而是给出中断处理在linux中的
线索,以资参考。

   中断或异常发生后,i386终止执行当前运行的指令流(参考:),保存必要的
状态根据当前的运行级别(内核/用户),从idt选择合适的gate,执行指定地址
的处理函数。
   我们以页面异常为例。

   首先看页面异常门的设置。首先要看idt地址。在系统启动的时候,boot程序已
经初始化了一个idt,但是在kernel中,还要重新指定定idt地址并用lidt加载内核
的idt。从 arch/i386/kernel/traps.c  的函数trap_init 看过去 . 
宏 set_trap_gate:

static void __init set_trap_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,15,0,addr);
}

   引用了idt地址, idt_table,定义在traps.c 文件的开始 处:
   

/*
 * The IDT has to be page-aligned to simplify the Pentium
 * F0 0F bug workaround.. We have a special link segment
 * for this.
 */
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt")))= { {0, 0}, };



    由此得知在内核链接的时候预留了一个section  .data.idt 作为idt表.
从init/main.c 的函数start_kernel开始内核 初始化经历 ->trap_init->cpu_init:
    __asm__ __volatile__("lidt %0": "=m" (idt_descr));
    
    至此,trap初始化,idt表设置已经完成. 深入下去的话include/asm-i386/desc.h
中定义:
    #define idt_descr (*(struct Xgt_desc_struct *)((char *)&idt - 2))
    并声明 
    extern struct desc_struct *idt, *gdt;
    而idt是编译链接程序生成的符号,地址即是.data.idt 

   
    函数trap_init 中 初始化14号异常,即页面异常入口为page_fault
          set_trap_gate(14,&page_fault);

    page_falut定义在arch/i386/kernel/entry.s:

ENTRY(page_fault)
	pushl $ SYMBOL_NAME(do_page_fault)
	jmp error_code

     在这里调用页面异常处理函数 do_page_fault(arch/i386/mm/fault.c).
     下面分析此函数, 分析在代码中以注释形式出现.
/*
 * 页面faults(故障) 处理入口.  主要任务时确定异常
 * 发生的地址和原因,然后把不同的故障传入不同
 * 的入口函数.
 *
 * error_code:
 *	bit 0 == 0 means no page found, 1 means protection fault
 *	bit 1 == 0 means read, 1 means write
 *	bit 2 == 0 means kernel, 1 means user-mode
 *
 *  有三种情况下陷入此函数:
 *    1. pmd, pgt, pte 有一个为空, 即未建立映射或已经撤销.
 *    2. 页面不在内存, 即,为内核交换到了磁盘.
 *    3. 权限不正确.
 *    
 *    情况1. 此种页面属于已经撤销的, 此种页面由page cache (address_space)管理.
 *             try_to_swap_out 把这种页面的映射断开, 即,把相应的pte 置成0 .
 *   
 *    情况2. 中的页面属于swapper_space 管理. try_to_swap_out 不会把这种 pte 
 *           置0, 而会换成相应的swp_entry_t.
 *   
 *    转交下一级函数处理时, 所有非法操作都在这个函数中
 *    处理掉了
 */    
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;

	/*
	 * 由于demand 我们陷入了内核虚拟空间. 我们使用
	 * 的 page table 是init_mm.pgd.
	 *
	 * NOTE! We MUST NOT take any locks for this case. We may
	 * be in an interrupt or a critical region, and should
	 * only copy the information from the master page table,
	 * nothing more.
	 */
	if (address >= TASK_SIZE)
		goto vmalloc_fault;  //异常发生于内核空间

	mm = tsk->mm;
	info.si_code = SEGV_MAPERR;

	/*
	 * 如果异常发生在中断中或者没有用户
	 * context(环境), we must not take the fault..
	 */
	if (in_interrupt() || !mm)
		goto no_context;

	down(&mm->mmap_sem);

    //下面的主要思路是根据异常发生的地址和vma 的
    //关系,分成几种不同情况进行处理
    
	vma = find_vma(mm, address); // 试着找一个vma , 其结束地址大于异常点
	if (!vma)
		goto bad_area; //没有这样的vma
	if (vma->vm_start <= address)
		goto good_area; //异常地址在一个vma 中

    //运行到这里表示, 这个地址落到了一个vma 空洞中
	if (!(vma->vm_flags & VM_GROWSDOWN))
		goto bad_area;  //如果这个空洞上的vma 不是堆栈, 肯定是越界

	if (error_code & 4) {
		/*
		 * accessing the stack below %esp is always a bug.
		 * The "+ 32" is there due to some instructions (like
		 * pusha) doing post-decrement on the stack and that
		 * doesn't show up until later..
		 */
		if (address + 32 < regs->esp)
			goto bad_area;  //异常点不在esp 附近时也是一种越界
			                //而不是正常的堆栈操作
	}
	//对于正常的堆栈操作越界,我们应该为用户扩展堆栈
	if (expand_stack(vma, address))
		goto bad_area; //扩展失败, 转到bad_area
	
/*
 * 堆栈扩展以后, vma 就正常了,因此流程自然进入good_area
 */
good_area: 
    //异常点在正确的vma 中
    //还要根据vma(代表OS) 容许的权限, 对异常分类
	info.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; //vma 不容许写, 是一种非法操作
			write++;
			break;
		case 1:		/* read, present */
			goto bad_area; // 权限错误, 非法
		case 0:		/* read, not present */
			if (!(vma->vm_flags & (VM_READ | VM_EXEC)))
				goto bad_area; //vma 不容许读的情况下非法
	}

	/*
	 * 如果处理陷入时到了这里, 
	 * 说明异常点在一个完好的vma 中,并且符合OS 赋予
	 * 用户的权限.
	 */
	switch (handle_mm_fault(mm, vma, address, write)) { 
	case 1:                                            
		tsk->min_flt++;
		break;
	case 2:
		tsk->maj_flt++;
		break;
	case 0:
		goto do_sigbus;
	default:
		goto out_of_memory;
	}

	/*
	 * 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(&mm->mmap_sem);
	return;

/*
 * Something tried to access memory that isn't in our memory map..
 * Fix it, but check if it's kernel or user first..
 */
bad_area:
	up(&mm->mmap_sem);

bad_area_nosemaphore:
	/* User mode accesses just cause a SIGSEGV */
	if (error_code & 4) {
		tsk->thread.cr2 = address;
		tsk->thread.error_code = error_code;
		tsk->thread.trap_no = 14;
		info.si_signo = SIGSEGV;
		info.si_errno = 0;
		/* info.si_code has been set above */
		info.si_addr = (void *)address;
		force_sig_info(SIGSEGV, &info, tsk);
		return;
	}

	/*
	 * Pentium F0 0F C7 C8 bug workaround.
	 */
	if (boot_cpu_data.f00f_bug) {
		unsigned long nr;
		
		nr = (address - idt) >> 3;

		if (nr == 6) {
			do_invalid_op(regs, 0);
			return;
		}
	}

no_context:
	/* Are we prepared to handle this kernel fault?  */
	if ((fixup = search_exception_table(regs->eip)) != 0) {
		regs->eip = fixup;
		return;
	}

/*
 * Oops. The kernel tried to access some bad page. We'll have to
 * terminate things with extreme prejudice.
 */

	bust_spinlocks();

	if (address < PAGE_SIZE)
		printk(KERN_ALERT "Unable to handle kernel NULL pointer dereference");
	else
		printk(KERN_ALERT "Unable to handle kernel paging request");
	printk(" at virtual address %08lx\n",address);
	printk(" printing eip:\n");
	printk("%08lx\n", regs->eip);
	asm("movl %%cr3,%0":"=r" (page));
	page = ((unsigned long *) __va(page))[address >> 22];
	printk(KERN_ALERT "*pde = %08lx\n", page);
	if (page & 1) {
		page &= PAGE_MASK;
		address &= 0x003ff000;
		page = ((unsigned long *) __va(page))[address >> PAGE_SHIFT];
		printk(KERN_ALERT "*pte = %08lx\n", page);
	}
	die("Oops", regs, error_code);
	do_exit(SIGKILL);

/*
 * We ran out of memory, or some other thing happened to us that made
 * us unable to handle the page fault gracefully.
 */
out_of_memory:
	up(&mm->mmap_sem);
	printk("VM: killing process %s\n", tsk->comm);
	if (error_code & 4)
		do_exit(SIGKILL);
	goto no_context;

do_sigbus:
	up(&mm->mmap_sem);

	/*
	 * Send a sigbus, regardless of whether we were in kernel
	 * or user mode.
	 */
	tsk->thread.cr2 = address;
	tsk->thread.error_code = error_code;
	tsk->thread.trap_no = 14;
	info.si_code = SIGBUS;
	info.si_errno = 0;
	info.si_code = BUS_ADRERR;
	info.si_addr = (void *)address;
	force_sig_info(SIGBUS, &info, tsk);

	/* Kernel mode? Handle exceptions or die */
	if (!(error_code & 4))
		goto no_context;
	return;

vmalloc_fault: /* 内核的vmalloc可能分配了一个页面但是task自己的页表之内核
                  部分还没有设置。注:每个task都有自己的“kernel 空间”,这里
                  进行同步,从内核自己的页表将kernel space 的映射同步到
                  task page table*/
	{
		/*
		 * Synchronize this task's top level page-table
		 * with the 'reference' page table.
		 */
		int offset = __pgd_offset(address);
		pgd_t *pgd, *pgd_k;
		pmd_t *pmd, *pmd_k;

		pgd = tsk->active_mm->pgd + offset;
		pgd_k = init_mm.pgd + offset;

		if (!pgd_present(*pgd)) {
			if (!pgd_present(*pgd_k))
				goto bad_area_nosemaphore;
			set_pgd(pgd, *pgd_k);
			return;
		}

		pmd = pmd_offset(pgd, address);
		pmd_k = pmd_offset(pgd_k, address);

		if (pmd_present(*pmd) || !pmd_present(*pmd_k))
			goto bad_area_nosemaphore;
		set_pmd(pmd, *pmd_k);
		return;
	}
}
     
 
     英文说的比较明白的就不敢添足了。

     幸亏上个函数对handle_mm_fault评论了一番,do_page_fault到这里也该收手了.