Linux缺页异常分析

1 概述

        本文分析了Linux内核关于缺页异常的处理流程,包括缺页原理和源码流程的分析,本文在应用申请内存原理基础上较为好理解,以arm64架构为例进行流程分析和理解。

2 原理

        产生缺页的原理,从根因上看主要由于MMU访问页表内存时出现异常导致进入缺页异常处理流程的,具体大致可分为以下三种情况:

        (1)页表映射空缺,页表体系仍不完整例如PTE页表还没填充等,此时该虚拟内存还没有映射物理内存。

        (2)访问的虚拟内存已经映射了物理内存,但是该内存在访问时已经被交换到磁盘(sawp),此时内核会产生一个缺页中断来重新分配映射一个物理内存。

        (3)访问的虚拟内存已经映射了物理内存,但是权限不足,典型的例如私有文件映射,此时会产生COW写时复制

        产生缺页中断时,进程正在使用的相关寄存器中的值被压入内核栈中,同时 CPU 还会将缺页异常的错误码 error_code 压入内核栈中。

3 源码分析 

3.1 缺页异常入口

        当产生一个缺页异常,陷入内核后通过异常向量调用,最终调用到arch\arm64\mm\fault.c下的

static int __kprobes do_page_fault(unsigned long far, unsigned long esr, struct pt_regs *regs)

        struct pt_regs 结构中存放的是缺页异常发生时,正在使用中的寄存器值的集合。address 表示触发缺页异常的虚拟内存地址。

        error_code 是对缺页异常的一个描述,目前内核只使用了 error_code 的前六个比特位来描述引起缺页异常的具体原因。

        far是产生异常时,传入当前访问的异常地址值。

do_page_fault主要对当前异常状态进行检查,根据异常类型:用户/内核还是读写等状态进行预处理

static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
				   struct pt_regs *regs)
{
	const struct fault_info *inf;
	struct mm_struct *mm = current->mm;//获取当前进程的内存描述符结构体 mm_struct
	vm_fault_t fault;
	unsigned long vm_flags;
	unsigned int mm_flags = FAULT_FLAG_DEFAULT;
	unsigned long addr = untagged_addr(far);//在 arm64 架构中,地址可能带有标签(tag),这个函数去除标签,获取纯净的故障虚拟地址
    //是否由于kprobe探针引起,kprobe在此用于调试跟踪,不能处理改异常,直接正确返回
	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.
	 */
    //如果当前没有用户空间,或者处于一个中断,则交由内核直接处理,no_context后面是内核异常处理入口
	if (faulthandler_disabled() || !mm)
		goto no_context;
    //判断故障发生时 CPU 是在用户模式(EL0)还是内核模式(EL1),并在 mm_flags 中标记,以便后续内存管理函数知道访问权限的限制。
	if (user_mode(regs))
		mm_flags |= FAULT_FLAG_USER;

	/*
	 * vm_flags tells us what bits we must have in vma->vm_flags
	 * for the fault to be benign, __do_page_fault() would check
	 * vma->vm_flags & vm_flags and returns an error if the
	 * intersection is empty
	 */
//以下是解析故障类型(读/写/执行):根据 esr 寄存器判断是哪种类型的访问导致了故障,并设置 vm_flags 和 mm_flags。这些标志用于后续检查目标虚拟内存区域 (VMA) 是否允许相应的操作(读、写、执行)。
	if (is_el0_instruction_abort(esr)) {
		/* It was exec fault */
		vm_flags = VM_EXEC;//执行权限错误
		mm_flags |= FAULT_FLAG_INSTRUCTION;
	} else if (is_write_abort(esr)) {
		/* It was write fault */
		vm_flags = VM_WRITE;//写错误
		mm_flags |= FAULT_FLAG_WRITE;
	} else {
		/* It was read fault */
		vm_flags = VM_READ;//读错误
		/* Write implies read */
		vm_flags |= VM_WRITE;
		/* If EPAN is absent then exec implies read */
		if (!cpus_have_const_cap(ARM64_HAS_EPAN))
			vm_flags |= VM_EXEC;
	}
//内核访问用户空间权限检查:如果故障发生在内核模式下,但访问的是用户空间地址(is_ttbr0_addr(addr)),并且发生了权限故障(is_el1_permission_fault),则进行严格检查。
	if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
		if (is_el1_instruction_abort(esr))//如果内核尝试执行用户空间的内存,直接触发内核错误 die_kernel_fault。
			die_kernel_fault("execution of user memory",
					 addr, esr, regs);
//如果内核访问用户空间内存但故障地址不在异常表(__ex_table)中(即没有使用 copy_from_user() 等标准 uaccess 函数),也触发内核错误(这确保了内核代码访问用户内存的安全性)
		if (!search_exception_tables(regs->pc))
			die_kernel_fault("access to user memory outside uaccess routines",
					 addr, esr, regs);
	}
    //性能事件记录:记录一个软件性能事件,用于统计页故障次数。
	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);

	....

//调用 __do_page_fault,这是内存管理子系统的核心函数,负责查找 VMA、分配物理页、建立页表映射、处理写时复制 (COW) 等具体工作。它返回一个 vm_fault_t 类型的标志,指示处理结果。
	fault = __do_page_fault(mm, addr, mm_flags, vm_flags, regs);

	/* Quick path to respond to signals */
//信号处理:如果核心故障处理过程中发现有待处理的信号,且发生在用户模式下,则返回 0,让用户进程返回用户空间处理信号。内核模式下发现信号则 goto no_context即交由内核处理
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))
			goto no_context;
		return 0;
	}
    //重试机制:如果 __do_page_fault 返回 VM_FAULT_RETRY,表示需要重试(例如,需要一个写锁而不是读锁才能完成操作)。如果允许重试标志被设置,则跳转回 retry 标签,重新获取锁并再次执行核心逻辑。否则,释放锁。
	if (fault & VM_FAULT_RETRY) {
		if (mm_flags & FAULT_FLAG_ALLOW_RETRY) {
			mm_flags |= FAULT_FLAG_TRIED;
			goto retry;
		}
	}
	mmap_read_unlock(mm);

	/*
	 * Handle the "normal" (no error) case first.
	 */
//如果 fault 标志没有包含任何错误(VM_FAULT_ERROR, VM_FAULT_BADMAP, VM_FAULT_BADACCESS),说明缺页已经成功解决,返回 0。
	if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP |
			      VM_FAULT_BADACCESS))))
		return 0;

	/*
	 * If we are in kernel mode at this point, we have no context to
	 * handle this fault with.
	 */
//内核错误处理:如果发生了错误,并且当前处于内核模式,说明无法优雅处理,跳转到 no_context。
	if (!user_mode(regs))
		goto no_context;
//OOM (Out Of Memory) 处理:如果错误是内存不足(VM_FAULT_OOM),则调用 OOM killer 杀死占用大量内存的进程,然后返回用户空间。
	if (fault & VM_FAULT_OOM) {
		/*
		 * We ran out of memory, call the OOM killer, and return to
		 * userspace (which will retry the fault, or kill us if we got
		 * oom-killed).
		 */
		pagefault_out_of_memory();
		return 0;
	}
/*
向用户空间发送信号:如果以上所有处理都失败了,说明是一个严重的用户空间错误,需要向用户进程发送信号来终止它。
VM_FAULT_SIGBUS: 发送 SIGBUS 信号(总线错误,通常是文件映射错误)。
VM_FAULT_HWPOISON_*: 发送硬件相关的内存故障信号。
其他错误(如 VM_FAULT_BADMAP):发送 SIGSEGV 信号(段错误,访问了无效地址)。
*/
	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, far, 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, far, 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,
				      far, inf->name);
	}

	return 0;

