14 min read

内存管理6_缺页异常

内存管理6_缺页异常

1、介绍

什么是缺页异常?

当进程尝试访问当前不在其工作集内存中的页面时,就会发生缺页异常。这种事件触发缺页异常中断,导致内核采取特定行动来处理这种情况。

缺页异常的类型

  • 次要缺页异常(Minor Page Faults): 当页面不在进程的当前工作集中,但仍然驻留在内存的某个地方时发生。
  • 主要缺页异常(Major Page Faults): 当需要从磁盘(如交换区或内存映射文件)获取页面时发生。

下边我们会以x86平台为例,分析一下缺页异常的整个流程。

2、CPU硬件

2.1-产生一个缺页

当进程准备访问内存时,会首先生成一个虚拟地址,然后MMU会通过这个进程的页表来查找虚拟地址对应的物理地址。如果虚拟地址对应的页表项(PTE)标记为不在物理内存中(例如,PTE 中的存在(Present)位被设置为0),则表示页面不在内存中,这时硬件检测到缺页情况,此时就会触发CPU的缺页异常。

2.2-缺页异常中断

而缺页异编号为 #PF,即 Page Fault,会触发一个中断,中断向量为14,其被定义在了arch\x86\include\asm\trapnr.h

#define X86_TRAP_PF		14	/* Page Fault */

内核通过中断向量表来查找对应中断的处理函数,也就是缺页异常处理函数。

arch\x86\include\asm\idtentry.h

DECLARE_IDTENTRY_RAW_ERRORCODE(X86_TRAP_PF,	exc_page_fault);

#define DECLARE_IDTENTRY_ERRORCODE(vector, func)			\
	idtentry vector asm_##func func has_error_code=1

并且系统会把异常访问的地址存入CR2寄存器之中,后续的处理函数会利用这个地址进行相应的处理流程。

2.3-缺页异常码

另外触发缺页异常的时候还会标记当前的错误码error_code,用来方便后续错误处理。

/*
 * Page fault error code bits:
 *
 *   bit 0 ==	 0: no page found	1: protection fault
 *   bit 1 ==	 0: read access		1: write access
 *   bit 2 ==	 0: kernel-mode access	1: user-mode access
 *   bit 3 ==				1: use of reserved bit detected
 *   bit 4 ==				1: fault was an instruction fetch
 *   bit 5 ==				1: protection keys block access
 *   bit 6 ==				1: shadow stack access fault
 *   bit 15 ==				1: SGX MMU page-fault
 */
enum x86_pf_error_code {
	X86_PF_PROT	=		1 << 0,
	X86_PF_WRITE	=		1 << 1,
	X86_PF_USER	=		1 << 2,
	X86_PF_RSVD	=		1 << 3,
	X86_PF_INSTR	=		1 << 4,
	X86_PF_PK	=		1 << 5,
	X86_PF_SHSTK	=		1 << 6,
	X86_PF_SGX	=		1 << 15,
};

发生缺页异常的时候,CPU会自动将error_code直接压入当前的堆栈。(这个流程存疑,到底是怎么存入的,组内的大佬说ARM64上是由硬件完成自动存入指定的寄存器)

在异常处理函数中从堆栈里直接读取该error_code值,相关的函数如下

arch/x86/entry/entry_64.S

/**
 * idtentry_body - Macro to emit code calling the C function
 * @cfunc:		C function to be called
 * @has_error_code:	Hardware pushed error code on stack
 */
.macro idtentry_body cfunc has_error_code:req
	......
	movq	%rsp, %rdi			/* pt_regs pointer into 1st argument*/
	.if \has_error_code == 1
		movq	ORIG_RAX(%rsp), %rsi	/* 读当前error_code值*/
		movq	$-1, ORIG_RAX(%rsp)	/* no syscall to restart */
	.endif
	......
.endm

3、缺页异常处理函数

exc_page_fault->handle_page_fault->do_kern_addr_fault

3.1-exc_page_fault

