MMU 反向映射(rmap)

共 23 题
📑 题目列表 23 题
#
★★★

1. anon_vma_chain 在 fork 时父→子的 VMA 链接与子 anon_vma 副本的工程价值?

在 fork 创建子进程时,内核如何通过 anon_vma_chain 建立父进程 VMA 到子进程 anon_vma 的链接,为什么需要为子进程创建 anon_vma 副本,这背后的工程价值是什么?

  • fork 时 anon_vma 与 anon_vma_chain 的建立逻辑
  • 父子进程共享匿名页的反向映射组织
  • 防止重复遍历与 O(N^2) 复杂度

fork 时父进程的每个匿名 VMA 会通过 anon_vma_chain 建立一条"父 VMA → 父 anon_vma"的链接,同时为子进程创建新的 anon_vma 副本,并建立"子 VMA → 子 anon_vma"的链接;这两个 anon_vma 通过 anon_vma 的 parent 指针与红黑树形成父子关系(父 anon_vma 的 rb_root 指向子 anon_vma)。这样当一个匿名页被某个进程的 rmap 遍历时,rmap_walk 会沿着该 anon_vma 的 rb 树遍历到所有后代 anon_vma,从而低成本找到所有映射该页的 VMA。其工程价值在于:如果只用一个共享的 anon_vma,那么在 fork 后再次 fork 时,链会把所有进程的 VMA 串成一条长链,rmap 遍历会退化为 O(N) 且需要反复比对,容易重复遍历;而"每个 VMA 一个 anon_vma + 树形父链"的结构让每个 VMA 只被精确遍历一次,并能在 unmap 单页时快速找到该页对应的全部映射,而无需扫描整棵进程地址空间。

匿名页反向映射的核心难点是"一个物理页可能被多个进程/多个 VMA 映射",而 anon_vma_chain 把"VMA → anon_vma"的对应关系显式记录下来,rmap walk 只需沿 anon_vma 的树形结构走,而不用遍历整个进程的 mm。这正是 rmap 在 fork 场景下保持可扩展性的关键。

#
★★

2. rmap walk 在 unmap 页与 swap out 的开销来源及 rmap_walk_control(rwc)回调与 TLB 批量失效(TTU_BATCH_FLUSH)的优化?

rmap_walk 在 unmap(解除页映射)与 swap out(换出页面)时会被调用,其开销主要来自哪里,内核通过哪些真实机制(rmap_walk_control 回调、TTU_BATCH_FLUSH 批量失效)控制开销、避免无效遍历?

  • rmap_walk 的遍历路径与开销来源
  • rmap_walk_control 的 trylock/提前终止回调
  • TLB 批量失效(TTU_BATCH_FLUSH)与单页 vs 多页遍历的优化

rmap_walk 的开销主要来自遍历所有映射该页的 anon_vma(匿名页)或 address_space 的 i_mmap 区间树(文件页),并在每个 VMA 上通过 pte_offset_map 定位对应的 PTE,再对每个 PTE 执行 unmap 或 swap 操作;当同一页被大量进程映射(如共享库、KSM 合并页)时,遍历范围会很大。注意:内核主线并没有"rmap_walk_already_done"之类的 per-mm 已处理缓存,实际控制开销依靠 rmap_walk_control(rwc)与 TLB 批处理机制:1) try_lock/contended——遍历用 trylock 获取 rmap 锁,遇锁竞争立即记录 contended 并退出,避免阻塞在慢锁上;2) done/invalid_vma 回调——提前终止遍历或跳过不感兴趣的 VMA,避免无效的 PTE 定位;3) TTU_BATCH_FLUSH 与 try_to_unmap_flush()——在批量 unmap(如 reclaim 换出大量页)时把 TLB 失效推迟到一批操作结束后统一执行(arch_tlbbatch 硬件批处理),显著减少跨核 IPI 与 TLB flush 次数;4) 以 folio 为单位处理,避免对同一 folio 的多个子页重复遍历映射结构。其工程价值在于:在 swap out 大量页面、或 unmap 一个巨大 mm 时,能显著减少重复的 PTE 定位、页表遍历与 TLB 失效,降低 CPU 开销。

rmap_walk 的代价是"按页×按映射"的乘积;内核并不缓存"某 mm 已处理"之类的计数,而是通过 rwc 回调(trylock 遇锁竞争退出、done 提前终止、invalid_vma 跳过 VMA)避免无效遍历,并用 TTU_BATCH_FLUSH 把 TLB 失效批量推迟执行,把重复工作与跨核 IPI 降为一次。

