Linux内存管理:(四)缺页异常处理

文章说明:

malloc()和mmap()只是建立了进程地址空间,没有建立虚拟内存和物理内存之间的映射关系,当进程访问这些还没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,也称为缺页中断。

1. ARM64 缺页异常的底层处理流程

缺页异常处理依赖于处理器的架构,因此缺页异常的底层处理流程实现在内核代码中特定于架构的部分中。下面以ARM64为例来介绍缺页异常的底层处理流程,如下图所示:

在这里插入图片描述

do_mem_abort()函数:

asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
					 struct pt_regs *regs)
{
    
    
	// esr_to_fault_info() 根据 DFSC 字段的值来查询 fault_info 表
	const struct fault_info *inf = esr_to_fault_info(esr);

	// 跳转到具体的异常处理函数中
	if (!inf->fn(addr, esr, regs))
		return;

	if (!user_mode(regs)) {
    
    
		pr_alert("Unhandled fault at 0x%016lx\n", addr);
		mem_abort_decode(esr);
		show_pte(addr);
	}

	// 如果在 fault_info 表中没有找到对应的异常,那么调用 arm64_notify_die() 来输出错误信息
	arm64_notify_die(inf->name, regs,
			 inf->sig, inf->code, (void __user *)addr, esr);
}

fault_info数据结构:

struct fault_info {
    
    
	// 定义一个函数指针,用于修复异常状态的函数指针
	int	(*fn)(unsigned long addr, unsigned int esr,
		      struct pt_regs *regs);
	// 处理失败时 Linux 内核要发送的信号类型
	int	sig;
	// 处理失败时 Linux 内核要发送的信号编码
	int	code;
	// 这条异常状态的名称
	const char *name;
};

异常向量表fault_info[]:

// 异常向量表
// do_translation_fault: 处理与页表转换相关的异常错误
// do_page_fault: 处理与页表访问或者权限相关的异常错误
// do_alignment_fault: 处理与对齐相关的异常错误
// do_bad: 处理与未知的错误或者硬件相关的错误,如 TLB 冲突等
static const struct fault_info fault_info[] = {
    
    
	...
};

2. do_page_fault()函数

缺页异常修复方案的核心函数是do_page_fault(),该函数的实现和具体的架构相关。对于ARM64架构,该函数实现在arch/arm64/mm/fault.c文件中:

// 缺页异常处理的核心函数
// 处理与页表访问或者权限相关的异常错误
// addr: 表示异常发生时的虚拟地址,由 FAR 提供
// esr: 表示异常发生时的异常状态,由 ESR 提供
// regs:异常发生时的 pt_regs
static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,
				   struct pt_regs *regs)
{
    
    
	...
	// 检查异常发生时,内核是否正在执行一些关键路径中的代码
	// 如果缺页异常发生在中断上下文或者内核线程(mm字段总为NULL)中,那么直接跳到 no_context 标签处的__do_kernel_fault()
	if (faulthandler_disabled() || !mm)
		goto no_context;

	// user_mode() 通过 PSTATE 寄存器来判断异常是否发生在 EL0
	if (user_mode(regs))
		// 若在用户模式下发生异常,设置 FAULT_FLAG_USER 标志位
		mm_flags |= FAULT_FLAG_USER;

	// is_el0_instruction_abort() 函数读取 ESR 的 EC 字段来判断异常是否为低异常等级的指令异常
	if (is_el0_instruction_abort(esr)) {
    
    
		...
	}

	// is_ttbr0_addr() 判断异常地址是否发生在用户空间
	// is_el1_permission_fault() 权限问题是否发生在 EL1 中
	if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
    
    
		/* regs->orig_addr_limit may be 0 if we entered from EL0 */
		if (regs->orig_addr_limit == KERNEL_DS)
			die_kernel_fault("access to user memory with fs=KERNEL_DS",
					 addr, esr, regs);

