LOADING

加载过慢请开启缓存 浏览器默认开启

ARM64 Linux 页内存异常处理

通过linux源码和deepseek学习arm64 linux设备对page fault的处理。本篇文章是使用脏页检测动态hook思路的基础。
先说一下检测方案(对于r-xp可执行段):

  1. 检测系统库时:利用smap判断是否有dirty和匿名内存;利用pagemap检测是否为file-backend,不为匿名且不为独占映射。
  2. 检测自己的库时,利用smap判断是否有dirty和匿名内存;利用pagemap检测是否为file-backend且不为匿名。
    为进一步提高效率,可以用lseek在记录偏移后加速定位。

将以“问题 - Deepseek回答”形式展开。

/arch/arm64/mm/fault.c

建议阅读

Q1

struct fault_info {
	int	(*fn)(unsigned long far, unsigned long esr,
		      struct pt_regs *regs);
	int	sig;
	int	code;
	const char *name;
};

static const struct fault_info 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_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 0 access flag fault"	},
	{ 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_page_fault,	SIGSEGV, SEGV_ACCERR,	"level 0 permission fault"	},
	{ 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_sea,		SIGBUS,  BUS_OBJERR,	"synchronous external abort"	},
	{ do_tag_check_fault,	SIGSEGV, SEGV_MTESERR,	"synchronous tag check fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 18"			},
	{ do_sea,		SIGKILL, SI_KERNEL,	"level -1 (translation table walk)"	},
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 0 (translation table walk)"	},
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 1 (translation table walk)"	},
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 2 (translation table walk)"	},
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 3 (translation table walk)"	},
	{ do_sea,		SIGBUS,  BUS_OBJERR,	"synchronous parity or ECC error" },	// Reserved when RAS is implemented
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 25"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 26"			},
	{ do_sea,		SIGKILL, SI_KERNEL,	"level -1 synchronous parity error (translation table walk)"	},	// Reserved when RAS is implemented
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 0 synchronous parity error (translation table walk)"	},	// Reserved when RAS is implemented
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 1 synchronous parity error (translation table walk)"	},	// Reserved when RAS is implemented
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 2 synchronous parity error (translation table walk)"	},	// Reserved when RAS is implemented
	{ do_sea,		SIGKILL, SI_KERNEL,	"level 3 synchronous parity error (translation table walk)"	},	// Reserved when RAS is implemented
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 32"			},
	{ do_alignment_fault,	SIGBUS,  BUS_ADRALN,	"alignment fault"		},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 34"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 35"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 36"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 37"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 38"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 39"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 40"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level -1 address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 42"			},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level -1 translation fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 44"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 45"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 46"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 47"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"TLB conflict abort"		},
	{ do_bad,		SIGKILL, SI_KERNEL,	"Unsupported atomic hardware update fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 50"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 51"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"implementation fault (lockdown abort)" },
	{ do_bad,		SIGBUS,  BUS_OBJERR,	"implementation fault (unsupported exclusive)" },
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 54"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 55"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 56"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 57"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 58" 			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 59"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 60"			},
	{ do_bad,		SIGKILL, SI_KERNEL,	"section domain fault"		},
	{ do_bad,		SIGKILL, SI_KERNEL,	"page domain fault"		},
	{ do_bad,		SIGKILL, SI_KERNEL,	"unknown 63"			},
};

这个内核结构是什么意思?内核异常时调用的处理函数是他定义的吗?

A1

这是一个 ARM64架构的内核异常处理表,定义了各种内存访问异常的处理方式。让我详细解释:

结构体含义

struct fault_info {
    int (*fn)(...);     // 处理函数指针
    int sig;            // 要发送给进程的信号
    int code;           // 信号码
    const char *name;   // 异常类型描述
};

关键信息

  1. 这是ARM64的异常表:处理MMU(内存管理单元)产生的各种异常

  2. 表的索引对应ESR寄存器

    • ARM64发生异常时,ESR(Exception Syndrome Register)寄存器包含异常原因
    • ESR的低6位(DFSC/IFSC)作为索引查找此表
    • 例如:ESR=0x04 → 索引4 → “level 0 translation fault”
  3. 常见异常处理函数

    • do_bad:无法处理的严重错误,直接发送SIGKILL杀死进程
    • do_translation_fault:页面未映射(缺页异常)
    • do_page_fault:权限错误(无访问权限)
    • do_sea:同步外部错误(内存硬件错误)
    • do_tag_check_fault:内存标签检查错误(MTE特性)
    • do_alignment_fault:对齐错误

异常分类

1. 地址大小错误(索引0-3)

  • TTBR或页表配置错误
  • 系统级错误,直接杀死进程

2. 转换错误(索引4-7)

  • 虚拟地址没有对应的物理地址
  • 触发缺页处理,可能分配新页面

3. 权限错误(索引8-15)

  • 有映射但无访问权限(如写只读页面)
  • 可能触发写时复制(COW)

4. 硬件错误(索引16-31)

  • 内存硬件故障、ECC错误等
  • 多数发送SIGBUS信号

5. 其他特殊错误

  • 索引17:MTE(内存标签扩展)错误
  • 索引32:对齐错误(非对齐访问)
  • 索引48:TLB冲突
  • 索引49:原子操作硬件不支持

工作流程

当ARM64发生数据中止异常时:

// 简化流程
asmlinkage void do_mem_abort(unsigned long addr, unsigned int esr,
                            struct pt_regs *regs)
{
    const struct fault_info *inf;
    inf = fault_info + (esr & 0x3f);  // 获取低6位作为索引
    
    // 调用对应的处理函数
    if (!inf->fn(addr, esr, regs)) {
        // 处理成功返回
        return;
    }
    
    // 处理失败,发送信号
    arm64_force_sig_fault(inf->sig, inf->code, addr, inf->name);
}

信号说明

  • SIGSEGV:段错误(非法内存访问)

    • SEGV_MAPERR:地址未映射
    • SEGV_ACCERR:权限错误
    • SEGV_MTESERR:内存标签错误
  • SIGBUS:总线错误(硬件错误)

    • BUS_OBJERR:对象硬件错误
    • BUS_ADRALN:地址对齐错误
  • SIGKILL:无法恢复的严重错误

实际例子

  1. 用户态访问NULL指针

    • 产生”level 0 translation fault”(索引4)
    • 调用do_translation_fault
    • 发现是非法地址,发送SIGSEGV(SEGV_MAPERR)
  2. 写只读页面

    • 产生”level 0 permission fault”(索引12)
    • 调用do_page_fault
    • 尝试COW或发送SIGSEGV(SEGV_ACCERR)
  3. 硬件内存故障

    • 产生”synchronous external abort”(索引16)
    • 调用do_sea,发送SIGBUS

这个表是ARM64内核异常处理的调度中心,决定了每种硬件异常如何转化为用户可见的信号或内部处理。

Q2

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;
	vm_fault_t fault;
	vm_flags_t vm_flags;
	unsigned int mm_flags = FAULT_FLAG_DEFAULT;
	unsigned long addr = untagged_addr(far);
	struct vm_area_struct *vma;
	int si_code;
	int pkey = -1;

	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.
	 */
	if (faulthandler_disabled() || !mm)
		goto no_context;

	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
	 */
	if (is_el0_instruction_abort(esr)) {
		/* It was exec fault */
		vm_flags = VM_EXEC;
		mm_flags |= FAULT_FLAG_INSTRUCTION;
	} else if (is_gcs_fault(esr)) {
		/*
		 * The GCS permission on a page implies both read and
		 * write so always handle any GCS fault as a write fault,
		 * we need to trigger CoW even for GCS reads.
		 */
		vm_flags = VM_WRITE;
		mm_flags |= FAULT_FLAG_WRITE;
	} 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 (!alternative_has_cap_unlikely(ARM64_HAS_EPAN))
			vm_flags |= VM_EXEC;
	}

	if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
		if (is_el1_instruction_abort(esr))
			die_kernel_fault("execution of user memory",
					 addr, esr, regs);

		if (!insn_may_access_user(regs->pc, esr))
			die_kernel_fault("access to user memory outside uaccess routines",
					 addr, esr, regs);
	}

	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);

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

	vma = lock_vma_under_rcu(mm, addr);
	if (!vma)
		goto lock_mmap;

	if (is_invalid_gcs_access(vma, esr)) {
		vma_end_read(vma);
		fault = 0;
		si_code = SEGV_ACCERR;
		goto bad_area;
	}

	if (!(vma->vm_flags & vm_flags)) {
		vma_end_read(vma);
		fault = 0;
		si_code = SEGV_ACCERR;
		count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
		goto bad_area;
	}

	if (fault_from_pkey(vma, mm_flags)) {
		pkey = vma_pkey(vma);
		vma_end_read(vma);
		fault = 0;
		si_code = SEGV_PKUERR;
		count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
		goto bad_area;
	}

	fault = handle_mm_fault(vma, addr, mm_flags | FAULT_FLAG_VMA_LOCK, regs);
	if (!(fault & (VM_FAULT_RETRY | VM_FAULT_COMPLETED)))
		vma_end_read(vma);

	if (!(fault & VM_FAULT_RETRY)) {
		count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
		goto done;
	}
	count_vm_vma_lock_event(VMA_LOCK_RETRY);
	if (fault & VM_FAULT_MAJOR)
		mm_flags |= FAULT_FLAG_TRIED;

	/* Quick path to respond to signals */
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))
			goto no_context;
		return 0;
	}
lock_mmap:

retry:
	vma = lock_mm_and_find_vma(mm, addr, regs);
	if (unlikely(!vma)) {
		fault = 0;
		si_code = SEGV_MAPERR;
		goto bad_area;
	}

	if (!(vma->vm_flags & vm_flags)) {
		mmap_read_unlock(mm);
		fault = 0;
		si_code = SEGV_ACCERR;
		goto bad_area;
	}

	if (fault_from_pkey(vma, mm_flags)) {
		pkey = vma_pkey(vma);
		mmap_read_unlock(mm);
		fault = 0;
		si_code = SEGV_PKUERR;
		goto bad_area;
	}

	fault = handle_mm_fault(vma, addr, mm_flags, regs);

	/* Quick path to respond to signals */
	if (fault_signal_pending(fault, regs)) {
		if (!user_mode(regs))
			goto no_context;
		return 0;
	}

	/* The fault is fully completed (including releasing mmap lock) */
	if (fault & VM_FAULT_COMPLETED)
		return 0;

	if (fault & VM_FAULT_RETRY) {
		mm_flags |= FAULT_FLAG_TRIED;
		goto retry;
	}
	mmap_read_unlock(mm);

done:
	/* Handle the "normal" (no error) case first. */
	if (likely(!(fault & VM_FAULT_ERROR)))
		return 0;

	si_code = SEGV_MAPERR;
bad_area:
	/*
	 * If we are in kernel mode at this point, we have no context to
	 * handle this fault with.
	 */
	if (!user_mode(regs))
		goto no_context;

	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;
	}

	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 {
		/*
		 * The pkey value that we return to userspace can be different
		 * from the pkey that caused the fault.
		 *
		 * 1. T1   : mprotect_key(foo, PAGE_SIZE, pkey=4);
		 * 2. T1   : set POR_EL0 to deny access to pkey=4, touches, page
		 * 3. T1   : faults...
		 * 4.    T2: mprotect_key(foo, PAGE_SIZE, pkey=5);
		 * 5. T1   : enters fault handler, takes mmap_lock, etc...
		 * 6. T1   : reaches here, sees vma_pkey(vma)=5, when we really
		 *	     faulted on a pte with its pkey=4.
		 */
		/* Something tried to access memory that out of memory map */
		if (si_code == SEGV_PKUERR)
			arm64_force_sig_fault_pkey(far, inf->name, pkey);
		else
			arm64_force_sig_fault(SIGSEGV, si_code, far, inf->name);
	}

	return 0;

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

将函数分为几块,每块详细注释,介绍处理的情况

A2

好的,我将这个复杂的页错误处理函数分块详细注释:

/**
 * do_page_fault - ARM64页错误处理函数
 * @far: 错误地址(FAR寄存器值)
 * @esr: 异常症状寄存器(ESR)值
 * @regs: 寄存器状态
 *
 * 处理权限错误(access flag fault/permission 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;  // 当前进程的内存描述符
    vm_fault_t fault;
    vm_flags_t vm_flags;                 // 需要的内存区域标志
    unsigned int mm_flags = FAULT_FLAG_DEFAULT;  // 错误处理标志
    unsigned long addr = untagged_addr(far);  // 去除地址标签(ARM MTE)
    struct vm_area_struct *vma;          // 虚拟内存区域
    int si_code;                         // 信号代码
    int pkey = -1;                       // 保护键

    /* 块1: kprobe探测点处理 */
    // 如果kprobe正在处理此错误,则直接返回
    if (kprobe_page_fault(regs, esr))
        return 0;

    /* 块2: 上下文检查 */
    /*
     * 如果我们在中断中或没有用户上下文(如内核线程),不能处理错误
     * faulthandler_disabled()检查是否在中断/临界区中
     * mm为空表示是内核线程,没有用户空间内存
     */
    if (faulthandler_disabled() || !mm)
        goto no_context;

    // 设置用户模式标志
    if (user_mode(regs))
        mm_flags |= FAULT_FLAG_USER;

    /* 块3: 错误类型解析 */
    /*
     * 根据ESR解析错误类型,确定需要的VMA标志
     * vm_flags表示VMA必须有的标志,否则是权限错误
     */
    if (is_el0_instruction_abort(esr)) {
        /* 执行错误 - 如执行不可执行的页面 */
        vm_flags = VM_EXEC;
        mm_flags |= FAULT_FLAG_INSTRUCTION;
    } else if (is_gcs_fault(esr)) {
        /* GCS(Guarded Control Stack)错误 */
        vm_flags = VM_WRITE;
        mm_flags |= FAULT_FLAG_WRITE;
    } else if (is_write_abort(esr)) {
        /* 写错误 - 如写入只读页面 */
        vm_flags = VM_WRITE;
        mm_flags |= FAULT_FLAG_WRITE;
    } else {
        /* 读错误 - 如读取无读权限的页面 */
        vm_flags = VM_READ;
        vm_flags |= VM_WRITE;           // 写权限隐含读权限
        // 如果没有EPAN(Enhanced Privileged Access Never)特性,执行权限隐含读权限
        if (!alternative_has_cap_unlikely(ARM64_HAS_EPAN))
            vm_flags |= VM_EXEC;
    }

    /* 块4: 内核访问用户内存检查 */
    /*
     * 检查内核是否合法访问用户内存
     * 仅允许在uaccess例程中访问用户内存
     */
    if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
        if (is_el1_instruction_abort(esr))
            die_kernel_fault("execution of user memory",
                     addr, esr, regs);

        if (!insn_may_access_user(regs->pc, esr))
            die_kernel_fault("access to user memory outside uaccess routines",
                     addr, esr, regs);
    }

    // 性能计数:记录页错误事件
    perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);

    /* 块5: 尝试RCU快速路径 */
    // 如果不是用户模式错误,跳过RCU路径
    if (!(mm_flags & FAULT_FLAG_USER))
        goto lock_mmap;

    // 尝试RCU方式锁定VMA(无锁读取)
    vma = lock_vma_under_rcu(mm, addr);
    if (!vma)
        goto lock_mmap;

    /* 块5.1: RCU路径下的权限检查 */
    if (is_invalid_gcs_access(vma, esr)) {
        vma_end_read(vma);
        fault = 0;
        si_code = SEGV_ACCERR;
        goto bad_area;
    }

    // 检查VMA标志是否符合要求
    if (!(vma->vm_flags & vm_flags)) {
        vma_end_read(vma);
        fault = 0;
        si_code = SEGV_ACCERR;
        count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
        goto bad_area;
    }

    // 检查保护键权限
    if (fault_from_pkey(vma, mm_flags)) {
        pkey = vma_pkey(vma);
        vma_end_read(vma);
        fault = 0;
        si_code = SEGV_PKUERR;
        count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
        goto bad_area;
    }

    /* 块5.2: RCU路径下的页错误处理 */
    // 使用VMA_LOCK标志调用通用页错误处理
    fault = handle_mm_fault(vma, addr, mm_flags | FAULT_FLAG_VMA_LOCK, regs);
    
    // 如果不需要重试且未完成,释放VMA读锁
    if (!(fault & (VM_FAULT_RETRY | VM_FAULT_COMPLETED)))
        vma_end_read(vma);

    // 如果成功完成,跳转到done
    if (!(fault & VM_FAULT_RETRY)) {
        count_vm_vma_lock_event(VMA_LOCK_SUCCESS);
        goto done;
    }
    
    // 需要重试,记录统计信息
    count_vm_vma_lock_event(VMA_LOCK_RETRY);
    if (fault & VM_FAULT_MAJOR)
        mm_flags |= FAULT_FLAG_TRIED;

    /* 块5.3: 信号检查(RCU路径) */
    if (fault_signal_pending(fault, regs)) {
        if (!user_mode(regs))
            goto no_context;
        return 0;
    }

    /* 块6: 标准mmap锁路径 */
lock_mmap:
retry:
    // 获取mmap锁并查找VMA
    vma = lock_mm_and_find_vma(mm, addr, regs);
    if (unlikely(!vma)) {
        fault = 0;
        si_code = SEGV_MAPERR;  // 地址未映射
        goto bad_area;
    }

    /* 块6.1: 标准路径下的权限检查 */
    if (!(vma->vm_flags & vm_flags)) {
        mmap_read_unlock(mm);
        fault = 0;
        si_code = SEGV_ACCERR;
        goto bad_area;
    }

    // 检查保护键权限
    if (fault_from_pkey(vma, mm_flags)) {
        pkey = vma_pkey(vma);
        mmap_read_unlock(mm);
        fault = 0;
        si_code = SEGV_PKUERR;
        goto bad_area;
    }

    /* 块6.2: 标准路径下的页错误处理 */
    fault = handle_mm_fault(vma, addr, mm_flags, regs);

    /* 块6.3: 信号检查(标准路径) */
    if (fault_signal_pending(fault, regs)) {
        if (!user_mode(regs))
            goto no_context;
        return 0;
    }

    // 如果已完成(包括释放mmap锁),直接返回
    if (fault & VM_FAULT_COMPLETED)
        return 0;

    // 如果需要重试,设置重试标志并跳回retry
    if (fault & VM_FAULT_RETRY) {
        mm_flags |= FAULT_FLAG_TRIED;
        goto retry;
    }
    
    // 释放mmap锁
    mmap_read_unlock(mm);

    /* 块7: 处理成功情况 */