#
★★

3. Folio 替代 compound page(head/tail)对 page cache 14 个 caller 的工程影响?

用 Folio 替代 compound page 的 head/tail 概念,对 page cache 中约 14 个调用者(caller)的工程影响是什么?

  • compound page 的 head/tail 分裂问题
  • folio 作为统一大页描述符
  • page cache 调用方简化

传统 compound page 中,一个物理大页由 head page 和若干 tail page 组成,head 保存复合页的元信息,tail 通过 compound_head() 回指 head;这导致 page cache 的调用者经常需要判断"当前是 head 还是 tail"、并通过 compound_head 反复跳转,逻辑繁琐且易错。Folio 工程把"整个复合页"抽象为一个统一的 folio 对象,内部直接持有 order、refcount、映射信息等,不再依赖 head/tail 的隐式关系。对 page cache 的约 14 个调用者(如 mark_page_accessed、SetPageDirty、page_mapping、lru add 等)而言,它们从"操作一个 page 并处理 head/tail 特判"改为"直接操作一个 folio",签名简化、分支减少,且批量操作(如一次设置整个 folio 的 dirty 标志)更高效。

folio 的工程价值不只是改名,而是把"复合页的复杂性"封装进一个对象,让大量调用者不再关心 head/tail 细节,从而减少 bug 面并让后续大页(THP、HugeTLB)在 page cache 中更自然地工作。

#
★★

4. KSM 的 madvise(MADV_MERGEABLE) 与 madvise(MADV_UNMERGEABLE) 接口?

KSM 通过 madvise(MADV_MERGEABLE) 和 madvise(MADV_UNMERGEABLE) 两个接口控制哪些匿名页允许被合并,这两个接口的语义与工程使用场景是什么?

  • MADV_MERGEABLE 的启用语义
  • MADV_UNMERGEABLE 的撤销语义
  • KSM 页的 COW 属性

应用调用 madvise(addr, len, MADV_MERGEABLE) 时,内核把该地址范围内的匿名 VMA 标记为可合并(设置 VM_MERGEABLE),KSM 守护线程随后会对这些页做写保护扫描,发现内容相同的页就合并为单个共享页并标记为写保护(COW);madvise(addr, len, MADV_UNMERGEABLE) 则清除 VM_MERGEABLE 标志,并撤销 kernel 对该范围内已合并页的合并,把这些页重新拆分为独立页(对应的 KSM 页计为 unmergeable)。工程上,MADV_UNMERGEABLE 常用于 KSM 页需要被写(mprotect 后缺页写)之前,或某些页面(如安全敏感数据)不希望被合并共享时显式排除。两个接口配合,使 KSM 能把"哪些页值得合并"的决策权交给应用,避免对不需要合并的页白白做写保护与哈希扫描。

通过 madvise 的显式接口,KSM 的合并范围从"全进程匿名页"收窄到"应用标记的页",从而降低扫描开销与写保护带来的 COW 代价,是 KSM 可实用化的关键。

#
★★

5. Linux 6.10+ mseal() 系统调用将 mmap 区域标记为"sealed"、禁止 munmap 与 mprotect 的安全语义?

Linux 6.10 引入的 mseal() 系统调用把 mmap 区域标记为"sealed",被 seal 的区域禁止 munmap 与 mprotect,其安全语义是什么?

  • mseal() 的调用方式与范围
  • sealed 区域对 munmap/mprotect 的禁止
  • 防篡改内存布局的用途

mseal() 系统调用对指定的地址范围调用 mprotect 语义,把范围内所有 VMA 标记为 VM_SEALED;一旦被 seal,该区域此后就不能再被 munmap、mprotect、以及改变其权限或映射的调用(如 mremap、mmap 覆盖)修改,任何这类尝试都会返回 EPERM。其安全语义在于:把关键内存区域(如代码段、VDSO、敏感数据结构)在初始化后"锁定",防止后续代码(尤其被 exploit 劫持后的代码)通过 mprotect/munmap 篡改内存布局或权限,从而为防御机制提供"不可变"保障。它把地址空间从"运行时可变"变为"部分只读不可拆",是缓解内存破坏类攻击的基础设施。

mseal 与 mprotect 的差异在于 mprotect 还允许改权限,而 mseal 一旦启用就永久禁止该区域的一切布局变更,形成一段"注册后即冻结"的内存,减少攻击面。

#
★★

6. mseal() 在防御 SROP、ROPgadget 与 VDSO 修补的工程价值?

