重写-linux内存管理-缺页异常分析(上)

在取指令或数据的时候,处理器的内存管理单元需要把虚拟地址转换成物理地址。如果虚拟页没有映射到物理页,或者没有访问权限,处理器将生成页错误异常。虚拟页没有映射到物理页,这种情况通常称为缺页异常,有以下几种情况。

  1. 访问用户栈的时候,超出了当前用户栈的范围,需要扩大用户栈。
  2. 当进程申请虚拟内存区域的时候,通常没有分配物理页,进程第一次访问的时候触发页错误异常。
  3. 内存不足的时候,内核把进程的匿名页换出到交换区。
  4. 一个文件页被映射到进程的虚拟地址空间,内存不足的时候,内核回收这个文件页,在进程的页表中删除这个文件页的映射。
  5. 程序错误,访问没有分配给进程的虚拟内存区域。

前面四种情况,如果页错误异常处理程序成功地把虚拟页映射到物理页,处理程序返回后,处理器重新执行触发异常的指令。第五种情况,页错误异常处理程序将会发送段违法( SIGSEGV )信号以杀死进程。

在ARM64 处理器在取指令或数据的时候,需要把虚拟地址转换成物理地址,分两种情况:

  1. 如果虚拟地址的高 16 位不是全 1 或全 0 (假设使用 48 位虚拟地址),是非法地址,生成页错误异常。
  2. 如果虚拟地址的高 16 位是全 1 或全 0 ,内存管理单元根据关键字 { 地址空间标识符,虚拟地址 } 查找 TLB 。
    通常使用寄存器 TTBR0_EL1 的高 16 位存放正在执行的进程的地址空间标识符( TTBR 是“ Translation Table Base Register ”的缩写,表示转换表基准寄存器; EL1 是“ Exception Level 1 ”的缩写,表示异常级别 1 )。寄存器 TTBR1_EL1 存放内核的页全局目录的物理地址,寄存器 TTBR0_EL1 存放进程的页全局目录的物理地址。

在查找TLB的时候,如果命中了 TLB 表项,从 TLB 表项读取访问权限,检查访问权限,如果没有访问权限,生成页错误异常。
如果没有命中 TLB 表项,内存管理单元将会查询内存中的页表,称为转换表遍历( translation table walk ),分两种情况。

  1. 如果虚拟地址的高 16 位全部是 1 ,说明是内核虚拟地址,应该查询内核的页表,从寄存器 TTBR1_EL1 取内核的页全局目录的物理地址。
  2. 如果虚拟地址的高 16 位全部是 0 ,说明是用户虚拟地址,应该查询进程的页表,从寄存器 TTBR0_EL1 取进程的页全局目录的物理地址。
    在上面两种情况如果在页表没有找到对应的物理地址,说明该虚拟地址没有建立映射关系。这是虚拟内存管理的一个特性。我们在kmalloc之后,仅仅分配了虚拟地址,但是对应的物理地址还没有分配给你,这时候,我们操作虚拟地址的时候,就会在页表找不到相应的物理地址,就是产生缺页异常。
    关于ARM64产生异常后的处理流程请看linux中断系列,处理器生成页错误异常,页错误异常属于同步异常,页错误异常都是交个 do_mem_abort 函数处理的,

1. do_mem_abort

//addr保存了发生异常的虚拟地址,esr是异常综合信息寄存器,regs为发生异常时的pt_regs指针
void do_mem_abort(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
	//根据esr寄存器的ISS字段找到fault_info结构体中相应的处理函数
	const struct fault_info *inf = esr_to_fault_info(esr);
	if (!inf->fn(addr, esr, regs))//执行fault_info结构体中相应的处理函数
		return;//处理成功后返回

	//处理失败,后果很严重,下面会输出一些信息后卡死,我们没有必要看
	if (!user_mode(regs)) {//如果发生异常时候的pt指针不是用户模式
		pr_alert("Unhandled fault at 0x%016lx\n", addr);
		mem_abort_decode(esr);//输出内存相关寄存器值
		show_pte(addr);//输出当前活动mm中与'addr'相关的页表。
	}
	//卡死
	arm64_notify_die(inf->name, regs,
			 inf->sig, inf->code, (void __user *)addr, esr);
}