no_context:
/*
内核错误处理标签:当无法在当前上下文中安全处理故障时(如中断上下文、内核线程),会跳转到这里调用 __do_kernel_fault,该函数会打印错误信息(OOPS)并可能导致内核崩溃(Panic),因为内核代码出现了预期之外的内存访问错误。
*/
	__do_kernel_fault(addr, esr, regs);
	return 0;
}

(1)do_page_fault处理了__do_page_fault各种返回情况,并在无法处理错误时交由内核处理__do_kernel_fault.

#define VM_FAULT_BADMAP		((__force vm_fault_t)0x010000)
#define VM_FAULT_BADACCESS	((__force vm_fault_t)0x020000)

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)
{
	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.
	 */
/* 
   	 * addr后面的vma的vm_flags含有VM_GROWSDOWN标志,说明这个vma属于栈的vma
	 * 即addr在栈中,有可能是栈空间不够时再进栈导致的访问错误
	 * 同时检查栈是否还能扩展,如果不能扩展则确认确实是栈溢出导致,即addr确实是栈中地址,不是非法地址
	 * 应进入缺页中断请求
	 */
	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, mm_flags, regs);
}

__do_page_fault对addr和权限进行检查后,调用handle_mm_fault进行一个新页框的申请流程。

(2)__do_kernel_fault

        __do_kernel_fault接口是arm64 Linux 内核中处理无法优雅恢复的内核模式内存故障的最后防线。当内核执行流在不允许进行复杂内存管理操作(如中断上下文、硬中断中)或者遇到无法通过常规内存管理解决的严重错误时,会调用此函数。


static void __do_kernel_fault(unsigned long addr, unsigned long esr,
			      struct pt_regs *regs)
{
	const char *msg;

	/*
	 * Are we prepared to handle this kernel fault?
	 * We are almost certainly not prepared to handle instruction faults.
	 */
/*
第一次尝试异常修复(Fixup Exception):
这是一个关键的检查。它首先确保这不是一个指令获取错误(Instruction Abort,内核通常无法修复指令错误)。
然后调用 fixup_exception(regs)。这个函数会查找 __ex_table。
如果 fixup_exception 成功(返回非零值),意味着这个故障是预期内的(比如在 copy_to_user 过程中),并且已经通过修改 regs->pc 设置了修复地址。函数立即返回,控制流跳转到修复代码继续执行。
*/
	if (!is_el1_instruction_abort(esr) && fixup_exception(regs))
		return;
/*
处理“伪造的”故障(Spurious Faults):
is_spurious_el1_translation_fault 检查这个故障是否可能是由某些硬件竞争条件或非标准行为引起的“伪造”翻译错误。
WARN_RATELIMIT 是一个调试宏,如果条件为真,它会打印一条警告信息(但限制频率),然后函数返回,忽略这个看起来不严重的故障。
*/
	if (WARN_RATELIMIT(is_spurious_el1_translation_fault(addr, esr, regs),
	    "Ignoring spurious kernel translation fault at virtual address %016lx\n", addr))
		return;
/*
处理 MTE (Memory Tagging Extension) 故障:
如果 arm64 架构支持 MTE(一种硬件内存安全特性),并且故障是标签检查失败,则调用 do_tag_recovery 尝试恢复或处理这个 MTE 违规。处理完成后,函数返回。
*/
	if (is_el1_mte_sync_tag_check_fault(esr)) {
		do_tag_recovery(addr, esr, regs);

		return;
	}
/*
分析故障原因(权限错误):
如果故障是权限相关的(is_el1_permission_fault),根据 esr 中的 WnR(Write/Not Read)位和其他标志,设置一个描述性的错误消息字符串 msg(例如“写入只读内存”、“从不可执行内存执行”)。
*/
	if (is_el1_permission_fault(addr, esr, regs)) {
		if (esr & ESR_ELx_WNR)
			msg = "write to read-only memory";
		else if (is_el1_instruction_abort(esr))
			msg = "execute from non-executable memory";
		else
			msg = "read from unreadable memory";
	} else if (addr < PAGE_SIZE) {
/*
分析故障原因(空指针或缺页):
如果不是权限错误,但地址接近 0,则很可能是内核代码解引用了空指针。设置 msg 为“NULL pointer dereference”。
*/
		msg = "NULL pointer dereference";
	} else {
		if (is_translation_fault(esr) &&
		    kfence_handle_page_fault(addr, esr & ESR_ELx_WNR, regs))
			return;
/*
分析故障原因(KFence 或普通缺页):
如果支持 KFence(Kernel Electric Fence,一种内存调试工具),尝试让 kfence_handle_page_fault 处理。如果它处理了,函数返回。
否则,这是一个常规的、无法解决的页翻译错误(缺页),设置 msg 为“paging request”。
*/
		msg = "paging request";
	}
/*
触发内核恐慌(Kernel Panic):
这是函数的最后一步。如果所有尝试(fixup_exception、忽略伪故障、MTE 恢复、KFence 处理)都失败了,说明内核遇到了一个致命错误,且无法安全恢复。
die_kernel_fault 函数会打印之前设置的错误消息 msg、故障地址、寄存器转储(dump)等详细信息,并最终调用 panic(),导致系统崩溃并重启。
*/
	die_kernel_fault(msg, addr, esr, regs);
}

3.2 handle_mm_fault 完善进程页表体系

        handle_mm_fault对进程访问当前vm权限检查和缺页次数登记后,主要还是调用了__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;
...

	count_vm_event(PGFAULT);
	count_memcg_event_mm(vma->vm_mm, 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;
...

	if (unlikely(is_vm_hugetlb_page(vma)))//大页处理
		ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
	else//正常页处理
		ret = __handle_mm_fault(vma, address, flags);
...
	mm_account_fault(regs, address, flags, ret);

	return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);

__handle_mm_fault则是对缺页原因进行分析,先查询每一级页表是否空缺,进行补充到PMD级。

/*
__handle_mm_fault 的主要工作是将进程页表中的三级页目录表 PGD,PUD,PMD 补齐,
然后获取到 pmd_t 就完成了,随后会把 pmd_t 送到 handle_pte_fault 函数中进行页表的处理。
 */
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
		unsigned long address, unsigned int flags)
{
	// vm_fault 结构用于封装后续缺页处理用到的相关参数
	struct vm_fault vmf = {
		.vma = vma,// 发生缺页的 vma
		.address = address & PAGE_MASK,// 引起缺页的虚拟内存地址
		.flags = flags,// 处理缺页的相关标记 FAULT_FLAG_xxx
		.pgoff = linear_page_index(vma, address),// address 在 vma 中的偏移,单位也页
		.gfp_mask = __get_fault_gfp_mask(vma),// 后续用于分配物理内存使用的相关掩码 gfp_mask
	};
	unsigned int dirty = flags & FAULT_FLAG_WRITE;
	struct mm_struct *mm = vma->vm_mm;  // 获取进程虚拟内存空间
	pgd_t *pgd; // 进程页表的顶级页表地址
	p4d_t *p4d;// 五级页表下会使用,在四级页表下 p4d 与 pgd 的值一样
	vm_fault_t ret;

	pgd = pgd_offset(mm, address);// 获取 address 在全局页目录表 PGD 中对应的目录项 pgd
	p4d = p4d_alloc(mm, pgd, address);// 在四级页表下,这里只是将 pgd 赋值给 p4d,后续均已 p4d 作为全局页目录项
	if (!p4d)
		return VM_FAULT_OOM;
// 首先 p4d_none 判断全局页目录项 p4d 是否是空的
    // 如果 p4d 是空的,则调用 __pud_alloc 分配一个新的上层页目录表 PUD,然后填充 p4d
    // 如果 p4d 不是空的,则调用 pud_offset 获取 address 在上层页目录 PUD 中的目录项 pud
	vmf.pud = pud_alloc(mm, p4d, address);
	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;
			}
		}
	}