mseal() 在防御 SROP(Sigreturn-Oriented Programming)、ROP gadget 与 VDSO 修补方面的工程价值是什么?

  • SROP 与 ROP gadget 的攻击原理
  • VDSO 页被修补的风险
  • mseal 锁定关键页的价值

SROP 利用 sigreturn 系统调用伪造信号帧来构造控制流,ROP 则通过串联代码中已有的 gadget 实现任意代码执行,两者都依赖攻击者能够读取/修改关键内存或利用可写代码区;VDSO 页若可被 mprotect 改为可写,攻击者就能植入 gadget 或篡改 vDSO 函数。mseal() 把 vDSO、关键代码段、以及某些安全库映射 seal 起来,使其不可 munmap、不可 mprotect 改权限,从而:1) 防止攻击者把只读代码页改写成可写,堵住 ROP gadget 的植入途径;2) 防止对 vDSO/sigreturn 相关页的修补,削弱 SROP 中伪造信号帧所依赖的页表操控;3) 让防御代码(如 seccomp、CFI 相关结构)一旦被 seal 就不再被篡改。其工程价值在于把一个"事后可被改写"的地址空间变成"关键部分不可变",显著提高内存破坏攻击的利用难度。

很多攻击链的最后一步是"用 mprotect 把某只读页改成可写再写入 shellcode",mseal 正是针对这一 common 的提权步骤设计,把攻击者常用的"改权限"手段从地址空间中移除。

#
★★

7. mseal() 与 vm_flags 的 VM_SEALED 字段及 userfaultfd 在 sealing region 的边界?

mseal() 如何通过 vm_flags 的 VM_SEALED 字段实现,userfaultfd 在 sealing region 上的操作边界是什么?

  • VM_SEALED 标志的存储位置
  • userfaultfd 对被 seal 区域的处理
  • seal 与页错误处理的边界

mseal() 的核心就是把 VMA 的 vm_flags 上置 VM_SEALED 位,此后内核在 munmap、mprotect、mremap、mmap 覆盖等操作时会检查该位,若设置则返回 EPERM,从而保证该区域的布局与权限不可变。VM_SEALED 是 VMA 级别的标志,因此 seal 的作用域是"整段 VMA"。对于 userfaultfd,其边界在于:userfaultfd 的注册(UFFDIO_REGISTER)与缺页处理本身不改变 VMA 的布局和权限,因此它仍可在这段区域上处理缺页;但用户态不能通过 userfaultfd 相关操作去违背 seal 的语义(例如在 sealed 区域上重新 mmap 或改动其在缺失页上的映射结构),对 sealed 区域重新映射/改权限的路径会被 VM_SEALED 拦截。也就是说,seal 保护的是"区域的存在与权限不透支",而 userfaultfd 的缺页服务(补页内容)仍可继续,二者边界清晰。

设计上 VM_SEALED 只阻止"破坏性布局变更",不阻止正常缺页填充,因此 userfaultfd 仍能服务于被 seal 的 VMA,只是不能借它绕过 seal 去改映射。

#
★★

8. rmap 遍历时 anon_vma 的双链表与红黑树结构如何避免重复遍历同一 VMA,fork 后父子进程共享匿名页时如何组织?

rmap 遍历时,anon_vma 的双链表与红黑树结构如何避免重复遍历同一 VMA,fork 后父子进程共享匿名页时这些结构如何组织?

  • anon_vma 的 rb_root 与 parent 指针
  • anon_vma_chain 的双链表
  • fork 后父子共享页的树形组织

一个 anon_vma 通过 rb_root 组织所有"以它为父"的子 anon_vma 的红黑树,并通过 parent 指针回指父 anon_vma;同时每个 anon_vma 通过一个 double list 链向该 VMA 上所有 anon_vma_chain 节点。fork 后,子进程的 VMA 创建一个新的 anon_vma,并把父 anon_vma 作为其 parent 插入父 anon_vma 的 rb 树;这样父子进程共享的匿名页就同时挂在父 anon_vma 的 rb 树(含父自己与所有子)下。rmap_walk 遍历一个页时,从该页关联的 anon_vma 出发,沿 rb 树遍历所有后代 anon_vma,每遇到一个 anon_vma 就通过其 double list 找到对应的 VMA 并处理对应的 PTE;由于每个 anon_vma 在树中只出现一次、且每个 VMA 通过明确的 anon_vma_chain 挂载,遍历不会重复访问同一 VMA,从而避免了重复 unmap 与重复工作。