done:
    // 正常情况:无错误,返回0
    if (likely(!(fault & VM_FAULT_ERROR)))
        return 0;

    // 设置默认错误代码
    si_code = SEGV_MAPERR;

    /* 块8: 错误处理 */
bad_area:
    // 如果是内核模式错误,无法恢复
    if (!user_mode(regs))
        goto no_context;

    /* 块8.1: 内存不足(OOM)处理 */
    if (fault & VM_FAULT_OOM) {
        /*
         * 内存不足,调用OOM killer
         * 返回用户空间后可以重试错误,或者如果被OOM杀死则结束
         */
        pagefault_out_of_memory();
        return 0;
    }

    /* 块8.2: 根据错误类型发送信号 */
    inf = esr_to_fault_info(esr);
    set_thread_esr(addr, esr);
    
    // SIGBUS: 总线错误(如硬件故障)
    if (fault & VM_FAULT_SIGBUS) {
        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);
    } 
    // SIGSEGV: 段错误
    else {
        /*
         * 注意:返回给用户空间的pkey可能与实际导致错误的pkey不同
         * 因为在错误处理期间pkey可能被其他线程修改
         */
        if (si_code == SEGV_PKUERR)
            arm64_force_sig_fault_pkey(far, inf->name, pkey);
        else
            arm64_force_sig_fault(SIGSEGV, si_code, far, inf->name);
    }

    return 0;

    /* 块9: 内核上下文错误处理 */
no_context:
    __do_kernel_fault(addr, esr, regs);
    return 0;
}

关键路径总结

  1. 权限检查路径:检查VMA标志是否匹配访问类型
  2. RCU快速路径:尝试无锁方式处理,减少竞争
  3. 标准mmap锁路径:获取完整的mmap锁进行处理
  4. 错误信号路径:根据错误类型发送相应的信号给进程

主要错误类型处理

  • 地址未映射 (SEGV_MAPERR):发送SIGSEGV
  • 权限错误 (SEGV_ACCERR):发送SIGSEGV
  • 保护键错误 (SEGV_PKUERR):发送SIGSEGV,附带pkey信息
  • 总线错误 (BUS_ADRERR):发送SIGBUS
  • 内存硬件错误:发送SIGBUS MCE
  • 内存不足:调用OOM killer

这个函数体现了Linux内核页错误处理的复杂性和完整性,涵盖了从快速路径优化到各种错误情况的全面处理。

Q3

vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
			   unsigned int flags, struct pt_regs *regs)
{
	/* If the fault handler drops the mmap_lock, vma may be freed */
	struct mm_struct *mm = vma->vm_mm;
	vm_fault_t ret;
	bool is_droppable;

	__set_current_state(TASK_RUNNING);

	ret = sanitize_fault_flags(vma, &flags);
	if (ret)
		goto out;

	if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
					    flags & FAULT_FLAG_INSTRUCTION,
					    flags & FAULT_FLAG_REMOTE)) {
		ret = VM_FAULT_SIGSEGV;
		goto out;
	}

	is_droppable = !!(vma->vm_flags & VM_DROPPABLE);

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

	if (unlikely(is_vm_hugetlb_page(vma)))
		ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
	else
		ret = __handle_mm_fault(vma, address, flags);

	/*
	 * Warning: It is no longer safe to dereference vma-> after this point,
	 * because mmap_lock might have been dropped by __handle_mm_fault(), so
	 * vma might be destroyed from underneath us.
	 */

	lru_gen_exit_fault();

	/* If the mapping is droppable, then errors due to OOM aren't fatal. */
	if (is_droppable)
		ret &= ~VM_FAULT_OOM;

	if (flags & FAULT_FLAG_USER) {
		mem_cgroup_exit_user_fault();
		/*
		 * 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.
		 */
		if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
			mem_cgroup_oom_synchronize(false);
	}
out:
	mm_account_fault(mm, regs, address, flags, ret);

	return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);

#ifndef __PAGETABLE_P4D_FOLDED
/*
 * Allocate p4d page table.
 * We've already handled the fast-path in-line.
 */
int __p4d_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{
	p4d_t *new = p4d_alloc_one(mm, address);
	if (!new)
		return -ENOMEM;

	spin_lock(&mm->page_table_lock);
	if (pgd_present(*pgd)) {	/* Another has populated it */
		p4d_free(mm, new);
	} else {
		smp_wmb(); /* See comment in pmd_install() */
		pgd_populate(mm, pgd, new);
	}
	spin_unlock(&mm->page_table_lock);
	return 0;
}

同上,分析

A3

好的,我来详细注释这个重要的缺页处理函数:

/**
 * handle_mm_fault - 处理内存缺页异常的核心函数
 * @vma: 发生错误的虚拟内存区域
 * @address: 触发错误的虚拟地址
 * @flags: 错误标志,定义错误类型和处理方式
 * @regs: 寄存器状态(用于统计和调试)
 *
 * 返回值: vm_fault_t 类型,表示错误处理结果
 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
               unsigned int flags, struct pt_regs *regs)
{
    /* 如果错误处理程序释放了mmap_lock,vma可能被释放 */
    struct mm_struct *mm = vma->vm_mm;  // 获取内存描述符
    vm_fault_t ret;                     // 返回值
    bool is_droppable;                  // 标记vma是否可丢弃(如临时映射)

    // 将当前任务状态设置为运行中
    __set_current_state(TASK_RUNNING);

    /* 块1: 标志位检查和清理 */
    // 检查并清理错误标志,确保与vma的权限一致
    ret = sanitize_fault_flags(vma, &flags);
    if (ret)
        goto out;  // 如果标志无效,直接跳转到out

    /* 块2: 架构特定的访问权限检查 */
    // 检查当前架构是否允许这种访问(如ARM64的PAN特性检查)
    if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
                        flags & FAULT_FLAG_INSTRUCTION,
                        flags & FAULT_FLAG_REMOTE)) {
        ret = VM_FAULT_SIGSEGV;  // 不允许访问,返回段错误
        goto out;
    }

    // 检查VMA是否可丢弃(如临时映射的内存)
    is_droppable = !!(vma->vm_flags & VM_DROPPABLE);

    /* 块3: 内存cgroup和LRU生成管理 */
    /*
     * 为用户空间触发的错误启用内存cgroup OOM处理
     * 内核错误处理更加优雅(不会触发OOM killer)
     */
    if (flags & FAULT_FLAG_USER)
        mem_cgroup_enter_user_fault();  // 进入内存cgroup用户错误上下文

    // 进入LRU(最近最少使用)生成错误处理上下文
    lru_gen_enter_fault(vma);

    /* 块4: 根据VMA类型分发到不同的错误处理 */
    if (unlikely(is_vm_hugetlb_page(vma)))
        // 处理大页(HugeTLB)错误
        ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
    else
        // 处理普通页错误
        ret = __handle_mm_fault(vma, address, flags);

    /*
     * 警告:在此之后,不能再安全地解引用vma
     * 因为__handle_mm_fault()可能已经释放了mmap_lock
     * 所以vma可能在我们不知情的情况下被销毁
     */

    // 退出LRU生成错误处理上下文
    lru_gen_exit_fault();

    /* 块5: 处理可丢弃VMA的OOM错误 */
    // 如果映射是可丢弃的,那么OOM错误不是致命的
    if (is_droppable)
        ret &= ~VM_FAULT_OOM;  // 清除OOM标志位

    /* 块6: 用户空间错误的内存cgroup处理 */
    if (flags & FAULT_FLAG_USER) {
        // 退出内存cgroup用户错误上下文
        mem_cgroup_exit_user_fault();
        /*
         * 任务可能已经进入内存cgroup OOM状态,但如果错误被优雅处理
         * (没有VM_FAULT_OOM),则不需要杀死任何东西。
         * 只需和平地清理OOM状态。
         */
        if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM))
            mem_cgroup_oom_synchronize(false);  // 同步内存cgroup OOM状态
    }
    
out:
    /* 块7: 内存错误统计 */
    // 记录内存错误统计信息
    mm_account_fault(mm, regs, address, flags, ret);

    return ret;  // 返回处理结果
}
EXPORT_SYMBOL_GPL(handle_mm_fault);

关键概念解释:

1. 错误标志位 (flags)

  • FAULT_FLAG_USER: 用户空间触发的错误
  • FAULT_FLAG_WRITE: 写错误
  • FAULT_FLAG_INSTRUCTION: 指令执行错误
  • FAULT_FLAG_REMOTE: 远程错误(如NUMA节点间访问)

2. 返回值类型 (vm_fault_t)

#define VM_FAULT_OOM         0x0001  // 内存不足
#define VM_FAULT_SIGBUS      0x0002  // 总线错误
#define VM_FAULT_MAJOR       0x0004  // 主要错误(需要I/O)
#define VM_FAULT_WRITE       0x0008  // 写错误
#define VM_FAULT_HWPOISON    0x0010  // 硬件中毒页
#define VM_FAULT_HWPOISON_LARGE 0x0020  // 大页硬件中毒
#define VM_FAULT_SIGSEGV     0x0040  // 段错误
#define VM_FAULT_NOPAGE      0x0100  // 无页面
#define VM_FAULT_LOCKED      0x0200  // 页面已锁定
#define VM_FAULT_RETRY       0x0400  // 需要重试
#define VM_FAULT_FALLBACK    0x0800  // 回退处理
#define VM_FAULT_DONE_COW    0x1000  // COW完成
#define VM_FAULT_NEEDDSYNC   0x2000  // 需要数据同步
#define VM_FAULT_COMPLETED   0x4000  // 已完成处理

3. VM_DROPPABLE标志

  • 标记VMA为可丢弃的临时映射
  • 用于ephemeral映射,OOM时不会杀死进程
  • 常见于:临时缓冲区、设备映射等

Q4

/*
 * On entry, we hold either the VMA lock or the mmap_lock
 * (FAULT_FLAG_VMA_LOCK tells you which).  If VM_FAULT_RETRY is set in
 * the result, the mmap_lock is not held on exit.  See filemap_fault()
 * and __folio_lock_or_retry().
 */
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,
		.real_address = address,
		.flags = flags,
		.pgoff = linear_page_index(vma, address),
		.gfp_mask = __get_fault_gfp_mask(vma),
	};
	struct mm_struct *mm = vma->vm_mm;
	vm_flags_t vm_flags = vma->vm_flags;
	pgd_t *pgd;
	p4d_t *p4d;
	vm_fault_t ret;

	pgd = pgd_offset(mm, address);
	p4d = p4d_alloc(mm, pgd, address);
	if (!p4d)
		return VM_FAULT_OOM;

	vmf.pud = pud_alloc(mm, p4d, address);
	if (!vmf.pud)
		return VM_FAULT_OOM;
retry_pud:
	if (pud_none(*vmf.pud) &&
	    thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PUD_ORDER)) {
		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)) {

			/*
			 * TODO once we support anonymous PUDs: NUMA case and
			 * FAULT_FLAG_UNSHARE handling.
			 */
			if ((flags & FAULT_FLAG_WRITE) && !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);
	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) &&
	    thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)) {
		ret = create_huge_pmd(&vmf);
		if (!(ret & VM_FAULT_FALLBACK))
			return ret;
	} else {
		vmf.orig_pmd = pmdp_get_lockless(vmf.pmd);

		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)) {
			if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))
				return do_huge_pmd_numa_page(&vmf);

			if ((flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) &&
			    !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);
}

同上分析

A4

好的,我来详细注释这个缺页处理的底层函数:

/*
 * 在进入时,我们持有VMA锁或mmap_lock
 * (FAULT_FLAG_VMA_LOCK标志告诉你持有哪个锁)。
 * 如果结果中设置了VM_FAULT_RETRY,则在退出时不持有mmap_lock。
 * 参见filemap_fault()和__folio_lock_or_retry()。
 */
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
        unsigned long address, unsigned int flags)
{
    /* 块1: 初始化vm_fault结构体 */
    // vm_fault结构体用于在缺页处理过程中传递状态
    struct vm_fault vmf = {
        .vma = vma,                     // 关联的VMA
        .address = address & PAGE_MASK, // 页对齐的地址
        .real_address = address,        // 原始地址
        .flags = flags,                 // 错误标志
        .pgoff = linear_page_index(vma, address), // 在文件中的页偏移
        .gfp_mask = __get_fault_gfp_mask(vma), // 内存分配标志
    };
    struct mm_struct *mm = vma->vm_mm;  // 内存描述符
    vm_flags_t vm_flags = vma->vm_flags; // VMA标志
    pgd_t *pgd;                         // PGD页表项指针
    p4d_t *p4d;                         // P4D页表项指针
    vm_fault_t ret;                     // 返回值

    /* 块2: PGD/P4D级别处理 */
    // 获取PGD页表项(Page Global Directory)
    pgd = pgd_offset(mm, address);
    // 分配P4D页表项(Page 4th Directory),ARM64中通常与PGD合并
    p4d = p4d_alloc(mm, pgd, address);
    if (!p4d)
        return VM_FAULT_OOM;  // 内存不足

    /* 块3: PUD级别处理 */
    // 分配PUD页表项(Page Upper Directory)
    vmf.pud = pud_alloc(mm, p4d, address);
    if (!vmf.pud)
        return VM_FAULT_OOM;  // 内存不足

retry_pud:
    /* 块4: PUD级别大页处理 */
    // 检查PUD是否为空且允许创建PUD级别的大页(1GB大页)
    if (pud_none(*vmf.pud) &&
        thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PUD_ORDER)) {
        // 尝试创建1GB大页
        ret = create_huge_pud(&vmf);
        if (!(ret & VM_FAULT_FALLBACK))
            return ret;  // 成功创建或处理了大页
        // 如果返回FALLBACK,则回退到普通页处理
    } else {
        // PUD已存在,获取当前PUD值
        pud_t orig_pud = *vmf.pud;

        barrier();  // 内存屏障,确保读取的顺序性

        /* 块4.1: 处理现有的PUD大页 */
        if (pud_trans_huge(orig_pud)) {
            /*
             * TODO: 一旦我们支持匿名PUD大页,需要处理NUMA情况和
             * FAULT_FLAG_UNSHARE处理
             */
            
            // 如果是写错误且PUD没有写权限,进行写时复制(COW)
            if ((flags & FAULT_FLAG_WRITE) && !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;  // 成功处理
            }
        }
    }

    /* 块5: PMD级别处理 */
    // 分配PMD页表项(Page Middle Directory)
    vmf.pmd = pmd_alloc(mm, vmf.pud, address);
    if (!vmf.pmd)
        return VM_FAULT_OOM;  // 内存不足

    /* 块6: 检查PUD是否不稳定(同时有其他线程在修改) */
    // PUD大页缺页与pmd_alloc竞争?如果是则重试
    if (pud_trans_unstable(vmf.pud))
        goto retry_pud;

    /* 块7: PMD级别大页处理 */
    // 检查PMD是否为空且允许创建PMD级别的大页(2MB大页)
    if (pmd_none(*vmf.pmd) &&
        thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)) {
        // 尝试创建2MB大页
        ret = create_huge_pmd(&vmf);
        if (!(ret & VM_FAULT_FALLBACK))
            return ret;  // 成功创建或处理了大页
        // 如果返回FALLBACK,则回退到普通页处理
    } else {
        // PMD已存在,获取当前PMD值
        vmf.orig_pmd = pmdp_get_lockless(vmf.pmd);

        /* 块7.1: 处理交换页表项 */
        // 检查是否为交换页表项(页面被换出到磁盘)
        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;  // 等待后重新处理
        }
        
        /* 块7.2: 处理现有的PMD大页 */
        if (pmd_trans_huge(vmf.orig_pmd)) {
            // 检查是否为保护性无访问权限的PMD大页(PROT_NONE)
            if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))
                return do_huge_pmd_numa_page(&vmf);  // NUMA平衡处理
            
            // 如果是写错误或UNSHARE错误且PMD没有写权限
            if ((flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) &&
                !pmd_write(vmf.orig_pmd)) {
                ret = wp_huge_pmd(&vmf);  // 写保护处理(COW)
                if (!(ret & VM_FAULT_FALLBACK))
                    return ret;
            } else {
                // 只是读访问或已有写权限,设置访问标志
                huge_pmd_set_accessed(&vmf);
                return 0;  // 成功处理
            }
        }
    }

    /* 块8: PTE级别处理 */
    // 如果不是大页或大页处理失败,回退到普通页(4KB)处理
    return handle_pte_fault(&vmf);
}

关键概念解释:

1. 页表层级和大小

  • PGD:Page Global Directory,全局页目录
  • P4D:Page 4th Directory,第4级目录(ARM64中通常折叠到PGD)
  • PUD:Page Upper Directory,上层目录,可管理1GB大页
  • PMD:Page Middle Directory,中间目录,可管理2MB大页
  • PTE:Page Table Entry,页表项,管理4KB普通页

2. 大页支持

  • PUD大页:1GB大小,对应PUD_ORDER
  • PMD大页:2MB大小,对应PMD_ORDER
  • 透明大页:由thp_vma_allowable_order()检查是否允许

3. 错误处理模式

  • VM_FAULT_FALLBACK:大页处理失败,回退到普通页处理
  • VM_FAULT_OOM:内存不足
  • VM_FAULT_RETRY:需要重试(如锁竞争)

4. 特殊处理情况

// 交换页表项:页面被换出到磁盘
if (unlikely(is_swap_pmd(vmf.orig_pmd)))

// 迁移项:页面正在迁移中
if (is_pmd_migration_entry(vmf.orig_pmd))

// 保护性无访问权限页(PROT_NONE)
if (pmd_protnone(vmf.orig_pmd) && vma_is_accessible(vma))

// 写时复制(COW)处理
if ((flags & FAULT_FLAG_WRITE) && !pud_write(orig_pud))

5. 并发处理机制

// 内存屏障确保读取顺序
barrier();

// 检查页表项是否在变化中
if (pud_trans_unstable(vmf.pud))

// 无锁获取PMD值(小心使用)
vmf.orig_pmd = pmdp_get_lockless(vmf.pmd);