看了do_mem_abort函数,我们知道他首先通过esr_to_fault_info函数查询esr寄存器的ISS字段,然后根据ISS字段去到struct fault_info结构体找到对应的处理函数。linux定义的struct fault_info如下代码:

static const struct fault_info fault_info[] = {
	{ do_bad,		SIGKILL, SI_KERNEL,	"ttbr address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level 1 address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level 2 address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level 3 address size fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 0 translation fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 1 translation fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 2 translation fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 3 translation fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 8"			},
	{ do_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 1 access flag fault"	},
	{ do_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 2 access flag fault"	},
	{ do_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 3 access flag fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 12"			},
	{ do_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 1 permission fault"	},
	{ do_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 2 permission fault"	},
	{ do_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 3 permission fault"	},
...
	{ do_alignment_fault,	SIGBUS,  BUS_ADRALN,	"alignment fault"		},
...
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 63"			},
};

我们可以看到主要的处理函数有几种:

  1. do_translation_fault:虚拟页没有映射到物理页的情况
  2. do_page_fault:权限错误、访问错误、无效描述符等情况
  3. do_alignment_fault:没有对齐
  4. do_bad:其他错误

1.1 do_translation_fault

static int __kprobes do_translation_fault(unsigned long addr,
					  unsigned int esr,
					  struct pt_regs *regs)
{
	if (is_ttbr0_addr(addr))//如果是用户虚拟地址
		return do_page_fault(addr, esr, regs);//调用do_page_fault申请物理页

	do_bad_area(addr, esr, regs);//内核虚拟地址
	return 0;
}

虚拟页没有映射到物理页,要知道,用户空间的虚拟地址是通过页表映射的,存在申请了虚拟内存但是由于没有使用过导致没有对应的物理内存的情况,而内核空间的虚拟地址是线性映射的,不存在没有映射的情况。do_translation_fault会判断一下没有映射的是用户虚拟地址还是内核虚拟地址,如果是用户虚拟地址,就调用do_page_fault申请物理页并且简历映射关系,如果是内核虚拟地址,就调用do_bad_area处理这些错误区域。do_page_fault后面就会将,这里不啰嗦。我们看看do_bad_area:

static void do_bad_area(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
	/*
	 * If we are in kernel mode at this point, we have no context to
	 * handle this fault with.
	 */
	if (user_mode(regs)) {//处于用户模式,找个进程处理一下
		//根据esr寄存器的ISS字段找到fault_info结构体中相应的信号
		const struct fault_info *inf = esr_to_fault_info(esr);

		set_thread_esr(addr, esr);
		//把相应的信号发送到对应的用户态进程,让用户态进程处理信号
		arm64_force_sig_fault(inf->sig, inf->code, (void __user *)addr,
				      inf->name);
	} else {//处于内核模式,则没有上下文来处理此错误,卡死吧
		__do_kernel_fault(addr, esr, regs);
	}
}

由于现在是在异常处理中,异常之前的进程的状态会保存在struct pt_regs *regs中,do_bad_area首先会判断出现问题的进程的是用户态进程还是内核进程,如果是用户态进程就好办了,调用函数查看esr寄存器的ISS字段,找到fault_info结构体中相应的信号,然后调用arm64_force_sig_fault函数给该进程发送相应的信号;如果出现问题的进程是内核进程,那就没有任何办法了,只能调用__do_kernel_fault输出一些信息然后卡死了。

1.2 do_page_fault

static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,
				   struct pt_regs *regs)
{
	const struct fault_info *inf;
	struct mm_struct *mm = current->mm;
	vm_fault_t fault;
	unsigned long vm_flags = VM_ACCESS_FLAGS;
	unsigned int mm_flags = FAULT_FLAG_DEFAULT;

	//kprobes处理了错误,但是这是不可能的。
	if (kprobe_page_fault(regs, esr))
		return 0;

	/*
	 * If we're in an interrupt or have no user context, we must not take
	 * the fault.
	 */
	//如果current不可以处理fault或者没有mm结构体的时候,说明没有上下文,就去no_context吧
	if (faulthandler_disabled() || !mm)
		goto no_context;

	if (user_mode(regs))//如果是在用户模式下生成的异常
		mm_flags |= FAULT_FLAG_USER;//那么 mm_flags 设置标志位FAULT_FLAG_USER 

	if (is_el0_instruction_abort(esr)) {//如果指令从较低的异常级别中止
		vm_flags = VM_EXEC;
		mm_flags |= FAULT_FLAG_INSTRUCTION;
	} else if (is_write_abort(esr)) {//写数据时生成页错误异常
		vm_flags = VM_WRITE;
		mm_flags |= FAULT_FLAG_WRITE;
	}

	//如果虚拟地址是用户态虚拟地址,并且EL1允许fault
	if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
		//进程在内核模式下把地址上界设置为内核虚拟地址空间上界不能访问用户虚拟地址
		if (regs->orig_addr_limit == KERNEL_DS)
			die_kernel_fault("access to user memory with fs=KERNEL_DS",
					 addr, esr, regs);
		//如果指令中止没有改变异常级别,说明进程在内核模式下试图执行用户空间的指令
		if (is_el1_instruction_abort(esr))
			die_kernel_fault("execution of user memory",
					 addr, esr, regs);
		//根据触发异常的指令的虚拟地址在异常表中没有找到异常修正程序
		if (!search_exception_tables(regs->pc))
			die_kernel_fault("access to user memory outside uaccess routines",
					 addr, esr, regs);
	}
	//perf报告缺页时间发生的信息
	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);

	/*
	 * As per x86, we may deadlock here. However, since the kernel only
	 * validly references user space from well defined areas of the code,
	 * we can bug out early if this is from code which shouldn't.
	 */
	if (!mmap_read_trylock(mm)) {
		//如果异常是发生在内核模式,并且在异常表中找到PC
		if (!user_mode(regs) && !search_exception_tables(regs->pc))
			goto no_context;
retry:
		mmap_read_lock(mm);
	} else {
		/*
		 * The above down_read_trylock() might have succeeded in which
		 * case, we'll have missed the might_sleep() from down_read().
		 */
		might_sleep();
#ifdef CONFIG_DEBUG_VM
		//如果异常是发生在内核模式,并且在异常表中没有找到异常修正程序
		if (!user_mode(regs) && !search_exception_tables(regs->pc)) {
			mmap_read_unlock(mm);
			goto no_context;
		}
#endif
	}

	fault = __do_page_fault(mm, addr, mm_flags, vm_flags, regs);//重要的缺页异常处理函数

	//如果需要VM_FAULT_RETRY但是current是pending
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))//内核进程可以卡死了
			goto no_context;
		return 0;//用户进程返回0
	}

	//如果需要VM_FAULT_RETRY但是current不是pending
	if (fault & VM_FAULT_RETRY) {
		if (mm_flags & FAULT_FLAG_ALLOW_RETRY) {
			mm_flags |= FAULT_FLAG_TRIED;
			goto retry;//回到上面进行重试
		}
	}
	mmap_read_unlock(mm);

	//成功地处理页错误异常,返回。
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP |
			      VM_FAULT_BADACCESS))))
		return 0;

	//来到这里说明处理页错误异常有点问题
	//如果current处于内核模式,去到no_context
	if (!user_mode(regs))
		goto no_context;

	if (fault & VM_FAULT_OOM) {//如果是因为内存空间耗尽,就调用oom杀死进程吧
		pagefault_out_of_memory();
		return 0;
	}

	//来到这里,说明处理页错误异常有点问题并且是在用户模式下生成的异常
	//根据esr寄存器的ISS字段找到fault_info结构体
	inf = esr_to_fault_info(esr);
	set_thread_esr(addr, esr);
	if (fault & VM_FAULT_SIGBUS) {
		/*
		 * We had some memory, but were unable to successfully fix up
		 * this page fault.
		 */
		arm64_force_sig_fault(SIGBUS, BUS_ADRERR, (void __user *)addr,
				      inf->name);
	} else if (fault & (VM_FAULT_HWPOISON_LARGE | VM_FAULT_HWPOISON)) {
		unsigned int lsb;

		lsb = PAGE_SHIFT;
		if (fault & VM_FAULT_HWPOISON_LARGE)
			lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault));

		arm64_force_sig_mceerr(BUS_MCEERR_AR, (void __user *)addr, lsb,
				       inf->name);
	} else {
		/*
		 * Something tried to access memory that isn't in our memory
		 * map.
		 */
		arm64_force_sig_fault(SIGSEGV,
				      fault == VM_FAULT_BADACCESS ? SEGV_ACCERR : SEGV_MAPERR,
				      (void __user *)addr,
				      inf->name);
	}

	return 0;