关键是把"一个 VMA 一个 anon_vma"与"父 anon_vma 通过 rb 树聚合所有子"结合,使 rmap 既能在共享页时覆盖所有映射,又能保证每个 VMA 只处理一次,复杂度从 O(进程数×VMA数) 降到 O(映射该页的 VMA 数)。

#
★★

9. mseal() 在 Android Bionic、glibc malloc meta 的工程应用?

mseal() 在 Android Bionic libc 和 glibc malloc 元数据(meta)方面的工程应用是什么?

  • Bionic 对 vDSO/代码段的 seal
  • glibc malloc 元数据保护
  • 防止堆溢出改写元数据

在 Android 上,Bionic libc 在启动早期对 vDSO、某些关键代码段以及安全敏感的映射执行 mseal(),把这些区域锁定为不可 munmap/mprotect,从而防止攻击者通过篡改这些页来植入 gadget 或破坏控制流,是 Android 内存安全加固的一部分。在 glibc 中,mseal 被用于保护 malloc 的元数据(meta)区域——例如把存放 chunk 头、arena 结构、bin 链表等关键数据的存储页 seal 起来,使堆溢出攻击即使改写了相邻数据,也无法通过 mprotect 把元数据页改成可写来继续操作,从而抬高堆利用的门槛。两者的共同点是:把"漏洞利用链条中依赖的、本应只读或受保护区"的页在初始化后冻结,减少攻击者可操纵的内存面。

堆元数据是堆溢出攻击的常见目标,seal 元数据页后,即使溢出也无法改变这些页的权限或布局,堵住了"改元数据→伪造 chunk→任意写"的经典路径。

#
★★

10. Linux kernel 反向映射(rmap)将 anon_vma 与 page→mapping 反向链表的工程动机?

Linux 内核反向映射(rmap)引入 anon_vma 与 page→mapping 反向链表的工程动机是什么?

  • 正向映射 vs 反向映射
  • 从页找引用它的进程/VMA
  • unmap 与写保护的需求

正向映射(页表)告诉我们"一个虚拟地址映射到哪个物理页",但很多操作(如 swap out、unmap、写保护、回收)需要反向:给定一个物理页,找出所有映射它的进程和 VMA。为此,rmap 为每个匿名页建立从 page 到 anon_vma 的联系,再通过 anon_vma 找到所有映射该页的 VMA 与 PTE,形成 page→anon_vma→VMA→PTE 的反向链表。工程动机是:如果没有反向映射,swap out 一个页或对某页做写保护时,内核只能遍历所有进程的所有页表来查找该页,代价是 O(总页表);有了 rmap,就能在 O(映射该页的 VMA 数) 内完成查找,使 swap、KSM、页迁移、NUMA balancing 等按页操作都可扩展。

rmap 是"以页为索引的映射表",本质是牺牲少量内存换取"按页查引用"的 O(映射数) 复杂度,是 swap/回收/迁移得以高效进行的基础。

#
★★

11. rmap 在 KSM(page merging)与 NUMA 迁移的工程价值?

rmap 在 KSM(页面合并)与 NUMA 迁移中的工程价值是什么?

  • KSM 需要写保护所有映射
  • NUMA 迁移需要更新所有 PTE
  • rmap 统一提供映射遍历

KSM 合并两个内容相同的页时,需要把这两个页的所有映射都改为写保护(设为 COW),以便后续写时缺页分裂;找出"一个页的所有映射"正是 rmap 的职责——KSM 通过 rmap 遍历把该页每个 PTE 都置为只读,从而安全地合并。NUMA 迁移(migrate_pages)把物理页从一个 node 搬到另一个 node 时,需要更新所有映射该页的 PTE 指向新页帧;rmap 提供遍历所有映射的入口,使迁移能一次性更新全部 PTE,并把原页置换为 migration PTE。两者共同体现 rmap 的价值:它让"对一个物理页做全局性操作(改权限、换页帧)"时,能精确、高效地作用到所有映射,而无需扫描整个地址空间。

没有 rmap,KSM 无法知道一个页被多少进程映射、NUMA 迁移也无法更新所有 PTE;rmap 把"按页操作"从理论上可行变为工程上高效。

#
★★

12. KSM 通过写保护(write protect)发现相同页面并 merge 为 COW page 的工程价值?

KSM 通过写保护(write protect)发现相同页面并合并为 COW page 的工程价值是什么?

  • 写保护 + 内容比较的合并流程
  • merge 为 COW 页
  • 写时分裂保持正确性

