背景
无锁页表遍历(仅凭 RCU)的好处是减少锁竞争、避免锁序顾虑,让页表遍历快速且正确。它的前提是页表只能在 RCU 宽限期后释放,否则正在被遍历的页表可能被中途释放。
此前的工作已经铺了大部分路:
- Hugh Dickins 2023 年引入 pte_free_defer(),让 khugepaged 的页表回收按 RCU 延迟。
- Qi Zheng 把 x86 的页表释放从 semi-RCU 改为 RCU,把多个架构转换到 tlb_remove_ptdesc(),并引入 CONFIG_HAVE_ARCH_TLB_REMOVE_TABLE 配置。
- Lance Yang 把批量分配失败的回退路径从 IPI 广播改为 synchronize_rcu(),回退路径也 RCU 安全。
经过这些工作,大多数架构已支持 CONFIG_MMU_GATHER_RCU_TABLE_FREE,但仍有架构未转换。另一个缺口在 THP:匿名映射折叠成 THP 时,会把当前由 PMD 映射的 PTE 页表 deposit 到 PMD 里,供日后 split 直接使用。今天这套做法无害:collapse 持有 mmap/VMA 写锁与 anon rmap 锁,普通遍历不会走错,GUP-fast 由 IPI 兜底;改用仅凭 RCU 的遍历后,这张可能仍被遍历的页表会被 deposit 出去,之后在无关 PMD 的 split 中复用改写,遍历可能读到不属于自己的内容。
页表提前释放或复用,RCU 遍历不安全
┌─────────────────────────────────────┐
│ 仅凭 RCU 遍历不加锁,前提是页表不被 │
│ 中途释放 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 未延迟释放的页表被 deposit 复用 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 遍历可能读到被释放或被改写的页表 │
└─────────────────────────────────────┘
问题
- 部分架构未延迟到 RCU 宽限期后释放页表。
- collapse 会把可能仍被遍历的页表 deposit 出去复用。
- 仅凭 RCU 的页表遍历可能读到被释放的页表。
- 各架构释放路径与配置长期分裂,死代码残留。
方案
把剩余架构全部转换到 RCU 宽限期后释放页表,并删除 CONFIG_MMU_GATHER_RCU_TABLE_FREE,让仅凭 RCU 的页表遍历安全。
先补上 THP 的缺口:collapse 时不再挪用当前由 PMD 映射的那张 PTE 页表,而是新分配一张清零的 PTE 页表来 deposit;旧表随即用 pte_free_defer() 等宽限期后释放。这样遍历持有的旧表只会延迟消失,不会再被 deposit 出去改写;deposit 的始终是一张没被遍历过的新表。collapse 路径本来就要分配 huge folio,多分配一张 PTE 表的开销可以忽略,而且 deposit 新表本来就是内核其他 deposit 路径的做法。
架构转换大多是机械切换,现成机制加配置或小改;三个架构需要额外处理:
- sh-X2 的 PMD 从 slab 分配,设 CONFIG_HAVE_ARCH_TLB_REMOVE_TABLE 自定义 __tlb_remove_table(),用指针最低位编码页表层级,按位走不同的释放路径。
- m68k-motorola 改动最大:call_rcu 让释放可能发生在 softirq,get_pointer_table() 与 free_pointer_table() 改用一把禁下半部的 spinlock,softirq 是唯一异步持锁上下文,禁下半部即足够;新 pointer table 的分配逻辑拆成 add_pointer_table(),让锁的正确性更直观。
- sparc32 页表建在共享后备页上,同样用指针最低位编码层级;softirq 释放路径没有 mm 且 mm->page_table_lock 非 IRQ 安全,于是新设一把 IRQ 安全 spinlock 全面取代它做共享页表互斥,nocache pool 的 bit_map 锁同样改为 IRQ 安全。
全部架构转换完成后,CONFIG_MMU_GATHER_RCU_TABLE_FREE 连同为不启用该选项准备的死代码一并删除,自定义释放统一走 CONFIG_HAVE_ARCH_TLB_REMOVE_TABLE。契约随之更新:mm 经 mmgrab() 保活时,遍历正在拆除的页表范围是安全的,但读到的条目在拿页表锁后仍须重新校验;改条目仍须页表锁,并须持有 mmap 或 VMA 锁之一来排除并发拆除。本系列提供的保证仅限于此:页表不会在遍历过程中被释放。
全部页表统一为宽限期后释放
┌─────────────────────────────────┐
│ 旧表延迟释放,改 deposit 新页表 │
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐
│ 剩余架构补齐,特殊架构改 IRQ 锁 │
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐
│ 删配置与死代码,RCU 遍历安全 │
└─────────────────────────────────┘
收益
作者未提供性能数据,从代码逻辑推断的预期收益:
- 仅凭 RCU 即可安全遍历页表,减少锁竞争并避免锁序顾虑。
- 为后续完全 RCU 页表遍历的改动提供前提。
- 各架构私有释放路径与死配置代码删除,页表释放行为统一。