背景
init_on_alloc 是页分配器的一个安全选项:开启后,每页从分配器拿出去前都要先清零,防止旧内容泄漏。kernel_init_pages 负责这件事,但它用的是 clear_highpage_kasan_tagged 逐页清零:每一页都单独 kmap_local_page 做一次临时映射、单独调一次架构清零原语,再 kunmap_local。
┌────────────────────────┐
│ 分配时按页逐个清零 │
└───────────┬────────────┘
▼
┌────────────────────────┐
│ 每页都要做临时映射再清 │ 每页单独调架构清零原语,连续内存的优势用不上
└───────────┬────────────┘
▼
┌────────────────────────┐
│ 大分配时清零开销显著 │
└────────────────────────┘
逐页清零的代价在连续大分配上尤为浪费:明明整段分配的物理页是连续的,架构清零原语(如 DC ZVA、REP STOSB)本可以一次覆盖一整片,却被拆成每页一次。per-page 的 kmap 开销和单页粒度的清零,叠加在大分配上就成了可观的 kernel time。
问题
- init_on_alloc 逐页清零,每页单独 kmap + 单独架构原语
- 架构清零原语无法覆盖连续范围
- per-page kmap 与单页清零粒度叠加
- 大分配的 kernel time 开销显著
方案
核心是把逐页清零换成整段批量清零。
新增 clear_highpages_kasan_tagged 批量 helper:在 !HIGHMEM 系统上,对整段连续分配范围直接调 clear_pages,一次清零覆盖整片,既绕过 per-page kmap,又让单次架构清零原语覆盖整个分配。
旧:逐页清零
┌──────────────────────────┐
│ 每页单独映射、单独清零 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 架构原语无法覆盖连续范围 │
└──────────────────────────┘
新:整段批量清零
┌──────────────────────────────────┐
│ 对整段连续分配一次清零 │
└────────────────┬─────────────────┘
▼
┌──────────────────────────────────┐
│ 单次架构原语覆盖整段,免逐页映射 │ 高端内存路径回退逐页清零
└──────────────────────────────────┘
HIGHMEM 路径仍需逐页 kmap 清零(这些页必须映射才能访问),所以批量路径只对 !HIGHMEM 生效,HIGHMEM 回退逐页。kernel_init_pages 由此变成 trivial wrapper,被直接调用替代。
收益
作者在 init_on_alloc=1 配置下测了 HugeTLB 分配 micro-benchmark 与两个真实 workload 的 kernel time(sys)。
HugeTLB 分配 micro-benchmark(8192 × 2MB HugeTLB = 16GB,init_on_alloc=1):
| 指标 | Before | After | 变化 |
|---|---|---|---|
| 分配耗时 | 0.445s | 0.166s | -62.7%(2.68x faster) |
真实 workload 的 kernel time(sys,init_on_alloc=1):
| Workload | Before | After | 变化 |
|---|---|---|---|
| Graph500 64C128T | 30m 41.8s | 15m 14.8s | -50.3% |
| Graph500 16C32T | 15m 56.7s | 9m 43.7s | -39.0% |
| Pagerank 32T | 1m 58.5s | 1m 12.8s | -38.5% |
| Pagerank 128T | 2m 36.3s | 1m 40.4s | -35.7% |
init_on_alloc 场景下,大分配与大数据集 workload 的 kernel time 普遍下降 35%~50%;HugeTLB 这种纯粹的大分配更是快了 2.68 倍。