缺页异常处理的入口函数

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
	unsigned long address = read_cr2(); // 读出cr2的值,获取触发缺页异常的地址
	irqentry_state_t state;

	prefetchw(&current->mm->mmap_lock);
    
	if (kvm_handle_async_pf(regs, (u32)address)) // 如果是KVM触发的,执行不同的逻辑,这里先忽略
		return;
    
	state = irqentry_enter(regs); // 开始进入中断处理,保存寄存器的状态

	instrumentation_begin(); // 启动代码区块的仪器化过程,主要用以trace分析,标记调用栈
	handle_page_fault(regs, error_code, address); // 进一步执行缺页异常处理
	instrumentation_end();

	irqentry_exit(regs, state); // 退出中断
}

接下来的handle_page_fault中会区分是内核触发的缺页异常还是用户,并分别调用对应的逻辑。

3.2-内核态缺页异常

内核态的缺页异常逻辑相对用户态较少,基本上是需要触发kpanic的严重错误,也就是bad_area的错误。

static void
do_kern_addr_fault(struct pt_regs *regs, unsigned long hw_error_code,
		   unsigned long address)
{
	......
	if (is_f00f_bug(regs, hw_error_code, address)) // 处理intel的f00f错误
		return;

	/* 检查是否假缺页异常,即由懒惰的 TLB无效化引起 */
	if (spurious_kernel_fault(hw_error_code, address))
		return;

	/* kprobes don't want to hook the spurious faults: */
	if (WARN_ON_ONCE(kprobe_page_fault(regs, X86_TRAP_PF))) // 是否Kprobes钩子需要在这个缺页异常上触发
		return;
    
	bad_area_nosemaphore(regs, hw_error_code, address); // 通用处理的内核态缺页异常
}

最终通过__bad_area_nosemaphore处理严重的缺页错误

static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
		       unsigned long address, u32 pkey, int si_code)
{
	struct task_struct *tsk = current;

	if (!user_mode(regs)) { // 内核态报错
		kernelmode_fixup_or_oops(regs, error_code, address,
					 SIGSEGV, si_code, pkey); // 尝试修复,不然的会就触发内核的oops
		return;
	}
......
}

可以修复或者忽略的内核态缺页异常包括以下:

  1. 非致命的内核异常:
    • 内核在执行某些操作时可能会遇到预期内的异常,比如某些硬件访问操作、读写错误的MSR。这些异常虽然需要处理,但不一定需要导致系统崩溃。
  2. 可恢复的错误:
    • 某些错误情况下,内核有可能通过简单的操作来恢复正常状态,比如重新映射一个地址、调整内存分配等。
  3. 特定的内核操作:
    • 比如在内存访问中,如果有可恢复的页表错误,内核可能会尝试修复,而不是直接导致崩溃。

具体的包含了下列类型

	case EX_TYPE_DEFAULT:
	case EX_TYPE_DEFAULT_MCE_SAFE:
		return ex_handler_default(e, regs);
	case EX_TYPE_FAULT:
	case EX_TYPE_FAULT_MCE_SAFE:
		return ex_handler_fault(e, regs, trapnr);
	case EX_TYPE_UACCESS:
		return ex_handler_uaccess(e, regs, trapnr, fault_addr);
	case EX_TYPE_COPY:
		return ex_handler_copy(e, regs, trapnr);
	case EX_TYPE_CLEAR_FS:
		return ex_handler_clear_fs(e, regs);
	case EX_TYPE_FPU_RESTORE:
		return ex_handler_fprestore(e, regs);
	case EX_TYPE_BPF:
		return ex_handler_bpf(e, regs);
	case EX_TYPE_WRMSR:
		return ex_handler_msr(e, regs, true, false, reg);
	case EX_TYPE_RDMSR:
		return ex_handler_msr(e, regs, false, false, reg);
	case EX_TYPE_WRMSR_SAFE:
		return ex_handler_msr(e, regs, true, true, reg);
	case EX_TYPE_RDMSR_SAFE:
		return ex_handler_msr(e, regs, false, true, reg);
	case EX_TYPE_WRMSR_IN_MCE:
		ex_handler_msr_mce(regs, true);
		break;
	case EX_TYPE_RDMSR_IN_MCE:
		ex_handler_msr_mce(regs, false);
		break;
	case EX_TYPE_POP_REG:
		regs->sp += sizeof(long);
		fallthrough;
	case EX_TYPE_IMM_REG:
		return ex_handler_imm_reg(e, regs, reg, imm);
	case EX_TYPE_FAULT_SGX:
		return ex_handler_sgx(e, regs, trapnr);
	case EX_TYPE_UCOPY_LEN:
		return ex_handler_ucopy_len(e, regs, trapnr, fault_addr, reg, imm);
	case EX_TYPE_ZEROPAD:
		return ex_handler_zeropad(e, regs, fault_addr);