处理流程总结:

  1. 页表层级遍历:从PGD→P4D→PUD→PMD逐级遍历
  2. 大页优先:尝试使用大页映射(1GB或2MB)
  3. 写时复制:处理写保护页面的COW
  4. 特殊状态处理:处理交换页、迁移页、NUMA平衡
  5. 回退机制:大页失败时回退到普通页处理
  6. 并发安全:使用适当的同步机制处理并发修改

关键设计思想:

  1. 惰性分配:页表只在需要时才分配
  2. 大页优化:优先使用大页减少TLB压力
  3. 写时复制:延迟实际的内存复制直到需要时
  4. 回退机制:大页不可用时优雅降级
  5. 并发安全:正确处理多线程同时缺页的情况

这个函数是Linux内核内存管理的核心之一,展示了现代操作系统如何高效地管理虚拟内存。

Q5

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

	if (unlikely(pmd_none(*vmf->pmd))) {
		/*
		 * 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;
		vmf->flags &= ~FAULT_FLAG_ORIG_PTE_VALID;
	} else {
		pmd_t dummy_pmdval;

		/*
		 * A regular pmd is established and it can't morph into a huge
		 * pmd by anon khugepaged, since that takes mmap_lock in write
		 * mode; but shmem or file collapse to THP could still morph
		 * it into a huge pmd: just retry later if so.
		 *
		 * Use the maywrite version to indicate that vmf->pte may be
		 * modified, but since we will use pte_same() to detect the
		 * change of the !pte_none() entry, there is no need to recheck
		 * the pmdval. Here we chooes to pass a dummy variable instead
		 * of NULL, which helps new user think about why this place is
		 * special.
		 */
		vmf->pte = pte_offset_map_rw_nolock(vmf->vma->vm_mm, vmf->pmd,
						    vmf->address, &dummy_pmdval,
						    &vmf->ptl);
		if (unlikely(!vmf->pte))
			return 0;
		vmf->orig_pte = ptep_get_lockless(vmf->pte);
		vmf->flags |= FAULT_FLAG_ORIG_PTE_VALID;

		if (pte_none(vmf->orig_pte)) {
			pte_unmap(vmf->pte);
			vmf->pte = NULL;
		}
	}

	if (!vmf->pte)
		return do_pte_missing(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);

	spin_lock(vmf->ptl);
	entry = vmf->orig_pte;
	if (unlikely(!pte_same(ptep_get(vmf->pte), entry))) {
		update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
		goto unlock;
	}
	if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
		if (!pte_write(entry))
			return do_wp_page(vmf);
		else if (likely(vmf->flags & FAULT_FLAG_WRITE))
			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_range(vmf, vmf->vma, vmf->address,
				vmf->pte, 1);
	} 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.
		 */
		if (vmf->flags & FAULT_FLAG_WRITE)
			flush_tlb_fix_spurious_fault(vmf->vma, vmf->address,
						     vmf->pte);
	}
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return 0;
}

同上

A5

好的,我来详细注释这个PTE级别的缺页处理函数:

/**
 * handle_pte_fault - 处理PTE级别的页错误
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 处理PTE级别的页错误,包括缺页、交换页、NUMA页、写时复制等
 */
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
    pte_t entry;  // 临时存储PTE值

    /* 块1: 获取PTE指针和原始PTE值 */
    // 检查PMD是否为空(即没有分配PTE表)
    if (unlikely(pmd_none(*vmf->pmd))) {
        /*
         * 将__pte_alloc()延迟到后面:因为vm_ops->fault可能想要分配大页,
         * 如果我们临时暴露页表,那么从并发错误和反向映射查找中撤消将很困难。
         */
        vmf->pte = NULL;  // 标记PTE为空
        vmf->flags &= ~FAULT_FLAG_ORIG_PTE_VALID;  // 清除原始PTE有效标志
    } else {
        pmd_t dummy_pmdval;  // 哑变量,用于接收可能的PMD修改

        /*
         * 一个常规的PMD已经建立,并且它不会因为anon khugepaged而变成大页PMD,
         * 因为这需要以写模式获取mmap_lock;但是shmem或文件折叠到THP仍然可能
         * 将其变成大页PMD:如果是这样,稍后重试。
         *
         * 使用maywrite版本表示vmf->pte可能会被修改,但是因为我们使用pte_same()
         * 来检测非空条目的变化,所以不需要重新检查pmdval。这里我们选择传递一个哑变量
         * 而不是NULL,以帮助新用户思考为什么这个地方是特殊的。
         */
        // 获取PTE指针,同时获取自旋锁(vmf->ptl)
        vmf->pte = pte_offset_map_rw_nolock(vmf->vma->vm_mm, vmf->pmd,
                            vmf->address, &dummy_pmdval,
                            &vmf->ptl);
        if (unlikely(!vmf->pte))
            return 0;  // 获取失败(可能是大页转换中)
        
        // 无锁获取原始的PTE值(小心并发)
        vmf->orig_pte = ptep_get_lockless(vmf->pte);
        vmf->flags |= FAULT_FLAG_ORIG_PTE_VALID;  // 设置原始PTE有效标志

        // 如果PTE为空(没有映射),则解除映射,将vmf->pte设置为NULL
        if (pte_none(vmf->orig_pte)) {
            pte_unmap(vmf->pte);
            vmf->pte = NULL;
        }
    }

    /* 块2: 根据PTE状态分发到不同的处理函数 */
    // 如果没有PTE指针(PMD为空或PTE为空),处理缺页
    if (!vmf->pte)
        return do_pte_missing(vmf);  // 分配新的物理页

    // 如果PTE存在但不是PRESENT状态(被交换到磁盘或其他特殊状态)
    if (!pte_present(vmf->orig_pte))
        return do_swap_page(vmf);  // 处理交换页(从磁盘换入)

    // 如果PTE是保护性的(PROT_NONE)且VMA可访问,处理NUMA页迁移
    if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
        return do_numa_page(vmf);  // NUMA平衡:迁移到本地节点

    /* 块3: 处理存在的PTE(页面已在内存中) */
    // 获取自旋锁(因为前面已经通过pte_offset_map_rw_nolock获取了锁,但这里重新获取以确保同步)
    spin_lock(vmf->ptl);
    
    // 再次获取当前PTE的值,与之前无锁获取的值进行比较,检查是否被并发修改
    entry = vmf->orig_pte;
    if (unlikely(!pte_same(ptep_get(vmf->pte), entry))) {
        // PTE已经被并发修改,更新MMU TLB并跳转到解锁
        update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);
        goto unlock;
    }

    /* 块4: 处理写错误或UNSHARE错误 */
    // 如果是写错误或UNSHARE错误
    if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
        // 如果PTE没有写权限,则进行写时复制(COW)处理
        if (!pte_write(entry))
            return do_wp_page(vmf);  // 写保护页:执行COW
        // 如果有写权限且是写错误,则标记页面为脏
        else if (likely(vmf->flags & FAULT_FLAG_WRITE))
            entry = pte_mkdirty(entry);  // 设置脏位
    }

    /* 块5: 更新访问标志(Accessed位) */
    // 将页面标记为年轻(最近访问)
    entry = pte_mkyoung(entry);
    
    // 尝试原子地更新PTE的访问标志(Accessed位,可能还有Dirty位)
    if (ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry,
                vmf->flags & FAULT_FLAG_WRITE)) {
        // 如果更新成功(标志位确实改变了),则更新MMU缓存
        update_mmu_cache_range(vmf, vmf->vma, vmf->address,
                vmf->pte, 1);
    } else {
        /* 对于重试的页错误,跳过虚假的TLB刷新 */
        if (vmf->flags & FAULT_FLAG_TRIED)
            goto unlock;
        
        /*
         * 这仅用于保护错误,但是架构代码还没有告诉我们这是否是一个保护错误。
         * 这仍然可以避免对于.text页错误的多线程情况下的无用TLB刷新。
         */
        // 如果是写错误,则刷新TLB以处理可能的虚假错误
        if (vmf->flags & FAULT_FLAG_WRITE)
            flush_tlb_fix_spurious_fault(vmf->vma, vmf->address,
                             vmf->pte);
    }

unlock:
    // 解锁并解除PTE映射
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return 0;  // 成功处理
}

关键概念解释:

1. PTE状态分类

// 1. 完全空(无映射)
if (!vmf->pte)                   → do_pte_missing()

// 2. 非PRESENT状态(被交换出)
if (!pte_present(vmf->orig_pte)) → do_swap_page()

// 3. 保护性无访问权限(PROT_NONE)
if (pte_protnone(vmf->orig_pte)) → do_numa_page()

// 4. PRESENT且可访问,但没有写权限的写错误
if (!pte_write(entry))           → do_wp_page()

// 5. PRESENT且有写权限的写错误
else                             → 标记脏位,更新访问标志

2. PTE标志位

// PTE中的标志位(部分)
#define _PAGE_PRESENT   (1UL << 0)   // 页面在内存中
#define _PAGE_RW        (1UL << 1)   // 可写
#define _PAGE_USER      (1UL << 2)   // 用户空间可访问
#define _PAGE_ACCESSED  (1UL << 5)   // 已访问
#define _PAGE_DIRTY     (1UL << 6)   // 脏页(已被修改)
#define _PAGE_PROTNONE  (1UL << 8)   // 保护性无访问权限

3. 处理函数说明

// do_pte_missing: 分配新的物理页
// 情况:首次访问匿名页、文件映射页

// do_swap_page: 从交换空间换入页面
// 情况:页面被换出到磁盘

// do_numa_page: NUMA平衡页面迁移
// 情况:页面在远程NUMA节点,需要迁移到本地

// do_wp_page: 写时复制(Copy-on-Write)
// 情况:写入只读页面(共享的匿名页或文件映射页)

4. 并发处理机制

// 1. 无锁读取原始PTE
vmf->orig_pte = ptep_get_lockless(vmf->pte);

// 2. 获取锁后验证PTE是否被修改
if (unlikely(!pte_same(ptep_get(vmf->pte), entry)))

// 3. 原子更新PTE标志
ptep_set_access_flags(vmf->vma, vmf->address, vmf->pte, entry, ...)

5. TLB管理

// 更新MMU TLB缓存
update_mmu_tlb(vmf->vma, vmf->address, vmf->pte);

// 更新MMU缓存范围
update_mmu_cache_range(vmf, vmf->vma, vmf->address, vmf->pte, 1);

// 刷新虚假TLB错误
flush_tlb_fix_spurious_fault(vmf->vma, vmf->address, vmf->pte);

处理流程总结:

  1. 获取PTE:尝试获取PTE指针,处理PMD为空或PTE为空的特殊情况
  2. 状态检查:根据PTE状态分发到不同的处理函数
  3. 并发验证:获取锁后验证PTE是否被其他线程修改
  4. 写错误处理:处理写时复制(COW)或标记脏页
  5. 访问标志更新:标记页面为已访问,更新硬件PTE
  6. TLB维护:根据需要更新TLB缓存

关键设计思想:

  1. 延迟分配:直到需要时才分配PTE表
  2. 状态驱动:根据PTE状态选择正确的处理路径
  3. 并发安全:使用锁和无锁读取组合处理并发
  4. 性能优化:避免不必要的TLB刷新
  5. NUMA感知:处理跨节点的页面迁移

这个函数是Linux内核缺页处理的最底层核心,直接操作页表项,展示了操作系统如何管理虚拟内存到物理内存的映射关系。

Q6

static inline int ptep_set_access_flags(struct vm_area_struct *vma,
				unsigned long addr, pte_t *ptep,
				pte_t entry, int dirty)
{
	pte_t orig_pte = __ptep_get(ptep);

	entry = pte_mknoncont(entry);

	if (likely(!pte_valid_cont(orig_pte)))
		return __ptep_set_access_flags(vma, addr, ptep, entry, dirty);

	return contpte_ptep_set_access_flags(vma, addr, ptep, entry, dirty);
}
int __ptep_set_access_flags(struct vm_area_struct *vma,
			    unsigned long address, pte_t *ptep,
			    pte_t entry, int dirty)
{
	pteval_t old_pteval, pteval;
	pte_t pte = __ptep_get(ptep);

	if (pte_same(pte, entry))
		return 0;

	/* only preserve the access flags and write permission */
	pte_val(entry) &= PTE_RDONLY | PTE_AF | PTE_WRITE | PTE_DIRTY;

	/*
	 * Setting the flags must be done atomically to avoid racing with the
	 * hardware update of the access/dirty state. The PTE_RDONLY bit must
	 * be set to the most permissive (lowest value) of *ptep and entry
	 * (calculated as: a & b == ~(~a | ~b)).
	 */
	pte_val(entry) ^= PTE_RDONLY;
	pteval = pte_val(pte);
	do {
		old_pteval = pteval;
		pteval ^= PTE_RDONLY;
		pteval |= pte_val(entry);
		pteval ^= PTE_RDONLY;
		pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
	} while (pteval != old_pteval);

	/* Invalidate a stale read-only entry */
	if (dirty)
		flush_tlb_page(vma, address);
	return 1;
}
int contpte_ptep_set_access_flags(struct vm_area_struct *vma,
					unsigned long addr, pte_t *ptep,
					pte_t entry, int dirty)
{
	unsigned long start_addr;
	pte_t orig_pte;
	int i;

	/*
	 * Gather the access/dirty bits for the contiguous range. If nothing has
	 * changed, its a noop.
	 */
	orig_pte = pte_mknoncont(ptep_get(ptep));
	if (pte_val(orig_pte) == pte_val(entry))
		return 0;

	/*
	 * We can fix up access/dirty bits without having to unfold the contig
	 * range. But if the write bit is changing, we must unfold.
	 */
	if (pte_write(orig_pte) == pte_write(entry)) {
		/*
		 * For HW access management, we technically only need to update
		 * the flag on a single pte in the range. But for SW access
		 * management, we need to update all the ptes to prevent extra
		 * faults. Avoid per-page tlb flush in __ptep_set_access_flags()
		 * and instead flush the whole range at the end.
		 */
		ptep = contpte_align_down(ptep);
		start_addr = addr = ALIGN_DOWN(addr, CONT_PTE_SIZE);

		/*
		 * We are not advancing entry because __ptep_set_access_flags()
		 * only consumes access flags from entry. And since we have checked
		 * for the whole contpte block and returned early, pte_same()
		 * within __ptep_set_access_flags() is likely false.
		 */
		for (i = 0; i < CONT_PTES; i++, ptep++, addr += PAGE_SIZE)
			__ptep_set_access_flags(vma, addr, ptep, entry, 0);

		if (dirty)
			__flush_tlb_range(vma, start_addr, addr,
							PAGE_SIZE, true, 3);
	} else {
		__contpte_try_unfold(vma->vm_mm, addr, ptep, orig_pte);
		__ptep_set_access_flags(vma, addr, ptep, entry, dirty);
	}

	return 1;
}

分析怎么处理pte的标识的

A6

我来详细注释这些PTE访问标志设置函数:

/**
 * ptep_set_access_flags - 设置PTE访问标志的入口函数
 * @vma: 虚拟内存区域
 * @addr: 虚拟地址
 * @ptep: PTE指针
 * @entry: 新的PTE值
 * @dirty: 是否设置脏位
 *
 * 根据PTE是否连续,分发给不同的处理函数
 */
static inline int ptep_set_access_flags(struct vm_area_struct *vma,
                unsigned long addr, pte_t *ptep,
                pte_t entry, int dirty)
{
    // 获取当前PTE的值
    pte_t orig_pte = __ptep_get(ptep);
    
    // 将新PTE标记为非连续(确保我们处理的是单个PTE)
    entry = pte_mknoncont(entry);
    
    // 如果原始PTE不是有效的连续PTE,使用普通处理函数
    if (likely(!pte_valid_cont(orig_pte)))
        return __ptep_set_access_flags(vma, addr, ptep, entry, dirty);
    
    // 否则,使用连续PTE处理函数
    return contpte_ptep_set_access_flags(vma, addr, ptep, entry, dirty);
}

/**
 * __ptep_set_access_flags - 处理单个PTE的访问标志设置
 * @vma: 虚拟内存区域
 * @address: 虚拟地址
 * @ptep: PTE指针
 * @entry: 新的PTE值
 * @dirty: 是否设置脏位
 *
 * 原子地更新PTE的访问/脏标志,处理潜在的并发修改
 */
int __ptep_set_access_flags(struct vm_area_struct *vma,
                unsigned long address, pte_t *ptep,
                pte_t entry, int dirty)
{
    pteval_t old_pteval, pteval;
    // 再次获取当前PTE(原子读取)
    pte_t pte = __ptep_get(ptep);
    
    // 如果新旧PTE相同,无需更新
    if (pte_same(pte, entry))
        return 0;
    
    /* 只保留访问标志和写权限(清除其他位)*/
    // 从entry中只保留我们关心的标志位:RDONLY(只读)、AF(访问)、WRITE(写)、DIRTY(脏)
    pte_val(entry) &= PTE_RDONLY | PTE_AF | PTE_WRITE | PTE_DIRTY;
    
    /*
     * 设置标志必须原子地进行,以避免与硬件更新访问/脏状态的竞争。
     * PTE_RDONLY位必须设置为*ptep和entry中最宽松(最低值)的值。
     * 计算方法:a & b == ~(~a | ~b)
     */
    pte_val(entry) ^= PTE_RDONLY;  // 反转RDONLY位,以便后续计算
    
    pteval = pte_val(pte);  // 当前PTE的值
    do {
        old_pteval = pteval;  // 保存旧值用于比较交换
        
        // 反转当前PTE的RDONLY位
        pteval ^= PTE_RDONLY;
        
        // 合并新的标志位
        pteval |= pte_val(entry);
        
        // 再次反转RDONLY位,恢复正确的表示
        pteval ^= PTE_RDONLY;
        
        // 使用比较交换原子更新PTE值
        pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
        // 如果ptep指向的值在更新期间被其他线程修改,循环重试
    } while (pteval != old_pteval);
    
    /* 使陈旧的只读条目无效(刷新TLB)*/
    // 如果设置了脏位,需要刷新TLB,因为写权限可能改变了
    if (dirty)
        flush_tlb_page(vma, address);
    return 1;  // 表示PTE已更新
}

