背景
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 通知与节点锁同步保证释放路径的正确性