背景

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 倍。