背景
DAMON 用一个叫 kdamond 的内核线程监控进程的内存访问,方法是在被监控区域的地址空间里随机取一个地址(sampling_addr),检查它对应 PTE 的 accessed 位,以此估计访问模式。取这个随机地址发生在采样的热路径上。
┌────────────────────────────────────┐
│ kdamond 在采样热路径反复取随机地址 │
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐
│ 每次都走带锁的加密随机数生成 │ 取本地锁、访问 per-CPU 熵池、周期性 ChaCha
└─────────────────┬──────────────────┘ 重填
▼
┌────────────────────────────────────┐
│ 区域数升高时锁开销主导 kdamond │ 区域多时占满大半个核的 CPU
└────────────────────────────────────┘
damon_rand 取随机地址,背后调的是 get_random_u32_below,一个加密级的随机数接口:它用 local_lock_irqsave 保护一个 per-CPU 的批量熵池,还周期性用 ChaCha20 重填。在监控区域数量小的时候这点锁开销不算什么,但当 nr_regions 升到 2 万以上,lock_acquire/local_lock 这对加上 __get_random_u32_below 本身,就成了 kdamond 性能 profile 的主角,把 kdamond 推到吃掉约 72% 单核。
问题
- kdamond 采样热路径用加密级随机数接口
- 该接口取本地锁、访问 per-CPU 熵池、周期性 ChaCha 重填
- 区域数升高时锁开销主导 kdamond CPU
- 把 kdamond 推到约 72% 单核
方案
核心是看清"这个随机数不必是加密级的",从而换掉那把锁。
DAMON 的 sampling_addr 只是选一个地址去看 PTE 的 accessed 位,不暴露给用户态、也不做任何安全决策,完全不需要加密强度的随机性。
旧:每次取随机都过加密随机数锁
┌──────────────────────────┐
│ 采样地址走带锁加密随机数 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 锁与熵池开销主导 CPU │
└──────────────────────────┘
新:每上下文一个无锁 PRNG
┌──────────────────────────────────┐
│ kdamond 是某上下文的唯一消费者 │
└────────────────┬─────────────────┘
▼
┌──────────────────────────────────┐
│ 用无锁 PRNG 产采样地址,无需同步 │ 采样地址非加密用途,仅作页表访问位探针
└──────────────────────────────────┘
于是把 damon_rand 换成一个 lockless 的 lfsr113 伪随机数生成器(struct rnd_state),每个 damon_ctx 持有一个,在 damon_new_ctx 里用 get_random_u64 播种。关键在于 kdamond 是一个 ctx 的唯一消费者,只有它读这个 PRNG,所以连同步都不需要。
范围映射(把 PRNG 输出投到监控区间里)用传统的 reciprocal multiplication,和原 get_random_u32_below 同路;区间跨度超过 U32_MAX(只有 64-bit 才够得到)时走 slow path,拼两个 u32 输出、用 mul_u64_u64_shr 做 64 位宽运算;32-bit 上这段 slow path 是死代码,被编译器消掉。
收益
作者用 paddr 监控、max_nr_regions=20000 实测:
| 指标 | Before | After |
|---|---|---|
| kdamond CPU 占单核 | ~72% | ~50% |
锁开销移除后,kdamond 在区域数高时少吃了约五分之一核,留给系统其它用途;这也让 DAMON 在大区域数监控(如 proactive reclaim)场景下更便宜,更适合长期常驻。