如果不在上列内容中,则会最终调用page_fault_oops触发内核的oops。另外,oops也会根据状态判断是否进一步触发panic

void oops_end(unsigned long flags, struct pt_regs *regs, int signr)
{
  ......
	if (!signr)
		return;
	if (in_interrupt()) // 中断上下文必须要触发panic
		panic("Fatal exception in interrupt");
	if (panic_on_oops) // 开启oops触发panic也必须panic
		panic("Fatal exception");
  ......
}

3.3-用户态缺页异常

用户态的缺页异常的处理相比内核态的会温和很多,主要存在当第一次访问没有分配物理内存的虚拟地址情况,所以优先会处理非严重的问题。

主要的工作就是根据error_code判断错误类型,执行对应的错误处理。

static inline
void do_user_addr_fault(struct pt_regs *regs,
			unsigned long error_code,
			unsigned long address)
{
	......
  // 在用户空间尝试执行内核模式代码指令的话直接oops
	if (unlikely((error_code & (X86_PF_USER | X86_PF_INSTR)) == X86_PF_INSTR)) {
		......
		page_fault_oops(regs, error_code, address);
		return;
	}
	......
	if (unlikely(error_code & X86_PF_RSVD)) // 由于页表项中的保留位设置不正确引起的异常
		pgtable_bad(regs, error_code, address);

	...... // 省略一些特定的error code处理

	if (!(flags & FAULT_FLAG_USER))
		goto lock_mmap;

	vma = lock_vma_under_rcu(mm, address); // 查找与引起缺页异常的物理地址对应的虚拟地址VMA
	if (!vma)
		goto lock_mmap;

	if (unlikely(access_error(error_code, vma))) {// 检查VMA的访问权限(例如只读、可写等)
		vma_end_read(vma);
		goto lock_mmap;
	}
	fault = handle_mm_fault(vma, address, flags | FAULT_FLAG_VMA_LOCK, regs); // 根据vma和物理地址尝试处理错误,并通过flag来标记错误处理的结果
	......
	count_vm_vma_lock_event(VMA_LOCK_RETRY);

	/* Quick path to respond to signals */
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))
			kernelmode_fixup_or_oops(regs, error_code, address,
						 SIGBUS, BUS_ADRERR,
						 ARCH_DEFAULT_PKEY);
		return;
	}
lock_mmap:

retry: // 在这里循环处理用户态缺页异常错误
	vma = lock_mm_and_find_vma(mm, address, regs); // 查找与引起缺页异常的物理地址对应的虚拟地址VMA
	if (unlikely(!vma)) {
		bad_area_nosemaphore(regs, error_code, address);
		return;
	}

	if (unlikely(access_error(error_code, vma))) { // 检查VMA的访问权限(例如只读、可写等)
		bad_area_access_error(regs, error_code, address, vma); // 如果权限有问题就执行严重的oops逻辑
		return;
	}

	fault = handle_mm_fault(vma, address, flags, regs); // 根据vma和物理地址尝试处理错误,并通过flag来标记错误处理的结果
	......
	/* The fault is fully completed (including releasing mmap lock) */
	if (fault & VM_FAULT_COMPLETED)
		return;
	......