KSM 对标记为合并的匿名页先做内容哈希/比较,发现内容相同的页后,把这些页统一改为写保护(只读),并把它们映射到同一个物理页上,从而把多个物理页合并为一个共享页;由于该页是只读的,任一进程写它时都会触发缺页,内核在写时缺页里把页复制出来(COW),保证写者看到独立副本、内容正确。工程价值在于:1) 用"只读 + COW"的机制保证了合并后语义正确(写者不互相影响);2) 通过把相同的只读页合并,显著降低内存占用(尤其在虚拟机场景中多个 guest 运行相同 OS/类库时);3) 写保护同时充当"延迟合并"的触发器——只有真正被写时才分裂,未被写的页保持共享,最大化节省内存。

KSM 的核心是"以只读为代价换取共享",COW 保证一旦有写者就自动恢复独立性,因此合并与正确的权衡得以兼顾。

#
★★

13. KSM 在 KVM(Kernel Samepage Merging for VMs)减少内存压力的工程应用?

KSM 在 KVM(Kernel Samepage Merging for VMs)中如何减少内存压力,其工程应用是什么?

  • 多个 guest 共享相同类库/OS 页
  • KSM 合并 guest 匿名页
  • 内存超卖与去重

在 KVM 场景下,多个虚拟机 guest 往往运行相似的 OS 与类库(如相同的 kernel、相同的 libc、相同的应用框架),这些只读/静态代码页在物理内存中会有大量重复。KSM 把这些 guest 的匿名页(QEMU 用户态看到的页)做内容去重,把内容相同的页合并为一个共享页,从而在物理内存层面消除重复,使内存在超卖(overcommit)下能容纳更多虚拟机。KSM 的守护线程周期性地扫描并合并,显著降低整体内存占用与宿主机的内存压力。工程应用上,需权衡 KSM 的 CPU 扫描开销与合并收益,通常对内存受限、运行成规模同类 VM 的宿主才值得开启。

KVM 是 KSM 最典型的落地场景,因为虚拟机间天然存在大量重复的内存内容,去重收益远大于系统内部去重,直接缓解内存超卖。

#
★★

14. KSM 的 full-scans(全量扫描)与扫描开销控制(checksum 过滤、pages_to_scan 限速、advisor)?

KSM 的 full-scans(全量扫描)如何工作,内核通过哪些真实机制(checksum 过滤、pages_to_scan 限速、max_page_sharing、advisor)控制扫描开销?

  • full-scans 周期性批量扫描候选页
  • checksum 过滤与 stable/unstable 树比较
  • pages_to_scan/sleep_millisecs 限速与扫描开销权衡

传统 KSM 采用周期性的全量扫描(full-scans):ksmd 守护线程每隔 sleep_millisecs 毫秒醒来一次,每轮按 pages_to_scan 批量遍历所有标记为 VM_MERGEABLE(MADV_MERGEABLE)的候选页,计算每页的 checksum,只有内容相对上次扫描发生变化(oldchecksum 不同)的页才进入 stable tree/unstable tree 的内容比较去尝试合并;若不做任何过滤,CPU 开销会随内存量线性增长。注意:内核主线并没有"smaps-aware mode"(KSM 不读取 /proc/pid/smaps),实际控制扫描开销的机制是:1) pages_to_scan/sleep_millisecs 限速——每轮只扫描固定页数、按固定周期推进,不一次性扫完所有页;2) checksum 过滤——未变化的页直接跳过,不再重复参与比较;3) use_zero_pages——checksum 与空页一致的页直接合并为零页,跳过树比较;4) max_page_sharing(默认 256)——限制单个 KSM 页最多共享数,防止热门页造成 stable_node 链膨胀与反复比较;5) 扫描 advisor(smart scanning)——根据 /sys/kernel/mm/ksm 下 pages_sharing/pages_shared/pages_unshared/pages_volatile 等统计自动调整 pages_to_scan(pages_sharing 高说明合并有效可加大扫描,pages_unshared 高说明白扫描需减小),并可对已合并页跳过若干轮扫描。因此 full-scans 开销大但不会漏掉潜在重复页,限速+过滤后的扫描更聚焦、开销更小,但可能漏掉尚未变化、尚未被重复发现的页;工程上始终在"扫描 CPU 成本"与"内存节省收益"之间权衡,用统计反馈指导扫描预算。

扫描开销控制的关键是"限速 + 过滤":用 pages_to_scan/sleep_millisecs 限速、用 checksum 跳过未变页、用 max_page_sharing 与零页合并减少无效比较,再用 pages_sharing 等统计反馈(advisor)动态调节扫描量,把有限的扫描预算用在更容易产生合并收益的页上。

