i386 Linux 处理缺页中断

一、情景描述

在保护模式页式映射中,应用程序如果要访问物理地址,需要将线性地址通过设置的页面映射表进行映射,才能最终访问到物理地址。当然,这个过程中可能会遇到映射失败的情况,这时CPU会产生一次页面出错异常(PAGE FAULT)实际上就是缺页中断,进而通过中断向量表(LDT)进入指定的页面异常处理程序,如果经内核判断不是非法地址,在页面 异常处理程序中会建立这个线性地址到物理地址的页面映射,然后回到应用程序中继续执行,这里造成映射失败的情况有以下几种:

  • 页面目录项或者页表是空,映射未建立或者已经撤销
  • 相应的物理页面不再内存中
  • 指令中规定的访问方式与页面权限不符,例如企图写一个“只读”页面

二、详细解释

关于Linux 中断异常处理机制会在其他篇总结,这里主要介绍页面异常服务程序的主体程序do_page_fault()

2.1 do_page_fault()

上面是do_page_fault()函数的实现代码。我们先介绍下参数,在介绍下大体流程,最后再介绍下调用的其他函数

2.2.2 函数参数解释

do_page_fault参数
参数类型及参数名 作用
struct pt_regs *regs 指向异常前CPU各个寄存器的信息的副本
unsigned long error_code

error_code知名映射失败的原因。下面是内核代码中关于error_code的注释:

bit 0 == 0 means no page found, 1 means protection fault(1代表有该页,0代表没有找到该页)
bit 1 == 0 means read, 1 means write(0代表是读页面,1代表是写页面)
bit 2 == 0 means kernel, 1 means user-mode(0代表是内核空间,1代表是用户空间)

2.2.3 流程解释

Linux对页面的管理是能不创建实际的物理页就不创建,到真正访问物理内存是,由CPU产生缺页中断后,才会产生去申请物理内存页。

我们来看下开头的一些代码。

//file  ============linux/arch/i386/mm/fault.c 106h~146h=====
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;

	/* get the address */
	__asm__("movl %%cr2,%0":"=r" (address));  //cpu出现页面异
                                                  //常时,会将触发异常的线性地址存放在CR2                     
                                                  //寄存器中,此行代码是将。页面出异常的线
                                                  //性地址存放在address变量中。

	tsk = current;      //current为触发页面异常的进程的task_struct结构

	/*
	 * We fault-in kernel-space virtual memory on-demand. The
	 * 'reference' page table is 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;

	/*
	 * If we're in an interrupt or have no user
	 * context, we must not take the fault..
	 */
	if (in_interrupt() || !mm)
		goto no_context;

	down(&mm->mmap_sem);

	vma = find_vma(mm, address);		//寻找该地址的虚存区间
	if (!vma)
		goto bad_area;
	if (vma->vm_start <= address)           //可能是用户空间通过调用brk()系统调用留下
                                                //的空洞
		goto good_area;
	if (!(vma->vm_flags & VM_GROWSDOWN))    //表明是正常的用户空间堆栈堆栈增长
		goto bad_area;
	if (error_code & 4) {                   //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;
	}
	if (expand_stack(vma, address))        //扩充堆栈,这个实际上是扩充该进程的虚拟内
                                               //内存空间
		goto bad_area;

我们需要注意的是16行的汇编代码,改行代码是将未映射的线性地址存放到变量address中,也就是说接下来的流程是检验这个地址的合法性,如果合法就建立该地址的映射;如果不合法,再去做一些处理,例如如果是用户态进程访问了非法地址,内核在缺页异常处理程序中就会发送SIGSEGV,就是我们C语言程序员喜闻乐见的段错误信号。

46行的find_vma()函数是通过线性地址address查找当前current进程的该线性地址的虚存区间,并将虚存区间返回。如果没有返回就跳入bad_area代码,这里我们就先不关注bad_area代码,就暂且认为会触发段错误。然后49行实际上指的是该地址在虚存区间的start~end内,这里会产生这个现象的情景有很多,例如:用户空间通过调用brk()系统调用留下的空洞(malloc()库函数在进程虚存区间用完时,会通过brk()系统调用扩展该进程的虚存区间),这里还有一个情形是用户正常的堆栈增长,这里情形也就是相当于我们在用户空间中的栈区申请大量的局部变量直到用户空间栈区虚存区间用光。关于这个情形相关代码为53行和66行。52行中的vm_flags标记为VM_GROWDOWN代表是向下增长,也就是栈区。我们来看下Linux 的虚存空间分配。