no_context:
	__do_kernel_fault(addr, esr, regs);//卡死吧
	return 0;
}

do_page_fault首先判断触发异常的情况是否为执行硬中断、执行软中断、禁止硬中断、禁止软中断、禁止内核抢占这几类原子上下文,这几种情况可以直接判死刑,卡死。接着在内核模式访问用户虚拟地址的情况也是die,然后调用__do_page_fault这个重要的缺页异常处理函数,根据返回值,如果oom或者其他错误都是发送信号杀死进程,成功则返回0。其他函数没什么好说的,只有__do_page_fault可以继续追:

static vm_fault_t __do_page_fault(struct mm_struct *mm, unsigned long addr,
				  unsigned int mm_flags, unsigned long vm_flags,
				  struct pt_regs *regs)
{	
	//从current的mm_struct中根据触发异常单的虚拟地址找到对应的vma
	struct vm_area_struct *vma = find_vma(mm, addr);

	//如果没有找到vma,说明该虚拟地址没有分配给进程,虚拟地址是非法的,
	if (unlikely(!vma))
		return VM_FAULT_BADMAP;

	/*
	 * Ok, we have a good vm_area for this memory access, so we can handle
	 * it.
	 */
	//如果找到的虚拟内存区域的起始地址比触发异常的虚拟地址大
	if (unlikely(vma->vm_start > addr)) {
		if (!(vma->vm_flags & VM_GROWSDOWN))//这个虚拟内存区域不是栈,返回错误
			return VM_FAULT_BADMAP;
		if (expand_stack(vma, addr))//扩大栈的虚拟内存区域,失败则返回错误
			return VM_FAULT_BADMAP;
	}