// 首先 pud_none 判断上层页目录项 pud 是不是空的
    // 如果 pud 是空的,则调用 __pmd_alloc 分配一个新的中间页目录表 PMD,然后填充 pud
    // 如果 pud 不是空的,则调用 pmd_offset 获取 address 在中间页目录 PMD 中的目录项 pmd
	vmf.pmd = pmd_alloc(mm, vmf.pud, address);
	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 {
		vmf.orig_pmd = *vmf.pmd;

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

			if (dirty && !pmd_write(vmf.orig_pmd)) {
				ret = wp_huge_pmd(&vmf);
				if (!(ret & VM_FAULT_FALLBACK))
					return ret;
			} else {
				huge_pmd_set_accessed(&vmf);
				return 0;
			}
		}
	}
// 进行页表的相关处理以及解析具体的缺页原因,后续针对性的进行缺页处理
	return handle_pte_fault(&vmf);
}

        用下图对上述函数流程进行总结,函数检查了从pgd到pmd每一级页表情况,如果空缺就补充,最后调用了handle_pte_fault进行pte级处理。

3.3 handle_pte_fault

        该函数对主要对当前缺页类型进行判断,走不同处理流程

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

	if (unlikely(pmd_none(*vmf->pmd))) {
		// 如果 pmd 是空的,说明现在连页表都没有,页表项 pte 自然是空的
		/*
		 * Leave __pte_alloc() until later: because vm_ops->fault may
		 * want to allocate huge page, and if we expose page table
		 * for an instant, it will be difficult to retract from
		 * concurrent faults and from rmap lookups.
		 */
		vmf->pte = NULL;
	} else {
		/*
		 * If a huge pmd materialized under us just retry later.  Use
		 * pmd_trans_unstable() via pmd_devmap_trans_unstable() instead
		 * of pmd_trans_huge() to ensure the pmd didn't become
		 * pmd_trans_huge under us and then back to pmd_none, as a
		 * result of MADV_DONTNEED running immediately after a huge pmd
		 * fault in a different thread of this mm, in turn leading to a
		 * misleading pmd_trans_huge() retval. All we have to ensure is
		 * that it is a regular pmd that we can walk with
		 * pte_offset_map() and we can do that through an atomic read
		 * in C, which is what pmd_trans_unstable() provides.
		 */
		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
        // 通过 pte_offset_map 定位到虚拟内存地址 address 对应在页表中的 pte
        // 这里根据 address 获取 pte_index,然后从 pmd 中提取页表起始虚拟内存地址相加获取 pte
		vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
		 //  vmf->orig_pte 表示发生缺页时,address 对应的 pte 值
		vmf->orig_pte = *vmf->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();
		// 这里 pmd 不是空的,表示现在是有页表存在的,但缺页虚拟内存地址在页表中的 pte 是空值
		if (pte_none(vmf->orig_pte)) {
			pte_unmap(vmf->pte);
			vmf->pte = NULL;
		}
	}
	// pte 是空的,表示缺页地址 address 还从来没有被映射过,接下来就要处理物理内存的映射
	if (!vmf->pte) {
		// 判断缺页的虚拟内存地址 address 所在的虚拟内存区域 vma 是否是匿名映射区
		if (vma_is_anonymous(vmf->vma))
			return do_anonymous_page(vmf);// 处理匿名映射区发生的缺页
		else
			return do_fault(vmf); // 处理文件映射区发生的缺页
	}
	// 走到这里表示 pte 不是空的,但是 pte 中的 p 比特位是 0 值,表示之前映射的物理内存页已不在内存中(swap out)
	if (!pte_present(vmf->orig_pte))
		return do_swap_page(vmf);// 将之前映射的物理内存页从磁盘中重新 swap in 到内存中

	// 这里表示 pte 背后映射的物理内存页在内存中,但是 NUMA Balancing 发现该内存页不在当前进程运行的 numa 节点上
    // 所以将该 pte 标记为 _PAGE_PROTNONE(无读写,可执行权限)
    // 进程访问该内存页时发生缺页中断,在这里的 do_numa_page 中,内核将该 page 迁移到进程运行的 numa 节点上。
	if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
		return do_numa_page(vmf);

	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)) // 这里说明 vma 是可写的,但是 pte 被标记为不可写,说明是写保护类型的中断
			return do_wp_page(vmf);// 进行写时复制处理,cow 就发生在这里
		entry = pte_mkdirty(entry);// 如果 pte 是可写的,就将 pte 标记为脏页
	}
	// 将 pte 的 access 比特位置 1 ,表示该 page 是活跃的。避免被 swap 出去
	entry = pte_mkyoung(entry);
	// 经过上面的缺页处理,这里会判断原来的页表项 entry(orig_pte) 值是否发生了变化
    // 如果发生了变化,就把 entry 更新到 vmf->pte 中。
	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); // pte 既然变化了,则刷新 mmu (体系结构相关)
	} else {
		/* Skip spurious TLB flush for retried page fault */
		if (vmf->flags & FAULT_FLAG_TRIED)
			goto unlock;
		/*
		 * This is needed only for protection faults but the arch code
		 * is not yet telling us if this is a protection fault or not.
		 * This still avoids useless tlb flushes for .text page faults
		 * with threads.
		 */
		  // 如果 pte 内容本身没有变化,则不需要刷新任何东西
        // 但是有个特殊情况就是写保护类型中断,产生的写时复制,产生了新的映射关系,需要刷新一下 tlb
		if (vmf->flags & FAULT_FLAG_WRITE)
			flush_tlb_fix_spurious_fault(vmf->vma, vmf->address);
	}
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return 0;
}

        前面提到过产生缺页异常的原因,从总体来说分为两大类,一类是缺页虚拟内存地址背后映射的物理内存页不在内存中,另一类是缺页虚拟内存地址背后映射的物理内存页在内存中。

        对于第一类来说,可以有三种情况:

        一是pmd是空的,二是pmd被填充但pte是空的,这两种相对类似,需要申请页表内存同时填充对应的页表,同时通过判断当前是否位匿名还是文件(通过vma->vm_ops是否为空,文件映射时vma->vm_ops会被指向文件操作相关函数,上层操作时相当于操作文件,匿名时会被置空),函数前面即在处理这些事情。

        三是pte虽然不是空的,即已经映射了物理内存,但该内存被交换到磁盘上。通过pte的第一bit判断,pte 的第 0 个比特位表示该 pte 映射的物理内存页是否在内存中,值为 1 表示物理内存页在内存中驻留,值为 0 表示物理内存页不在内存中,可能被 swap 到磁盘上了。

#define _PAGE_BIT_PRESENT 0 /* is present */
#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT)

static inline int pte_present(pte_t a)
{
 return pte_flags(a) & (_PAGE_PRESENT | _PAGE_PROTNONE);
}

        对于第二类来说可有两种情况,一种是NUMA架构下CPU 访问内存的速度不一致引起的缺页异常,另外一种则是常见的写时复制COW。

        第一种原理是当当前进程所有CPU,映射的物理内存在其他CPU下时,内核NUMA Balancing机制会在当前CPU节点申请内存将在其他CPU节点下的内存内容拷贝过来,即内存跟着 CPU 走。但也存在CPU跟着内存走,即当大多数内存都在其他CPU节点时,内核干脆把当前CPU进程,调度到其他内存所在CPU上,函数调用do_numa_page用来处理这种情况。(NUMA Balancing 机制看起来非常好,但是同时也会为系统引入很多开销,一般情况下还是将 NUMA Balancing 关闭)

 if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
        return do_numa_page(vmf);

        第二种是写时复制,即进程调用fork创建另外一个进程时,会把进程虚拟内存pte设置为写保护,当进程访问内存时,产生的缺页异常会重新为该虚拟内存映射新的物理内存同时将旧的物理内存内容拷贝到当前内存上,同时引用减1。当另外一个进程访问虚拟内存时,此时改内存的PTE还是写保护权限,触发缺页异常,但由于此时前面进程已经重新分配映射的新的物理内存,另外一个进程只是将当前内存权限去掉写保护后就退出了,函数中调用do_wp_page来处理这些情况。

        handle_pte_fault处理了明确的缺页异常类型(匿名页缺页,文件页缺页等),并调用明确的缺页异常类型函数进行处理,接下来章节将逐个分析这些异常类型处理流程。