/**
 * contpte_ptep_set_access_flags - 处理连续PTE的访问标志设置
 * @vma: 虚拟内存区域
 * @addr: 虚拟地址
 * @ptep: PTE指针(可能指向连续块中的任意一个PTE)
 * @entry: 新的PTE值
 * @dirty: 是否设置脏位
 *
 * 处理连续PTE(CONT_PTES个连续页面)的访问标志更新
 */
int contpte_ptep_set_access_flags(struct vm_area_struct *vma,
                    unsigned long addr, pte_t *ptep,
                    pte_t entry, int dirty)
{
    unsigned long start_addr;
    pte_t orig_pte;
    int i;
    
    /*
     * 收集连续范围的访问/脏位。如果没有变化,就什么都不做。
     */
    // 获取原始PTE并标记为非连续(用于比较)
    orig_pte = pte_mknoncont(ptep_get(ptep));
    
    // 如果值相同,无需更新
    if (pte_val(orig_pte) == pte_val(entry))
        return 0;
    
    /*
     * 我们可以在不展开连续范围的情况下修复访问/脏位。
     * 但是,如果写位正在改变,我们必须展开连续块。
     */
    if (pte_write(orig_pte) == pte_write(entry)) {
        /*
         * 对于硬件访问管理,技术上我们只需要更新范围内的一个PTE的标志。
         * 但对于软件访问管理,我们需要更新所有PTE以防止额外的错误。
         * 避免在__ptep_set_access_flags()中进行逐页TLB刷新,
         * 而是在最后刷新整个范围。
         */
        
        // 对齐到连续PTE块的起始地址
        ptep = contpte_align_down(ptep);
        start_addr = addr = ALIGN_DOWN(addr, CONT_PTE_SIZE);
        
        /*
         * 我们不推进entry,因为__ptep_set_access_flags()只从entry中消耗访问标志。
         * 由于我们已经检查了整个contpte块并提前返回,因此__ptep_set_access_flags()
         * 中的pte_same()很可能为false。
         */
        
        // 更新连续块中的所有PTE
        for (i = 0; i < CONT_PTES; i++, ptep++, addr += PAGE_SIZE)
            __ptep_set_access_flags(vma, addr, ptep, entry, 0);  // 传递dirty=0,稍后统一刷新
        
        // 如果需要设置脏位,刷新整个范围的TLB
        if (dirty)
            __flush_tlb_range(vma, start_addr, addr,
                            PAGE_SIZE, true, 3);
    } else {
        // 写位改变,必须展开连续块
        __contpte_try_unfold(vma->vm_mm, addr, ptep, orig_pte);
        // 然后更新单个PTE
        __ptep_set_access_flags(vma, addr, ptep, entry, dirty);
    }
    
    return 1;  // 表示PTE已更新
}

关键概念解释:

1. 连续PTE(Contiguous PTE)

  • CONT_PTES:连续PTE块中的PTE数量(ARM64通常是16个,64KB块)
  • 优势:减少TLB压力,提高大块内存访问性能
  • 展开(Unfold):当连续块的属性不一致时需要拆分为单个PTE

2. PTE标志位宏定义(ARM64示例)

#define PTE_VALID       (_AT(pteval_t, 1) << 0)      // PTE有效
#define PTE_WRITE       (_AT(pteval_t, 1) << 1)      // 可写
#define PTE_DIRTY       (_AT(pteval_t, 1) << 2)      // 脏页
#define PTE_AF          (_AT(pteval_t, 1) << 3)      // 已访问(Accessed)
#define PTE_NG          (_AT(pteval_t, 1) << 4)      // 非全局
#define PTE_CONT        (_AT(pteval_t, 1) << 5)      // 连续PTE
#define PTE_PXN         (_AT(pteval_t, 1) << 6)      // 特权执行从不
#define PTE_UXN         (_AT(pteval_t, 1) << 7)      // 用户执行从不
#define PTE_RDONLY      (_AT(pteval_t, 1) << 8)      // 只读(软件位)

3. 原子更新机制

// 使用比较交换(Compare-and-Swap)原子更新
do {
    old_pteval = pteval;
    // ... 计算新值 ...
    pteval = cmpxchg_relaxed(&pte_val(*ptep), old_pteval, pteval);
} while (pteval != old_pteval);

// 这是经典的"读取-修改-写入"循环模式

4. TLB刷新策略

// 单个页面刷新
flush_tlb_page(vma, address);

// 范围刷新(更高效)
__flush_tlb_range(vma, start_addr, addr, PAGE_SIZE, true, 3);

5. RDONLY位的特殊处理

// RDONLY位是软件位,不是硬件位
// 算法确保设置最宽松的权限:
// 1. 反转entry的RDONLY位:entry ^= PTE_RDONLY
// 2. 反转当前PTE的RDONLY位:pteval ^= PTE_RDONLY
// 3. 合并:pteval |= entry
// 4. 再次反转:pteval ^= PTE_RDONLY

// 这样保证如果任何一个PTE是可写的,整个操作结果就是可写的

处理流程总结:

单个PTE处理:

  1. 检查必要性:如果新旧PTE相同,直接返回
  2. 提取标志:只保留关心的标志位
  3. 原子更新:使用CAS循环原子更新PTE值
  4. TLB刷新:如果设置了脏位,刷新对应页面的TLB

连续PTE处理:

  1. 检查变化:如果整个块没有变化,直接返回
  2. 写位检查
    • 如果写位不变:批量更新所有PTE,最后刷新整个范围TLB
    • 如果写位改变:展开连续块,然后按单个PTE处理

关键设计思想:

  1. 原子性:使用CAS确保PTE更新在并发情况下的正确性
  2. 性能优化
    • 连续PTE的批量处理减少TLB刷新次数
    • 避免不必要的TLB刷新
  3. 兼容性:处理硬件与软件管理标志的差异
  4. 选择性展开:只有在必要(写权限改变)时才展开连续块
  5. 分层设计:ptep_set_access_flags作为分发器,隐藏实现细节

这些函数展示了Linux内核如何高效管理页表标志位,特别是在ARM64连续PTE特性下的优化处理。

Q7

static vm_fault_t do_pte_missing(struct vm_fault *vmf)
{
	if (vma_is_anonymous(vmf->vma))
		return do_anonymous_page(vmf);
	else
		return do_fault(vmf);
}
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
	return !vma->vm_ops;
}
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	unsigned long addr = vmf->address;
	struct folio *folio;
	vm_fault_t ret = 0;
	int nr_pages = 1;
	pte_t entry;

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

	/*
	 * Use pte_alloc() instead of pte_alloc_map(), so that OOM can
	 * be distinguished from a transient failure of pte_offset_map().
	 */
	if (pte_alloc(vma->vm_mm, vmf->pmd))
		return VM_FAULT_OOM;

	/* 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 (!vmf->pte)
			goto unlock;
		if (vmf_pte_changed(vmf)) {
			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. */
	ret = vmf_anon_prepare(vmf);
	if (ret)
		return ret;
	/* Returns NULL on OOM or ERR_PTR(-EAGAIN) if we must retry the fault */
	folio = alloc_anon_folio(vmf);
	if (IS_ERR(folio))
		return 0;
	if (!folio)
		goto oom;

	nr_pages = folio_nr_pages(folio);
	addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);

	/*
	 * The memory barrier inside __folio_mark_uptodate makes sure that
	 * preceding stores to the page contents become visible before
	 * the set_pte_at() write.
	 */
	__folio_mark_uptodate(folio);

	entry = folio_mk_pte(folio, vma->vm_page_prot);
	entry = pte_sw_mkyoung(entry);
	if (vma->vm_flags & VM_WRITE)
		entry = pte_mkwrite(pte_mkdirty(entry), vma);

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, addr, &vmf->ptl);
	if (!vmf->pte)
		goto release;
	if (nr_pages == 1 && vmf_pte_changed(vmf)) {
		update_mmu_tlb(vma, addr, vmf->pte);
		goto release;
	} else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
		update_mmu_tlb_range(vma, addr, vmf->pte, nr_pages);
		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);
		folio_put(folio);
		return handle_userfault(vmf, VM_UFFD_MISSING);
	}

	folio_ref_add(folio, nr_pages - 1);
	add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
	count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC);
	folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
	folio_add_lru_vma(folio, vma);
setpte:
	if (vmf_orig_pte_uffd_wp(vmf))
		entry = pte_mkuffd_wp(entry);
	set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);

	/* No need to invalidate - it was non-present before */
	update_mmu_cache_range(vmf, vma, addr, vmf->pte, nr_pages);
unlock:
	if (vmf->pte)
		pte_unmap_unlock(vmf->pte, vmf->ptl);
	return ret;
release:
	folio_put(folio);
	goto unlock;
oom:
	return VM_FAULT_OOM;
}

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
	 */
	if (!vma->vm_ops->fault) {
		vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
					       vmf->address, &vmf->ptl);
		if (unlikely(!vmf->pte))
			ret = VM_FAULT_SIGBUS;
		else {
			/*
			 * 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.
			 */
			if (unlikely(pte_none(ptep_get(vmf->pte))))
				ret = VM_FAULT_SIGBUS;
			else
				ret = VM_FAULT_NOPAGE;

			pte_unmap_unlock(vmf->pte, vmf->ptl);
		}
	} else if (!(vmf->flags & FAULT_FLAG_WRITE))
		ret = do_read_fault(vmf);
	else if (!(vma->vm_flags & VM_SHARED))
		ret = do_cow_fault(vmf);
	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;
}

分析

A7

我来详细注释这两个函数,它们是缺页处理的核心部分:

/**
 * do_pte_missing - 处理PTE缺失(无映射)的情况
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 当PTE为空(无映射)时,根据VMA类型分发到不同的处理函数
 */
static vm_fault_t do_pte_missing(struct vm_fault *vmf)
{
    // 判断是否为匿名映射(没有关联的文件操作)
    if (vma_is_anonymous(vmf->vma))
        return do_anonymous_page(vmf);  // 处理匿名页缺页
    else
        return do_fault(vmf);           // 处理文件映射缺页
}

/**
 * vma_is_anonymous - 判断VMA是否为匿名映射
 * @vma: 虚拟内存区域
 *
 * 匿名映射没有关联的vm_ops(虚拟内存操作)
 * 常见匿名映射:堆、栈、共享内存(匿名)
 */
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
    return !vma->vm_ops;  // 没有文件操作表示匿名映射
}

/**
 * do_anonymous_page - 处理匿名页面缺页
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 处理匿名内存(堆、栈等)的缺页异常
 * 两种情况:1. 读缺页使用零页面 2. 写缺页分配新页面
 */
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    unsigned long addr = vmf->address;
    struct folio *folio;      // 内存页的数据结构
    vm_fault_t ret = 0;       // 返回值
    int nr_pages = 1;         // 页面数量(可能为大页)
    pte_t entry;              // 要设置的PTE值

    /* 文件映射但没有 ->vm_ops?这种情况不应该发生 */
    // 如果是共享映射(VM_SHARED)但没有vm_ops,返回总线错误
    if (vma->vm_flags & VM_SHARED)
        return VM_FAULT_SIGBUS;

    /*
     * 使用pte_alloc()而不是pte_alloc_map(),以便区分OOM
     * 和pte_offset_map()的临时失败。
     */
    // 分配PTE表(如果需要)
    if (pte_alloc(vma->vm_mm, vmf->pmd))
        return VM_FAULT_OOM;  // 内存不足

    /* 块1: 读缺页处理 - 使用零页面 */
    // 如果是读错误且允许使用零页面
    if (!(vmf->flags & FAULT_FLAG_WRITE) &&
            !mm_forbids_zeropage(vma->vm_mm)) {
        // 创建指向零页面的特殊PTE
        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 (!vmf->pte)
            goto unlock;
        
        // 检查PTE是否被并发修改
        if (vmf_pte_changed(vmf)) {
            update_mmu_tlb(vma, vmf->address, vmf->pte);
            goto unlock;
        }
        
        // 检查地址空间是否稳定(如mmap序列号)
        ret = check_stable_address_space(vma->vm_mm);
        if (ret)
            goto unlock;
        
        /* 将缺页传递给用户空间,在PT锁内检查 */
        // 如果使用userfaultfd,则让用户空间处理缺页
        if (userfaultfd_missing(vma)) {
            pte_unmap_unlock(vmf->pte, vmf->ptl);
            return handle_userfault(vmf, VM_UFFD_MISSING);
        }
        goto setpte;  // 跳转到设置PTE
    }

    /* 块2: 写缺页处理 - 分配私有页面 */
    // 准备匿名页面分配(检查资源限制等)
    ret = vmf_anon_prepare(vmf);
    if (ret)
        return ret;
    
    /* 分配我们自己的私有页面。 */
    // 分配匿名页面:成功返回folio,NULL表示OOM,ERR_PTR(-EAGAIN)表示需要重试
    folio = alloc_anon_folio(vmf);
    if (IS_ERR(folio))
        return 0;  // 重试错误
    if (!folio)
        goto oom;  // 内存不足

    // 获取folio包含的页面数(可能是大页)
    nr_pages = folio_nr_pages(folio);
    // 地址对齐到大页边界
    addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);

    /*
     * __folio_mark_uptodate中的内存屏障确保在set_pte_at()写入之前,
     * 对页面内容的先前存储变得可见。
     */
    // 标记页面内容已就绪(用于文件映射的预读优化)
    __folio_mark_uptodate(folio);

    // 创建PTE条目,包含年轻标志(Accessed位)
    entry = folio_mk_pte(folio, vma->vm_page_prot);
    entry = pte_sw_mkyoung(entry);
    
    // 如果VMA有写权限,设置PTE的写标志和脏标志
    if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry), vma);

    // 获取PTE指针并锁定
    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, addr, &vmf->ptl);
    if (!vmf->pte)
        goto release;
    
    // 检查PTE是否被并发修改(单个页面情况)
    if (nr_pages == 1 && vmf_pte_changed(vmf)) {
        update_mmu_tlb(vma, addr, vmf->pte);
        goto release;
    } 
    // 检查大页范围的PTE是否被并发修改
    else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
        update_mmu_tlb_range(vma, addr, vmf->pte, nr_pages);
        goto release;
    }

    // 再次检查地址空间稳定性
    ret = check_stable_address_space(vma->vm_mm);
    if (ret)
        goto release;

    /* 将缺页传递给用户空间,在PT锁内检查 */
    if (userfaultfd_missing(vma)) {
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        folio_put(folio);
        return handle_userfault(vmf, VM_UFFD_MISSING);
    }

    // 增加folio引用计数(减去已有一个引用)
    folio_ref_add(folio, nr_pages - 1);
    // 增加匿名页面计数器
    add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
    // 统计透明大页分配
    count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC);
    // 添加反向映射(rmap)条目,标记为独占(无共享)
    folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);
    // 将页面添加到LRU链表
    folio_add_lru_vma(folio, vma);

setpte:
    // 如果原始PTE有userfaultfd写保护标志,则设置新PTE的uffd_wp标志
    if (vmf_orig_pte_uffd_wp(vmf))
        entry = pte_mkuffd_wp(entry);
    
    // 设置PTE(可能设置多个页面)
    set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);

    /* 无需无效化TLB - 它之前不存在 */
    // 更新MMU缓存
    update_mmu_cache_range(vmf, vma, addr, vmf->pte, nr_pages);
    
unlock:
    // 解锁并解除PTE映射
    if (vmf->pte)
        pte_unmap_unlock(vmf->pte, vmf->ptl);
    return ret;

release:
    // 释放folio并跳转到解锁
    folio_put(folio);
    goto unlock;

oom:
    return VM_FAULT_OOM;  // 内存不足
}

/**
 * do_fault - 处理文件映射缺页
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 处理文件映射内存(mmap文件)的缺页异常
 * 三种情况:读缺页、私有写缺页(COW)、共享写缺页
 */
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;

    /*
     * VMA在mmap()时未完全填充或缺少VM_DONTEXPAND
     */
    // 如果没有定义fault操作,说明VMA不支持缺页处理
    if (!vma->vm_ops->fault) {
        vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
                           vmf->address, &vmf->ptl);
        if (unlikely(!vmf->pte))
            ret = VM_FAULT_SIGBUS;  // 获取PTE失败,返回总线错误
        else {
            /*
             * 确保这不是PTE的临时清除。通过持有ptl并再次检查。
             * PTE的读/修改/写更新涉及:获取ptl,清除PTE以便
             * 我们没有硬件的并发修改,然后进行更新。
             */
            if (unlikely(pte_none(ptep_get(vmf->pte))))
                ret = VM_FAULT_SIGBUS;  // PTE仍然为空,总线错误
            else
                ret = VM_FAULT_NOPAGE;  // PTE存在但不是PRESENT状态

            pte_unmap_unlock(vmf->pte, vmf->ptl);
        }
    } 
    // 如果是读错误
    else if (!(vmf->flags & FAULT_FLAG_WRITE))
        ret = do_read_fault(vmf);       // 读缺页:从文件读取
    // 如果是私有映射的写错误(VM_SHARED未设置)
    else if (!(vma->vm_flags & VM_SHARED))
        ret = do_cow_fault(vmf);        // 写时复制:分配新页面并复制内容
    // 如果是共享映射的写错误
    else
        ret = do_shared_fault(vmf);     // 共享写:更新文件并保持共享

    /* 预分配的页表未使用:释放它 */
    if (vmf->prealloc_pte) {
        pte_free(vm_mm, vmf->prealloc_pte);
        vmf->prealloc_pte = NULL;
    }
    return ret;
}

关键概念解释:

1. 匿名映射 vs 文件映射

// 匿名映射:没有关联的文件
// - 堆(malloc)、栈、匿名共享内存
// - 缺页时:读使用零页面,写分配新页面

// 文件映射:关联到文件(mmap)
// - 可执行文件、共享库、文件IO
// - 缺页时:从文件读取数据

2. 零页面(Zero Page)

  • 特殊的只读页面,所有字节为0
  • 用于匿名映射的读缺页,避免分配物理内存
  • 当写入时触发COW,分配真正的物理页面
  • 节省内存,因为许多初始化为0的页面可以共享同一个零页面

3. 三种文件映射缺页类型

// 1. 读缺页(do_read_fault)
//    - 从文件读取数据到页面
//    - 设置页面为只读(如果文件是只读的)

// 2. 私有写缺页(do_cow_fault)
//    - 写时复制:分配新页面,复制文件数据
//    - 新页面可写,与原文件解耦
//    - 用于私有映射(如可执行文件的.data段)