done: // 根据处理后的flag执行对应的收尾逻辑
	if (likely(!(fault & VM_FAULT_ERROR)))
		return;

	if (fatal_signal_pending(current) && !user_mode(regs)) {
		kernelmode_fixup_or_oops(regs, error_code, address,
					 0, 0, ARCH_DEFAULT_PKEY);
		return;
	}

	if (fault & VM_FAULT_OOM) { // 处理因为没有内存需要oom导致的缺页异常错误
    ......
		pagefault_out_of_memory(); // 尝试执行oom
	} else {
		if (fault & (VM_FAULT_SIGBUS|VM_FAULT_HWPOISON|
			     VM_FAULT_HWPOISON_LARGE)) // 非法内存访问(例如访问了未映射的物理内存)、硬件损坏引起的内存错误(例如ECC错误)或大页硬件损坏引起的
			do_sigbus(regs, error_code, address, fault);
		else if (fault & VM_FAULT_SIGSEGV) // 无效的内存访问引起的,例如访问了不属于进程地址空间的内存
			bad_area_nosemaphore(regs, error_code, address);
		else
			BUG();
	}
}

handle_mm_fault会进一步处理缺页异常

vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
			   unsigned int flags, struct pt_regs *regs)
{
	......
	if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE, // 检查VMA地址的访问权限(读/写、指令获取等)
					    flags & FAULT_FLAG_INSTRUCTION,
					    flags & FAULT_FLAG_REMOTE)) {
		ret = VM_FAULT_SIGSEGV;
		goto out;
	}

	/*
	 * 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();

	lru_gen_enter_fault(vma); // 通知LRU,当前地址进入缺页异常处理。

	if (unlikely(is_vm_hugetlb_page(vma)))
		ret = hugetlb_fault(vma->vm_mm, vma, address, flags); // 处理大页的缺页异常
	else
		ret = __handle_mm_fault(vma, address, flags);// 处理通用的缺页异常
	......zzzz
	return ret;
}

我们主要关注一下通用的错误处理缺页异常,进入到这一步的时候,主要就是用户态进程访问了一个没分配实际物理地址的虚拟地址,这里也就意味着不是严重的缺页异常错误,只需要分配一个合理的地址即可,因此__handle_mm_fault函数的主要通将虚拟地址映射到物理内存。

static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
		unsigned long address, unsigned int flags)
{
	......
	pgd = pgd_offset(mm, address);
	p4d = p4d_alloc(mm, pgd, address); // 分配p4d
	if (!p4d)
		return VM_FAULT_OOM;

	vmf.pud = pud_alloc(mm, p4d, address); // 分配pud(Page Upper Directory)并检查是否成功。
	if (!vmf.pud)
		return VM_FAULT_OOM;
retry_pud:
	if (pud_none(*vmf.pud) &&
	    hugepage_vma_check(vma, vm_flags, false, true, true)) { // 判断当前的虚拟地址是否需要一个大型PUD
		ret = create_huge_pud(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	}
  ......

	vmf.pmd = pmd_alloc(mm, vmf.pud, address); // 分配pmd(Page Middle Directory)并检查是否成功
	if (!vmf.pmd)
		return VM_FAULT_OOM;
	......

	if (pmd_none(*vmf.pmd) &&
	    hugepage_vma_check(vma, vm_flags, false, true, true)) { // 判断当前的虚拟地址是否需要一个大型PMD
		ret = create_huge_pmd(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	} 
  ......

	return handle_pte_fault(&vmf); // 最后一步是处理页表项,建立或者更新页表项的映射。
}

handle_pte_fault主要是处理页表项(PTE),包括建立或更新虚拟地址到物理地址的映射,处理交换页、NUMA页等特殊情况,以及维护内存系统的一致性。

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

	// 步骤1:检查对应的PMD(Page Middle Directory)是否为空
	if (unlikely(pmd_none(*vmf->pmd))) {
		vmf->pte = NULL;
		vmf->flags &= ~FAULT_FLAG_ORIG_PTE_VALID;
	} else {
		// 步骤2:获取PMD对应的PTE
		vmf->pte = pte_offset_map_nolock(vmf->vma->vm_mm, vmf->pmd,
						 vmf->address, &vmf->ptl);
		if (unlikely(!vmf->pte))
			return 0;
		vmf->orig_pte = ptep_get_lockless(vmf->pte);
		vmf->flags |= FAULT_FLAG_ORIG_PTE_VALID;

		// 检查PTE是否为空
		if (pte_none(vmf->orig_pte)) {
			pte_unmap(vmf->pte);
			vmf->pte = NULL;
		}
	}

	// 步骤3:处理缺失的页表项
	if (!vmf->pte)
		return do_pte_missing(vmf); // 根据页表类型调用相应分配逻辑:do_anonymous_page和do_fault

	// 步骤4:处理交换页
	if (!pte_present(vmf->orig_pte))
		return do_swap_page(vmf);

	// 步骤5:处理NUMA页
	if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
		return do_numa_page(vmf);

	spin_lock(vmf->ptl);
	entry = vmf->orig_pte;
	......
	entry = pte_mkyoung(entry); // 将pte设置为_PAGE_ACCESSED,也就是可以访问的状态,同也等价于标记了young标志,避免被swap

	// 步骤6:更新MMU缓存
	if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
				vmf->flags & FAULT_FLAG_WRITE)) { // 更新页表项的访问标志
		update_mmu_cache_range(vmf, vmf->vma, vmf->address,
				vmf->pte, 1); // 刷新MMU的cache也就是TLB
	}
  ......

unlock:
	// 步骤7:解锁PTE并返回0表示成功
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return 0;
}

3.3.1-do_anonymous_page

在我们malloc时候,通常会通过do_anonymous_page给匿名页分配物理地址,主要用于存储进程的堆、栈和其他动态分配的内存。

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	.....
	if (pte_alloc(vma->vm_mm, vmf->pmd)) // 为pte分配一个内存
		return VM_FAULT_OOM;
	......
	/* Allocate our own private page. */
	if (unlikely(anon_vma_prepare(vma)))
		goto oom;
	folio = vma_alloc_zeroed_movable_folio(vma, vmf->address); // 分配一个新的0页
	if (!folio)
		goto oom;
	......
	__folio_mark_uptodate(folio); // 将新分配的folio标记为已更新。这意味着folio已经准备好被访问。

	entry = mk_pte(&folio->page, vma->vm_page_prot); // 函数为新分配的folio创建一个新的页表项(PTE)
	entry = pte_sw_mkyoung(entry); // 使用pte_sw_mkyoung()函数将PTE标记为年轻(已访问)
	if (vma->vm_flags & VM_WRITE)
		entry = pte_mkwrite(pte_mkdirty(entry), vma); // 函数将PTE标记为可写和脏,表示该在写入后需要将其写回到磁盘
	......
  // 检查地址空间是否稳定,避免例如其他线程可能在此期间对地址空间进行修改,导致在设置PTE时使用无效或过时信息
	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);
		folio_put(folio);
		return handle_userfault(vmf, VM_UFFD_MISSING);
	}

	inc_mm_counter(vma->vm_mm, MM_ANONPAGES); 
	folio_add_new_anon_rmap(folio, vma, vmf->address);// 将新分配的页添加到匿名内存映射中,
	folio_add_lru_vma(folio, vma); // 并将其添加到LRU列表中。