3.3 do_anonymous_page 匿名页缺页处理

        前面分析过该函数匿名页的处理是在PTE缺失进来的,即申请映射物理内存后对PTE相关页表进行填充,如下补充的示意图:

do_anonymous_page实现在mm\memory.c:

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;// 缺页地址 address 所在的虚拟内存区域 vma
	struct page *page;  // 指向分配的物理内存页,后面与虚拟内存进行映射
	vm_fault_t ret = 0;
	pte_t entry;// 临时的 pte 用于构建 pte 中的值,后续会赋值给 address 在页表中对应的真正 pte

	/* File mapping without ->vm_ops ? */
	if (vma->vm_flags & VM_SHARED)
		return VM_FAULT_SIGBUS;

	/*
	 * Use pte_alloc() instead of pte_alloc_map().  We can't run
	 * pte_offset_map() on pmds where a huge pmd might be created
	 * from a different thread.
	 *
	 * pte_alloc_map() is safe to use under mmap_write_lock(mm) or when
	 * parallel threads are excluded by other means.
	 *
	 * Here we only have mmap_read_lock(mm).
	 */
	 // 如果 pmd 是空的,表示现在还没有一级页表
    // pte_alloc 这里会创建一级页表,并填充 pmd 中的内容
	if (pte_alloc(vma->vm_mm, vmf->pmd))
		return VM_FAULT_OOM;

	/* See comment in handle_pte_fault() */
	if (unlikely(pmd_trans_unstable(vmf->pmd)))
		return 0;

	/* Use the zero-page for reads */
	if (!(vmf->flags & FAULT_FLAG_WRITE) &&
			!mm_forbids_zeropage(vma->vm_mm)) {
		entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
						vma->vm_page_prot));
		vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				vmf->address, &vmf->ptl);
		if (!pte_none(*vmf->pte)) {
			update_mmu_tlb(vma, vmf->address, vmf->pte);
			goto unlock;
		}
		ret = check_stable_address_space(vma->vm_mm);
		if (ret)
			goto unlock;
		/* Deliver the page fault to userland, check inside PT lock */
		if (userfaultfd_missing(vma)) {
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			return handle_userfault(vmf, VM_UFFD_MISSING);
		}
		goto setpte;
	}

	/* Allocate our own private page. */
	if (unlikely(anon_vma_prepare(vma)))
		goto oom;
	// 页表创建好之后,这里从伙伴系统中分配一个 4K 物理内存页出来
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
	if (!page)
		goto oom;

	if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))
		goto oom_free_page;
	cgroup_throttle_swaprate(page, GFP_KERNEL);

	/*
	 * The memory barrier inside __SetPageUptodate makes sure that
	 * preceding stores to the page contents become visible before
	 * the set_pte_at() write.
	 */
	__SetPageUptodate(page);
	// 将 page 的 pfn 以及相关权限标记位 vm_page_prot 初始化一个临时 pte 出来 
	entry = mk_pte(page, vma->vm_page_prot);
	entry = pte_sw_mkyoung(entry);
	 // 如果 vma 是可写的,则将 pte 标记为可写,脏页。
	if (vma->vm_flags & VM_WRITE)
		entry = pte_mkwrite(pte_mkdirty(entry));
	// 锁定一级页表,并获取 address 在页表中对应的真实 pte
	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);
	// 是否有其他线程在并发处理缺页
	if (!pte_none(*vmf->pte)) {
		update_mmu_cache(vma, vmf->address, vmf->pte);
		goto release;
	}

	ret = check_stable_address_space(vma->vm_mm);
	if (ret)
		goto release;

	/* Deliver the page fault to userland, check inside PT lock */
	if (userfaultfd_missing(vma)) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		put_page(page);
		return handle_userfault(vmf, VM_UFFD_MISSING);
	}
	 // 增加 进程 rss 相关计数,匿名内存页计数 + 1
	inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
	// 建立匿名页反向映射关系
	page_add_new_anon_rmap(page, vma, vmf->address, false);
	// 将匿名页添加到 LRU 链表中
	lru_cache_add_inactive_or_unevictable(page, vma);
setpte:
  // 将 entry 赋值给真正的 pte,这里 pte 就算被填充好了,进程页表体系也就补齐了
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
	// 刷新 mmu 
	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);// 解除 pte 的映射
	return ret;
release:
	put_page(page);// 释放 page 
	goto unlock;
oom_free_page:
	put_page(page);// 释放 page 
oom:
	return VM_FAULT_OOM;
}

3.4 do_fault 文件页缺页处理

        do_fault 对文件缺页异常原因进行分类处理,vma->vm_ops->fault 函数就是专门用于处理文件映射区缺页的,其实现为文件系统的回调,例如ext4文件系统:

static int ext4_file_mmap(struct file *file, struct vm_area_struct *vma)
{     
      vma->vm_ops = &ext4_file_vm_ops;
}

        do_fault处理vma->vm_ops->fault为空的异常情况,并根据缺页类型分支处理:

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

	/*
	 * The VMA was not fully populated on mmap() or missing VM_DONTEXPAND
	 */
	 // 处理 vm_ops->fault 为 null 的异常情况
	if (!vma->vm_ops->fault) {
		/*
		 * If we find a migration pmd entry or a none pmd entry, which
		 * should never happen, return SIGBUS
		 */
		 // 如果中间页目录 pmd 指向的一级页表不在内存中,则返回 SIGBUS 错误
		if (unlikely(!pmd_present(*vmf->pmd)))
			ret = VM_FAULT_SIGBUS;
		else {
			// 获取缺页的页表项 pte
			vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
						       vmf->pmd,
						       vmf->address,
						       &vmf->ptl);
			/*
			 * Make sure this is not a temporary clearing of pte
			 * by holding ptl and checking again. A R/M/W update
			 * of pte involves: take ptl, clearing the pte so that
			 * we don't have concurrent modification by hardware
			 * followed by an update.
			 */
			 // pte 为空,则返回 SIGBUS 错误
			if (unlikely(pte_none(*vmf->pte)))
				ret = VM_FAULT_SIGBUS;
			else
				ret = VM_FAULT_NOPAGE; // pte 不为空,返回 NOPAGE,即本次缺页处理不会分配物理内存页

			pte_unmap_unlock(vmf->pte, vmf->ptl);
		}
	} else if (!(vmf->flags & FAULT_FLAG_WRITE))
		ret = do_read_fault(vmf);// 缺页如果是读操作引起的,进入 do_read_fault 处理
	else if (!(vma->vm_flags & VM_SHARED))
		ret = do_cow_fault(vmf);// 缺页是由私有映射区的写入操作引起的,则进入 do_cow_fault 处理写时复制
	else
		ret = do_shared_fault(vmf);// 处理共享映射区的写入缺页

	/* preallocated pagetable is unused: free it */
	if (vmf->prealloc_pte) {
		pte_free(vm_mm, vmf->prealloc_pte);
		vmf->prealloc_pte = NULL;
	}
	return ret;
}