#
★★

15. try_to_unmap 与 try_to_unmap_one 在是否写 PTE 的差异?

try_to_unmap 与 try_to_unmap_one 在是否写 PTE 上的差异是什么?

  • try_to_unmap 的遍历入口
  • try_to_unmap_one 的 per-PTE 处理
  • 写 PTE 的位置与条件

try_to_unmap 是 rmap_walk 的入口,负责遍历一个页的所有映射(anon_vma 及其 VMA),对每个映射调用 try_to_unmap_one;try_to_unmap_one 是具体处理单个 PTE 的函数,它根据调用参数(如是否要写保护、是否要 swap、unmap 的理由)决定如何处理该 PTE——若需要写保护(如 KSM、NUMA hint),则只把 PTE 改为只读(写 PTE 权限位);若需要换出(swap out),则把 PTE 替换为 swap 条目或 migration PTE,并记录页表状态。差异核心在"分工":try_to_unmap 负责遍历调度与汇总结果,try_to_unmap_one 负责对单个 PTE 做实际修改(写页表),并可能通过 ptep_clear_flush 触发 TLB 失效。写 PTE 的实际动作发生在 try_to_unmap_one 内部,且依据模式决定写入的是"只读位"还是"swap/migration 条目"。

这是典型的"遍历层 / 单点处理层"分层,try_to_unmap 不关心具体 PTE 内容,只负责把遍历交给 try_to_unmap_one 逐个处理,从而让 swap、写保护、hint 等不同模式复用同一套遍历框架。

#
★★

16. KSM 与 THP(Transparent Hugepage)在 merge 策略的差异?

KSM 与 THP(Transparent Hugepage)在 merge(合并)策略上的差异是什么?

  • KSM 按页内容合并物理页
  • THP 按虚拟地址合并为 2MB 大页
  • 合并目标与触发方式不同

KSM 与 THP 虽然都叫"merge/合并",但目标完全不同。KSM 是"内容去重":它比较不同进程/不同虚拟地址上内容相同的匿名页,把它们合并为同一个物理页(共享),以减少物理内存占用,合并粒度是 4KB 页,与虚拟地址连续性无关。THP 是"地址整合":它把一段连续虚拟地址(2MB)对应的多个 4KB 页合并为一个大页(pmd),以降低页表开销、提高 TLB 命中率,即使这些页内容不同也会合并。触发方式也不同:KSM 由守护线程周期性扫描 + madvise 标记驱动,THP 由缺页分配或 khugepaged 后台把映射折叠为大页驱动。两者可以同时存在但互不冲突:KSM 合并内容相同的页,THP 合并地址连续的页,前者省内存、后者提性能。

理解差异的关键是"合并的依据":KSM 按内容、THP 按地址。因此 KSM 节省内存面向重复内容,THP 提升性能面向地址局部性。

#
★★

17. mseal() 与 mprotect 的 PROT_READ 页面卸载语义差异?

mseal() 与 mprotect 的 PROT_READ 页面卸载(unmap)语义差异是什么?

  • mprotect 只改权限不拆映射
  • mseal 锁定区域不可 unmap
  • 两者的持久性差异

mprotect(addr, len, PROT_READ) 只是把指定区域的访问权限改为只读,区域本身仍然存在、其映射关系不变,且之后仍可再次调用 mprotect 改回其他权限,或对该区域执行 munmap 卸载——即 mprotect 是"可逆的、一次性的权限调整"。mseal() 则不同:它把区域标记为 VM_SEALED,此后该区域被"永久锁定",既不能 munmap 卸载,也不能再用 mprotect 改变权限,任何后续布局或权限变更都会被拒绝(EPERM)。语义差异的本质是:mprotect 是"改变权限位"而非"锁定",可反复调用;mseal 是"一次性永久冻结",把该区域从"可变"变为"不可变"。因此对 PROT_READ 页,mprotect 只读是可撤销的,mseal 的只读则是加上"不可再卸载/不可改权限"的强约束。

两者看似都让页面只读,但 mprotect 只保证"当前只读",mseal 额外保证"永远只读不可拆",后者提供了防护所需的不可变语义。

#
★★

18. NUMA balancing 如何使用 rmap 找到页面的所有 PTE 来设置 hint 位,page_referenced 扫描的开销如何控制?