setpte:
	.....

	update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1); // 更新MMU缓存
unlock:
	if (vmf->pte)
		pte_unmap_unlock(vmf->pte, vmf->ptl);
	return ret;
	......
}

3.3.2-do_fault

通过do_fault给文件页分配物理地址。

static vm_fault_t do_fault(struct vm_fault *vmf)
{
	.....
  else if (!(vmf->flags & FAULT_FLAG_WRITE))
		ret = do_read_fault(vmf); // 读操作处理
	else if (!(vma->vm_flags & VM_SHARED))
		ret = do_cow_fault(vmf); // copy on write 处理
	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;
}

上述几种处理最终都会调用到__do_fault

static vm_fault_t __do_fault(struct vm_fault *vmf)
{
	......
	if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) { // 在获取页锁之前预分配一个页表项(PTE)
		vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
		if (!vmf->prealloc_pte)
			return VM_FAULT_OOM;
	}

	ret = vma->vm_ops->fault(vmf); // VMA的fault操作来处理缺页异常,通常定义在对应的文件系统里
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
			    VM_FAULT_DONE_COW)))
		return ret;
	// 检查当前页是否为硬件毒页,硬件毒页是指内存中的某个页出现了不可恢复的硬件错误,例如内存芯片的某个位发生了永久性故障。
	if (unlikely(PageHWPoison(vmf->page))) { 
		struct page *page = vmf->page;
		vm_fault_t poisonret = VM_FAULT_HWPOISON;
		......
		put_page(page); // 释放页,直接返回
		vmf->page = NULL;
		return poisonret;
	}
	......
	return ret;
}