3.4.1do_read_fault 由读操作引起的缺页

        上层进行文件映射时,一般内核处理只是在虚拟内存中申请一个合适的区域并返回,此时并没有绑定对应的文件页,只有到访问时触发缺页异常,才会走到这里进行缺页处理,主要是从page cache中找到对应的页,将页映射到当前虚拟地址上,期间有预读取临近页以降低缺页异常次数问题。

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

	/*
	 * Let's call ->map_pages() first and use ->fault() as fallback
	 * if page by the offset is not ready to be mapped (cold cache or
	 * something).
	 */
	 // map_pages 用于提前预先映射文件页相邻的若干文件页到相关 pte 中,从而减少缺页次数
    // fault_around_bytes 控制预先映射的的字节数默认初始值为 65536(16个物理内存页)
	if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
		if (likely(!userfaultfd_minor(vmf->vma))) {
			 // 这里会尝试使用 map_pages 将缺页地址 address 附近的文件页预读进 page cache
        // 然后填充相关的 pte,目的是减少缺页次数
			ret = do_fault_around(vmf);
			if (ret)
				return ret;
		}
	}
 // 如果不满足预先映射的条件,则只映射本次需要的文件页
    // 首先会从 page cache 中读取文件页,如果 page cache 中不存在则从磁盘中读取,并预读若干文件页到 page cache 中
	ret = __do_fault(vmf);// 这里需要负责获取文件页,并不映射
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		return ret;

	ret |= finish_fault(vmf);    // 将本次缺页所需要的文件页映射到 pte 中。
	unlock_page(vmf->page);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		put_page(vmf->page);
	return ret;
}

        如果不满足多读取临近页以减少缺页异常次数,则后续将专注与当前页的映射处理。其中__do_fault主要调用了对应文件系统的缺页处理回调,先把缺页所需要的文件页获取出来,为后面的映射做准备。

        前面申请得到的内存页处理完后,还需将该页映射到当前虚拟地址上,finish_fault用来最后处理此映射步骤:

vm_fault_t finish_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;// 为本次缺页准备好的物理内存页,即后续需要用 pte 映射的内存页
	vm_fault_t ret;

	/* Did we COW the page? */
	if ((vmf->flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED))
		page = vmf->cow_page;// 如果是写时复制场景,那么 pte 要映射的是这个 cow 复制过来的内存页
	else
		page = vmf->page;// 在 filemap_fault 函数中读取到的文件页,后面需要将文件页映射到 pte 中

	/*
	 * check even for read faults because we might have lost our CoWed
	 * page
	 */
	 // 对于私有映射来说,这里需要检查进程地址空间是否被标记了 MMF_UNSTABLE
    // 如果是,那么 oom 后续会回收这块地址空间,这会导致私有映射的文件页丢失
    // 所以在为私有映射建立 pte 映射之前,需要检查一下
	if (!(vma->vm_flags & VM_SHARED)) {
		// 地址空间没有被标记 MMF_UNSTABLE 则会返回0
		ret = check_stable_address_space(vma->vm_mm);
		if (ret)
			return ret;
	}
	//pmd为空时,需要先填充pte物理地址
	if (pmd_none(*vmf->pmd)) {
		if (PageTransCompound(page)) {
			ret = do_set_pmd(vmf, page);
			if (ret != VM_FAULT_FALLBACK)
				return ret;
		}

		if (vmf->prealloc_pte) {
			vmf->ptl = pmd_lock(vma->vm_mm, vmf->pmd);
			if (likely(pmd_none(*vmf->pmd))) {
				mm_inc_nr_ptes(vma->vm_mm);
				pmd_populate(vma->vm_mm, vmf->pmd, vmf->prealloc_pte);
				vmf->prealloc_pte = NULL;
			}
			spin_unlock(vmf->ptl);
		} else if (unlikely(pte_alloc(vma->vm_mm, vmf->pmd))) {
			return VM_FAULT_OOM;
		}
	}

	/*
	 * See comment in handle_pte_fault() for how this scenario happens, we
	 * need to return NOPAGE so that we drop this page.
	 */
	if (pmd_devmap_trans_unstable(vmf->pmd))
		return VM_FAULT_NOPAGE;

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				      vmf->address, &vmf->ptl);
	ret = 0;
	/* Re-check under ptl */
	if (likely(pte_none(*vmf->pte)))
		do_set_pte(vmf, page, vmf->address);// 将创建出来的物理内存页映射到 address 对应在页表中的 pte 中
	else
		ret = VM_FAULT_NOPAGE;

	update_mmu_tlb(vma, vmf->address, vmf->pte);
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return ret;
}

        这里还再次检查了因为OOM(内存不足)被系统回收的情况。

        此时因为读引起的缺页异常处理完成,但因为私有映射情况下,进程进行读时会触发写时复制。

3.4.2 do_cow_fault 私有文件映射的写时复制处理

        如果当我们采用的是私有文件映射时,在映射之后立马进行写入操作时,就会发生写时复制,写时复制的缺页处理流程内核封装在do_cow_fault 函数中。由于我们这里要进行写时复制,所以首先要调用alloc_page_vma从伙伴系统中重新申请一个物理内存页出来,我们先把这个刚刚新申请出来用于写时复制的内存页称为 cow_page,然后调用上小节中介绍的 __do_fault 函数,将原来的文件页从 page cache 中读取出来,我们把原来的文件页称为 page 。

        最后调用 copy_user_highpage 将原来文件页 page 中的内容拷贝到刚刚新申请的内存页 cow_page 中,完成写时复制之后,接着调用 finish_fault 将 cow_page 映射到缺页地址 address 在进程页表中的 pte 上。

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

	if (unlikely(anon_vma_prepare(vma)))
		return VM_FAULT_OOM;
	// 从伙伴系统重新申请一个用于写时复制的物理内存页 cow_page
	vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
	if (!vmf->cow_page)
		return VM_FAULT_OOM;

	if (mem_cgroup_charge(vmf->cow_page, vma->vm_mm, GFP_KERNEL)) {
		put_page(vmf->cow_page);
		return VM_FAULT_OOM;
	}
	cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);
	// 从  page cache 读取原来的文件页
	ret = __do_fault(vmf);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		goto uncharge_out;
	if (ret & VM_FAULT_DONE_COW)
		return ret;
	// 将原来文件页中的内容拷贝到 cow_page 中完成写时复制
	copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
	__SetPageUptodate(vmf->cow_page);
	// 将 cow_page 重新映射到缺页地址 address 对应在页表中的 pte 上。
	ret |= finish_fault(vmf);
	unlock_page(vmf->page);
	// 原来的文件页引用计数 - 1
	put_page(vmf->page);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		goto uncharge_out;
	return ret;
uncharge_out:
	put_page(vmf->cow_page);
	return ret;
}

3.4.3 do_shared_fault 共享文件映射页缺页处理

        do_shared_fault处理相对简单,进入该处理流程时,先从page cache将文件页读取出来,再将该页物理地址和当前的虚拟地址映射起来。

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

	ret = __do_fault(vmf); // 从 page cache 中读取文件页
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		return ret;

	/*
	 * Check if the backing address space wants to know that the page is
	 * about to become writable
	 */
	if (vma->vm_ops->page_mkwrite) {
		unlock_page(vmf->page);
		// 将文件页变为可写状态,并为后续记录文件日志做一些准备工作
		tmp = do_page_mkwrite(vmf);
		if (unlikely(!tmp ||
				(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
			put_page(vmf->page);
			return tmp;
		}
	}
	// 将文件页映射到缺页 address 在页表中对应的 pte 上
	ret |= finish_fault(vmf);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
					VM_FAULT_RETRY))) {
		unlock_page(vmf->page);
		put_page(vmf->page);
		return ret;
	}
 // 将 page 标记为脏页,记录相关文件系统的日志,防止数据丢失
    // 判断是否将脏页回写
	ret |= fault_dirty_shared_page(vmf);
	return ret;
}

