mm: use VMA lock for kernel faults on user addresses

内存管理 · per-VMA 锁应用扩展 · 内核 uaccess 缺页绕开 mmap_lock

💡 一句话总结

在系统调用等场景下,内核自身通过 copy_from_user()/copy_to_user() 访问用户地址、触发缺页时,本补丁(RFC)让这类"内核态缺页"从无条件走 mmap_lock 全局锁,改为先走 per-VMA 锁快路径(锁不到才回退 mmap_lock),从而降低多线程下 mmap_lock 争用,并顺带让各架构 fault.c 里"为内核态缺页准备但实际不可达"的信号处理代码生效。作为 RFC 仅在 x86/arm64 上演示,未提供基准数据。

📋 补丁基本信息

项目内容
补丁类型优化(性能 + 一致性清理,RFC 探索性)
状态RFC v1 · 讨论中(Suren / Lorenzo 已回复)
当前版本RFC v1 · x86 补丁 / arm64 补丁
系列规模2 patches(x86 + arm64)· cover letter 0/2
作者机构Barry Song (Xiaomi) · Co-developed-by Bo Zhang (Xiaomi)
提交日期2026-08-02
改动范围2 文件:arch/x86/mm/fault.c、arch/arm64/mm/fault.c · +3/-4 行
核心函数do_user_addr_fault() / do_page_fault() / lock_vma_under_rcu()
关联系列mm: Unconditional per-VMA locks and cleanups(Suren / Dave Hansen,v3)

📊 速览卡片

核心机制
per-VMA 锁
优化目标
绕开 mmap_lock
适用场景
内核 uaccess 缺页
实测提升
未提供基准

🎯 解决什么问题

背景 / 原始动机
内核里对用户地址的访问(如 copy_from_user() / copy_to_user() 这类 uaccess 例程)访问到一个还没映射的页时,会触发一次缺页。这类缺页的地址是用户地址,但触发者是内核代码,因此叫"内核态缺页(kernel faults on user addresses)"。当前实现里,这类缺页无条件回退到 mmap_lock 路径——即便地址明明在用户空间、本可以走更细粒度的 per-VMA 锁。cover letter 原话:"unconditionally fall back to the mmap_lock path"。
系统层面:mmap_lock 全局读锁 vs per-VMA 锁
缺页处理有两种锁路径:mmap_lock 是整个地址空间的读写锁,任何线程按地址查找 VMA 都要拿它——多线程并发缺页时是全局争用点;per-VMA 锁(lock_vma_under_rcu() + RCU + maple tree)只锁单个 VMA,粒度细得多,是内核近年为消除 mmap_lock 争用引入的快路径。当前 x86/arm64 的 per-VMA 锁快路径只服务 user_mode(regs) 的缺页,内核态缺页一律 goto lock_mmap,白白错过了这个细粒度锁。
场景层面:为什么值得改
cover letter 给出三点理由:
① 这类缺页很常见——典型 Ubuntu 系统上,python3、apt-esm-hook、package-data-do、teamviewerd、bash、scudo、cscope、gnome-shell、systemd 等进程每秒发生数百次内核态用户地址缺页;都在 mmap_lock 下处理会不必要地放大锁争用。
② 为简化 filemap_fault() 扫清障碍——Matthew Wilcox 提出去掉缺页重试路径、持锁做 I/O 的方案 [1];基于该思路,Hongru 已报告内核态用户地址缺页在 mmap_lock 下做 I/O 导致回归 [2]。本补丁让这类缺页不再依赖 mmap_lock,等于移除该障碍。
③ 消除不一致的死代码——各架构 fault.c 的 per-VMA 锁路径里保留了 !user_mode 的信号处理分支(如 x86 的 kernelmode_fixup_or_oops()),但既然内核态缺页从不走这条路,这段代码实际是死的。
受影响负载:内核态频繁访问用户内存(系统调用/uaccess 密集)+ 多线程并发 · 因果:内核态缺页改走 per-VMA 锁 → 全局 mmap_lock 争用降低

🧩 核心机制

核心是把"只有用户态缺页才走 per-VMA 锁"的门槛放宽到"内核态访问用户地址的缺页也能走 per-VMA 锁",x86 与 arm64 各自实现方式略有不同。

