__setup_per_zone_wmarks()是内存管理中用于计算并设置每个 zone 水位线(watermark)的核心函数。水位线决定了 kswapd 何时启动回收、直接回收何时触发,以及分配器在什么条件下允许分配。
核心作用与背景
每个 zone 有三个水位线:
WMARK_MIN:最低水位。低于此值,分配器会触发直接回收,甚至 OOM。WMARK_LOW:低水位。低于此值,唤醒 kswapd 后台回收。WMARK_HIGH:高水位。kswapd 回收到此值以上才停止。
__setup_per_zone_wmarks()根据全局的min_free_kbytes和watermark_scale_factor,为每个 zone 计算这三个值。
典型实现
static void __setup_per_zone_wmarks(void) { unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10); unsigned long lowmem_pages = 0; struct zone *zone; unsigned long flags;/* Calculate total number of !ZONE_HIGHMEM pages */ for_each_zone(zone) { if (!is_highmem(zone)) lowmem_pages += zone->managed_pages; } for_each_zone(zone) { u64 tmp; spin_lock_irqsave(&zone->lock, flags); /* * 按 zone 在低端内存中的占比分配 min_free_kbytes */ tmp = (u64)pages_min * zone->managed_pages; do_div(tmp, lowmem_pages); if (is_highmem(zone)) { /* * HIGHMEM 没有自己独立的水位,使用低端内存水位的 * 四分之一作为近似值。 */ unsigned long min_pages; min_pages = zone->managed_pages / 1024; min_pages = clamp_val(min_pages, SWAP_CLUSTER_MAX, 128UL * 1024 * 1024 / PAGE_SIZE); min_pages >>= 2; zone->_watermark[WMARK_MIN] = min_pages; } else { zone->_watermark[WMARK_MIN] = tmp; } /* * 设置 LOW 和 HIGH 水位 * 间距由 watermark_scale_factor 控制 */ tmp = max_t(u64, tmp >> 2, mult_frac(zone->managed_pages, watermark_scale_factor, 10000)); zone->watermark_boost = 0; zone->_watermark[WMARK_LOW] = min_wmark_pages(zone) + tmp; zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2; zone->watermark_boost = 0; spin_unlock_irqrestore(&zone->lock, flags); } /* 更新 totalreserve_pages */ calculate_totalreserve_pages();}
逐步解析
1. 计算pages_min和lowmem_pages
unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10); unsigned long lowmem_pages = 0; for_each_zone(zone) { if (!is_highmem(zone)) lowmem_pages += zone->managed_pages; }min_free_kbytes从 KB 转成页数。lowmem_pages:所有非 HIGHMEMzone 的 managed_pages 总和。HIGHMEM 不参与min_free_kbytes的分配。
2. 按比例分配WMARK_MIN
tmp = (u64)pages_min * zone->managed_pages; do_div(tmp, lowmem_pages);
每个非 HIGHMEM zone 按自己在低端内存中的占比,分到
min_free_kbytes的一部分。用
do_div做 64 位除法,避免溢出。
3. HIGHMEM 的特殊处理
if (is_highmem(zone)) { min_pages = zone->managed_pages / 1024; min_pages = clamp_val(min_pages, SWAP_CLUSTER_MAX, 128UL * 1024 * 1024 / PAGE_SIZE); min_pages >>= 2; zone->_watermark[WMARK_MIN] = min_pages; }HIGHMEM 没有独立的 min_free_kbytes 分配,用
managed_pages / 1024作为近似值。clamp_val限制在[SWAP_CLUSTER_MAX, 128MB]之间。>> 2即除以 4,进一步降低 HIGHMEM 的 min 水位。
4. 计算WMARK_LOW和WMARK_HIGH
tmp = max_t(u64, tmp >> 2, mult_frac(zone->managed_pages, watermark_scale_factor, 10000)); zone->_watermark[WMARK_LOW] = min_wmark_pages(zone) + tmp; zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;
tmp是 LOW 和 HIGH 之间的间距:至少是
WMARK_MIN / 4(tmp >> 2);或者由
watermark_scale_factor决定(默认 10,即 0.1% 的 managed_pages)。取两者较大值。
WMARK_LOW = WMARK_MIN + tmp;WMARK_HIGH = WMARK_MIN + 2 * tmp。
5. 重置watermark_boost
zone->watermark_boost = 0;
boost_watermark()会临时抬高水位。重新计算水位时,先清零 boost。
6. 更新totalreserve_pages
calculate_totalreserve_pages();
calculate_totalreserve_pages():水位变化后,重新汇总系统保留页数。
调用场景
它被setup_per_zone_wmarks()封装调用,后者在以下时机执行:
启动初始化:
init_per_zone_wmark_min();内存热插拔:zone 大小变化后;
CPU 热插拔:在线 CPU 数变化影响水位计算时;
sysctl 修改:调整
min_free_kbytes、watermark_scale_factor后。
历史演进
2021 年:Mel Gorman 引入watermark_scale_factor和mult_frac计算,替代了之前按managed_pages / 100的粗略方式,使水位间距更可配置。
2023 年:boost_watermark()和watermark_boost_factor机制被引入,用于在碎片化事件时临时抬高水位。
watermark_boost的重置:每次重新计算水位时清零,避免过期的 boost 影响新水位。
总结
__setup_per_zone_wmarks()是水位线的计算器:它把全局的min_free_kbytes按各非 HIGHMEM zone 的managed_pages占比分配为WMARK_MIN,HIGHMEM 用managed_pages / 1024 / 4近似;然后用max(WMARK_MIN/4, watermark_scale_factor * managed_pages / 10000)作为间距,计算出WMARK_LOW和WMARK_HIGH;最后清零watermark_boost并更新totalreserve_pages。它是内存回收触发时机和分配器水位判断的基础。