3.5  do_wp_page 进行写时复制

         do_wp_page进行的写实复制时机,跟前面分析的do_cow_fault写实复制时机不同,do_cow_fault是在pte为空的情况下,进行直接进行写时触发调用的缺页异常调用,实际场景来说,在用户进行私有内存映射后,立即进行写操作,此时会触发do_cow_fault的调用。

        do_wp_page则是在已经有物理内存映射条件下调用,即此时pte不为空,但权限错误(写保护)或者当前物理内存引用大于1,实际场景来说,当用户两个进程进行私有文件映射后先进行读操作,此时内核会为此虚拟内存映射到该文件对应的page cache页上,当两条进程再进行读操作时此时会触发缺页异常,此时调用的是do_wp_page。此外,进程的私有匿名映射也会触发该流程,具体来说就是父进程fork一个子进程时,子进程会复制父进程全部页表,此时父子进程映射的是同一个物理内存,引用为2,但有一条进程访问该内存时,会触发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;

	if (userfaultfd_pte_wp(vma, *vmf->pte)) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		return handle_userfault(vmf, VM_UFFD_WP);
	}

	/*
	 * Userfaultfd write-protect can defer flushes. Ensure the TLB
	 * is flushed in this case before copying.
	 */
	if (unlikely(userfaultfd_wp(vmf->vma) &&
		     mm_tlb_flush_pending(vmf->vma->vm_mm)))
		flush_tlb_page(vmf->vma, vmf->address);
	 // 获取 pte 映射的物理内存页
	vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
	if (!vmf->page) {
		/*
		 * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
		 * VM_PFNMAP VMA.
		 *
		 * We should not cow pages in a shared writeable mapping.
		 * Just mark the pages writable and/or call ops->pfn_mkwrite.
		 */
		if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
				     (VM_WRITE|VM_SHARED))
			return wp_pfn_shared(vmf);

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

	/*
	 * Take out anonymous pages first, anonymous shared vmas are
	 * not dirty accountable.
	 */
	 // 物理内存页为匿名页的情况
	if (PageAnon(vmf->page)) {
		struct page *page = vmf->page;

		/* PageKsm() doesn't necessarily raise the page refcount */
		//断匿名页的引用计数是否为 1
		if (PageKsm(page) || page_count(page) != 1)
			goto copy;
		if (!trylock_page(page))
			goto copy;
		if (PageKsm(page) || page_mapcount(page) != 1 || page_count(page) != 1) {
			unlock_page(page);
			goto copy;
		}
		/*
		 * Ok, we've got the only map reference, and the only
		 * page count reference, and the page is locked,
		 * it's dark out, and we're wearing sunglasses. Hit it.
		 */
		unlock_page(page);
		// 如果当前物理内存页的引用计数为 1 ,并且只有当前进程在引用该物理内存页
        // 则不做写时复制处理,而是复用当前物理内存页,只是将 pte 改为可写即可 
		wp_page_reuse(vmf);
		return VM_FAULT_WRITE;
	} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
					(VM_WRITE|VM_SHARED))) {
        // 处理共享可写的内存页
        // 由于大家都可写,所以这里也只是调用 wp_page_reuse 复用当前内存页即可,不做写时复制处理
        // 由于是共享的,对于文件页来说是可以回写到磁盘上的,所以会额外调用一次 fault_dirty_shared_page 判断是否进行脏页的回写
		return wp_page_shared(vmf);
	}
copy:
	/*
	 * Ok, we need to copy. Oh, well..
	 */
  	// 走到这里表示当前物理内存页的引用计数大于 1 被多个进程引用
    // 对于私有可写的虚拟内存区域来说,就要发生写时复制
    // 而对于私有文件页的情况来说,不必判断内存页的引用计数
    // 因为是私有文件页,不管文件页的引用计数是不是 1 ,都要进行写时复制
	get_page(vmf->page);
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return wp_page_copy(vmf);
}

do_wp_page分情况处理调用对应流程,当为父子进程触发的缺页异常,且子进程已经先触发缺页异常为自己分配好物理内存后,此时父进程再进入缺页异常时,发现当前映射的物理内存引用只有1时,直接改写当前页权限后直接返回:

static inline void wp_page_reuse(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = vmf->page;
	pte_t entry;
	/*
	 * Clear the pages cpupid information as the existing
	 * information potentially belongs to a now completely
	 * unrelated process.
	 */
	if (page)
		page_cpupid_xchg_last(page, (1 << LAST_CPUPID_SHIFT) - 1);
// 先将 tlb cache 中缓存的 address 对应的 pte 刷出缓存
	flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
	 // 将原来 pte 的 access 位置 1 ,表示该 pte 映射的物理内存页是活跃的
	entry = pte_mkyoung(vmf->orig_pte);
	// 将原来只读的 pte 改为可写的,并标记为脏页
	entry = maybe_mkwrite(pte_mkdirty(entry), vma);
	// 将更新后的 entry 值设置到页表 pte 中
	if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
		update_mmu_cache(vma, vmf->address, vmf->pte); // 更新 mmu 
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	count_vm_event(PGREUSE);
}

否则的话,则调用wp_page_copy进行写实复制流程:

static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;// 缺页地址 address 所在 vma
	struct mm_struct *mm = vma->vm_mm;// 当前进程地址空间
	struct page *old_page = vmf->page; // 原来映射的物理内存页,pte 为只读
	struct page *new_page = NULL;// 用于写时复制的新内存页
	pte_t entry;// 写时复制之后,需要修改原来的 pte,这里是临时构造的一个 pte 值
	int page_copied = 0;// 是否发生写时复制
	struct mmu_notifier_range range;
	int ret;

	//检查内存情况
	if (unlikely(anon_vma_prepare(vma)))
		goto oom;
	// 如果 pte 原来映射的是一个零页
	if (is_zero_pfn(pte_pfn(vmf->orig_pte))) {
		// 新申请一个零页出来,内存页中的内容被零初始化
		new_page = alloc_zeroed_user_highpage_movable(vma,
							      vmf->address);
		if (!new_page)
			goto oom;
	} else {
		// 新申请一个物理内存页
		new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
				vmf->address);
		if (!new_page)
			goto oom;
		// 将原来内存页 old page 中的内容拷贝到新内存页 new page 中
		ret = cow_user_page(new_page, old_page, vmf);
		if (ret) {
			/*
			 * COW failed, if the fault was solved by other,
			 * it's fine. If not, userspace would re-fault on
			 * the same address and we will handle the fault
			 * from the second attempt.
			 * The -EHWPOISON case will not be retried.
			 */
			put_page(new_page);
			if (old_page)
				put_page(old_page);

			return ret == -EHWPOISON ? VM_FAULT_HWPOISON : 0;
		}
	}

	if (mem_cgroup_charge(new_page, mm, GFP_KERNEL))
		goto oom_free_new;
	cgroup_throttle_swaprate(new_page, GFP_KERNEL);

	__SetPageUptodate(new_page);

	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma, mm,
				vmf->address & PAGE_MASK,
				(vmf->address & PAGE_MASK) + PAGE_SIZE);
	mmu_notifier_invalidate_range_start(&range);

	/*
	 * Re-check the pte - we dropped the lock
	 */
	 // 给页表加锁,并重新获取 address 在页表中对应的 pte
	vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
	// 判断加锁前的 pte (orig_pte)与加锁后的 pte (vmf->pte)是否相同
    // 目的是判断此时是否有其他线程正在并发修改 pte
	if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {
		if (old_page) {
			// 更新进程常驻内存信息 rss_state
			if (!PageAnon(old_page)) {
				// 减少 MM_FILEPAGES 计数
				dec_mm_counter_fast(mm,
						mm_counter_file(old_page));
				// 由于发生写时复制,这里匿名页个数加 1
				inc_mm_counter_fast(mm, MM_ANONPAGES);
			}
		} else {
			inc_mm_counter_fast(mm, MM_ANONPAGES);//匿名页个数加 1
		}
		// 将旧的 tlb 缓存刷出
		flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
		// 创建一个临时的 pte 映射到新内存页 new page 上
		entry = mk_pte(new_page, vma->vm_page_prot);
		entry = pte_sw_mkyoung(entry);
		// 设置 entry 为可写的,正是这里pte的权限由只读变为了可写
		entry = maybe_mkwrite(pte_mkdirty(entry), vma);

		/*
		 * Clear the pte entry and flush it first, before updating the
		 * pte with the new entry, to keep TLBs on different CPUs in
		 * sync. This code used to set the new PTE then flush TLBs, but
		 * that left a window where the new PTE could be loaded into
		 * some TLBs while the old PTE remains in others.
		 */
		ptep_clear_flush_notify(vma, vmf->address, vmf->pte);
		// 为新的内存页建立反向映射关系
		page_add_new_anon_rmap(new_page, vma, vmf->address, false);
		 // 将新的内存页加入到 LRU active 链表中
		lru_cache_add_inactive_or_unevictable(new_page, vma);
		/*
		 * We call the notify macro here because, when using secondary
		 * mmu page tables (such as kvm shadow page tables), we want the
		 * new page to be mapped directly into the secondary page table.
		 */
		 // 将 entry 值重新设置到子进程页表 pte 中
		set_pte_at_notify(mm, vmf->address, vmf->pte, entry);
		// 更新 mmu
		update_mmu_cache(vma, vmf->address, vmf->pte);
		if (old_page) {
			/*
			 * Only after switching the pte to the new page may
			 * we remove the mapcount here. Otherwise another
			 * process may come and find the rmap count decremented
			 * before the pte is switched to the new page, and
			 * "reuse" the old page writing into it while our pte
			 * here still points into it and can be read by other
			 * threads.
			 *
			 * The critical issue is to order this
			 * page_remove_rmap with the ptp_clear_flush above.
			 * Those stores are ordered by (if nothing else,)
			 * the barrier present in the atomic_add_negative
			 * in page_remove_rmap.
			 *
			 * Then the TLB flush in ptep_clear_flush ensures that
			 * no process can access the old page before the
			 * decremented mapcount is visible. And the old page
			 * cannot be reused until after the decremented
			 * mapcount is visible. So transitively, TLBs to
			 * old page will be flushed before it can be reused.
			 */
			// 将原来的内存页从当前进程的反向映射关系中解除
			page_remove_rmap(old_page, false);
		}

		/* Free the old page.. */
		new_page = old_page;
		page_copied = 1;
	} else {
		update_mmu_tlb(vma, vmf->address, vmf->pte);
	}

	if (new_page)//为新申请页表引用减1
		put_page(new_page);

	pte_unmap_unlock(vmf->pte, vmf->ptl);
	/*
	 * No need to double call mmu_notifier->invalidate_range() callback as
	 * the above ptep_clear_flush_notify() did already call it.
	 */
	mmu_notifier_invalidate_range_only_end(&range);
	if (old_page) {
		/*
		 * Don't let another task, with possibly unlocked vma,
		 * keep the mlocked page.
		 */
		if (page_copied && (vma->vm_flags & VM_LOCKED)) {
			lock_page(old_page);	/* LRU manipulation */
			if (PageMlocked(old_page))
				munlock_vma_page(old_page);
			unlock_page(old_page);
		}
		if (page_copied)
			free_swap_cache(old_page);
		put_page(old_page);// 旧内存页的引用计数减 1
	}
	return page_copied ? VM_FAULT_WRITE : 0;
oom_free_new:
	put_page(new_page);
oom:
	if (old_page)
		put_page(old_page);
	return VM_FAULT_OOM;
}

        在 wp_page_copy 函数中,内核会首先为子进程分配一个新的物理内存页 new_page,然后调用 cow_user_page 将原有内存页 old_page 中的内容全部拷贝到新内存页中。创建一个临时的页表项 entry,然后让 entry 指向新的内存页,将 entry 重新设置为可写,通过 set_pte_at_notify 将 entry 值设置到子进程页表中的 pte 上。最后将原有内存页 old_page 的引用计数减 1 。

3.6 do_swap_page 处理 swap 缺页异常

        首先内核会通过 pte_to_swp_entry 将进程页表中的 pte 转换为 swp_entry_t,通过 lookup_swap_cache 根据 swp_entry_t 到 swap cache 中查找是否已经有其他进程将内存页 swap 进来了。如果 swap cache 没有对应的内存页,则调用 swapin_readahead 启动预读,在这个过程中,内核会重新分配物理内存页,并将这个物理内存页加入到 swap cache 中,随后通过 swap_readpage 将交换区的内容读取到这个内存页中。现在我们需要的内存页已经 swap in 到内存中了,后面的流程就和普通的缺页处理一样了,根据 swap in 进来的内存页地址重新创建初始化一个新的 pte,然后用这个新的 pte,将进程页表中原来的  swp_entry_t 替换掉。为新的内存页建立反向映射关系,加入 lru active list 中,最后 swap_free 释放交换区中的资源。