i386 Linux 处理缺页中断_第1张图片

我们看上图,对于进程来说,增长的内存有两种,一个是堆,是向上增长,一个是栈是向下增长。了解了这个后我们再看下66行,expand_stack()实际上是扩充虚存区间,在后面我们再来看这个函数相关代码。

无论那种正常情形(合法地址)现在都会进入good_area的代码:

//file  ============linux/arch/i386/mm/fault.c 169h~189h=====
good_area:
	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;
			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;
	}

这里的代码实际上是为了判断虚存区间属性和触发缺页中断指令执行操作属性是否一致,如12行,如果该指令是写指令,但是该虚存区间没有写属性,就转入bad_area,21行也类似。

检查完后,进入handle_mm_fault(),在该函数开始为线性地址和物理地址建立映射。

​	/*
	 * 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 1:
		tsk->min_flt++;
		break;
	case 2:
		tsk->maj_flt++;
		break;
	case 0:
		goto do_sigbus;
	default:
		goto out_of_memory;
	}

2.2 expand_stack()

该函数的作用是为了扩展进程的栈空间虚拟区间。代码如下

//===========include/linux/mm.h==========487h,504h
/* vma is the first one with  address < vma->vm_end,
 * and even  address < vma->vm_start. Have to extend vma. */
static inline int expand_stack(struct vm_area_struct * vma, unsigned long address)	//实际上这个也就是扩充了进程虚拟空间
{
	unsigned long grow;

	address &= PAGE_MASK;
	grow = (vma->vm_start - address) >> PAGE_SHIFT;						//取得增长的物理页面个数
	if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur ||	//查看是否大于当前进程的内存限制
	    ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur)
		return -ENOMEM;
	vma->vm_start = address;											//将start扩充为adress地址
	vma->vm_pgoff -= grow;
	vma->vm_mm->total_vm += grow;										//总共页面
	if (vma->vm_flags & VM_LOCKED)
		vma->vm_mm->locked_vm += grow;
	return 0;
}

2.3 handle_mm_fault()

该函数的作用是建立起线性地址address和物理地址之间的映射

//=============mm/memery.c========1189h,1208h=================
/*
 * 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)
{
	int ret = -1;
	pgd_t *pgd;
	pmd_t *pmd;

	pgd = pgd_offset(mm, address);                //获取pgd
	pmd = pmd_alloc(pgd, address);                //分配pmd

	if (pmd) {
		pte_t * pte = pte_alloc(pmd, address);    //分配pte
		if (pte)
                        //将pte和具体的物理地址所在的页面联系起来
			ret = handle_pte_fault(mm, vma, address, write_access, pte);
	}
	return ret;
}

这里我们主要看下handle_pte_fault()函数,这个函是将pte和具体的物理页面联系起来。

//=============mm/memery.c======1153h,1187h=================
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);
	}

	if (write_access) {
		if (!pte_write(entry))
			return do_wp_page(mm, vma, address, pte, entry);

		entry = pte_mkdirty(entry);
	}
	entry = pte_mkyoung(entry);
	establish_pte(vma, address, pte, entry);            //建立起页面和pte关系
	spin_unlock(&mm->page_table_lock);
	return 1;
}

这里我们需要注意的一个地方时调用的do_no_page(),do_no_page()中关键的一行是1105行(代码太多不全列出)

    new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, (vma->vm_flags & VM_SHARED)?0:write_access);

这行代码实际上是调用了函数指针指向的函数,也就是说不同的虚存区间申请页面的所需要的操作是不同的。为什么会有这样的操作呢,我们想象下,正常的堆栈增长只需要在申请一个物理页就可以了,但是对于将文件映射到虚存区间的那些内存,可能会需要申请物理页,并且从磁盘中读取部分数据,而共享内存映射的物理页,则有可能需要处理下内核和共享内存相关的流程。所以不同的虚存区间是不相同的。

你可能感兴趣的:(Linux内核,linux)