		// 判断是否是 EL1 的指令异常
		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);
	}

	...
	// 上述判断完成之后,可以断定缺页异常没有发生在中断上下文、内核线程,以及一些特殊情况下
	// 接下来,要检查由进程地址空间而引发的缺页异常
	...

	// 进行进一步的处理,返回 vm_fault_t 类型的处理结果
	fault = __do_page_fault(mm, addr, mm_flags, vm_flags, tsk);
	major |= fault & VM_FAULT_MAJOR;

	// 处理需要重试的情况
	if (fault & VM_FAULT_RETRY) {
    
    
	...
    
	// __do_page_fault() 函数返回正确的情况
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP |
			      VM_FAULT_BADACCESS)))) {
    
    
		...

		return 0;
	}

	// __do_page_fault() 函数返回错误的情况
	if (!user_mode(regs))
		goto no_context;

	// 处理 VM_FAULT_OOM 错误的情况
	if (fault & VM_FAULT_OOM) {
    
    
		pagefault_out_of_memory();
		return 0;
	}

	...
	// 处理 VM_FAULT_SIGBUS 错误情况
	if (fault & VM_FAULT_SIGBUS) {
    
    
		...
	}

	return 0;

no_context:
	__do_kernel_fault(addr, esr, regs);
	return 0;
}

do_page_fault->__do_page_fault->find_vma

在这里插入图片描述

3. 匿名页面缺页中断

在Linux内核中没有关联到文件映射的页面称为匿名页面(anonymous page)。在缺页中断处理中,匿名页面处理的核心函数是do_anonymous_page(),该函数的流程图如下所示:

在这里插入图片描述

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

// 匿名页面缺页异常处理函数
// 该函数依然持有进程的 mmap_sem 信号量
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	struct mem_cgroup *memcg;
	struct page *page;
	vm_fault_t ret = 0;
	pte_t entry;

	// 主要目的是防止共享的 VMA 进入匿名页面的缺页中断里
	if (vma->vm_flags & VM_SHARED)
		return VM_FAULT_SIGBUS;

	// 分配一个 PTE,并且把 PTE 设置到对应的 PMD 页表项中
	if (pte_alloc(vma->vm_mm, vmf->pmd))
		return VM_FAULT_OOM;

	if (unlikely(pmd_trans_unstable(vmf->pmd)))
		return 0;

	// 根据参数 flags 判断是否需要可写权限,当需要分配得到内存只有只读属性时,系统会使用一个内容全填充为 0 的全局页面
	// empty_zero_page,称为零页(ZERO_PAGE)。这个零页是一个特殊映射的页面
	if (!(vmf->flags & FAULT_FLAG_WRITE) &&
			!mm_forbids_zeropage(vma->vm_mm)) {
    
    
		// my_zero_pfn() 获取系统零页的帧号
		// pte_mkspecial() 宏用来设置 PTE 中的 PTE_SPECIAL 位,表示这是特殊映射页面
		entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
						vma->vm_page_prot));
		// 获取 PTE
		vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				vmf->address, &vmf->ptl);
		if (!pte_none(*vmf->pte))
			goto unlock;
		ret = check_stable_address_space(vma->vm_mm);
		if (ret)
			goto unlock;
		if (userfaultfd_missing(vma)) {
    
    
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			return handle_userfault(vmf, VM_UFFD_MISSING);
		}
		goto setpte;
	}

	// 处理 VMA 属性可写的情况
	// anon_vma_prepare() 为建立 RMAP 做准备
	if (unlikely(anon_vma_prepare(vma)))
		goto oom;
	// 分配一个可移动的匿名页面
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
	if (!page)
		goto oom;

	...

	// 生成一个新的 PTE,这个新的 PTE 是基于刚才分配的物理页面的帧号来创建的
	entry = mk_pte(page, vma->vm_page_prot);
	if (vma->vm_flags & VM_WRITE)
		entry = pte_mkwrite(pte_mkdirty(entry));

	// 在调用 set_pte_at() 设置进程页表时,需要使用 pte_offset_map_lock() 宏来获取 page_table_lock 自旋锁,防止其它 CPU 同时修改进程的页表
	// pte_offset_map_lock() 函数获取 address 对应的 PTE,这里会申请一个自旋锁
	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);
	if (!pte_none(*vmf->pte))
		goto release;

	...

	// 增加进程匿名页面计数,匿名页面的计数类型是 MM_ANONPAGES
	inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
	// 把匿名页面添加到 RMAP 系统中
	page_add_new_anon_rmap(page, vma, vmf->address, false);
	mem_cgroup_commit_charge(page, memcg, false, false);
	// 把匿名页面添加到 LRU 链表中,在 kswap 内核模块中会用到 LRU 链表
	lru_cache_add_active_or_unevictable(page, vma);