	/*
	 * Check that the permissions on the VMA allow for the fault which
	 * occurred.
	 */
	if (!(vma->vm_flags & vm_flags))//如果虚拟内存区域没有授予触发页错误异常的访问权限
		return VM_FAULT_BADACCESS;
	return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags, regs);//处理页错误异常的函数
}

__do_page_fault首先通过find_vma根据触发异常单的虚拟地址找到对应的vma,然后判断vma是否合法和是否拥有触发页错误异常的访问权限,没有则返回错误,有则通过handle_mm_fault处理页错误异常。下面我们看看find_vma和handle_mm_fault函数吧。

1.2.1 find_vma
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
	struct rb_node *rb_node;
	struct vm_area_struct *vma;

	/* Check the cache first. */
	//在task_struct结构中的vmacache(存放最近访问过的VMA的数组)查找addr
	vma = vmacache_find(mm, addr);
	if (likely(vma))
		return vma;

	rb_node = mm->mm_rb.rb_node;//取出current的mm_struct的rb_node

	while (rb_node) {//遍历红黑树,找到vma
		struct vm_area_struct *tmp;

		tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);

		if (tmp->vm_end > addr) {
			vma = tmp;
			if (tmp->vm_start <= addr)
				break;
			rb_node = rb_node->rb_left;
		} else
			rb_node = rb_node->rb_right;
	}

	if (vma)//如果找到vma,跟新vmacache
		vmacache_update(addr, vma);
	return vma;
}