// 3. 共享写缺页(do_shared_fault)
//    - 直接修改文件映射的页面
//    - 可能写回文件(取决于同步策略)
//    - 用于共享映射(如进程间通信)

4. userfaultfd机制

  • 用户空间缺页处理框架
  • 允许用户空间程序处理自己的缺页异常
  • 用于:检查点/恢复、内存迁移、调试等
  • 当检测到userfaultfd时,将缺页传递给用户空间处理

5. 透明大页(Transparent Huge Page)

// 检查是否允许分配大页
thp_vma_allowable_order(vma, vm_flags, TVA_PAGEFAULT, PMD_ORDER)

// 统计大页分配
count_mthp_stat(folio_order(folio), MTHP_STAT_ANON_FAULT_ALLOC)

6. 内存管理关键操作

// 分配匿名页面
folio = alloc_anon_folio(vmf);

// 添加反向映射(rmap)
folio_add_new_anon_rmap(folio, vma, addr, RMAP_EXCLUSIVE);

// 添加到LRU链表
folio_add_lru_vma(folio, vma);

// 设置PTE(可能多个页面)
set_ptes(vma->vm_mm, addr, vmf->pte, entry, nr_pages);

处理流程总结:

匿名映射缺页:

  1. 读缺页:映射到共享的零页面,节省内存
  2. 写缺页:分配新的物理页面,建立映射

文件映射缺页:

  1. 检查VMA有效性:是否有fault操作
  2. 读错误:从文件读取数据到页面
  3. 私有写错误:写时复制,分配新页面并复制数据
  4. 共享写错误:直接修改共享页面,可能写回文件

关键设计思想:

  1. 惰性分配:物理内存只在需要时才分配
  2. 写时复制:延迟实际的内存复制直到写入时
  3. 零页面优化:共享零页面节省内存
  4. 大页支持:透明地使用大页提高性能
  5. 用户空间处理:支持userfaultfd让用户空间参与缺页处理
  6. 并发安全:使用锁和原子操作处理并发缺页

这些函数展示了Linux内核如何高效地处理不同类型的内存缺页,平衡性能和内存使用。

Q8