setpte:
	// 通过 set_pte_at() 函数设置到硬件页表中
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);

	...
}

4. 文件映射缺页中断

文件映射是指映射和实际文件相关联,通常把文件内容映射到进程地址空间,这样应用程序就可以像操作进程地址空间一样读写文件,如下图所示:

在这里插入图片描述

在没有找到对应的PTE并且是文件映射的情况下,会调用do_fault()函数,该函数的流程图如下图所示:

在这里插入图片描述

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

do_fault

static vm_fault_t do_fault(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret;

	// 处理 VMA 中的 vm_ops() 方法集中没有实现 fault() 的情况,有些内核模块或者驱动的 mmap() 函数并没有实现 fault() 回调函数
	if (!vma->vm_ops->fault) {
    
    
		...
	// 检查 vmf->flags 标志位,若这次缺页异常是由读内存导致的,那么调用 do_read_fault()
	} else if (!(vmf->flags & FAULT_FLAG_WRITE))
		ret = do_read_fault(vmf);
	// 若这次缺页异常是由写内存导致的并且 VMA 的属性中没有设置 VM_SHARED,
	// 即这个 VMA 是属于私有映射的,那么调用 do_cow_fault() 函数
	else if (!(vma->vm_flags & VM_SHARED))
		ret = do_cow_fault(vmf);
	// 若 VMA 属于共享映射并且这次异常是写内存导致的缺页异常,那么调用 do_shared_fault() 函数
	else
		ret = do_shared_fault(vmf);

	if (vmf->prealloc_pte) {
    
    
		// 释放 prealloc_pte
		pte_free(vma->vm_mm, vmf->prealloc_pte);
		vmf->prealloc_pte = NULL;
	}
	return ret;
}

do_fault->do_read_fault

// 处理因为读内存导致的缺页中断
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret = 0;

	// 若 VMA 定义了 map_pages() 函数,那么可以在缺页异常地址附近提前映射尽可能多的页面
	// 提前建立进程地址空间和页面高速缓存的映射关系有利于减少发生缺页异常的次数,从而提高效率
	// 注意:这里只是和现存的页面高速缓存提前建立映射关系,而不会创建页面高速缓存
	if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
    
    
		// do_fault_around() 函数以当前缺页异常地址(addr)为中心,先取 16 个页面对齐的地址,
		// 如果该地址小于 VMA 的起始地址 vm_start,则取 vm_start从,从 addr 开始到 end_pgoff,
		// 检查每个 PTE,若 PTE 内容为空,那么调用 vm_ops->map_pages,映射 PTE 
		ret = do_fault_around(vmf);
		if (ret)
			return ret;
	}

	// 创建新的页面高速缓存,真正为异常地址分配物理页面
	ret = __do_fault(vmf);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		return ret;

	ret |= finish_fault(vmf);
	// 释放页锁并且唤醒等待这个页锁的进程
	unlock_page(vmf->page);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		put_page(vmf->page);
	return ret;
}

do_fault->do_cow_fault

// 主要处理由写内存导致地缺页异常,而且 VMA 的属性是具有私有映射的,
// 也就是处理在私有文件映射的 VMA 中发生了写时复制
static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret;

	// anon_vma_prepare() 函数检查该 VMA 是否初始化了 RMAP
	if (unlikely(anon_vma_prepare(vma)))
		return VM_FAULT_OOM;

	// 以 GFP_HIGHUSER_MOVABLE 为分配掩码,为 cow_page 分配一个新的物理页面,也就是优先使用高端内存
	vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
	if (!vmf->cow_page)
		return VM_FAULT_OOM;

	...

	// __do_fault() 函数通过 vma->vm_ops->fault() 函数读取文件内容到 vmf->page 页面里
	ret = __do_fault(vmf);
	...

	// 把 fault_page 的内容复制到刚才新分配的 cow_page 中
	copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
	__SetPageUptodate(vmf->cow_page);

	// 使用 cow_page 来创建一个 PTE,将其设置到物理页表中,并把这个 cow_page 添加到匿名页面的 RMAP 机制中
	ret |= finish_fault(vmf);
	// 释放 vmf->page 的页锁,并且释放这个页面
	unlock_page(vmf->page);
	put_page(vmf->page);
	...
}