架构改动判断依据
x86删除 !(flags & FAULT_FLAG_USER) → goto lock_mmap 门槛地址经 do_user_addr_fault() 处理即已确认是用户地址
arm64新增 uaccess 标志(v1);后改为 is_ttbr0_addr(addr) 一行判断(自评修订)arm64 用 TTBR0/TTBR1 区分用户/内核地址空间
从系统层面看:为什么放宽是安全的
x86 的 do_user_addr_fault() 只处理用户地址缺页(内核地址缺页走 do_kern_addr_fault()),所以"地址是用户地址"已由入口函数保证;FAULT_FLAG_USER 只是根据 user_mode(regs) 标记"触发者是不是用户态"。作者去掉这个判断,让内核态触发者也能进 per-VMA 锁快路径。Suren(per-VMA 锁作者)在讨论中确认:"如果拿 mmap_lock 是安全的,那拿 VMA 锁也安全"(两者都需要进程上下文,而 VMA 锁只在单个 VMA 上)。arm64 侧不能直接照搬,因为 arm64 缺页入口 do_page_fault() 同时服务内核/用户地址,需要额外用 is_ttbr0_addr(addr)(地址落在 TTBR0 用户地址空间)来限定只放宽"用户地址"部分。

🔬 关键代码

① x86 —— 删一行门槛(核心)

diff --git a/arch/x86/mm/fault.c b/arch/x86/mm/fault.c
index 45b99c3b1442..c22b74e0eeaf 100644
--- a/arch/x86/mm/fault.c
+++ b/arch/x86/mm/fault.c
@@ -1328,9 +1328,6 @@ void do_user_addr_fault(struct pt_regs *regs,
 	}
 #endif
 
-	if (!(flags & FAULT_FLAG_USER))
-		goto lock_mmap;
-
 	vma = lock_vma_under_rcu(mm, address);
 	if (!vma)
 		goto lock_mmap;

▲ 删掉 FAULT_FLAG_USER 门槛后,内核态缺页会先试 lock_vma_under_rcu()(per-VMA 锁快路径),拿不到锁才 goto lock_mmap 回退。因为 do_user_addr_fault() 本身只处理用户地址,这个放宽是安全的。此后 handle_mm_fault() 返回重试且有信号挂起时,!user_mode(regs) 分支(kernelmode_fixup_or_oops)不再是一段不可达的死代码。

② arm64 —— 加 uaccess 标志(v1 原始方案)

diff --git a/arch/arm64/mm/fault.c b/arch/arm64/mm/fault.c
index 85e23388f9bb..241f1ab07ab3 100644
--- a/arch/arm64/mm/fault.c
+++ b/arch/arm64/mm/fault.c
@@ -607,6 +607,7 @@ static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
 	unsigned int mm_flags = FAULT_FLAG_DEFAULT;
 	unsigned long addr = untagged_addr(far);
 	struct vm_area_struct *vma;
+	bool uaccess = false;
 	int si_code;
 	int pkey = -1;
 
@@ -663,6 +664,7 @@ static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
 		if (!insn_may_access_user(regs->pc, esr))
 			die_kernel_fault("access to user memory outside uaccess routines",
 					 addr, esr, regs);