find_vma很简单,首先从current的这个task_struct结构中的vmacache中的vma查找,是否有合适的vma;找不到再从其中的mm_struct
查找所有的vma,这些vma组成一个红黑树,我们遍历这个红黑树来查找vma,找到vma则跟新vmacache,返回vma;找不到则返回NULL。级组合么简单。

1.2.2 handle_mm_fault
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
			   unsigned int flags, struct pt_regs *regs)
{
	vm_fault_t ret;

	__set_current_state(TASK_RUNNING);//把当前的进程设置为running状态

	count_vm_event(PGFAULT);//当前cpu的PGFAULT这个事件数量加一
	count_memcg_event_mm(vma->vm_mm, PGFAULT);//对应的memcg的PGFAULT这个事件数量加一

	/* do counter updates before entering really critical section. */
	check_sync_rss_stat(current);//空函数

	if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
					    flags & FAULT_FLAG_INSTRUCTION,
					    flags & FAULT_FLAG_REMOTE))
		return VM_FAULT_SIGSEGV;

	/*
	 * Enable the memcg OOM handling for faults triggered in user
	 * space.  Kernel faults are handled more gracefully.
	 */
	if (flags & FAULT_FLAG_USER)//如果是用户空间触发的故障,需要设置进程状态
		mem_cgroup_enter_user_fault();//设置current的in_user_fault为1

	if (unlikely(is_vm_hugetlb_page(vma)))//如果是巨型页
		ret = hugetlb_fault(vma->vm_mm, vma, address, flags);//巨型页缺页处理
	else//不是巨型页
		ret = __handle_mm_fault(vma, address, flags);//普通页缺页处理

	if (flags & FAULT_FLAG_USER) {//如果是用户空间触发的故障,需要解除进程状态
		mem_cgroup_exit_user_fault();//设置current的in_user_fault为0
		/*
		 * The task may have entered a memcg OOM situation but
		 * if the allocation error was handled gracefully (no
		 * VM_FAULT_OOM), there is no need to kill anything.
		 * Just clean up the OOM state peacefully.
		 */
		//如果任务进入了memcg OOM,但是如果正确地处理了分配错误(没有VM_FAULT_OOM),
		if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
			mem_cgroup_oom_synchronize(false);//直接返回错误,所以看不懂
	}

	mm_account_fault(regs, address, flags, ret);//统计缺页异常计数

	return ret;
}

handle_mm_fault首先把当前的进程设置为running状态,然后设置进程current的in_user_fault状态为1。接着判断vma是巨型页还是普通页,巨型页则调用hugetlb_fault进行巨型页缺页处理,普通也则调用__handle_mm_fault进行普通页缺页处理。最后设置进程current的in_user_fault状态为0,调用mm_account_fault统计缺页异常计数,返回缺页处理结果。我们要好好看看hugetlb_fault和__handle_mm_fault。

1.2.2.1 hugetlb_fault

这是巨型页的缺页异常处理函数,放到后面专门讲解各种情况页面的缺页处理集合中。