static vm_fault_t do_wp_page(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
	struct vm_area_struct *vma = vmf->vma;
	struct folio *folio = NULL;
	pte_t pte;

	if (likely(!unshare)) {
		if (userfaultfd_pte_wp(vma, ptep_get(vmf->pte))) {
			if (!userfaultfd_wp_async(vma)) {
				pte_unmap_unlock(vmf->pte, vmf->ptl);
				return handle_userfault(vmf, VM_UFFD_WP);
			}

			/*
			 * Nothing needed (cache flush, TLB invalidations,
			 * etc.) because we're only removing the uffd-wp bit,
			 * which is completely invisible to the user.
			 */
			pte = pte_clear_uffd_wp(ptep_get(vmf->pte));

			set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
			/*
			 * Update this to be prepared for following up CoW
			 * handling
			 */
			vmf->orig_pte = pte;
		}

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

	vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);

	if (vmf->page)
		folio = page_folio(vmf->page);

	/*
	 * Shared mapping: we are guaranteed to have VM_WRITE and
	 * FAULT_FLAG_WRITE set at this point.
	 */
	if (vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) {
		/*
		 * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
		 * VM_PFNMAP VMA. FS DAX also wants ops->pfn_mkwrite called.
		 *
		 * We should not cow pages in a shared writeable mapping.
		 * Just mark the pages writable and/or call ops->pfn_mkwrite.
		 */
		if (!vmf->page || is_fsdax_page(vmf->page)) {
			vmf->page = NULL;
			return wp_pfn_shared(vmf);
		}
		return wp_page_shared(vmf, folio);
	}

	/*
	 * Private mapping: create an exclusive anonymous page copy if reuse
	 * is impossible. We might miss VM_WRITE for FOLL_FORCE handling.
	 *
	 * If we encounter a page that is marked exclusive, we must reuse
	 * the page without further checks.
	 */
	if (folio && folio_test_anon(folio) &&
	    (PageAnonExclusive(vmf->page) || wp_can_reuse_anon_folio(folio, vma))) {
		if (!PageAnonExclusive(vmf->page))
			SetPageAnonExclusive(vmf->page);
		if (unlikely(unshare)) {
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			return 0;
		}
		wp_page_reuse(vmf, folio);
		return 0;
	}
	/*
	 * Ok, we need to copy. Oh, well..
	 */
	if (folio)
		folio_get(folio);

	pte_unmap_unlock(vmf->pte, vmf->ptl);
#ifdef CONFIG_KSM
	if (folio && folio_test_ksm(folio))
		count_vm_event(COW_KSM);
#endif
	return wp_page_copy(vmf);
}
static vm_fault_t wp_pfn_shared(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;

	if (vma->vm_ops && vma->vm_ops->pfn_mkwrite) {
		vm_fault_t ret;

		pte_unmap_unlock(vmf->pte, vmf->ptl);
		ret = vmf_can_call_fault(vmf);
		if (ret)
			return ret;

		vmf->flags |= FAULT_FLAG_MKWRITE;
		ret = vma->vm_ops->pfn_mkwrite(vmf);
		if (ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))
			return ret;
		return finish_mkwrite_fault(vmf, NULL);
	}
	wp_page_reuse(vmf, NULL);
	return 0;
}
static inline void wp_page_reuse(struct vm_fault *vmf, struct folio *folio)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;
	pte_t entry;

	VM_BUG_ON(!(vmf->flags & FAULT_FLAG_WRITE));
	VM_WARN_ON(is_zero_pfn(pte_pfn(vmf->orig_pte)));

	if (folio) {
		VM_BUG_ON(folio_test_anon(folio) &&
			  !PageAnonExclusive(vmf->page));
		/*
		 * Clear the folio's cpupid information as the existing
		 * information potentially belongs to a now completely
		 * unrelated process.
		 */
		folio_xchg_last_cpupid(folio, (1 << LAST_CPUPID_SHIFT) - 1);
	}

	flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
	entry = pte_mkyoung(vmf->orig_pte);
	entry = maybe_mkwrite(pte_mkdirty(entry), vma);
	if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
		update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	count_vm_event(PGREUSE);
}
static vm_fault_t wp_page_shared(struct vm_fault *vmf, struct folio *folio)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret = 0;

	folio_get(folio);

	if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
		vm_fault_t tmp;

		pte_unmap_unlock(vmf->pte, vmf->ptl);
		tmp = vmf_can_call_fault(vmf);
		if (tmp) {
			folio_put(folio);
			return tmp;
		}

		tmp = do_page_mkwrite(vmf, folio);
		if (unlikely(!tmp || (tmp &
				      (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
			folio_put(folio);
			return tmp;
		}
		tmp = finish_mkwrite_fault(vmf, folio);
		if (unlikely(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {
			folio_unlock(folio);
			folio_put(folio);
			return tmp;
		}
	} else {
		wp_page_reuse(vmf, folio);
		folio_lock(folio);
	}
	ret |= fault_dirty_shared_page(vmf);
	folio_put(folio);

	return ret;
}
/*
 * Handle the case of a page which we actually need to copy to a new page,
 * either due to COW or unsharing.
 *
 * Called with mmap_lock locked and the old page referenced, but
 * without the ptl held.
 *
 * High level logic flow:
 *
 * - Allocate a page, copy the content of the old page to the new one.
 * - Handle book keeping and accounting - cgroups, mmu-notifiers, etc.
 * - Take the PTL. If the pte changed, bail out and release the allocated page
 * - If the pte is still the way we remember it, update the page table and all
 *   relevant references. This includes dropping the reference the page-table
 *   held to the old page, as well as updating the rmap.
 * - In any case, unlock the PTL and drop the reference we took to the old page.
 */
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
	const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
	struct vm_area_struct *vma = vmf->vma;
	struct mm_struct *mm = vma->vm_mm;
	struct folio *old_folio = NULL;
	struct folio *new_folio = NULL;
	pte_t entry;
	int page_copied = 0;
	struct mmu_notifier_range range;
	vm_fault_t ret;
	bool pfn_is_zero;

	delayacct_wpcopy_start();

	if (vmf->page)
		old_folio = page_folio(vmf->page);
	ret = vmf_anon_prepare(vmf);
	if (unlikely(ret))
		goto out;

	pfn_is_zero = is_zero_pfn(pte_pfn(vmf->orig_pte));
	new_folio = folio_prealloc(mm, vma, vmf->address, pfn_is_zero);
	if (!new_folio)
		goto oom;

	if (!pfn_is_zero) {
		int err;

		err = __wp_page_copy_user(&new_folio->page, vmf->page, vmf);
		if (err) {
			/*
			 * 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.
			 */
			folio_put(new_folio);
			if (old_folio)
				folio_put(old_folio);

			delayacct_wpcopy_end();
			return err == -EHWPOISON ? VM_FAULT_HWPOISON : 0;
		}
		kmsan_copy_page_meta(&new_folio->page, vmf->page);
	}

	__folio_mark_uptodate(new_folio);

	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, 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
	 */
	vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
	if (likely(vmf->pte && pte_same(ptep_get(vmf->pte), vmf->orig_pte))) {
		if (old_folio) {
			if (!folio_test_anon(old_folio)) {
				dec_mm_counter(mm, mm_counter_file(old_folio));
				inc_mm_counter(mm, MM_ANONPAGES);
			}
		} else {
			ksm_might_unmap_zero_page(mm, vmf->orig_pte);
			inc_mm_counter(mm, MM_ANONPAGES);
		}
		flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
		entry = folio_mk_pte(new_folio, vma->vm_page_prot);
		entry = pte_sw_mkyoung(entry);
		if (unlikely(unshare)) {
			if (pte_soft_dirty(vmf->orig_pte))
				entry = pte_mksoft_dirty(entry);
			if (pte_uffd_wp(vmf->orig_pte))
				entry = pte_mkuffd_wp(entry);
		} else {
			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(vma, vmf->address, vmf->pte);
		folio_add_new_anon_rmap(new_folio, vma, vmf->address, RMAP_EXCLUSIVE);
		folio_add_lru_vma(new_folio, vma);
		BUG_ON(unshare && pte_write(entry));
		set_pte_at(mm, vmf->address, vmf->pte, entry);
		update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
		if (old_folio) {
			/*
			 * 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
			 * folio_remove_rmap_pte() with the ptp_clear_flush
			 * above. Those stores are ordered by (if nothing else,)
			 * the barrier present in the atomic_add_negative
			 * in folio_remove_rmap_pte();
			 *
			 * 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.
			 */
			folio_remove_rmap_pte(old_folio, vmf->page, vma);
		}

		/* Free the old page.. */
		new_folio = old_folio;
		page_copied = 1;
		pte_unmap_unlock(vmf->pte, vmf->ptl);
	} else if (vmf->pte) {
		update_mmu_tlb(vma, vmf->address, vmf->pte);
		pte_unmap_unlock(vmf->pte, vmf->ptl);
	}

	mmu_notifier_invalidate_range_end(&range);

	if (new_folio)
		folio_put(new_folio);
	if (old_folio) {
		if (page_copied)
			free_swap_cache(old_folio);
		folio_put(old_folio);
	}

	delayacct_wpcopy_end();
	return 0;
oom:
	ret = VM_FAULT_OOM;
out:
	if (old_folio)
		folio_put(old_folio);

	delayacct_wpcopy_end();
	return ret;
}

A8

好的,我来详细注释这个复杂的写时复制(COW)处理函数:

/**
 * do_wp_page - 处理写保护页错误(写时复制)
 * @vmf: 包含错误信息的vm_fault结构体
 * 
 * 处理写保护页面的缺页异常,包括COW和UNSHARE操作
 */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
    __releases(vmf->ptl)  // 函数会释放ptl锁
{
    const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;  // 是否是UNSHARE操作
    struct vm_area_struct *vma = vmf->vma;  // 虚拟内存区域
    struct folio *folio = NULL;  // 内存页数据结构
    pte_t pte;  // 临时PTE值

    /* 块1: userfaultfd写保护处理 */
    if (likely(!unshare)) {  // 如果不是UNSHARE操作
        // 检查原始PTE是否有userfaultfd写保护标志
        if (userfaultfd_pte_wp(vma, ptep_get(vmf->pte))) {
            // 如果不是异步模式,需要用户空间处理
            if (!userfaultfd_wp_async(vma)) {
                pte_unmap_unlock(vmf->pte, vmf->ptl);
                return handle_userfault(vmf, VM_UFFD_WP);
            }

            /*
             * 不需要做任何事(缓存刷新、TLB无效等),因为我们只是移除uffd-wp位,
             * 这对用户是完全不可见的。
             */
            // 清除uffd写保护标志
            pte = pte_clear_uffd_wp(ptep_get(vmf->pte));
            // 更新PTE
            set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
            /*
             * 更新orig_pte,为后续的COW处理做准备
             */
            vmf->orig_pte = pte;
        }

        /*
         * Userfaultfd写保护可能延迟刷新。确保在复制之前TLB被刷新。
         */
        if (unlikely(userfaultfd_wp(vmf->vma) &&
                 mm_tlb_flush_pending(vmf->vma->vm_mm)))
            flush_tlb_page(vmf->vma, vmf->address);  // 刷新单个页面的TLB
    }

    /* 块2: 获取对应的物理页 */
    // 从PTE获取对应的物理页(如果是正常映射)
    vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
    // 如果有物理页,获取对应的folio
    if (vmf->page)
        folio = page_folio(vmf->page);

    /*
     * 共享映射:此时我们保证有VM_WRITE和FAULT_FLAG_WRITE设置
     */
    // 检查是否是共享映射
    if (vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) {
        /*
         * VM_MIXEDMAP !pfn_valid()情况,或者在VM_PFNMAP VMA上清除VM_SOFTDIRTY。
         * FS DAX也希望调用ops->pfn_mkwrite。
         *
         * 我们不应该在共享可写映射中COW页面。
         * 只需标记页面可写和/或调用ops->pfn_mkwrite。
         */
        // 如果没有物理页或是FS DAX页面,处理特殊情况
        if (!vmf->page || is_fsdax_page(vmf->page)) {
            vmf->page = NULL;  // 清空page指针
            return wp_pfn_shared(vmf);  // 处理PFN特殊映射
        }
        // 处理普通的共享页面COW
        return wp_page_shared(vmf, folio);
    }

    /*
     * 私有映射:如果无法重用,则创建独占的匿名页面副本。
     * 我们可能因为FOLL_FORCE处理而缺少VM_WRITE。
     *
     * 如果我们遇到标记为独占的页面,我们必须重用该页面而不进行进一步检查。
     */
    // 检查是否可以重用匿名页面(是否独占)
    if (folio && folio_test_anon(folio) &&
        (PageAnonExclusive(vmf->page) || wp_can_reuse_anon_folio(folio, vma))) {
        // 如果页面还不是独占的,设置为独占
        if (!PageAnonExclusive(vmf->page))
            SetPageAnonExclusive(vmf->page);
        // 如果是UNSHARE操作,直接返回
        if (unlikely(unshare)) {
            pte_unmap_unlock(vmf->pte, vmf->ptl);
            return 0;
        }
        // 重用页面:直接修改PTE标志位
        wp_page_reuse(vmf, folio);
        return 0;
    }
    
    /*
     * 好的,我们需要复制。唉,好吧...
     */
    // 增加页面的引用计数
    if (folio)
        folio_get(folio);
    
    // 释放页表锁
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    
#ifdef CONFIG_KSM
    // 如果启用了KSM,统计KSM COW事件
    if (folio && folio_test_ksm(folio))
        count_vm_event(COW_KSM);
#endif
    
    // 执行页面复制
    return wp_page_copy(vmf);
}

/**
 * wp_pfn_shared - 处理共享PFN映射的写保护
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 处理没有实际页面结构(只有PFN)的共享映射写保护
 */
static vm_fault_t wp_pfn_shared(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;

    // 如果定义了pfn_mkwrite操作,调用它
    if (vma->vm_ops && vma->vm_ops->pfn_mkwrite) {
        vm_fault_t ret;
        
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        // 检查是否可以调用fault操作
        ret = vmf_can_call_fault(vmf);
        if (ret)
            return ret;
        
        // 设置MKWRITE标志,调用pfn_mkwrite
        vmf->flags |= FAULT_FLAG_MKWRITE;
        ret = vma->vm_ops->pfn_mkwrite(vmf);
        if (ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))
            return ret;
        // 完成mkwrite操作
        return finish_mkwrite_fault(vmf, NULL);
    }
    // 如果没有pfn_mkwrite操作,直接重用页面
    wp_page_reuse(vmf, NULL);
    return 0;
}

/**
 * wp_page_reuse - 重用现有页面(不复制)
 * @vmf: 包含错误信息的vm_fault结构体
 * @folio: 要重用的folio
 *
 * 直接修改PTE的写权限位,而不进行页面复制
 */
static inline void wp_page_reuse(struct vm_fault *vmf, struct folio *folio)
    __releases(vmf->ptl)  // 函数会释放ptl锁
{
    struct vm_area_struct *vma = vmf->vma;
    pte_t entry;

    // 调试断言:必须是写错误
    VM_BUG_ON(!(vmf->flags & FAULT_FLAG_WRITE));
    // 警告:不能是零页面
    VM_WARN_ON(is_zero_pfn(pte_pfn(vmf->orig_pte)));

    if (folio) {
        // 调试断言:如果是匿名页,必须是独占的
        VM_BUG_ON(folio_test_anon(folio) &&
                  !PageAnonExclusive(vmf->page));
        /*
         * 清除folio的cpupid信息,因为现有的信息可能属于一个现在完全无关的进程。
         */
        folio_xchg_last_cpupid(folio, (1 << LAST_CPUPID_SHIFT) - 1);
    }

    // 刷新缓存
    flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
    // 设置年轻标志和脏标志
    entry = pte_mkyoung(vmf->orig_pte);
    entry = maybe_mkwrite(pte_mkdirty(entry), vma);
    
    // 原子更新PTE访问标志
    if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
        update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
    
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    count_vm_event(PGREUSE);  // 统计页面重用事件
}

/**
 * wp_page_shared - 处理共享页面的写保护
 * @vmf: 包含错误信息的vm_fault结构体
 * @folio: 要处理的folio
 *
 * 处理共享映射页面的COW,可能调用page_mkwrite操作
 */
static vm_fault_t wp_page_shared(struct vm_fault *vmf, struct folio *folio)
    __releases(vmf->ptl)  // 函数会释放ptl锁
{
    struct vm_area_struct *vma = vmf->vma;
    vm_fault_t ret = 0;

    // 增加引用计数
    folio_get(folio);

    // 如果定义了page_mkwrite操作
    if (vma->vm_ops && vma->vm_ops->page_mkwrite) {
        vm_fault_t tmp;

        pte_unmap_unlock(vmf->pte, vmf->ptl);
        // 检查是否可以调用fault操作
        tmp = vmf_can_call_fault(vmf);
        if (tmp) {
            folio_put(folio);
            return tmp;
        }

        // 执行page_mkwrite操作
        tmp = do_page_mkwrite(vmf, folio);
        if (unlikely(!tmp || (tmp &
                      (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
            folio_put(folio);
            return tmp;
        }
        // 完成mkwrite操作
        tmp = finish_mkwrite_fault(vmf, folio);
        if (unlikely(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE))) {
            folio_unlock(folio);
            folio_put(folio);
            return tmp;
        }
    } else {
        // 没有page_mkwrite操作,直接重用页面
        wp_page_reuse(vmf, folio);
        folio_lock(folio);  // 锁定页面
    }
    
    // 标记页面为脏
    ret |= fault_dirty_shared_page(vmf);
    folio_put(folio);  // 减少引用计数

    return ret;
}

/*
 * 处理需要复制到新页面的情况,无论是由于COW还是UNSHARE。
 *
 * 在持有mmap_lock且旧页面被引用但未持有ptl的情况下调用。
 *
 * 高级逻辑流程:
 *
 * - 分配一个页面,将旧页面的内容复制到新页面。
 * - 处理簿记和统计 - cgroups、mmu-notifiers等。
 * - 获取PTL。如果pte已更改,则退出并释放分配的页面。
 * - 如果pte仍然是我们记忆中的样子,更新页表和所有相关引用。
 *   这包括减少页表对旧页面的引用,以及更新反向映射(rmap)。
 * - 无论如何,解锁PTL并减少我们对旧页面持有的引用。
 */
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
    const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;  // 是否是UNSHARE操作
    struct vm_area_struct *vma = vmf->vma;  // 虚拟内存区域
    struct mm_struct *mm = vma->vm_mm;  // 内存描述符
    struct folio *old_folio = NULL;  // 旧页面
    struct folio *new_folio = NULL;  // 新页面
    pte_t entry;  // 新PTE值
    int page_copied = 0;  // 是否成功复制页面
    struct mmu_notifier_range range;  // MMU通知器范围
    vm_fault_t ret;  // 返回值
    bool pfn_is_zero;  // 是否是零页面

    // 开始延迟统计
    delayacct_wpcopy_start();

    // 获取旧页面的folio
    if (vmf->page)
        old_folio = page_folio(vmf->page);
    
    // 准备匿名页面分配
    ret = vmf_anon_prepare(vmf);
    if (unlikely(ret))
        goto out;

    // 检查是否是零页面
    pfn_is_zero = is_zero_pfn(pte_pfn(vmf->orig_pte));
    // 预分配新页面
    new_folio = folio_prealloc(mm, vma, vmf->address, pfn_is_zero);
    if (!new_folio)
        goto oom;  // 内存不足

    // 如果不是零页面,需要复制内容
    if (!pfn_is_zero) {
        int err;

        // 复制页面内容
        err = __wp_page_copy_user(&new_folio->page, vmf->page, vmf);
        if (err) {
            /*
             * COW失败,如果错误已被其他方式解决,那也没关系。
             * 如果没有,用户空间将在相同的地址上重新触发错误,
             * 我们将从第二次尝试中处理错误。
             * -EHWPOISON情况不会被重试。
             */
            folio_put(new_folio);
            if (old_folio)
                folio_put(old_folio);

            delayacct_wpcopy_end();
            return err == -EHWPOISON ? VM_FAULT_HWPOISON : 0;
        }
        // 复制KMSAN元数据
        kmsan_copy_page_meta(&new_folio->page, vmf->page);
    }

    // 标记新页面为就绪状态
    __folio_mark_uptodate(new_folio);

    // 初始化MMU通知器范围
    mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
                vmf->address & PAGE_MASK,
                (vmf->address & PAGE_MASK) + PAGE_SIZE);
    // 开始MMU通知器范围
    mmu_notifier_invalidate_range_start(&range);

    /*
     * 重新检查pte - 我们之前释放了锁
     */
    vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
    // 检查PTE是否仍然是原始值(没有被并发修改)
    if (likely(vmf->pte && pte_same(ptep_get(vmf->pte), vmf->orig_pte))) {
        // 更新内存计数器
        if (old_folio) {
            // 如果是文件映射,减少文件页面计数,增加匿名页面计数
            if (!folio_test_anon(old_folio)) {
                dec_mm_counter(mm, mm_counter_file(old_folio));
                inc_mm_counter(mm, MM_ANONPAGES);
            }
        } else {
            // 如果没有旧页面(零页面),增加匿名页面计数
            ksm_might_unmap_zero_page(mm, vmf->orig_pte);
            inc_mm_counter(mm, MM_ANONPAGES);
        }
        
        // 刷新缓存
        flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
        // 创建新PTE条目
        entry = folio_mk_pte(new_folio, vma->vm_page_prot);
        entry = pte_sw_mkyoung(entry);  // 设置年轻标志
        
        // 根据unshare设置不同的标志
        if (unlikely(unshare)) {
            if (pte_soft_dirty(vmf->orig_pte))
                entry = pte_mksoft_dirty(entry);
            if (pte_uffd_wp(vmf->orig_pte))
                entry = pte_mkuffd_wp(entry);
        } else {
            entry = maybe_mkwrite(pte_mkdirty(entry), vma);
        }

        /*
         * 清除pte条目并首先刷新它,然后在用新条目更新pte之前,
         * 以保持不同CPU上的TLB同步。这个代码过去先设置新PTE然后刷新TLB,
         * 但那样会留下一个窗口期,新PTE可能被加载到一些TLB中,
         * 而旧PTE仍然保留在其他TLB中。
         */
        // 清除旧PTE并刷新TLB
        ptep_clear_flush(vma, vmf->address, vmf->pte);
        
        // 添加反向映射
        folio_add_new_anon_rmap(new_folio, vma, vmf->address, RMAP_EXCLUSIVE);
        // 添加到LRU链表
        folio_add_lru_vma(new_folio, vma);
        
        // 调试断言:unshare时PTE不能有写权限
        BUG_ON(unshare && pte_write(entry));
        
        // 设置新PTE
        set_pte_at(mm, vmf->address, vmf->pte, entry);
        // 更新MMU缓存
        update_mmu_cache_range(vmf, vma, vmf->address, vmf->pte, 1);
        
        if (old_folio) {
            /*
             * 只有在将pte切换到新页面后,我们才能在这里移除反向映射计数。
             * 否则另一个进程可能在我们将pte切换到新页面之前发现反向映射计数减少,
             * 并"重用"旧页面,向其中写入内容,而我们的pte仍然指向它,
             * 并且可以被其他线程读取。
             *
             * 关键问题是使这个folio_remove_rmap_pte()与上面的ptp_clear_flush顺序正确。
             * 这些存储操作是通过(如果没有其他东西)folio_remove_rmap_pte()
             * 中的atomic_add_negative屏障来排序的。
             *
             * 然后ptep_clear_flush中的TLB刷新确保在减少的映射计数可见之前,
             * 没有进程可以访问旧页面。而旧页面在减少的映射计数可见之前不能被重用。
             * 因此,传递性地,在旧页面可以被重用之前,到旧页面的TLB将被刷新。
             */
            // 移除旧页面的反向映射
            folio_remove_rmap_pte(old_folio, vmf->page, vma);
        }

        /* 释放旧页面... */
        new_folio = old_folio;  // 现在new_folio指向旧页面(要释放的)
        page_copied = 1;  // 标记为已复制
        pte_unmap_unlock(vmf->pte, vmf->ptl);  // 解锁
    } else if (vmf->pte) {
        // PTE已改变,更新MMU TLB
        update_mmu_tlb(vma, vmf->address, vmf->pte);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
    }

    // 结束MMU通知器范围
    mmu_notifier_invalidate_range_end(&range);

    // 释放不再需要的页面
    if (new_folio)
        folio_put(new_folio);
    if (old_folio) {
        if (page_copied)
            free_swap_cache(old_folio);  // 如果已复制,释放交换缓存
        folio_put(old_folio);  // 释放旧页面
    }

    delayacct_wpcopy_end();
    return 0;  // 成功

oom:
    ret = VM_FAULT_OOM;  // 内存不足
out:
    if (old_folio)
        folio_put(old_folio);  // 释放旧页面

    delayacct_wpcopy_end();
    return ret;
}

关键概念解释:

1. 写时复制(Copy-on-Write)

  • 原理:当多个进程共享同一内存页时,只读共享;当有进程尝试写入时,内核会复制该页面,让写入进程拥有自己的副本
  • 应用场景
    • fork()创建子进程时
    • 私有文件映射(如可执行文件的.data段)
    • 共享匿名映射的写操作

2. UNSHARE标志

  • 用途:用于内存去共享操作,如madvise(MADV_UNSHARE)
  • 与COW的区别:UNSHARE会立即复制页面,即使还没有写入操作

3. userfaultfd机制

  • userfaultfd_pte_wp:检查PTE是否有userfaultfd写保护标志
  • 异步模式:允许内核直接处理,无需用户空间介入
  • 同步模式:需要用户空间处理缺页

4. 页面类型

  • 匿名页面:没有关联的文件(堆、栈)
  • 文件映射页面:关联到文件(代码段、数据段)
  • 零页面:特殊的全零页面,用于惰性分配

5. 关键标志位

// VM区域标志
VM_SHARED      // 共享映射(多个进程可见)
VM_MAYSHARE    // 可能共享
VM_WRITE       // 可写

// 错误处理标志
FAULT_FLAG_WRITE     // 写错误
FAULT_FLAG_UNSHARE   // 去共享操作
FAULT_FLAG_MKWRITE   // 需要调用mkwrite操作

// 页面标志
PageAnonExclusive    // 匿名页面独占标志

6. mmu_notifier机制

  • 用于通知其他子系统页表变化
  • 如KVM、GPU驱动等需要知道内存映射变化
  • invalidate_range_start/end:标记TLB无效化范围

7. 反向映射(Reverse Mapping)

  • 目的:从物理页找到所有映射它的虚拟地址
  • 操作
    • folio_add_new_anon_rmap:添加新的匿名反向映射
    • folio_remove_rmap_pte:移除PTE级别的反向映射
  • 重要性:用于页面回收、迁移等操作

处理流程总结:

1. 权限检查

  • 检查是否是userfaultfd写保护页面
  • 检查是否UNSHARE操作
  • 刷新TLB(如果需要)

2. 映射类型判断

  • 共享映射:调用pfn_mkwrite或page_mkwrite
  • 私有映射:检查是否可以重用页面

3. 页面重用条件

  • 页面必须是匿名的
  • 页面必须是独占的(或可以安全重用的)
  • 不是UNSHARE操作

4. 页面复制流程

  1. 预分配新页面
  2. 复制内容(如果不是零页面)
  3. 重新检查PTE(防止并发修改)
  4. 更新页表和统计信息
  5. 刷新TLB和MMU缓存
  6. 更新反向映射
  7. 清理旧页面

关键设计思想:

  1. 惰性复制:只有在真正需要时才复制页面
  2. 并发安全:使用锁和原子操作处理并发访问
  3. 性能优化:避免不必要的复制和TLB刷新
  4. 内存重用:尽可能重用现有页面
  5. 通知机制:使用mmu_notifier通知相关子系统

这个函数展示了Linux内核写时复制机制的精妙设计,平衡了内存使用效率和性能。

Q9

static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
	vm_fault_t ret = 0;
	struct folio *folio;

	/*
	 * 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).
	 */
	if (should_fault_around(vmf)) {
		ret = do_fault_around(vmf);
		if (ret)
			return ret;
	}

	ret = vmf_can_call_fault(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);
	folio = page_folio(vmf->page);
	folio_unlock(folio);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		folio_put(folio);
	return ret;
}
/* Return true if we should do read fault-around, false otherwise */
static inline bool should_fault_around(struct vm_fault *vmf)
{
	/* No ->map_pages?  No way to fault around... */
	if (!vmf->vma->vm_ops->map_pages)
		return false;

	if (uffd_disable_fault_around(vmf->vma))
		return false;

	/* A single page implies no faulting 'around' at all. */
	return fault_around_pages > 1;
}

/*
 * do_fault_around() tries to map few pages around the fault address. The hope
 * is that the pages will be needed soon and this will lower the number of
 * faults to handle.
 *
 * It uses vm_ops->map_pages() to map the pages, which skips the page if it's
 * not ready to be mapped: not up-to-date, locked, etc.
 *
 * This function doesn't cross VMA or page table boundaries, in order to call
 * map_pages() and acquire a PTE lock only once.
 *
 * fault_around_pages defines how many pages we'll try to map.
 * do_fault_around() expects it to be set to a power of two less than or equal
 * to PTRS_PER_PTE.
 *
 * The virtual address of the area that we map is naturally aligned to
 * fault_around_pages * PAGE_SIZE rounded down to the machine page size
 * (and therefore to page order).  This way it's easier to guarantee
 * that we don't cross page table boundaries.
 */
static vm_fault_t do_fault_around(struct vm_fault *vmf)
{
	pgoff_t nr_pages = READ_ONCE(fault_around_pages);
	pgoff_t pte_off = pte_index(vmf->address);
	/* The page offset of vmf->address within the VMA. */
	pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
	pgoff_t from_pte, to_pte;
	vm_fault_t ret;

	/* The PTE offset of the start address, clamped to the VMA. */
	from_pte = max(ALIGN_DOWN(pte_off, nr_pages),
		       pte_off - min(pte_off, vma_off));

	/* The PTE offset of the end address, clamped to the VMA and PTE. */
	to_pte = min3(from_pte + nr_pages, (pgoff_t)PTRS_PER_PTE,
		      pte_off + vma_pages(vmf->vma) - vma_off) - 1;

	if (pmd_none(*vmf->pmd)) {
		vmf->prealloc_pte = pte_alloc_one(vmf->vma->vm_mm);
		if (!vmf->prealloc_pte)
			return VM_FAULT_OOM;
	}

	rcu_read_lock();
	ret = vmf->vma->vm_ops->map_pages(vmf,
			vmf->pgoff + from_pte - pte_off,
			vmf->pgoff + to_pte - pte_off);
	rcu_read_unlock();

	return ret;
}

static inline vm_fault_t vmf_can_call_fault(const struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;

	if (vma->vm_ops->map_pages || !(vmf->flags & FAULT_FLAG_VMA_LOCK))
		return 0;
	vma_end_read(vma);
	return VM_FAULT_RETRY;
}
/*
 * The mmap_lock must have been held on entry, and may have been
 * released depending on flags and vma->vm_ops->fault() return value.
 * See filemap_fault() and __lock_page_retry().
 */
static vm_fault_t __do_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct folio *folio;
	vm_fault_t ret;

	/*
	 * Preallocate pte before we take page_lock because this might lead to
	 * deadlocks for memcg reclaim which waits for pages under writeback:
	 *				lock_page(A)
	 *				SetPageWriteback(A)
	 *				unlock_page(A)
	 * lock_page(B)
	 *				lock_page(B)
	 * pte_alloc_one
	 *   shrink_folio_list
	 *     wait_on_page_writeback(A)
	 *				SetPageWriteback(B)
	 *				unlock_page(B)
	 *				# flush A, B to clear the writeback
	 */
	if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
		vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
		if (!vmf->prealloc_pte)
			return VM_FAULT_OOM;
	}

	ret = vma->vm_ops->fault(vmf);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
			    VM_FAULT_DONE_COW)))
		return ret;

	folio = page_folio(vmf->page);
	if (unlikely(PageHWPoison(vmf->page))) {
		vm_fault_t poisonret = VM_FAULT_HWPOISON;
		if (ret & VM_FAULT_LOCKED) {
			if (page_mapped(vmf->page))
				unmap_mapping_folio(folio);
			/* Retry if a clean folio was removed from the cache. */
			if (mapping_evict_folio(folio->mapping, folio))
				poisonret = VM_FAULT_NOPAGE;
			folio_unlock(folio);
		}
		folio_put(folio);
		vmf->page = NULL;
		return poisonret;
	}

	if (unlikely(!(ret & VM_FAULT_LOCKED)))
		folio_lock(folio);
	else
		VM_BUG_ON_PAGE(!folio_test_locked(folio), vmf->page);

	return ret;
}
/**
 * finish_fault - finish page fault once we have prepared the page to fault
 *
 * @vmf: structure describing the fault
 *
 * This function handles all that is needed to finish a page fault once the
 * page to fault in is prepared. It handles locking of PTEs, inserts PTE for
 * given page, adds reverse page mapping, handles memcg charges and LRU
 * addition.
 *
 * The function expects the page to be locked and on success it consumes a
 * reference of a page being mapped (for the PTE which maps it).
 *
 * Return: %0 on success, %VM_FAULT_ code in case of error.
 */
vm_fault_t finish_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;
	struct folio *folio;
	vm_fault_t ret;
	bool is_cow = (vmf->flags & FAULT_FLAG_WRITE) &&
		      !(vma->vm_flags & VM_SHARED);
	int type, nr_pages;
	unsigned long addr;
	bool needs_fallback = false;