例如我们分析ext4的文件页缺页处理

static const struct vm_operations_struct ext4_file_vm_ops = {
	.fault		= filemap_fault,
	......
};

最终会调用到通用的filemap_fault

vm_fault_t filemap_fault(struct vm_fault *vmf)
{
	......

	max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE); // 计算文件映射的最大索引
	if (unlikely(index >= max_idx)) // 如果请求的虚拟地址超出了文件大小
		return VM_FAULT_SIGBUS; // 返回VM_FAULT_SIGBUS表示访问了一个无效的内存区域。

	/*
	 * 尝试从文件映射中获取一个已缓存的folio。
	 */
	folio = filemap_get_folio(mapping, index);
	if (likely(!IS_ERR(folio))) {
		/*
		 * We found the page, so try async readahead before waiting for
		 * the lock.
		 */
		if (!(vmf->flags & FAULT_FLAG_TRIED))
			fpin = do_async_mmap_readahead(vmf, folio); // 如果找到了folio,尝试进行异步读取。
			.....
	} else {
		// 如果folio不存在,标记一次主缺页异常(major fault),
		count_vm_event(PGMAJFAULT);
		count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
		ret = VM_FAULT_MAJOR;
		fpin = do_sync_mmap_readahead(vmf); // 并执行同步读取。
		......
	}

	if (!lock_folio_maybe_drop_mmap(vmf, folio, &fpin)) // 如果folio已更新,锁定并返回它。
		goto out_retry;

	/* Did it get truncated? */
	if (unlikely(folio->mapping != mapping)) { // 如果folio被截断
		folio_unlock(folio); // 解锁folio
		folio_put(folio); // 释放folio
		goto retry_find;
	}
	VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);
	......

	vmf->page = folio_file_page(folio, index); // 如果folio已更新并且索引在文件大小范围内,那么返回锁定的folio。
	return ret | VM_FAULT_LOCKED;

page_not_uptodate:
	fpin = maybe_unlock_mmap_for_io(vmf, fpin);
	error = filemap_read_folio(file, mapping->a_ops->read_folio, folio); // 如果folio未更新,尝试同步读取folio
	if (fpin)
		goto out_retry;
	folio_put(folio);

	if (!error || error == AOP_TRUNCATED_PAGE)
		goto retry_find;
	filemap_invalidate_unlock_shared(mapping);

	return VM_FAULT_SIGBUS;

out_retry:
	/*
	 * 释放所有资源并返回VM_FAULT_RETRY。
	 */
	if (!IS_ERR(folio))
		folio_put(folio);
	if (mapping_locked)
		filemap_invalidate_unlock_shared(mapping);
	if (fpin)
		fput(fpin);
	return ret | VM_FAULT_RETRY;
}