vm_fault_t do_swap_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = NULL, *swapcache;
	struct swap_info_struct *si = NULL;
	swp_entry_t entry;
	pte_t pte;
	int locked;
	int exclusive = 0;
	vm_fault_t ret = 0;
	void *shadow = NULL;

	if (!pte_unmap_same(vma->vm_mm, vmf->pmd, vmf->pte, vmf->orig_pte))
		goto out;
	// 将缺页内存地址 address 对应的 pte 转换为 swp_entry_t
	entry = pte_to_swp_entry(vmf->orig_pte);
	if (unlikely(non_swap_entry(entry))) {
		if (is_migration_entry(entry)) {
			migration_entry_wait(vma->vm_mm, vmf->pmd,
					     vmf->address);
		} else if (is_device_exclusive_entry(entry)) {
			vmf->page = pfn_swap_entry_to_page(entry);
			ret = remove_device_exclusive_entry(vmf);
		} else if (is_device_private_entry(entry)) {
			vmf->page = pfn_swap_entry_to_page(entry);
			ret = vmf->page->pgmap->ops->migrate_to_ram(vmf);
		} else if (is_hwpoison_entry(entry)) {
			ret = VM_FAULT_HWPOISON;
		} else {
			print_bad_pte(vma, vmf->address, vmf->orig_pte, NULL);
			ret = VM_FAULT_SIGBUS;
		}
		goto out;
	}

	/* Prevent swapoff from happening to us. */
	si = get_swap_device(entry);
	if (unlikely(!si))
		goto out;

	delayacct_set_flag(current, DELAYACCT_PF_SWAPIN);
	// 首先利用 swp_entry_t 到 swap cache 查找,看内存页已经其他进程被 swap in 进来
	page = lookup_swap_cache(entry, vma, vmf->address);
	swapcache = page;
	// 处理匿名页不在 swap cache 的情况
	if (!page) {
		// 针对 fast swap storage 比如 zram 等 swap 的性能优化,跳过 swap cache
		if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
		    __swap_count(entry) == 1) {
			/* skip swapcache */
			// 当只有单进程引用这个匿名页的时候,直接跳过 swap cache
            // 从伙伴系统中申请内存页 page,注意这里的 page 并不会加入到 swap cache 中
			page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
							vmf->address);
			if (page) {
				__SetPageLocked(page);
				__SetPageSwapBacked(page);

				if (mem_cgroup_swapin_charge_page(page,
					vma->vm_mm, GFP_KERNEL, entry)) {
					ret = VM_FAULT_OOM;
					goto out_page;
				}
				mem_cgroup_swapin_uncharge_swap(entry);

				shadow = get_shadow_from_swap_cache(entry);
				if (shadow)
					workingset_refault(page, shadow);
				// 加入 lru 链表
				lru_cache_add(page);

				/* To provide entry to swap_readpage() */
				set_page_private(page, entry.val);
				// 直接从 fast storage device 中读取被换出的内容到 page 中
				swap_readpage(page, true);
				set_page_private(page, 0);
			}
		} else {
			 // 启动 swap 预读
			page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
						vmf);
			swapcache = page;
		}

		if (!page) {
			/*
			 * Back out if somebody else faulted in this pte
			 * while we released the pte lock.
			 */
			vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
					vmf->address, &vmf->ptl);
			if (likely(pte_same(*vmf->pte, vmf->orig_pte)))
				ret = VM_FAULT_OOM;
			delayacct_clear_flag(current, DELAYACCT_PF_SWAPIN);
			goto unlock;
		}

		/* Had to read the page from swap area: Major fault */
		 // 因为涉及到了磁盘 IO,所以本次缺页异常属于 FAULT_MAJOR 类型
		ret = VM_FAULT_MAJOR;
		count_vm_event(PGMAJFAULT);
		count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
	} else if (PageHWPoison(page)) {
		/*
		 * hwpoisoned dirty swapcache pages are kept for killing
		 * owner processes (which may be unknown at hwpoison time)
		 */
		ret = VM_FAULT_HWPOISON;
		delayacct_clear_flag(current, DELAYACCT_PF_SWAPIN);
		goto out_release;
	}

	locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);

	delayacct_clear_flag(current, DELAYACCT_PF_SWAPIN);
	if (!locked) {
		ret |= VM_FAULT_RETRY;
		goto out_release;
	}

	/*
	 * Make sure try_to_free_swap or reuse_swap_page or swapoff did not
	 * release the swapcache from under us.  The page pin, and pte_same
	 * test below, are not enough to exclude that.  Even if it is still
	 * swapcache, we need to check that the page's swap has not changed.
	 */
	if (unlikely((!PageSwapCache(page) ||
			page_private(page) != entry.val)) && swapcache)
		goto out_page;

	page = ksm_might_need_to_copy(page, vma, vmf->address);
	if (unlikely(!page)) {
		ret = VM_FAULT_OOM;
		page = swapcache;
		goto out_page;
	}

	cgroup_throttle_swaprate(page, GFP_KERNEL);

	/*
	 * Back out if somebody else already faulted in this pte.
	 */
	 // 现在之前被换出的内存页已经被内核重新 swap in 到内存中了。
    // 下面就是重新设置 pte,将原来页表中的 swp_entry_t 替换掉
	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);
	if (unlikely(!pte_same(*vmf->pte, vmf->orig_pte)))
		goto out_nomap;

	if (unlikely(!PageUptodate(page))) {
		ret = VM_FAULT_SIGBUS;
		goto out_nomap;
	}

	/*
	 * The page isn't present yet, go ahead with the fault.
	 *
	 * Be careful about the sequence of operations here.
	 * To get its accounting right, reuse_swap_page() must be called
	 * while the page is counted on swap but not yet in mapcount i.e.
	 * before page_add_anon_rmap() and swap_free(); try_to_free_swap()
	 * must be called after the swap_free(), or it will never succeed.
	 */
	// 增加匿名页的统计计数
	inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
	// 减少 swap entries 计数
	dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);
	// 根据被 swap in 进来的新内存页重新创建 pte
	pte = mk_pte(page, vma->vm_page_prot);
	if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) {
		pte = maybe_mkwrite(pte_mkdirty(pte), vma);
		vmf->flags &= ~FAULT_FLAG_WRITE;
		ret |= VM_FAULT_WRITE;
		exclusive = RMAP_EXCLUSIVE;
	}
	flush_icache_page(vma, page);
	if (pte_swp_soft_dirty(vmf->orig_pte))
		pte = pte_mksoft_dirty(pte);
	if (pte_swp_uffd_wp(vmf->orig_pte)) {
		pte = pte_mkuffd_wp(pte);
		pte = pte_wrprotect(pte);
	}
	// 用新的 pte 替换掉页表中的 swp_entry_t
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
	arch_do_swap_page(vma->vm_mm, vma, vmf->address, pte, vmf->orig_pte);
	vmf->orig_pte = pte;

	/* ksm created a completely new copy */
	if (unlikely(page != swapcache && swapcache)) {
		page_add_new_anon_rmap(page, vma, vmf->address, false);
		lru_cache_add_inactive_or_unevictable(page, vma);
	} else {
		// 建立新内存页的反向映射关系
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);
	}

	swap_free(entry);
	if (mem_cgroup_swap_full(page) ||
	    (vma->vm_flags & VM_LOCKED) || PageMlocked(page))
		try_to_free_swap(page);
	unlock_page(page);
	if (page != swapcache && swapcache) {
		/*
		 * Hold the lock to avoid the swap entry to be reused
		 * until we take the PT lock for the pte_same() check
		 * (to avoid false positives from pte_same). For
		 * further safety release the lock after the swap_free
		 * so that the swap count won't change under a
		 * parallel locked swapcache.
		 */
		unlock_page(swapcache);
		put_page(swapcache);
	}

	if (vmf->flags & FAULT_FLAG_WRITE) {
		ret |= do_wp_page(vmf);
		if (ret & VM_FAULT_ERROR)
			ret &= VM_FAULT_ERROR;
		goto out;
	}
	 // 刷新 mmu cache
	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
out:
	if (si)
		put_swap_device(si);
	return ret;
out_nomap:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
out_page:
	unlock_page(page);
out_release:
	put_page(page);
	if (page != swapcache && swapcache) {
		unlock_page(swapcache);
		put_page(swapcache);
	}
	if (si)
		put_swap_device(si);
	return ret;
}

4 附录

4.1 PTE页表项bitmap

        arm64 页表入口 (PTE) 详细位含义 (4KB 页)

        PTE 的 64 个比特位被划分为几个区域,用于定义物理地址和内存访问属性。

位范围名称含义及作用
[63:55]ING, Ignored忽略位。这些位可供软件使用,但硬件忽略它们。
[54]PXN (Privileged Execute Never)特权执行从不。如果为 1,禁止 EL1/EL2(内核态)执行此页面上的代码。
[53]XN (Execute Never)执行从不。如果为 1,禁止 EL0(用户态)执行此页面上的代码。
[52:N]Output Address (OA)输出物理地址。这些位包含了页面的物理基地址,指向 4KB 对齐的物理内存(N 取决于配置,对于 4KB 页通常 N=12)。
[11:10]SWP, Software bits软件控制位。由操作系统(Linux 内核)使用,用于标记页面状态(如脏、最近未使用、交换条目等)。硬件忽略。
[9]AP[2] (Access Permissions)访问权限位 [2]。与 AP[1:0] 组合定义读/写权限。
[8]SH[1:0] (Shareability)可共享性。定义在多核系统中该内存区域如何共享(例如:非共享、外部共享、内部共享)。通常设置为“内部共享”(Inner Shareable) 以确保缓存一致性。
[7]AF (Access Flag)访问标志。如果为 0,表示该页自页表建立以来从未被访问过。硬件会在首次访问时将其置 1。
[6:2]AttrIndx[2:0] (Memory Attributes Index)内存属性索引。这 3 个位是索引,指向一个位于系统控制寄存器 (MAIR_EL1) 中的 8 位属性描述符,定义了缓存策略(例如:可缓存、不可缓存、写直达等)。
[1]NS (Non-Secure)非安全。在支持 TrustZone 的系统中,控制该页是否位于安全世界(通常为 0,表示安全)。
[0]Valid/Present bit有效/存在位必须是 1 表示该 PTE 有效,否则会触发缺页异常。

4.2 引用

https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247489107&idx=1&sn=8380aea1178ff3b0b763fecc55feeb37&chksm=ce77d014f90059020823e9f66c1377d6db5009a9e910c56cdca8d8c0b8d0cea2270a1faa5fcc&cur_album_id=2559805446807928833&scene=189#wechat_redirect

https://pzh2386034.github.io/Black-Jack/linux-memory/2019/09/15/ARM64%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%8D%81%E4%BA%94-do_page_fault%E7%BC%BA%E9%A1%B5%E4%B8%AD%E6%96%AD/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值