fallback:
	addr = vmf->address;

	/* Did we COW the page? */
	if (is_cow)
		page = vmf->cow_page;
	else
		page = vmf->page;

	folio = page_folio(page);
	/*
	 * check even for read faults because we might have lost our CoWed
	 * page
	 */
	if (!(vma->vm_flags & VM_SHARED)) {
		ret = check_stable_address_space(vma->vm_mm);
		if (ret)
			return ret;
	}

	if (!needs_fallback && vma->vm_file) {
		struct address_space *mapping = vma->vm_file->f_mapping;
		pgoff_t file_end;

		file_end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);

		/*
		 * Do not allow to map with PTEs beyond i_size and with PMD
		 * across i_size to preserve SIGBUS semantics.
		 *
		 * Make an exception for shmem/tmpfs that for long time
		 * intentionally mapped with PMDs across i_size.
		 */
		needs_fallback = !shmem_mapping(mapping) &&
			file_end < folio_next_index(folio);
	}

	if (pmd_none(*vmf->pmd)) {
		if (!needs_fallback && folio_test_pmd_mappable(folio)) {
			ret = do_set_pmd(vmf, folio, page);
			if (ret != VM_FAULT_FALLBACK)
				return ret;
		}

		if (vmf->prealloc_pte)
			pmd_install(vma->vm_mm, vmf->pmd, &vmf->prealloc_pte);
		else if (unlikely(pte_alloc(vma->vm_mm, vmf->pmd)))
			return VM_FAULT_OOM;
	}

	nr_pages = folio_nr_pages(folio);

	/* Using per-page fault to maintain the uffd semantics */
	if (unlikely(userfaultfd_armed(vma)) || unlikely(needs_fallback)) {
		nr_pages = 1;
	} else if (nr_pages > 1) {
		pgoff_t idx = folio_page_idx(folio, page);
		/* The page offset of vmf->address within the VMA. */
		pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
		/* The index of the entry in the pagetable for fault page. */
		pgoff_t pte_off = pte_index(vmf->address);

		/*
		 * Fallback to per-page fault in case the folio size in page
		 * cache beyond the VMA limits and PMD pagetable limits.
		 */
		if (unlikely(vma_off < idx ||
			    vma_off + (nr_pages - idx) > vma_pages(vma) ||
			    pte_off < idx ||
			    pte_off + (nr_pages - idx)  > PTRS_PER_PTE)) {
			nr_pages = 1;
		} else {
			/* Now we can set mappings for the whole large folio. */
			addr = vmf->address - idx * PAGE_SIZE;
			page = &folio->page;
		}
	}

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				       addr, &vmf->ptl);
	if (!vmf->pte)
		return VM_FAULT_NOPAGE;

	/* Re-check under ptl */
	if (nr_pages == 1 && unlikely(vmf_pte_changed(vmf))) {
		update_mmu_tlb(vma, addr, vmf->pte);
		ret = VM_FAULT_NOPAGE;
		goto unlock;
	} else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
		needs_fallback = true;
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		goto fallback;
	}

	folio_ref_add(folio, nr_pages - 1);
	set_pte_range(vmf, folio, page, nr_pages, addr);
	type = is_cow ? MM_ANONPAGES : mm_counter_file(folio);
	add_mm_counter(vma->vm_mm, type, nr_pages);
	ret = 0;

unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return ret;
}
/**
 * page_folio - Converts from page to folio.
 * @p: The page.
 *
 * Every page is part of a folio.  This function cannot be called on a
 * NULL pointer.
 *
 * Context: No reference, nor lock is required on @page.  If the caller
 * does not hold a reference, this call may race with a folio split, so
 * it should re-check the folio still contains this page after gaining
 * a reference on the folio.
 * Return: The folio which contains this page.
 */
#define page_folio(p)		(_Generic((p),				\
	const struct page *:	(const struct folio *)_compound_head(p), \
	struct page *:		(struct folio *)_compound_head(p)))
/**
 * folio_put - Decrement the reference count on a folio.
 * @folio: The folio.
 *
 * If the folio's reference count reaches zero, the memory will be
 * released back to the page allocator and may be used by another
 * allocation immediately.  Do not access the memory or the struct folio
 * after calling folio_put() unless you can be sure that it wasn't the
 * last reference.
 *
 * Context: May be called in process or interrupt context, but not in NMI
 * context.  May be called while holding a spinlock.
 */
static inline void folio_put(struct folio *folio)
{
	if (folio_put_testzero(folio))
		__folio_put(folio);
}

A9

好的,我来详细注释这些文件映射缺页处理的关键函数:

/**
 * do_read_fault - 处理文件映射的读缺页
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 处理文件映射的读错误(第一次访问文件页)
 */
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
    vm_fault_t ret = 0;       // 返回值
    struct folio *folio;      // 内存页数据结构

    /*
     * 让我们先调用->map_pages(),如果偏移处的页面尚未准备好映射(冷缓存等),
     * 则使用->fault()作为回退。
     */
    // 尝试fault-around优化:预读周围页面
    if (should_fault_around(vmf)) {
        ret = do_fault_around(vmf);
        if (ret)
            return ret;
    }

    // 检查是否可以调用fault处理函数
    ret = vmf_can_call_fault(vmf);
    if (ret)
        return ret;

    // 执行主要的fault处理:从文件读取页面
    ret = __do_fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        return ret;

    // 完成fault处理:建立页表映射
    ret |= finish_fault(vmf);
    
    // 获取页面的folio并解锁
    folio = page_folio(vmf->page);
    folio_unlock(folio);
    
    // 如果出错,释放页面引用
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
        folio_put(folio);
    return ret;
}

/* 如果应该执行读fault-around,返回true,否则返回false */
static inline bool should_fault_around(struct vm_fault *vmf)
{
    /* 没有->map_pages?无法进行fault-around... */
    if (!vmf->vma->vm_ops->map_pages)
        return false;

    // 如果userfaultfd禁用了fault-around,返回false
    if (uffd_disable_fault_around(vmf->vma))
        return false;

    /* 单个页面意味着根本不进行fault-around。 */
    // 只有fault_around_pages > 1时才启用预读
    return fault_around_pages > 1;
}

/*
 * do_fault_around()尝试映射故障地址周围的几个页面。
 * 希望这些页面很快会被需要,从而减少需要处理的故障数量。
 *
 * 它使用vm_ops->map_pages()来映射页面,如果页面尚未准备好映射
 *(未更新、锁定等),则会跳过该页面。
 *
 * 此函数不跨越VMA或页表边界,以便只调用一次map_pages()和获取一次PTE锁。
 *
 * fault_around_pages定义了我们尝试映射的页面数量。
 * do_fault_around()期望它被设置为小于或等于PTRS_PER_PTE的2的幂。
 *
 * 我们映射区域的虚拟地址自然对齐到fault_around_pages * PAGE_SIZE
 * 向下舍入到机器页面大小(因此也向下舍入到页面顺序)。
 * 这样更容易保证我们不跨越页表边界。
 */
static vm_fault_t do_fault_around(struct vm_fault *vmf)
{
    pgoff_t nr_pages = READ_ONCE(fault_around_pages);  // 预读页面数
    pgoff_t pte_off = pte_index(vmf->address);         // 地址在PTE中的偏移
    /* vmf->address在VMA内的页面偏移。 */
    pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
    pgoff_t from_pte, to_pte;  // 预读范围的起始和结束PTE偏移
    vm_fault_t ret;

    /* 起始地址的PTE偏移,钳制到VMA范围内。 */
    from_pte = max(ALIGN_DOWN(pte_off, nr_pages),
               pte_off - min(pte_off, vma_off));

    /* 结束地址的PTE偏移,钳制到VMA和PTE范围内。 */
    to_pte = min3(from_pte + nr_pages, (pgoff_t)PTRS_PER_PTE,
              pte_off + vma_pages(vmf->vma) - vma_off) - 1;

    // 如果PMD为空,预分配PTE表
    if (pmd_none(*vmf->pmd)) {
        vmf->prealloc_pte = pte_alloc_one(vmf->vma->vm_mm);
        if (!vmf->prealloc_pte)
            return VM_FAULT_OOM;
    }

    // 在RCU保护下调用map_pages进行预读
    rcu_read_lock();
    ret = vmf->vma->vm_ops->map_pages(vmf,
            vmf->pgoff + from_pte - pte_off,  // 起始文件偏移
            vmf->pgoff + to_pte - pte_off);   // 结束文件偏移
    rcu_read_unlock();

    return ret;
}

/**
 * vmf_can_call_fault - 检查是否可以调用fault处理函数
 * @vmf: 包含错误信息的vm_fault结构体
 *
 * 当使用VMA_LOCK时,需要检查是否允许调用fault操作
 */
static inline vm_fault_t vmf_can_call_fault(const struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;

    // 如果有map_pages操作,或者不是VMA_LOCK模式,可以直接调用
    if (vma->vm_ops->map_pages || !(vmf->flags & FAULT_FLAG_VMA_LOCK))
        return 0;
    
    // 如果是VMA_LOCK模式但没有map_pages,需要释放锁并重试
    vma_end_read(vma);
    return VM_FAULT_RETRY;
}

/*
 * 在进入时必须持有mmap_lock,根据flags和vma->vm_ops->fault()返回值
 * 可能已释放。参见filemap_fault()和__lock_page_retry()。
 */
static vm_fault_t __do_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct folio *folio;
    vm_fault_t ret;

    /*
     * 在获取page_lock之前预分配pte,因为这可能导致memcg回收的死锁,
     * 后者等待处于写回状态的页面:
     *                lock_page(A)
     *                SetPageWriteback(A)
     *                unlock_page(A)
     * lock_page(B)
     *                lock_page(B)
     * pte_alloc_one
     *   shrink_folio_list
     *     wait_on_page_writeback(A)
     *                SetPageWriteback(B)
     *                unlock_page(B)
     *                # 刷新A,B以清除写回状态
     */
    // 如果PMD为空且没有预分配PTE,则预分配一个PTE
    if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
        vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
        if (!vmf->prealloc_pte)
            return VM_FAULT_OOM;
    }

    // 调用文件系统或驱动提供的fault处理函数
    ret = vma->vm_ops->fault(vmf);
    if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
                VM_FAULT_DONE_COW)))
        return ret;

    // 获取页面对应的folio
    folio = page_folio(vmf->page);
    
    // 检查页面是否硬件中毒
    if (unlikely(PageHWPoison(vmf->page))) {
        vm_fault_t poisonret = VM_FAULT_HWPOISON;
        if (ret & VM_FAULT_LOCKED) {
            // 如果页面有映射,解除映射
            if (page_mapped(vmf->page))
                unmap_mapping_folio(folio);
            /* 如果从缓存中移除了一个干净的folio,则重试。 */
            if (mapping_evict_folio(folio->mapping, folio))
                poisonret = VM_FAULT_NOPAGE;
            folio_unlock(folio);
        }
        folio_put(folio);
        vmf->page = NULL;
        return poisonret;
    }

    // 如果fault操作没有锁定页面,则锁定它
    if (unlikely(!(ret & VM_FAULT_LOCKED)))
        folio_lock(folio);
    else
        // 如果已锁定,进行调试检查
        VM_BUG_ON_PAGE(!folio_test_locked(folio), vmf->page);

    return ret;
}

/**
 * finish_fault - 在准备好要缺页的页面后完成页面错误处理
 *
 * @vmf: 描述错误的结构体
 *
 * 此函数处理完成页面错误所需的所有操作:锁定PTE、为给定页面插入PTE、
 * 添加反向页面映射、处理memcg计数和LRU添加。
 *
 * 该函数期望页面被锁定,并且在成功时会消耗被映射页面的引用(对于映射它的PTE)。
 *
 * 返回值:成功时为%0,错误时为%VM_FAULT_代码。
 */
vm_fault_t finish_fault(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page;
    struct folio *folio;
    vm_fault_t ret;
    bool is_cow = (vmf->flags & FAULT_FLAG_WRITE) &&
              !(vma->vm_flags & VM_SHARED);  // 是否为COW(写私有映射)
    int type, nr_pages;
    unsigned long addr;
    bool needs_fallback = false;  // 是否需要回退到单页处理

fallback:
    addr = vmf->address;

    /* 我们是否执行了COW? */
    if (is_cow)
        page = vmf->cow_page;  // COW页面
    else
        page = vmf->page;      // 普通页面

    folio = page_folio(page);
    
    /*
     * 即使是读错误也要检查,因为我们可能丢失了COW页面
     */
    if (!(vma->vm_flags & VM_SHARED)) {
        ret = check_stable_address_space(vma->vm_mm);
        if (ret)
            return ret;
    }

    // 检查是否需要进行回退(页面超出文件大小)
    if (!needs_fallback && vma->vm_file) {
        struct address_space *mapping = vma->vm_file->f_mapping;
        pgoff_t file_end;

        // 计算文件总页数
        file_end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);

        /*
         * 不要允许映射超出i_size的PTE,以及跨越i_size的PMD,
         * 以保留SIGBUS语义。
         *
         * 为shmem/tmpfs做一个例外,它们长期以来故意用跨越i_size的PMD映射。
         */
        needs_fallback = !shmem_mapping(mapping) &&
            file_end < folio_next_index(folio);
    }

    // 如果PMD为空,尝试使用大页映射
    if (pmd_none(*vmf->pmd)) {
        // 如果folio支持PMD映射且不需要回退,尝试设置PMD大页
        if (!needs_fallback && folio_test_pmd_mappable(folio)) {
            ret = do_set_pmd(vmf, folio, page);
            if (ret != VM_FAULT_FALLBACK)
                return ret;
        }

        // 安装预分配的PTE或分配新的PTE
        if (vmf->prealloc_pte)
            pmd_install(vma->vm_mm, vmf->pmd, &vmf->prealloc_pte);
        else if (unlikely(pte_alloc(vma->vm_mm, vmf->pmd)))
            return VM_FAULT_OOM;
    }

    nr_pages = folio_nr_pages(folio);  // folio包含的页面数

    /* 使用每页错误来维护uffd语义 */
    if (unlikely(userfaultfd_armed(vma)) || unlikely(needs_fallback)) {
        nr_pages = 1;  // userfaultfd或需要回退时只处理单页
    } else if (nr_pages > 1) {
        // 检查大页是否在VMA和PTE边界内
        pgoff_t idx = folio_page_idx(folio, page);
        /* vmf->address在VMA内的页面偏移。 */
        pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
        /* 错误页面在页表中的条目索引。 */
        pgoff_t pte_off = pte_index(vmf->address);

        /*
         * 如果页面缓存中的folio大小超过VMA限制和PMD页表限制,
         * 则回退到每页错误。
         */
        if (unlikely(vma_off < idx ||
                vma_off + (nr_pages - idx) > vma_pages(vma) ||
                pte_off < idx ||
                pte_off + (nr_pages - idx)  > PTRS_PER_PTE)) {
            nr_pages = 1;
        } else {
            /* 现在我们可以为整个大folio设置映射。 */
            addr = vmf->address - idx * PAGE_SIZE;
            page = &folio->page;  // 指向folio的第一个页面
        }
    }

    // 获取PTE指针和锁
    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
                       addr, &vmf->ptl);
    if (!vmf->pte)
        return VM_FAULT_NOPAGE;

    /* 在ptl下重新检查 */
    if (nr_pages == 1 && unlikely(vmf_pte_changed(vmf))) {
        // 单页被并发修改,更新TLB
        update_mmu_tlb(vma, addr, vmf->pte);
        ret = VM_FAULT_NOPAGE;
        goto unlock;
    } else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages)) {
        // 大页范围被占用,需要回退
        needs_fallback = true;
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        goto fallback;
    }

    // 增加folio引用计数
    folio_ref_add(folio, nr_pages - 1);
    // 设置PTE映射
    set_pte_range(vmf, folio, page, nr_pages, addr);
    // 根据是否为COW选择计数器类型
    type = is_cow ? MM_ANONPAGES : mm_counter_file(folio);
    // 增加内存计数器
    add_mm_counter(vma->vm_mm, type, nr_pages);
    ret = 0;

unlock:
    // 解锁并解除PTE映射
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return ret;
}

/**
 * page_folio - 从page转换为folio
 * @p: 页面指针
 *
 * 每个页面都是一个folio的一部分。此函数不能在NULL指针上调用。
 *
 * 上下文:不需要引用或锁定@p。如果调用者不持有引用,
 * 此调用可能与folio拆分竞争,因此应该在获得folio上的引用后
 * 重新检查该folio是否仍包含此页面。
 * 返回值:包含此页面的folio。
 */
#define page_folio(p)        (_Generic((p),                \
    const struct page *:    (const struct folio *)_compound_head(p), \
    struct page *:        (struct folio *)_compound_head(p)))

/**
 * folio_put - 减少folio的引用计数
 * @folio: folio
 *
 * 如果folio的引用计数达到零,内存将被释放回页面分配器,
 * 并可能立即被另一个分配使用。在调用folio_put()后,
 * 除非你能确定它不是最后一个引用,否则不要访问内存或struct folio。
 *
 * 上下文:可以在进程或中断上下文中调用,但不能在NMI上下文中调用。
 * 可以在持有自旋锁时调用。
 */
static inline void folio_put(struct folio *folio)
{
    if (folio_put_testzero(folio))
        __folio_put(folio);
}

关键概念解释:

1. Fault-around预读优化

// 原理:预读周围的页面到内存,减少后续缺页异常
// 触发条件:fault_around_pages > 1(通常16或32)
// 机制:调用vm_ops->map_pages()预读多个页面

2. 文件映射的fault处理流程

// 1. 尝试预读(do_fault_around)
// 2. 检查是否可以调用fault(vmf_can_call_fault)
// 3. 执行主要读取(__do_fault)
// 4. 完成映射(finish_fault)

3. vm_ops操作结构

// 文件系统/驱动提供的虚拟内存操作
struct vm_operations_struct {
    vm_fault_t (*fault)(struct vm_fault *vmf);          // 主要fault处理
    vm_fault_t (*map_pages)(struct vm_fault *vmf,       // 预读多个页面
                           pgoff_t start, pgoff_t end);
    void (*page_mkwrite)(struct vm_fault *vmf);         // 页面变为可写
    vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);    // PFN映射变为可写
};

4. 页面引用计数管理

// 引用计数操作:
folio_ref_add(folio, nr_pages - 1);  // 增加引用
folio_put(folio);                    // 减少引用

// 重要规则:finish_fault成功后会消耗一个引用

5. 大页映射与回退机制

// 尝试使用大页(PMD级别)映射
if (folio_test_pmd_mappable(folio))
    ret = do_set_pmd(vmf, folio, page);

// 回退条件:
// 1. 超出文件大小
// 2. 超出VMA边界
// 3. 超出页表边界
// 4. 被userfaultfd保护

6. COW与共享映射

// COW(写私有映射)
bool is_cow = (vmf->flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED);

// 共享映射:多个进程共享同一物理页
// 私有映射:写时复制,每个进程有自己的副本

