decay_pcp_high()是 PCP(Per-CPU Pages)缓存自适应调节机制的核心函数,负责在系统负载降低时逐步降低 PCP 的高水位线(pcp->high),并释放多余的空闲页回伙伴系统。
核心作用与背景
PCP 缓存的high和batch值会根据分配压力动态调整:当分配频繁时,high会被抬高以缓存更多页、减少锁竞争;当分配压力下降后,high需要逐步衰减回最小值,避免空闲页长期滞留在 PCP 中、无法被其他用途(如内存回收、高阶分配)使用。
decay_pcp_high()正是执行“衰减”动作的函数,通常由vmstat_work周期性触发。
核心逻辑
根据内核补丁中的实现,它的典型逻辑如下:
bool decay_pcp_high(struct zone *zone, struct per_cpu_pages *pcp) { int high_min, to_drain, batch; bool todo = false; high_min = READ_ONCE(pcp->high_min); batch = READ_ONCE(pcp->batch); /* * Decrease pcp->high periodically to try to free possible * idle PCP pages. */ if (pcp->high > high_min) { pcp->high = max3(pcp->count - (batch << CONFIG_PCP_BATCH_SCALE_MAX), pcp->high * 4 / 5, high_min); if (pcp->high > high_min) todo = true; } /* * Drain pages if pcp->count exceeds pcp->high. */ to_drain = pcp->count - pcp->high; if (to_drain > 0) { spin_lock(&pcp->lock); free_pcppages_bulk(zone, to_drain, pcp, 0); spin_unlock(&pcp->lock); todo = true; } return todo; }逐步说明
1. 衰减pcp->high
pcp->high = max3(pcp->count - (batch << CONFIG_PCP_BATCH_SCALE_MAX), pcp->high * 4 / 5, high_min);
每次调用将
pcp->high乘以 4/5(即降低 20%),逐步逼近high_min。同时有两个下限保护:
pcp->count - (batch << CONFIG_PCP_BATCH_SCALE_MAX):确保high不会低于当前缓存页数太多,避免free_pcppages_bulk一次性释放过多页(CONFIG_PCP_BATCH_SCALE_MAX默认 5,即batch * 32);high_min:绝不低于最小高水位。
若衰减后
pcp->high仍大于high_min,说明还有“工作要做”,todo置为true。
2. 释放超出high的页
to_drain = pcp->count - pcp->high; if (to_drain > 0) { spin_lock(&pcp->lock); free_pcppages_bulk(zone, to_drain, pcp, 0); spin_unlock(&pcp->lock); todo = true; }如果当前 PCP 中的页数超过了新水位,就把多出的部分通过
free_pcppages_bulk()归还伙伴系统。释放时持有
pcp->lock,释放后立即解锁。
2025 年的批处理优化
2025 年 9 月的补丁(Joshua Hahn)发现一个问题:to_drain可能远大于pcp->batch。如果一次性释放所有超出页,free_pcppages_bulk()可能长时间持有pcp->lock和zone->lock,饿死其他需要这些锁的线程。
修复方案是将单次if (to_drain > 0)改为while循环,每次只释放min(to_drain, batch)页,并在每次迭代之间释放并重新获取pcp->lock:
while (to_drain > 0) { to_drain_batched = min(to_drain, batch); spin_lock(&pcp->lock); free_pcppages_bulk(zone, to_drain_batched, pcp, 0); spin_unlock(&pcp->lock); todo = true; to_drain -= to_drain_batched; }这样既保证了总释放页数不变,又给了其他线程获取锁的机会,避免锁饥饿。
在 PCP 机制中的位置
它由vmstat_shepherd周期性调用(通过vmstat_work),是 PCP“涨落”自适应的下降沿:
分配压力大 → pcp->high 被抬高(缓存更多页) 分配压力小 → decay_pcp_high() 周期性触发 ├── pcp->high *= 4/5,逐步降低 └── free_pcppages_bulk() 释放超出页
与 NOHZ 的交互问题
在CONFIG_NOHZ_FULL配置下,进入 NOHZ 的 CPU 可能长时间不执行vmstat_work,导致decay_pcp_high()无法被调用,pcp->high一直停留在高位,大量空闲页滞留在 PCP 中不被释放。