do_fault->do_shared_fault

// 处理共享文件映射中发生写缺页异常的情况
static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
    
    
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret, tmp;

	// __do_fault() 函数通过 vma->vm_ops->fault() 函数读取文件内容到 vmf->page 页面里
	ret = __do_fault(vmf);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		return ret;

	// 如果 VMA 的操作函数中定义了 page_mkwrite() 方法,那么调用 page_mkwrite() 来通知进程地址空间,页面将变成可写的。
	// 若一个页面变成可写的,那么进程可能需要等待这个页面的内容回写成功
	if (vma->vm_ops->page_mkwrite) {
    
    
		...
	}

	// 使用 vmf->page 页面来制作一个 PTE 并将其设置到物理页表中,并把这个 vmf->page 添加到文件页面的 RMAP 机制中
	ret |= finish_fault(vmf);
	...
	
	// 设置 vmf->page 为脏页,通过 balance_dirty_pages_ratelimited() 函数来平衡并回写一部分脏页
	fault_dirty_shared_page(vma, vmf->page);
	return ret;
}

5. 写时复制

当用户试图修改只有只读属性的页面时,CPU触发异常,在do_wp_page()函数里尝试修复这些页面,通常做法是新分配一个页面并且复制旧页面内容到新的页面中,这个新分配的页面具有可写的属性。do_wp_page()函数的流程图如下图所示:

在这里插入图片描述

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

do_wp_page

static vm_fault_t do_wp_page(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
    
    
	struct vm_area_struct *vma = vmf->vma;

	// 通过 vm_normal_page() 函数查找缺页异常地址(addr)对应页面的 page 数据结构,返回普通映射页面。
	// 若返回的 page 指针为 NULL,说明这是一个特殊映射页面
	vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
	if (!vmf->page) {
    
    
		// 若发生缺页异常的页面是一个特殊映射页面,并且 VMA 的属性是可写并且共享的,
		// 那么会调用 wp_pfn_shared() 函数
		if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
				     (VM_WRITE|VM_SHARED))
			return wp_pfn_shared(vmf);

		pte_unmap_unlock(vmf->pte, vmf->ptl);
		// 若发生缺页异常的页面不是一个可写的共享页面,那么跳转到 wp_page_copy() 函数中
		return wp_page_copy(vmf);
	}

	// 开始处理缺页异常页面是一个普通映射页面的情况
	// 首先判断当前页面是否为不属于 KSM 的匿名页面
	// PageAnon() 宏判断当前页面是否为匿名页面,它利用 page->mapping 指针的最低两位来判断
	if (PageAnon(vmf->page) && !PageKsm(vmf->page)) {
    
    
		int total_map_swapcount;
		// trylock_page() 函数判断当前的 vmf->page 是否已经加锁
		// 若返回 false,说明这个页面已经被别的进程加锁
		if (!trylock_page(vmf->page)) {
    
    
			get_page(vmf->page);
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			// 等待其他进程释放锁,会睡眠等待
			lock_page(vmf->page);
			// 获取 PTE
			vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
					vmf->address, &vmf->ptl);
			// 判断 PTE 是否发生了变化,若发生变化,那只能退出这一次异常处理了
			if (!pte_same(*vmf->pte, vmf->orig_pte)) {
    
    
				unlock_page(vmf->page);
				pte_unmap_unlock(vmf->pte, vmf->ptl);
				put_page(vmf->page);
				return 0;
			}
			put_page(vmf->page);
		}
		// reuse_swap_page() 函数判断 vmf->page 页面是否只有一个进程映射的匿名页面
		if (reuse_swap_page(vmf->page, &total_map_swapcount)) {
    
    
			...
		}
		unlock_page(vmf->page);
	// 处理可写的共享页面的情况
	} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
					(VM_WRITE|VM_SHARED))) {
    
    
		return wp_page_shared(vmf);
	}

	// 处理写时复制的情况
	get_page(vmf->page);

	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return wp_page_copy(vmf);
}

猜你喜欢

转载自blog.csdn.net/qq_58538265/article/details/135218916