7. 内存计数器类型

// 匿名页面计数
MM_ANONPAGES

// 文件页面计数
mm_counter_file(folio)  // 根据folio类型返回MM_FILEPAGES或MM_SHMEMPAGES

处理流程总结:

1. 预读阶段

  • 检查是否启用fault-around
  • 计算预读范围(确保不跨越VMA/页表边界)
  • 调用map_pages预读多个页面

2. 主要读取阶段

  • 预分配PTE(避免死锁)
  • 调用文件系统的fault处理函数
  • 处理硬件中毒页面
  • 确保页面已锁定

3. 映射完成阶段

  • 检查是否需要回退(超出边界等)
  • 尝试大页映射
  • 建立PTE映射
  • 更新反向映射和内存统计
  • 处理引用计数

关键设计思想:

  1. 预读优化:fault-around减少后续缺页次数
  2. 大页优先:尝试使用大页映射提高性能
  3. 安全回退:大页失败时优雅回退到单页
  4. 并发安全:使用锁保护页表操作
  5. 内存统计:准确统计不同类型的内存使用
  6. 引用计数:精确管理页面生命周期

这些函数展示了Linux内核如何高效处理文件映射的缺页异常,结合了性能优化(预读、大页)和鲁棒性(错误处理、边界检查)。

Q10

/**
 * filemap_fault - read in file data for page fault handling
 * @vmf:	struct vm_fault containing details of the fault
 *
 * filemap_fault() is invoked via the vma operations vector for a
 * mapped memory region to read in file data during a page fault.
 *
 * The goto's are kind of ugly, but this streamlines the normal case of having
 * it in the page cache, and handles the special cases reasonably without
 * having a lot of duplicated code.
 *
 * vma->vm_mm->mmap_lock must be held on entry.
 *
 * If our return value has VM_FAULT_RETRY set, it's because the mmap_lock
 * may be dropped before doing I/O or by lock_folio_maybe_drop_mmap().
 *
 * If our return value does not have VM_FAULT_RETRY set, the mmap_lock
 * has not been released.
 *
 * We never return with VM_FAULT_RETRY and a bit from VM_FAULT_ERROR set.
 *
 * Return: bitwise-OR of %VM_FAULT_ codes.
 */
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
	int error;
	struct file *file = vmf->vma->vm_file;
	struct file *fpin = NULL;
	struct address_space *mapping = file->f_mapping;
	struct inode *inode = mapping->host;
	pgoff_t max_idx, index = vmf->pgoff;
	struct folio *folio;
	vm_fault_t ret = 0;
	bool mapping_locked = false;

	max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
	if (unlikely(index >= max_idx))
		return VM_FAULT_SIGBUS;

	trace_mm_filemap_fault(mapping, index);

	/*
	 * Do we have something in the page cache already?
	 */
	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);
		if (unlikely(!folio_test_uptodate(folio))) {
			filemap_invalidate_lock_shared(mapping);
			mapping_locked = true;
		}
	} else {
		ret = filemap_fault_recheck_pte_none(vmf);
		if (unlikely(ret))
			return ret;

		/* No page in the page cache at all */
		count_vm_event(PGMAJFAULT);
		count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
		ret = VM_FAULT_MAJOR;
		fpin = do_sync_mmap_readahead(vmf);
retry_find:
		/*
		 * See comment in filemap_create_folio() why we need
		 * invalidate_lock
		 */
		if (!mapping_locked) {
			filemap_invalidate_lock_shared(mapping);
			mapping_locked = true;
		}
		folio = __filemap_get_folio(mapping, index,
					  FGP_CREAT|FGP_FOR_MMAP,
					  vmf->gfp_mask);
		if (IS_ERR(folio)) {
			if (fpin)
				goto out_retry;
			filemap_invalidate_unlock_shared(mapping);
			return VM_FAULT_OOM;
		}
	}

	if (!lock_folio_maybe_drop_mmap(vmf, folio, &fpin))
		goto out_retry;

	/* Did it get truncated? */
	if (unlikely(folio->mapping != mapping)) {
		folio_unlock(folio);
		folio_put(folio);
		goto retry_find;
	}
	VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);

	/*
	 * We have a locked folio in the page cache, now we need to check
	 * that it's up-to-date. If not, it is going to be due to an error,
	 * or because readahead was otherwise unable to retrieve it.
	 */
	if (unlikely(!folio_test_uptodate(folio))) {
		/*
		 * If the invalidate lock is not held, the folio was in cache
		 * and uptodate and now it is not. Strange but possible since we
		 * didn't hold the page lock all the time. Let's drop
		 * everything, get the invalidate lock and try again.
		 */
		if (!mapping_locked) {
			folio_unlock(folio);
			folio_put(folio);
			goto retry_find;
		}

		/*
		 * OK, the folio is really not uptodate. This can be because the
		 * VMA has the VM_RAND_READ flag set, or because an error
		 * arose. Let's read it in directly.
		 */
		goto page_not_uptodate;
	}

	/*
	 * We've made it this far and we had to drop our mmap_lock, now is the
	 * time to return to the upper layer and have it re-find the vma and
	 * redo the fault.
	 */
	if (fpin) {
		folio_unlock(folio);
		goto out_retry;
	}
	if (mapping_locked)
		filemap_invalidate_unlock_shared(mapping);

	/*
	 * Found the page and have a reference on it.
	 * We must recheck i_size under page lock.
	 */
	max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
	if (unlikely(index >= max_idx)) {
		folio_unlock(folio);
		folio_put(folio);
		return VM_FAULT_SIGBUS;
	}

	vmf->page = folio_file_page(folio, index);
	return ret | VM_FAULT_LOCKED;

page_not_uptodate:
	/*
	 * Umm, take care of errors if the page isn't up-to-date.
	 * Try to re-read it _once_. We do this synchronously,
	 * because there really aren't any performance issues here
	 * and we need to check for errors.
	 */
	fpin = maybe_unlock_mmap_for_io(vmf, fpin);
	error = filemap_read_folio(file, mapping->a_ops->read_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:
	/*
	 * We dropped the mmap_lock, we need to return to the fault handler to
	 * re-find the vma and come back and find our hopefully still populated
	 * page.
	 */
	if (!IS_ERR(folio))
		folio_put(folio);
	if (mapping_locked)
		filemap_invalidate_unlock_shared(mapping);
	if (fpin)
		fput(fpin);
	return ret | VM_FAULT_RETRY;
}

A10

我来详细注释这个文件映射缺页处理的核心函数:

/**
 * filemap_fault - 为页面错误处理读取文件数据
 * @vmf: 包含错误详细信息的struct vm_fault
 *
 * filemap_fault()通过映射内存区域的vma操作向量调用,
 * 在页面错误期间读取文件数据。
 *
 * goto语句有点丑陋,但这简化了页面缓存的正常情况,
 * 并在没有大量重复代码的情况下合理地处理特殊情况。
 *
 * 进入时必须持有vma->vm_mm->mmap_lock。
 *
 * 如果我们的返回值设置了VM_FAULT_RETRY,那是因为
 * 在执行I/O之前或lock_folio_maybe_drop_mmap()可能释放了mmap_lock。
 *
 * 如果我们的返回值没有设置VM_FAULT_RETRY,则mmap_lock尚未释放。
 *
 * 我们从不返回同时设置了VM_FAULT_RETRY和VM_FAULT_ERROR位的值。
 *
 * 返回值:%VM_FAULT_代码的按位或。
 */
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
    int error;
    struct file *file = vmf->vma->vm_file;  // 关联的文件
    struct file *fpin = NULL;  // 用于预读的文件指针
    struct address_space *mapping = file->f_mapping;  // 地址空间(页缓存)
    struct inode *inode = mapping->host;  // 文件的inode
    pgoff_t max_idx, index = vmf->pgoff;  // 页面索引(文件偏移)
    struct folio *folio;  // 内存页数据结构
    vm_fault_t ret = 0;   // 返回值
    bool mapping_locked = false;  // 地址空间锁是否已获取

    // 计算文件的最大页面索引(文件大小除以页面大小)
    max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
    // 如果索引超出文件大小,返回SIGBUS
    if (unlikely(index >= max_idx))
        return VM_FAULT_SIGBUS;

    // 跟踪文件映射错误事件
    trace_mm_filemap_fault(mapping, index);

    /*
     * 页面缓存中已经有内容了吗?
     */
    // 尝试从页缓存中获取页面
    folio = filemap_get_folio(mapping, index);
    if (likely(!IS_ERR(folio))) {
        /*
         * 我们找到了页面,所以在等待锁之前尝试异步预读。
         */
        // 如果尚未尝试过预读,执行异步预读
        if (!(vmf->flags & FAULT_FLAG_TRIED))
            fpin = do_async_mmap_readahead(vmf, folio);
        
        // 如果页面不是最新的(需要从磁盘读取)
        if (unlikely(!folio_test_uptodate(folio))) {
            // 获取地址空间的共享锁
            filemap_invalidate_lock_shared(mapping);
            mapping_locked = true;
        }
    } else {
        // 页面不在缓存中,需要从磁盘读取
        
        // 重新检查PTE是否为空(防止竞争)
        ret = filemap_fault_recheck_pte_none(vmf);
        if (unlikely(ret))
            return ret;

        /* 页面缓存中完全没有页面 */
        // 统计主要错误事件(需要磁盘I/O)
        count_vm_event(PGMAJFAULT);
        count_memcg_event_mm(vmf->vma->vm_mm, PGMAJFAULT);
        ret = VM_FAULT_MAJOR;  // 标记为主要错误
        
        // 执行同步预读
        fpin = do_sync_mmap_readahead(vmf);
        
retry_find:
        /*
         * 参见filemap_create_folio()中的注释,了解为什么需要invalidate_lock
         */
        // 确保地址空间锁已获取
        if (!mapping_locked) {
            filemap_invalidate_lock_shared(mapping);
            mapping_locked = true;
        }
        
        // 再次尝试获取页面,如果不存在则创建
        folio = __filemap_get_folio(mapping, index,
                      FGP_CREAT|FGP_FOR_MMAP,
                      vmf->gfp_mask);
        if (IS_ERR(folio)) {
            // 获取失败,如果有关联的预读文件,重试
            if (fpin)
                goto out_retry;
            // 释放地址空间锁,返回内存不足
            filemap_invalidate_unlock_shared(mapping);
            return VM_FAULT_OOM;
        }
    }

    /*
     * 锁定页面,可能会释放mmap_lock
     * 如果不能锁定,跳转到out_retry重试
     */
    if (!lock_folio_maybe_drop_mmap(vmf, folio, &fpin))
        goto out_retry;

    /* 页面是否被截断了? */
    // 检查页面的映射是否改变(文件可能被截断)
    if (unlikely(folio->mapping != mapping)) {
        folio_unlock(folio);
        folio_put(folio);
        goto retry_find;  // 重新查找页面
    }
    // 调试检查:页面必须包含请求的索引
    VM_BUG_ON_FOLIO(!folio_contains(folio, index), folio);

    /*
     * 我们在页面缓存中有一个锁定的folio,现在我们需要检查
     * 它是否是最新的。如果不是,那将是由于错误,
     * 或者因为预读无法检索它。
     */
    if (unlikely(!folio_test_uptodate(folio))) {
        /*
         * 如果没有持有invalidate锁,folio在缓存中并且是最新的,
         * 但现在不是了。奇怪但可能,因为我们没有一直持有页面锁。
         * 让我们丢弃所有东西,获取invalidate锁并重试。
         */
        if (!mapping_locked) {
            folio_unlock(folio);
            folio_put(folio);
            goto retry_find;
        }

        /*
         * 好的,folio确实不是最新的。这可能是因为
         * VMA设置了VM_RAND_READ标志,或者因为发生了错误。
         * 让我们直接读取它。
         */
        goto page_not_uptodate;
    }

    /*
     * 我们已经走到这一步,并且我们可能已经释放了mmap_lock,
     * 现在是时候返回到上层,让它重新查找vma并重新执行错误。
     */
    if (fpin) {
        folio_unlock(folio);
        goto out_retry;
    }
    
    // 释放地址空间锁
    if (mapping_locked)
        filemap_invalidate_unlock_shared(mapping);

    /*
     * 找到了页面并持有其引用。
     * 我们必须在页面锁下重新检查i_size。
     */
    // 再次检查文件大小(在页面锁保护下)
    max_idx = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
    if (unlikely(index >= max_idx)) {
        folio_unlock(folio);
        folio_put(folio);
        return VM_FAULT_SIGBUS;  // 页面超出文件大小
    }

    // 设置返回的页面指针
    vmf->page = folio_file_page(folio, index);
    // 返回成功,并标记页面已锁定
    return ret | VM_FAULT_LOCKED;

page_not_uptodate:
    /*
     * 嗯,如果页面不是最新的,请处理错误。
     * 尝试重新读取它_一次_。我们同步执行此操作,
     * 因为这里确实没有性能问题,我们需要检查错误。
     */
    // 为I/O准备解锁mmap
    fpin = maybe_unlock_mmap_for_io(vmf, fpin);
    // 从文件读取页面内容
    error = filemap_read_folio(file, mapping->a_ops->read_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:
    /*
     * 我们释放了mmap_lock,需要返回到错误处理程序以
     * 重新查找vma,然后回来找到我们希望仍然存在的页面。
     */
    if (!IS_ERR(folio))
        folio_put(folio);  // 释放页面引用
    if (mapping_locked)
        filemap_invalidate_unlock_shared(mapping);  // 释放地址空间锁
    if (fpin)
        fput(fpin);  // 释放预读文件引用
    return ret | VM_FAULT_RETRY;  // 返回重试标志
}
/**
 * folio_file_page - 获取特定索引的页面
 * @folio: 包含此索引的folio
 * @index: 我们想要查找的索引
 *
 * 有时在页面缓存中查找folio后,我们需要获取特定索引的页面(例如页面错误处理)。
 *
 * 原理:folio可能包含多个连续的物理页面(大页),
 * 这个函数计算给定文件偏移在folio内的相对页面位置。
 *
 * 例子:如果folio包含16个页面(64KB),且支持大页,
 * 那么index & (folio_nr_pages(folio) - 1)计算在folio内的偏移。
 *
 * 返回值:包含此索引文件数据的页面。
 */
static inline struct page *folio_file_page(struct folio *folio, pgoff_t index)
{
    // 计算索引在folio内的位置:
    // 1. folio_nr_pages(folio): 获取folio包含的页面数
    // 2. folio_nr_pages(folio) - 1: 创建掩码(例如16个页面则掩码为0b1111)
    // 3. index & mask: 获取在folio内的相对偏移
    return folio_page(folio, index & (folio_nr_pages(folio) - 1));
}

/**
 * folio_page - 从folio中返回一个页面
 * @folio: folio
 * @n: 要返回的页面号
 *
 * @n是相对于folio起始位置的页面号。
 * 此函数不检查页面号是否在@folio内;假定调用者已经持有页面的引用。
 * 
 * 原理:folio本质上是多个连续物理页面的复合结构,
 * 通过folio->page + n可以获取第n个页面的page结构。
 *
 * 宏定义:避免函数调用开销,直接指针运算
 */
#define folio_page(folio, n)    (&(folio)->page + (n))

关键概念解释:

1. 页缓存(Page Cache)

  • Linux内核的文件缓存系统
  • 缓存最近访问的文件页面
  • 通过address_space结构管理
  • filemap_get_folio()从页缓存获取页面

2. 页面状态标志

// folio标志位
folio_test_uptodate(folio)  // 页面内容是最新的(与磁盘一致)
folio_test_locked(folio)    // 页面被锁定(防止并发修改)

3. 预读(Readahead)

  • 异步预读:当页面在缓存中但不是最新时触发
  • 同步预读:当页面完全不在缓存中时触发
  • 目的:预读后续页面,减少后续缺页异常

4. 锁管理

// 地址空间锁(invalidate_lock)
filemap_invalidate_lock_shared(mapping)    // 获取共享锁
filemap_invalidate_unlock_shared(mapping)  // 释放共享锁

// 页面锁
folio_lock(folio)      // 锁定页面
folio_unlock(folio)    // 解锁页面

// mmap_lock管理
lock_folio_maybe_drop_mmap()  // 锁定页面,可能释放mmap_lock

5. 错误类型

VM_FAULT_MAJOR      // 主要错误(需要磁盘I/O)
VM_FAULT_LOCKED     // 页面已锁定
VM_FAULT_RETRY      // 需要重试(锁被释放)
VM_FAULT_SIGBUS     // 总线错误(如超出文件大小)
VM_FAULT_OOM        // 内存不足

6. 文件截断(Truncation)处理

// 检查页面是否被截断
if (unlikely(folio->mapping != mapping))

// 处理截断的页面
if (error == AOP_TRUNCATED_PAGE)
    goto retry_find;

处理流程总结:

1. 边界检查

  • 检查请求的页面是否超出文件大小
  • 如果超出,返回VM_FAULT_SIGBUS

2. 缓存查找

  • 命中缓存:检查页面是否最新,尝试异步预读
  • 未命中缓存:执行同步预读,创建新页面

3. 页面锁定

  • 锁定页面,可能释放mmap_lock
  • 检查页面是否被截断

4. 数据有效性检查

  • 检查页面内容是否最新
  • 如果不是最新,从磁盘读取

5. 最终检查

  • 在页面锁保护下重新检查文件大小
  • 设置返回的页面指针

6. 错误处理

  • 处理I/O错误
  • 处理页面截断
  • 清理资源,返回适当的错误码

关键设计思想:

  1. 缓存优先:优先从页缓存获取,减少磁盘I/O
  2. 惰性读取:只在需要时才从磁盘读取数据
  3. 预读优化:预测性读取后续页面
  4. 锁管理优化:必要时释放mmap_lock避免死锁
  5. 错误恢复:处理截断、I/O错误等特殊情况
  6. 重试机制:使用VM_FAULT_RETRY让上层重试

性能优化点:

  1. 异步预读:当页面在缓存中但不是最新时
  2. 同步预读:当页面不在缓存中时
  3. 锁降级lock_folio_maybe_drop_mmap()可能释放mmap_lock
  4. 无效锁:保护页面缓存免受截断操作影响

这个函数展示了Linux内核如何处理文件映射的缺页异常,包括缓存管理、锁优化和错误处理等复杂逻辑。