内存管理-77-kernel-internals系列-Memory专题-Advanced-8-Per-VMA Locks
一、Per-VMA Locks 翻译
注: 翻译自 https://kernel-internals.org/mm/per-vma-locks/
Per-VMA 锁(Per-VMA Locks)
消除缺页处理中的 mmap_lock 瓶颈。
1. mmap_lock
每个进程都有一个属于自身 `mm_struct`、名为 `mmap_lock` 的读写信号量。从历史上看,它曾是几乎所有涉及进程虚拟内存布局的操作的必经之门:
- 缺页异常(page fault),**以读模式**获取它(与其他缺页处理共享访问);
- `mmap()`、`munmap()`、`mprotect()`、`mremap()`、`brk()` **以写模式**获取它(独占);
读侧的竞争问题并不那么直观。rwsem 中的读锁是共享的,因此多个线程同时发生缺页时本应能够并行推进——确实如此,直到有写者出现。任意线程中一次 `mmap()` 或 `munmap()` 调用就会获取写锁,并**阻塞该进程内所有并发的缺页处理**,直到写者完成。
对于频繁缺页与偶发 VMA 修改混合的多线程负载(试想:大量工作线程不断缺页换入新的堆页面,而内存分配器偶尔调用 `mmap()`),`mmap_lock` 会成为一个串行化点,限制缺页处理的吞吐量。
`mmap_lock` 的粒度还很粗:它保护的是整棵 VMA 树。如果线程 A 正在处理区域 X 的缺页,而线程 B 正在处理区域 Y 的缺页,即便它们操作的是完全不同的 VMA,也仍然会在同一把锁上竞争。
2. per-VMA 锁的设计
per-VMA 锁(合入 Linux 6.4,但是Android 6.1内核上已带入,实测6.1.115内核上有这个)为每个 `vm_area_struct` 提供自己的轻量级锁。缺页处理路径首先尝试这把 VMA 范围内的锁,只有在 VMA 正被并发修改时才回退到 `mmap_lock`。
2.1 VMA 中的锁状态
`vm_area_struct` 中有两个字段实现 per-VMA 锁(由 `CONFIG_PER_VMA_LOCK` 控制):
/* In vm_area_struct (include/linux/mm_types.h) */ unsigned int vm_lock_seq; /* sequence number, updated on each write-lock */ refcount_t vm_refcnt; /* reference count + reader/writer state */
对应的每 `mm_struct` 字段是:
/* In mm_struct */ int mm_lock_seq; /* bumped every time mmap_lock is write-locked */
当 `vma->vm_lock_seq != mm->mm_lock_seq` 时,该 VMA 被视为**未加锁**(unlocked)。每当 mm 被加上写锁(例如执行 `mmap()` 时),`mm_lock_seq` 就会递增,从而一次性使所有现存的 per-VMA 读锁失效——且无需逐个触碰每个 VMA。
`vm_refcnt` 字段编码了读者计数,外加一个特殊的 `VM_REFCNT_EXCLUDE_READERS_FLAG` 标志位,用于写加锁和 VMA 摘除(detach)过程。详细的状态机记录在 `include/linux/mm_types.h` 中 `vm_area_struct` 的定义处。
注意:序列计数器允许溢出: 内核中关于 `vm_lock_seq` 的注释明确指出溢出是可接受的。溢出最多只会导致偶尔一次不必要的回退到 `mmap_lock`,而不会造成正确性错误。
2.2 VMA 查找:maple tree + RCU
快速缺页路径中的 VMA 查找在 RCU 保护下使用 maple tree(`mm->mm_mt`,类型为 `struct maple_tree`)。maple tree 是一种 RCU 安全的 B 树,以地址区间为键存储 VMA。
`mm/memory.c` 中的 `lock_vma_under_rcu()` 是入口:
struct vm_area_struct *lock_vma_under_rcu(struct mm_struct *mm, unsigned long address)
{
MA_STATE(mas, &mm->mm_mt, address, address);
struct vm_area_struct *vma;
retry:
rcu_read_lock();
vma = mas_walk(&mas); /* RCU-protected maple tree walk */
if (!vma) { ... goto inval; }
if (!vma_start_read(vma)) /* attempt per-VMA read lock */
goto inval; /* lock failed (VMA changed), fall back */
rcu_read_unlock();
/* validate address range */
if (address < vma->vm_start || address >= vma->vm_end) {
vma_end_read(vma);
goto inval;
}
return vma; /* stable locked VMA, no mmap_lock held */
...
}
`vma_start_read()` 会检查 `vma->vm_lock_seq == mm->mm_lock_seq`。若二者相等,说明该 VMA 当前处于写锁状态,函数返回 NULL(回退到 `mmap_lock`)。若二者不等,它使用 acquire 栅栏(acquire fence)递增 `vm_refcnt` 并重新检查,从而建立起读锁。
3. 缺页处理路径
`mm/memory.c` 中的 `handle_mm_fault()` 由各架构的缺页处理程序调用,调用时要么持有 VMA 锁,要么持有 `mmap_lock`,具体由 `FAULT_FLAG_VMA_LOCK` 标志指示:
/* mm/memory.c */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address, unsigned int flags, struct pt_regs *regs)
{
/*
* By the time we get here, we already hold either the VMA lock
* or the mmap_lock (FAULT_FLAG_VMA_LOCK tells you which).
*/
if (unlikely(is_vm_hugetlb_page(vma)))
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
else
ret = __handle_mm_fault(vma, address, flags);
...
}
与架构相关的缺页处理程序(例如 x86 上的 `do_user_addr_fault()`)首先尝试 `lock_vma_under_rcu()`。成功后它会设置 `FAULT_FLAG_VMA_LOCK` 并调用 `handle_mm_fault()`。若 RCU 快速路径失败(VMA 修改正在进行、VMA 未找到,或地址区间不匹配),则按常规方式回退为以读模式获取 `mmap_lock`。
如果 `__handle_mm_fault()` 内部的缺页处理逻辑断定不持有 `mmap_lock` 就无法继续——例如在阻塞式 I/O 之后需要重试时——它会用 `vma_end_read()` 释放 VMA 锁并返回 `VM_FAULT_RETRY`。随后调用者会重新获取 `mmap_lock`。
4. 对 VMA 加写锁
当必须修改某个 VMA 时(例如 `mprotect()` 期间、`mmap()` 过程中的拆分/合并,或 `munmap()` 期间的解除映射),内核会调用 `vma_start_write()`:
/* mm/mmap.c and mm/memory.c — callers */ vma_start_write(vma);
`vma_start_write()`(位于 `mm/mmap_lock.c`)要求 `mmap_lock` 已被以写模式持有。它设置 `vma->vm_lock_seq = mm->mm_lock_seq`,这会使得针对该 VMA 的 `vma_start_read()` 失败,直到写锁被释放且 `mm_lock_seq` 再次递增。
该协议保证了正确性:任何修改 VMA 的操作都必须先持有 `mmap_lock` 写锁,然后调用 `vma_start_write()`。持有 per-VMA 读锁的缺页处理程序因此不会被暴露于(VMA 的)中间修改状态。
5. 仍然需要 mmap_lock 的操作
per-VMA 锁只加速缺页的**读**侧路径。以下操作仍然需要以写模式持有 `mmap_lock`:
------------------------------------------------------------------------------------------------------------------------------------- 操作 原因 ------------------------------------------------------------------------------------------------------------------------------------- mmap()/munmap() VMA 树的插入/移除 mprotect() VMA 标志修改 mremap() VMA 地址区间变更 brk() 堆边界变更 缺页期间的 VMA 拆分/合并 会修改相邻 VMA execve() 拆除整个地址空间 -------------------------------------------------------------------------------------------------------------------------------------
对于 per-VMA 锁路径失败的任何缺页,仍会回退为以读模式使用 `mmap_lock`。
6. 可观测性
per-VMA 锁没有 sysfs 开关——只要 `CONFIG_PER_VMA_LOCK=y`,该特性就始终启用(在支持的架构上,这是 6.4 及之后内核的默认配置)。
有四个 vmstat 风格的计数器跟踪 per-VMA 锁的活动情况(定义于 `include/linux/vm_event_item.h`):
------------------------------------------------------------------------------------------------------------------------------------- 计数器 含义 ------------------------------------------------------------------------------------------------------------------------------------- VMA_LOCK_SUCCESS 快速路径成功;缺页在 per-VMA 锁下完成处理 VMA_LOCK_ABORT 快速路径失败;回退到了 mmap_lock VMA_LOCK_RETRY 快速路径被重试(例如瞬时失败之后) VMA_LOCK_MISS VMA 在查找与加锁之间被摘除;已重试 -------------------------------------------------------------------------------------------------------------------------------------
这些计数器可通过标准的 `count_vm_vma_lock_event()` 基础设施读取。在启用了 `CONFIG_PER_VMA_LOCK` 的内核上,它们会出现在 `/proc/vmstat` 中。
# cat /proc/vmstat | grep vma vma_lock_success 12338183 vma_lock_abort 342657 vma_lock_retry 2675024 vma_lock_miss 0
提示:诊断竞争:若 `VMA_LOCK_ABORT` 相对于 `VMA_LOCK_SUCCESS` 偏高,说明该负载在 VMA 修改期间频繁发生缺页。如果应用从某个同时承受大量缺页的线程中高频调用 `mmap()`/`munmap()`,这属于预期现象。
7. 局限与特殊情况
(1) hugetlb 缺页不使用 per-VMA 锁。`handle_mm_fault()` 会把 `VM_HUGETLB` 页路由到 `hugetlb_fault()`,后者使用自己的 `hugetlb_vma_lock_read()` / `hugetlb_vma_lock_write()` 基础设施 —— 一套针对 hugetlb VMA 的独立锁方案。
(2) FAULT_FLAG_RETRY_NOWAIT 不能与 `FAULT_FLAG_VMA_LOCK` 组合使用。内核在 `sanitize_fault_flags()` 中对此做了断言:
/* mm/memory.c */
if (WARN_ON_ONCE((*flags & (FAULT_FLAG_VMA_LOCK | FAULT_FLAG_RETRY_NOWAIT)) == (FAULT_FLAG_VMA_LOCK | FAULT_FLAG_RETRY_NOWAIT)))
return VM_FAULT_SIGSEGV;
原因在于:`FAULT_FLAG_RETRY_NOWAIT` 假定在发生 `VM_FAULT_RETRY` 时不释放锁,而 per-VMA 锁协议则要求释放该锁。
(3) userfaultfd 有其自身的交互方式。userfaultfd 代码(`mm/userfaultfd.c`)在某些路径中直接使用 `lock_vma_under_rcu()`,并显式调用 `vma_start_read_locked()` 和 `vma_end_read()`。
(4) madvise 对于某些行为(例如针对区间的 `MADV_DONTNEED`),会在近期内核新增的逐区间路径中使用 `lock_vma_under_rcu()`。
警告:per-VMA 锁需要 CONFIG_PER_VMA_LOCK
如果未设置 `CONFIG_PER_VMA_LOCK`,整个特性会被编译移除掉。在此类内核上,所有缺页都走 `mmap_lock`。大多数生产发行版内核从 6.4 起都会启用它。
8. 延伸阅读
8.1 内核源码
- [include/linux/mmap_lock.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/mmap_lock.h) — `vma_start_read()`、`vma_end_read()`、`vma_start_write()` 以及序列计数器辅助函数
- [mm/mmap_lock.c](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/mm/mmap_lock.c) — `lock_vma_under_rcu()`:结合 RCU 与 per-VMA 锁的快速路径 VMA 查找
- [include/linux/mm_types.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/mm_types.h) — `vm_lock_seq`、`vm_refcnt` 和 `mm_lock_seq` 字段定义及其加锁约定
8.2 内核提交
- [5e31275cc997](https://git.kernel.org/linus/5e31275cc997) — Linux 6.4:"mm: add per-VMA lock and helper functions to control it"(Suren Baghdasaryan,Google)
- [d4af56c5c7c6](https://git.kernel.org/linus/d4af56c5c7c6) — Linux 6.1:maple tree VMA 存储,可扩展 per-VMA 锁的前置条件
8.3 相关页面
- [rcu-mm.md](https://kernel-internals.org/mm/rcu-mm/) — `SLAB_TYPESAFE_BY_RCU` 与 maple tree 如何实现无锁的 VMA 查找,per-VMA 锁正是建立在这一基础之上
- [mmap.md](https://kernel-internals.org/mm/mmap/) — 仍然需要粗粒度 `mmap_lock` 写侧的操作
8.4 LWN 文章
- [LWN: Per-VMA locks](https://lwn.net/Articles/906852/) — 2022 年的文章,介绍 per-VMA 锁的设计与动机
- [LWN: Scalable page-fault handling](https://lwn.net/Articles/932298/) — 后续文章,介绍 Linux 6.4 的合入情况与基准测试
posted on 2026-10-06 14:12 Hello-World3 阅读(7) 评论(0) 收藏 举报
浙公网安备 33010602011771号