mm: use VMA lock for kernel faults on user addresses
💡 一句话总结
在系统调用等场景下,内核自身通过 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) |
📊 速览卡片
🎯 解决什么问题
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"。
lock_vma_under_rcu() + RCU + maple tree)只锁单个 VMA,粒度细得多,是内核近年为消除 mmap_lock 争用引入的快路径。当前 x86/arm64 的 per-VMA 锁快路径只服务 user_mode(regs) 的缺页,内核态缺页一律 goto lock_mmap,白白错过了这个细粒度锁。
① 这类缺页很常见——典型 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()),但既然内核态缺页从不走这条路,这段代码实际是死的。
🧩 核心机制
核心是把"只有用户态缺页才走 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 区分用户/内核地址空间 |
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 小时内已有多轮实质性讨论:
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 缺页的架构都需要跟进更新。
与 vma_start_read_unlocked() 的关系:Suren 系列若先合入,本系列的 x86 改动可用新 API 重写得更干净。
覆盖面:RFC 只演示 x86/arm64,其它架构的 fault.c 需同步更新。
⚠️ 风险与局限
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 简化存在依赖/重叠,需协调避免重复劳动。
🔗 交叉引用
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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。