内存管理-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)    收藏  举报

导航