+		uaccess = true;
 	}
 
 	if (is_pkvm_stage2_abort(esr)) {
@@ -674,7 +676,7 @@ static int __kprobes do_page_fault(unsigned long far, unsigned long esr,
 
 	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
 
-	if (!(mm_flags & FAULT_FLAG_USER))
+	if (!(mm_flags & FAULT_FLAG_USER) && !uaccess)
 		goto lock_mmap;

▲ arm64 的 do_page_fault() 同时处理用户/内核地址,不能照抄 x86 删门槛。v1 用 uaccess 标志标记"在内核 uaccess 例程中访问用户内存"的缺页(由 is_el1_permission_fault() 块判定),只有这类缺页才放宽到 per-VMA 锁。

③ arm64 —— 自评修订的一行方案(讨论后的演进方向)

diff --git a/arch/arm64/mm/fault.c b/arch/arm64/mm/fault.c
index 85e23388f9bb..e1b406d667aa 100644
--- a/arch/arm64/mm/fault.c
+++ b/arch/arm64/mm/fault.c
@@ -674,7 +674,7 @@ static int __kprobes do_page_fault(unsigned long
 far, unsigned long esr,
 
 	perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
 
-	if (!(mm_flags & FAULT_FLAG_USER))
+	if (!(mm_flags & FAULT_FLAG_USER) && !is_ttbr0_addr(addr))
 		goto lock_mmap;

▲ 作者在自评中承认 v1 的 uaccess 方案漏掉了"translation fault"(demand paging),改为直接用 is_ttbr0_addr(addr) 判断"地址在用户地址空间"——一个更简洁、覆盖面更全的一行判断。这是讨论中得出的关键修订(详见"方案演进")。

📈 性能影响

场景/用例运行环境改进前改进后
内核态访问用户地址缺页(copy_from_user 等,每秒数百次)多线程并发 + uaccess 密集负载每次拿 mmap_lock 全局读锁(争用点)先试 per-VMA 锁,仅失败时回退(逻辑分析)

说明:补丁未提供基准数据。cover letter 给出的论据是场景层面的("典型 Ubuntu 每秒数百次内核态缺页"),收益为逻辑分析(解读(AI 分析)):per-VMA 锁粒度细于 mmap_lock,内核态缺页密集时能减少全局锁排队;具体提升幅度取决于 mmap_lock 争用有多严重,需要基准验证。

🔄 方案演进与讨论焦点

这是 RFC(探索性补丁),重点在讨论而非成熟度。系列自 2026-08-02 发出后,48 小时内已有多轮实质性讨论:

v1 发出(08-02)→ 讨论推进(08-02 ~ 08-03)
Barry 自评修订 arm64 方案(自评帖):外部 AI 审阅(sashiko.dev)指出 v1 的 uaccess 标志只会在 is_el1_permission_fault()(CoW / PAN 违规这类权限故障)时置位;如果内核 uaccess 访问的是未映射地址,发生的是 translation fault(demand paging 的典型情形),is_el1_permission_fault() 为假,uaccess 不会被置位——于是这类最常见的缺页还是会回退 mmap_lock,错过优化。作者承认:"Good catch! I should have only modified a single line." 并给出 is_ttbr0_addr(addr) 一行方案。

Suren Baghdasaryan(回复 x86 patch):per-VMA 锁作者表示"不记得当初为什么把内核态缺页排除在外,当初可能只想限制在最常见的情形、避免副作用";同意"如果拿 mmap_lock 安全,拿 VMA 锁也安全"。

Lorenzo Stoakes(回复 x86 patch):认可方向,但提出三个协调点:① 建议等 Suren/Dave 系列的 vma_start_read_unlocked() 落地后做更干净的清理;② 提醒 Matthew(Willy)在做缺页路径的大重构,应先对齐;③ 指出所有做 per-VMA 缺页的架构都需要跟进更新。
当前讨论的开放问题
是否应并入 Matthew 的大重构:Lorenzo 在回复 arm64 patch中直言"这(指 permanent VMA flags 改动)可能真的需要等 Matthew 的系列",因为每个做 VMA 缺页的架构都要动。
与 vma_start_read_unlocked() 的关系:Suren 系列若先合入,本系列的 x86 改动可用新 API 重写得更干净。
覆盖面:RFC 只演示 x86/arm64,其它架构的 fault.c 需同步更新。

⚠️ 风险与局限

潜在回归 / 并发 / 边界
架构覆盖不全:RFC 仅 x86/arm64,其它架构(loongarch/riscv/powerpc/s390 等)走 per-VMA 缺页的路径需逐个确认(Lorenzo 点名此点)。
arm64 v1 方案的缺陷:初版 uaccess 标志漏掉 translation fault,只覆盖 permission fault,需按自评修订为 is_ttbr0_addr() 才完整(解读(AI 分析):is_ttbr0_addr() 判断覆盖所有用户地址缺页类型)。
信号处理路径语义:让 !user_mode 的"快速响应信号"分支生效后,内核态缺页持 VMA 锁遇到 VM_FAULT_RETRY + 挂起信号时行为改变(进 kernelmode_fixup_or_oops),需确认与原 mmap_lock 路径语义一致(解读(AI 分析):RFC 尚未讨论到这一层的验证)。
依赖链:与 Suren 的 per-VMA 锁全配置化、Matthew 的 filemap_fault 简化存在依赖/重叠,需协调避免重复劳动。
严重度:MINOR(RFC 探索性)· 讨论焦点:是否并入 Matthew 大重构、arm64 方案修订

🔗 交叉引用

📌 同日报关联工作
mm: Unconditional per-VMA locks and cleanups(Suren / Dave Hansen,v3)——per-VMA 锁全配置可用化 + 引入 vma_start_read_unlocked() API。本系列在讨论中明确提到"等 vma_start_read_unlocked() 落地后重写更干净",二者是同一方向(per-VMA 锁应用)的两个切面。

✅ 关键洞察

  • 发现:把内核态用户地址缺页从 mmap_lock 迁移到 per-VMA 锁,是消除 mmap_lock 全局争用的又一关键应用场景——这类缺页在典型系统上每秒数百次,且是简化 filemap_fault()(持锁做 I/O)的前置障碍
  • 证据:补丁未提供基准;机制收益基于"锁粒度更细"的逻辑分析(解读(AI 分析));讨论中获得 Suren(per-VMA 锁作者)"拿 mmap_lock 安全则拿 VMA 锁也安全"的背书
  • 边界:仅 x86/arm64 演示;arm64 需用 is_ttbr0_addr() 覆盖全部用户地址缺页类型(v1 的 uaccess 方案有遗漏);其它架构待跟进
  • 风险 / 建议:RFC 尚在讨论——大概率会并入 Matthew 的缺页大重构或基于 Suren 的 vma_start_read_unlocked() 重写,短期内独立合入的可能性较低(解读(AI 分析))
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。