1.2.2.2 __handle_mm_fault
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
		unsigned long address, unsigned int flags)
{
	struct vm_fault vmf = {
		.vma = vma,
		.address = address & PAGE_MASK,
		.flags = flags,
		.pgoff = linear_page_index(vma, address),
		.gfp_mask = __get_fault_gfp_mask(vma),
	};
	unsigned int dirty = flags & FAULT_FLAG_WRITE;
	struct mm_struct *mm = vma->vm_mm;
	pgd_t *pgd;
	p4d_t *p4d;
	vm_fault_t ret;

	pgd = pgd_offset(mm, address);//查找页全局目录表项
	p4d = p4d_alloc(mm, pgd, address);//在pgd中查找页四级目录表项,如果不存在则创建页四级目录表项
	if (!p4d)
		return VM_FAULT_OOM;

	vmf.pud = pud_alloc(mm, p4d, address);//在p4d中查找页上层目录表项,如果不存在则创建页上层目录表项
	if (!vmf.pud)
		return VM_FAULT_OOM;
retry_pud:
	if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
		ret = create_huge_pud(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	} else {
		pud_t orig_pud = *vmf.pud;

		barrier();
		if (pud_trans_huge(orig_pud) || pud_devmap(orig_pud)) {

			/* NUMA case for anonymous PUDs would go here */

			if (dirty && !pud_write(orig_pud)) {
				ret = wp_huge_pud(&vmf, orig_pud);
				if (!(ret & VM_FAULT_FALLBACK))
					return ret;
			} else {
				huge_pud_set_accessed(&vmf, orig_pud);
				return 0;
			}
		}
	}

	vmf.pmd = pmd_alloc(mm, vmf.pud, address);//在pud中查找页中间目录表项,如果不存在则创建页中间目录表项
	if (!vmf.pmd)
		return VM_FAULT_OOM;

	/* Huge pud page fault raced with pmd_alloc? */
	if (pud_trans_unstable(vmf.pud))
		goto retry_pud;

	if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
		ret = create_huge_pmd(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	} else {
		pmd_t orig_pmd = *vmf.pmd;

		barrier();
		if (unlikely(is_swap_pmd(orig_pmd))) {
			VM_BUG_ON(thp_migration_supported() &&
					  !is_pmd_migration_entry(orig_pmd));
			if (is_pmd_migration_entry(orig_pmd))
				pmd_migration_entry_wait(mm, vmf.pmd);
			return 0;
		}
		if (pmd_trans_huge(orig_pmd) || pmd_devmap(orig_pmd)) {
			if (pmd_protnone(orig_pmd) && vma_is_accessible(vma))
				return do_huge_pmd_numa_page(&vmf, orig_pmd);

			if (dirty && !pmd_write(orig_pmd)) {
				ret = wp_huge_pmd(&vmf, orig_pmd);
				if (!(ret & VM_FAULT_FALLBACK))
					return ret;
			} else {
				huge_pmd_set_accessed(&vmf, orig_pmd);
				return 0;
			}
		}
	}

	return handle_pte_fault(&vmf);//在pmd中肯定找不到页表,在这里处理。
}

__handle_mm_fault函数是普通页缺页处理函数,首先通过mm_struct找到页全局目录表项;然后在pgd中查找页四级目录表项,如果不存在则创建页四级目录表项;然后在p4d中查找页上层目录表项,如果不存在则创建页上层目录表项;然后在pud中查找页中间目录表项,如果不存在则创建页中间目录表项;最后在pmd中肯定找不到页表,调用handle_pte_fault处理。我们重点看看handle_pte_fault:

static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
	pte_t entry;

	if (unlikely(pmd_none(*vmf->pmd))) {//如果页中间目录表项是空表项
		vmf->pte = NULL;//说明直接页表不存在,vmf->pte 设置成空指针。
	} else {//页中间目录表项存在
		if (pmd_devmap_trans_unstable(vmf->pmd))
			return 0;
		/*
		 * A regular pmd is established and it can't morph into a huge
		 * pmd from under us anymore at this point because we hold the
		 * mmap_lock read mode and khugepaged takes it in write mode.
		 * So now it's safe to run pte_offset_map().
		 */
		vmf->pte = pte_offset_map(vmf->pmd, vmf->address);//在pmd中查找页表项
		vmf->orig_pte = *vmf->pte;//vmf->pte存放表项的地址,vmf->orig_pte存放页表项的值

		/*
		 * some architectures can have larger ptes than wordsize,
		 * e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and
		 * CONFIG_32BIT=y, so READ_ONCE cannot guarantee atomic
		 * accesses.  The code below just needs a consistent view
		 * for the ifs and we later double check anyway with the
		 * ptl lock held. So here a barrier will do.
		 */
		barrier();
		if (pte_none(vmf->orig_pte)) {//如果页表项是空表项,
			pte_unmap(vmf->pte);//空操作
			vmf->pte = NULL;//vmf->pte没必要存放表项的地址,设置成空指针
		}
	}

	if (!vmf->pte) {//如果页表项不存在(直接页表不存在或者页表项是空表项)
		if (vma_is_anonymous(vmf->vma))//如果是私有匿名映射,
			return do_anonymous_page(vmf);//处理匿名页的缺页异常
		else
			return do_fault(vmf);//处理文件页的缺页异常(共享匿名映射是在内核的文件页)
	}

	if (!pte_present(vmf->orig_pte))//如果页表项存在,但是页不在物理内存中
		return do_swap_page(vmf);//说明页被换出到交换区,把页从交换区读到内存中。

	if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
		return do_numa_page(vmf);//NUMA的情况,涉及的PAGE的迁移

	//到这里说明页表项存在,并且页在物理内存中,也就是页错误异常是由访问权限触发的。
	vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);//获取页表锁
	spin_lock(vmf->ptl);//给页表加锁
	entry = vmf->orig_pte;
	if (unlikely(!pte_same(*vmf->pte, entry))) {//重新读取页表项的值,如果和前面读取的不相同
		update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);//说明其他处理器可能正在修改同一个页表项,不是问题,等待其他处理器处理完就好
		goto unlock;
	}
	if (vmf->flags & FAULT_FLAG_WRITE) {//如果页错误异常是由写操作触发的
		if (!pte_write(entry))//如果页表项没有写权限
			return do_wp_page(vmf);//执行写时复制
		entry = pte_mkdirty(entry);//有写权限则设置页表项的脏标志位
	}
	entry = pte_mkyoung(entry);//设置页表项的访问标志位,表示页刚刚被访问过

	//设置页表项,如果页表项发生变化
	if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
				vmf->flags & FAULT_FLAG_WRITE)) {
		//更新处理器的内存管理单元的页表缓存
		update_mmu_cache(vmf->vma, vmf->address, vmf->pte);
	} else {//页表项没有发生变化,
		//如果这个错误已经试过一次了,返回吧
		if (vmf->flags & FAULT_FLAG_TRIED)
			goto unlock;

		if (vmf->flags & FAULT_FLAG_WRITE)//页错误异常是由写操作触发的
			//页错误异常可能是TLB表项和页表项不一致导致的,那么使TLB表项失效
			flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
	}
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);//给页表解锁
	return 0;
}

handle_pte_fault的处理分为两种情况:

  1. 页表指向的页不在物理内存中
  2. 页表指向的页在物理内存中

页表指向的页不在物理内存中,就需要根据页的种类来进行物理页的申请和映射了,分别是匿名页缺页、文件页缺页、被交换到swap中、在其他内存节点上。这些情况放到后面专门讲解各种情况页面的缺页处理集合中。
页表指向的页在物理内存中,首先重新读取页表项的值,如果和前面读取的不相同,说明其他处理器可能正在修改同一个页表项,不是问题,等待其他处理器处理完就好,返回。如果页错误异常是由写操作触发的,并且页表项没有写权限,那就是cow了,需要调用do_wp_page执行写时复制后返回。do_wp_page也放到后面专门讲解各种情况页面的缺页处理集合中。

1.3 do_alignment_fault

static int do_alignment_fault(unsigned long addr, unsigned int esr,
			      struct pt_regs *regs)
{
	do_bad_area(addr, esr, regs);
	return 0;
}

看到do_alignment_fault代码,我们知道如果遇到内存没有对齐的问题,他直接调用do_bad_area处理,do_bad_areaw我们在前面的1.1 do_translation_fault里面讲解过了,这里不重复。

1.4 do_bad

static int do_bad(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
	return 1; /* "fault" */
}

do_bad里面也没有什么处理,仅仅返回1。

总结

从do_mem_abort追下来,大部分异常我们linux都是无法处理的,就直接把系统卡死。只有小部分系统情况我们可以申请物理内存等情况是可以处理的:

  1. 巨型页缺页
  2. 匿名页缺页
  3. 文件页缺页
  4. 页在交换分区中
  5. 也在其他内存节点中
  6. 写时复制

这些情况我们在下节中详细分析。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小坚学Linux

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值