NUMA balancing 如何使用 rmap 找到页面的所有 PTE 来设置 hint 位,page_referenced 扫描的开销如何控制?

  • rmap 遍历设置 hint 位
  • page_referenced 的扫描机制
  • 扫描速率与采样控制

NUMA balancing 通过 rmap 遍历一个页的所有映射,对每个 PTE 设置 hint 位(如把 PTE 标记为"该页被访问"的提示),随后在访问时通过缺页(page fault)记录该页被哪个 CPU 节点访问,从而把访问模式与 node 关联起来。page_referenced 则用于检查页是否在一个周期内被引用过,它同样借助 rmap 遍历映射并检查 PTE 的 accessed/protect 位,返回是否被引用。为控制开销,内核不会对每个页每个时钟周期都做全量扫描,而是:1) 通过 numa_balancing_scan_size/scan_rate 限制每轮扫描的页数与速率,按比例抽样而不是全扫;2) 只在 hint 位被置位后触发一次缺页采样,避免持续扫描;3) 用 rmap_walk_control 的 trylock(遇锁竞争即退出)、提前终止回调与 TLB 批量失效(TTU_BATCH_FLUSH)控制单次遍历开销。这样把"找引用"的成本从"全量扫描"降到"抽样 + 事件驱动"。

rmap 提供了"按页找所有 PTE"的能力,但 NUMA balancing 的实用性取决于把扫描频率控制住,通过采样、比例限制与事件触发,在检测访问模式与付出扫描开销之间取得平衡。

#
★★

19. THP 大页在部分映射时如何利用 rmap 找到每个 PTE,拆分(split)大页时反向映射如何更新?

THP 大页在部分映射时如何利用 rmap 找到每个 PTE,拆分(split)大页时反向映射如何更新?

  • THP 的 pmd 级映射与 rmap
  • 部分映射时按 subpage 找 PTE
  • split 大页时的反向映射重建

THP 大页通常以 pmd 级映射(2MB)存在,此时 rmap 用 folio 的 anon_vma 通过 pmd 遍历找到对应的大页映射;但当大页被部分映射(例如一个大页被 split 成多个 4KB 页,或一个 THP 只被部分 PTE 引用)时,rmap 需要按 subpage(每个 4KB 子页)分别找到对应的 PTE。内核为此在 anon_vma 中维护了按 subpage 索引的映射计数与结构,rmap 遍历时能定位每个子页对应的 PTE。split 大页时,内核把该页的 pmd 映射拆成 512 个 pte 映射,并同时更新反向映射:把大页的 folio 映射信息拆分为子页的映射,重建每个子页的 anon_vma 关联,使后续对单页的 rmap 操作(swap、写保护、迁移)能正确按子页工作。这一过程保证 rmap 在"大页"与"拆分后的子页"两种粒度下都一致、正确。

THP 与 rmap 的交互难点在于粒度切换:大页时按 pmd 找映射,拆分后按 pte 找每个子页的映射,split 必须同步更新反向映射,否则后续按页操作会失准。

#

20. Linux 5.16 引入 folio 统一"page_head + tail page"、6.18 起内存描述符(memdesc)重构继续演进页描述符的工程价值?

Linux 5.16 引入的 folio 把"page_head + tail page"统一为 folio,Linux 6.18 起内存描述符(memdesc)重构又继续把页描述符从 struct page 中拆分,这两步的工程价值是什么?

  • folio(5.16)与 memdesc(6.18+)重构的背景
  • head/tail 统一为 folio
  • 简化代码与减少歧义

传统上复合页(compound page)用 page_head 携带元数据、tail page 通过 compound_head 回指 head,处理大页时存在 head/tail 分裂与歧义(很多函数要区分 head vs tail)。Linux 5.16 引入 folio(struct folio):把整个复合页抽象为统一对象,head 与 tail 的差异被封装进 folio 内部,不再让调用者关心,这是第一步。6.18 起,内核开始推进 memdesc(memory descriptor)重构:以 Matthew Wilcox 的 memdesc 系列(首个实质合入为 6.18 的 "mm: introduce memdesc_flags_t",2025 年随 mm-stable 进入主线,后续版本持续推进)把 struct page 中的字段进一步拆分、以 memdesc 概念承载页描述信息,按项目路线图最终把 struct page 大幅瘦身(如 Page2026 目标 16 字节的 memdesc+private 布局),并使 slab、folio、page 等各自持有独立的描述符。工程价值在于:1) 消除大量 head/tail 判断与 compound_head 跳转,简化代码、减少错误;2) folio 独立承载 refcount、order、映射等元数据,便于后续扩展(如更大的页、不同内存类型);3) 让 THP、HugeTLB、page cache 等按 folio 统一操作,降低维护成本;4) memdesc 阶段进一步把"页的描述信息"与 struct page 解耦,为单独分配内存描述符、彻底摆脱 struct page 布局约束(如 64KB/更大页粒度扩展)奠定基础。

