背景

vmalloc 提供了 vrealloc,可以在原地址附近调整一段 vmalloc 分配的大小。当它缩小分配时,只更新记账(requested_size、KASAN shadow 等),并不会归还底层的物理页:那些不再被使用的尾部页,要一直占用到整段分配被 vfree 才释放。

┌────────────────────────────────┐
│      扩展分配接口缩小分配      │
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│   只更新记账,物理页照旧占用   │  不再使用的尾部页仍占着,直到彻底释放
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│ 缩小后多余的物理内存被白白浪费 │
└────────────────────────────────┘

这意味着,一段分配一旦缩小,多出来的物理内存就在它的剩余生命周期里被白白占用。vrealloc 早期就留了 TODO:缩小跨过页边界时,应该 unmap 并释放不再需要的尾部页,把物理内存及时还回去。本系列就是兑现这个 TODO。

问题

  • vrealloc 缩小只更新记账,不释放物理页
  • 不再使用的尾部页占用到 vfree
  • 缩小后多余的物理内存被白白浪费
  • 这段浪费持续整个分配的剩余生命周期

方案

核心是给 vrealloc 的缩小路径补上释放。

当新大小跨过一个页边界,就把不再需要的尾部页 unmap 并归还给 page allocator。启发式很简单:只要有至少一个完整页变为未使用就释放。

旧:缩小只改记账
┌────────────────────────┐
│  缩小后尾部页不再使用  │
└───────────┬────────────┘
            ▼
┌────────────────────────┐
│ 物理页仍占用到彻底释放 │
└────────────────────────┘

新:缩小即释放尾部页
┌────────────────────────────────┐
│ 跨页边界则取消映射并释放尾部页 │
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐  用节点锁同步并发,并通知 kmemleak 缩小后的范
│  大页、权限、用户映射分配跳过  │  围
└────────────────────────────────┘

并非所有分配都能这样释放,所以跳过几类。大页分配要释放一部分,得先把大页拆开,不在 shrink 路径的职责内,跳过。带「归还页前须重置 direct-map 权限」标记的分配也跳过:那道权限重置归 vfree 管,shrink 路径不便越俎代庖。带用户映射的分配同样跳过:用户映射的合法性仍按分配的原大小校验,一旦释放了尾部页,那段未映射范围就会在校验时查不到对应的页。

释放还要处理两件配套:

  • 并发:shrink 路径先拿节点锁同步,再释放页,避免与并发读者相撞
  • kmemleak:通知它分配变小了,免得它的 scanner 在新取消映射的虚拟地址上触发 fault

虚拟地址范围则刻意保留不动,为将来可能的 grow-in-place 留地址。

系列前段为这次释放铺路:从 vfree 抽出一个「释放一段页」的 helper,并把 grow-in-place 检查与对 VM_ALLOC 区域的读取都改成按物理页数判断,让页数账目可信。

收益

作者未提供性能数据。从机制与代码逻辑推断的预期收益:

  • vrealloc 缩小后立即归还不再使用的物理页,避免它们被白白占用到 vfree
  • 大幅缩小的分配不再长期背负多余的物理内存,vmalloc 占用更紧贴实际使用量
  • 配套的 kmemleak 通知与节点锁同步保证释放路径的正确性