重构的实质是两步:folio 把"复合页是一个整体"在类型层面显式化(5.16),memdesc 再把页描述符从 struct page 中拆出(6.18+,持续推进),使内核不再把页碎片化为 head/tail 的隐式关系,提升可读性与可扩展性。

#

21. folio_add/folio_put 的 refcount 是否双计数在 testfolio 和生产 folio 的工程边界?

folio_add/folio_put 的 refcount 在 testfolio 与生产 folio 中是否存在双计数,其工程边界是什么?

  • folio 的 refcount 语义
  • testfolio(测试页)与生产页的区别
  • 双计数问题的边界

在 folio 重构中,folio 的 refcount 是"整个 folio 一次计数",而不是按 head+tail 各计一次——即 folio 级别的引用计数不会把 head 和 tail 当作两个独立对象双计数。testfolio(测试/临时的 folio 包装)通常只是把单个 page 包装成 order-0 的 folio 用于统一 API,其 refcount 与生产 folio 共享同一套 semantics,不会出现"同一个物理页被 head 和 tail 各 +1"的双计数。工程边界在于:folio_add/folio_put 操作的是"folio 整体"的引用,调用方必须保证在一个 folio 生命周期内 refcount 的增减成对,且不能把同一 folio 的 refcount 与其中某个 subpage 的计数混为一谈。这样保证了在测试与生产路径上 refcount 的一致性,避免页被提前释放或泄漏。

双计数问题源于"一个对象被两个结构表示",folio 通过"一次算一个 folio"的 refcount 语义消除了 head/tail 各计一次的不一致,testfolio 只是复用同一语义,不引入额外计数。

#

22. folio 在 bio / splice / mmap 的 page descriptor 简化代码?

folio 在 bio、splice、mmap 等路径中如何简化 page descriptor 的处理代码?

  • bio 基于 folio 的批次处理
  • splice 的 folio 操作
  • mmap 的 folio 映射统一

在 bio(块 I/O)路径中,此前一个 bio 段常以 page 为单位组织,处理大页时需考虑 head/tail 与 compound 边界;基于 folio 后,bio 可以直接以 folio 为单位处理,免去逐页判断 head/tail,简化了 I/O 请求的构建与完成回调。在 splice(管道数据传输)路径中,splice_read/write 原先对每个 page 做映射与引用计数,使用 folio 后可以整块处理一个 folio,减少重复的 page 操作。在 mmap 路径中,folio 统一了文件映射与大页映射的 page descriptor 语义,使 mmap 的 fault 处理、缓存操作都按 folio 进行,API 更一致。总的效果是:这些路径不再需要区分 page 与 compound page 的细节,代码更短、边界更少、批量处理更高效。

folio 的收益是"把以页为单位的零散操作提升为以 folio 为单位的批量操作",在 bio、splice、mmap 这些高频路径上同时减少了代码量与逐页开销。

#

23. memdesc 重构与 GUP(get_user_pages)的协同?

memdesc 重构与 GUP(get_user_pages)如何协同?

  • GUP 的 pin 操作
  • memdesc 对页描述符的抽象
  • pin 与 folio 的交互

GUP(get_user_pages)用于把用户虚拟页固定(pin)下来,得到物理页的引用,供内核(如 DMA、bio、特殊 I/O)使用,其核心是增加页的 refcount 并防止页被回收/迁移。folio 重构(5.16 起,memdesc 在此基础上继续演进)把页描述符统一抽象为 folio,GUP 的接口也随之改为基于 folio(如 try_grab_folio、pin_user_pages),对一个 folio 做一次 pin 即可固定整个大页,而无需逐页 pin 且处理 head/tail。二者协同的工程价值在于:GUP 借助 folio 的 refcount 语义更清晰地管理 pin 计数,避免 tail 计数不一致;同时 DMA/I/O 双方以 folio 为单位协调,pin 与 unpin 成对,减少了错误与开销,并让 GUP 与页回收、迁移等偶发路径在 folio 语义下正确协作。

folio/memdesc 让 GUP 的"固定一个页"统一为"固定一个 folio",既简化了 pin 管理,也避免了 head/tail 造成的 pin 计数歧义,是重构与关键子系统协同的例证。