内存管理-77-kernel-internals系列-Memory专题-Caching & Reclaim-5-Zone Reclaim Policy
一、Zone Reclaim Policy 章节翻译
注: 翻译自 https://kernel-internals.org/mm/zone-reclaim/
Zone 回收策略(Zone Reclaim Policy),内核如何按 zone 回收内存、vm.zone_reclaim_mode 的作用(使能 CONFIG_NUMA 才有),以及为什么它默认是关闭的。
1. 关键源文件
------------------------------------------------------------------------------------------------------------------------- 文件 关联内容 ------------------------------------------------------------------------------------------------------------------------- mm/vmscan.c node_reclaim()、balance_pgdat()、node_reclaim_mode mm/page_alloc.c get_page_from_freelist()、zone_watermark_fast()、zone_allows_reclaim()、boost_watermark() include/linux/mmzone.h struct zone、enum zone_watermarks、enum zone_type、水线访问函数 include/uapi/linux/mempolicy.h RECLAIM_ZONE、RECLAIM_WRITE、RECLAIM_UNMAP 位定义 include/linux/topology.h RECLAIM_DISTANCE 默认值 -------------------------------------------------------------------------------------------------------------------------
2. 内存 Zones
Linux 把物理内存划分成多个 zone,这样有不同寻址约束的分配才能各得其所、不造成浪费。Zone 类型在 include/linux/mmzone.h 的 enum zone_type 中定义:
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA, /* 最低 ~16MB:ISA/传统 DMA 设备 */
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32, /* 最低 4GB:32 位 DMA 设备 */
#endif
ZONE_NORMAL, /* 其余直接映射的物理内存 */
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,/* 32 位架构上 ZONE_NORMAL 之上的部分 */
#endif
ZONE_MOVABLE,/* 只放可迁移页;便于热插拔/THP */
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE, /* 持久化/设备内存 */
#endif
__MAX_NR_ZONES
};
Zone 之间有层级关系。需要 ZONE_DMA 内存的分配只能用 ZONE_DMA。请求 ZONE_NORMAL 内存的分配在必要时可以降级(fallback)到 ZONE_DMA32 或 ZONE_DMA,但反过来不行。每个 zone 是独立的:有自己的空闲链表、自己的水线、自己的回收压力(但是 lru 是 per-node 的)。
物理地址空间(典型 64 位 x86):
0 16MB 4GB RAM top
|———ZONE_DMA———|———ZONE_DMA32———|———————ZONE_NORMAL————————|
GFP_DMA GFP_DMA32 GFP_KERNEL(默认)
3. Zone 水线(Watermark)
每个 zone 有三个水线,控制何时开始回收。它们存在 struct zone._watermark[] 中,通过内联辅助函数访问 —— 永远不要直接读数组。
3.1 enum zone_watermarks
/* include/linux/mmzone.h */
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
WMARK_PROMO, /* 用于内存分层 / NUMA balancing */
NR_WMARK
};
三个主要级别:
------------------------------------------------------------------------------------ 水线 含义 ------------------------------------------------------------------------------------ WMARK_MIN 最后防线。空闲页回升到该水线之上前,直接回收会卡住分配进程 WMARK_LOW 后台阈值。跌破这里会唤醒 kswapd WMARK_HIGH 目标。kswapd 回收到空闲页达到该水线后休眠 ------------------------------------------------------------------------------------
3.2 水线访问宏
struct zone 把原始水线值存在 _watermark[] 里,读取时再加上 watermark_boost 字段,以实现临时抬升(见水线 Boost一节)。永远用这些访问函数:
/* include/linux/mmzone.h */
struct zone {
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;
/* ... */
};
static inline unsigned long wmark_pages(const struct zone *z, enum zone_watermarks w)
{
return z->_watermark[w] + z->watermark_boost;
}
static inline unsigned long min_wmark_pages(const struct zone *z)
{
return wmark_pages(z, WMARK_MIN);
}
static inline unsigned long low_wmark_pages(const struct zone *z)
{
return wmark_pages(z, WMARK_LOW);
}
static inline unsigned long high_wmark_pages(const struct zone *z)
{
return wmark_pages(z, WMARK_HIGH);
}
水线值由 vm.min_free_kbytes 派生而来。调大该 sysctl,会按比例抬高所有 zone 的三个级别。
3.3 检查水线:zone_watermark_fast() 与 zone_watermark_ok()
分配器在热路径调用 zone_watermark_fast(),慢速完整检查则回退到 zone_watermark_ok()。两者都在 mm/page_alloc.c:
bool zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, int highest_zoneidx, unsigned int alloc_flags); static inline bool zone_watermark_fast(struct zone *z, unsigned int order,unsigned long mark, int highest_zoneidx, unsigned int alloc_flags, gfp_t gfp_mask);
zone_watermark_fast() 先走快速路径:如果空闲页超过 mark + lowmem_reserve[highest_zoneidx],且不考虑 order-N 的碎片问题,就直接返回 true。否则调用 __zone_watermark_ok(),按所有空闲链表 order 做完整核算。
4. Per-Zone 的 kswapd:balance_pgdat()
kswapd 是 per-node(按 pgdat)的内核线程,但回收时是逐个 zone 进行的。核心循环是 mm/vmscan.c 中的 balance_pgdat():
/* * kswapd 按 highmem->normal->dma 方向扫描 zone, 即由高到低扫描zone。它跳过 free_pages > high_wmark_pages(zone) 的 zone,但一旦发现 * free_pages <= high_wmark_pages(zone) 的 zone,该 zone 及更低 zone 中的任何页在同一轮中都可被回收,直到至少一个可用 zone 被均衡。 */ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
方向很关键:kswapd 从最高 zone 索引向下扫描(例如 ZONE_MOVABLE → ZONE_NORMAL → ZONE_DMA32 → ZONE_DMA)。一旦发现某个 zone 低于 WMARK_HIGH,该 zone 及所有更低的 zone 在同一轮中都成为回收候选。循环持续到至少一个满足触发分配条件的 zone 被均衡回 WMARK_HIGH 以上。
kswapd zone 扫描顺序(从高到低):
ZONE_MOVABLE ← 最先扫描 ZONE_NORMAL ZONE_DMA32 ZONE_DMA ← 最后扫描
如果 ZONE_NORMAL 跌破 WMARK_HIGH,同一轮会从 ZONE_NORMAL、ZONE_DMA32 和 ZONE_DMA 回收页面。
kswapd 还要处理水线 boost:每次进入 balance_pgdat() 之初,它把每个 zone 的 zone->watermark_boost 快照到局部 zone_boosts[] 数组。每次 shrink_node 迭代后,按快照量递减 zone->watermark_boost,随着回收推进逐步消化掉 boost。
5. node_reclaim() 与 vm.zone_reclaim_mode
5.1 The sysctl
vm.zone_reclaim_mode 是内核变量 node_reclaim_mode(在 mm/vmscan.c 中声明)的用户可见名字(使能 CONFIG_NUMA 才有):
/* mm/vmscan.c */ int node_reclaim_mode __read_mostly; /* 默认:0(关闭) */ //它的位定义在 include/uapi/linux/mempolicy.h: #define RECLAIM_ZONE (1<<0) /* 启用 zone reclaim */ #define RECLAIM_WRITE (1<<1) /* 回收期间写出页面 */ #define RECLAIM_UNMAP (1<<2) /* 回收期间 unmap 页面 */
各位可叠加:
------------------------------------------------------------------ 值 效果 ------------------------------------------------------------------ 0 关闭 zone reclaim(默认) 1 回收本地节点的干净文件页 2 + 允许回收期间回写脏页 4 + 允许 unmap 已映射的文件页 7 启用所有回收类型 ------------------------------------------------------------------
只要任一位被置位,node_reclaim_enabled() 就返回 true:
/* mm/internal.h */
static inline bool node_reclaim_enabled(void)
{
return node_reclaim_mode & (RECLAIM_ZONE|RECLAIM_WRITE|RECLAIM_UNMAP);
}
5.2 get_page_from_freelist() 中的调用点
钩子在 mm/page_alloc.c 的 get_page_from_freelist() 里,这是主分配路径。某个 zone 水线检查失败后,分配器会判断是否值得尝试 node reclaim:
/* mm/page_alloc.c — 在 get_page_from_freelist() 内, 注: 若不使能 CONFIG_NUMA 这里恒 continue 走了 */
if (!node_reclaim_enabled() || !zone_allows_reclaim(zonelist_zone(ac->preferred_zoneref), zone))
continue;
ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
switch (ret) {
case NODE_RECLAIM_NOSCAN: /* 未扫描 */
continue;
case NODE_RECLAIM_FULL: /* 扫了但不可回收 */
continue;
default: /* 回收够了吗? */
if (zone_watermark_ok(zone, order, mark, ac->highest_zoneidx, alloc_flags))
goto try_this_zone;
continue;
}
如果回收成功、该 zone 回到水线以上,分配就从该 zone 继续。否则分配器转向 zonelist 中的下一个 zone。
5.3 node_reclaim() 内部
mm/vmscan.c 中的 node_reclaim() 是同步的、按节点的回收路径。它由分配器直接调用,会阻塞分配进程:
int node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
{
const unsigned long nr_pages = 1 << order;
struct scan_control sc = {
.nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX),
.gfp_mask = current_gfp_context(gfp_mask),
.order = order,
.priority = NODE_RECLAIM_PRIORITY, /* = 4:扫描 zone 的 1/16 */
.may_writepage = !!(node_reclaim_mode & RECLAIM_WRITE),
.may_unmap = !!(node_reclaim_mode & RECLAIM_UNMAP),
.may_swap = 1,
.reclaim_idx = gfp_zone(gfp_mask),
};
/* ... */
}
在真正扫描前,它先检查两个短路条件:
(1) 可回收阈值:如果 node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages,且 slab 可回收量低于 pgdat->min_slab_pages,直接返回 NODE_RECLAIM_FULL(值得回收的东西太少,不值得试)。
(2) CPU 亲和性:如果该节点上有 CPU,且不是调用 CPU 所在的节点,返回 NODE_RECLAIM_NOSCAN。这保证 node_reclaim 只在本节点或无 CPU 的纯内存节点上执行。
成功时 PGSCAN_ZONE_RECLAIM_SUCCESS 加一,失败时 PGSCAN_ZONE_RECLAIM_FAILED 加一。两个计数器在 /proc/vmstat 中显示为 zone_reclaim_success 和 zone_reclaim_failed。
6. 为什么 zone_reclaim_mode 默认关闭
当 zone_reclaim_mode=1 时,内核倾向于先回收本地内存,再回退到远端 NUMA 节点分配。本意是保持 NUMA 亲和性,但实践中有个严重问题:node_reclaim() 会卡住分配进程。 因为它同步运行在分配热路径上,进程要在扫描-回收整个周期里阻塞。在有实质内存压力的系统上,这会给应用带来延迟尖刺。
通过 zonelist 回退到远端节点分配,通常比等本地回收快得多。权衡如下:
-------------------------------------------------------------------------------------------------------------------------------- 场景 建议行为 -------------------------------------------------------------------------------------------------------------------------------- 远端访问只增加 < 2 倍延迟开销 zone_reclaim_mode=0 —— 直接远端分配 应用对 NUMA 亲和要求严格、工作集能装进单节点 zone_reclaim_mode=1 可能有帮助 流式或 I/O 密集型负载 zone_reclaim_mode=0 —— page cache 周转快,本地回收适得其反 大 buffer pool 按节点钉住的数据库 zone_reclaim_mode=1 可减少跨节点流量 --------------------------------------------------------------------------------------------------------------------------------
mm/vmscan.c 中的内核注释说得很直白:
如果非零,当空闲页跌破水线时调用 node_reclaim。
那个“如果非零”就是在暗示“零才是安全的默认值”。现代服务器负载几乎一律保持为 0。
7. RECLAIM_DISTANCE 与 zone_allows_reclaim()
即使 zone_reclaim_mode 非零,对拓扑上离请求节点很近的 zone,内核也不会调用 node_reclaim()。阈值是 include/linux/topology.h 中的 RECLAIM_DISTANCE:
#ifndef RECLAIM_DISTANCE /* * 如果系统中节点间距离大于 RECLAIM_DISTANCE (以 node_distance() 返回的架构相关单位计), * 且启用了 node_reclaim_mode,VM 只会对该距离内的节点调用 node_reclaim()。 */ #define RECLAIM_DISTANCE 30 #endif extern int __read_mostly node_reclaim_distance;
运行时变量 node_reclaim_distance 初始为 RECLAIM_DISTANCE(30),可被平台代码覆盖。例如 AMD EPYC 机器会调大它,因为 2 跳距离(ACPI SLIT 距离 32)的跨节点访问在与回收代价权衡后仍能提升性能。
检查函数是 mm/page_alloc.c 中的 zone_allows_reclaim():
#ifdef CONFIG_NUMA int __read_mostly node_reclaim_distance = RECLAIM_DISTANCE; static bool zone_allows_reclaim(struct zone *local_zone, struct zone *zone) { return node_distance(zone_to_nid(local_zone), zone_to_nid(zone)) <= node_reclaim_distance; }
ACPI SLIT 距离值:LOCAL_DISTANCE=10(同节点),REMOTE_DISTANCE=20(1 跳)。30 的含义是:本地(距离 10)做回收,1 跳远的候选 zone(距离 20 ≤ 30)也允许回收。超过 2 跳(距离 > 30)的 zone 则跳过回收,直接远端分配。
node_distance(local, candidate) <= node_reclaim_distance ?
是 → 可能运行 node_reclaim()
否 → 跳过回收,直接从远端 zone 分配
8. DMA Zone 压力
ZONE_DMA(最低 ~16MB)和 ZONE_DMA32(最低 4GB)相对整机内存很小。它们被设备专属分配(GFP_DMA、GFP_DMA32)占满后不易回收。
8.1 DMA zone 为什么会耗尽
(*) 设备驱动用 GFP_DMA 或 GFP_DMA32 分配 bounce buffer、描述符环、firmware 区域。
(*) 这些分配常常长期持有(随设备生命周期)。
(*) 内核不会自动把 DMA 分配迁移到其他 zone。
8.2 回退与 OOM 行为
(*) GFP_KERNEL 分配(普通内存)在 ZONE_NORMAL 耗尽时可以降级到 ZONE_DMA32 或 ZONE_DMA,但反过来不行。
(*) ZONE_DMA 没有空闲页时的 GFP_DMA 分配,即使 ZONE_NORMAL 还有几个 GB 空闲,也会触发 OOM killer,因为设备访问不到 ZONE_NORMAL 内存。
(*) GFP_KERNEL 分配路径(zonelist 回退顺序):ZONE_NORMAL → ZONE_DMA32 → ZONE_DMA(只能向下,不能向上); GFP_DMA 分配路径:只有 ZONE_DMA —— 耗尽就 OOM
8.3 如何发现 DMA zone 压力
# 对比各 zone 空闲页与水线 grep -A 10 "zone" /proc/zoneinfo | grep -E "Node|zone|free|min|low|high" # 健康 DMA zone 示例: # Node 0, zone DMA # pages free 2652 # min 33 # low 41 # high 49
危险信号:DMA 或 DMA32 的 free 接近 min,同时 dmesg 出现 DMA-zone allocation failure 之类的分配失败日志。
9. 水线 Boost(Watermark Boosting)
9.1 要解决的问题
当高 order 分配(例如 THP)回退到低 migratetype(例如从 MIGRATE_MOVABLE 偷 MIGRATE_UNMOVABLE 的页)时,会污染低 order 空闲链表。若不干预,后续高 order 分配会持续失败,而 kswapd 永远不会被唤醒 —— 因为 zone 名义上还在正常水线之上,只是内部碎片化了。//TODO: ???
9.2 如何工作
mm/page_alloc.c 中的 boost_watermark() 在 try_to_claim_block() 检测到 order 小于 pageblock_order 的 migratetype 借用(fallback)时被调用,临时抬高 zone->watermark_boost:
static inline bool boost_watermark(struct zone *zone)
{
unsigned long max_boost;
if (!watermark_boost_factor)
return false;
/* 太小的 zone 跳过,boost 会导致 OOM */
if ((pageblock_nr_pages * 4) > zone_managed_pages(zone))
return false;
max_boost = mult_frac(zone->_watermark[WMARK_HIGH], watermark_boost_factor, 10000);
if (!max_boost)
return false;
max_boost = max(pageblock_nr_pages, max_boost);
zone->watermark_boost = min(zone->watermark_boost + pageblock_nr_pages, max_boost);
return true;
}
watermark_boost_factor 默认 15000(即 WMARK_HIGH 的 150%)。boost 会加到每次 wmark_pages() 调用上,让 zone 看起来比实际更“满” —— 从而即使名义上在 WMARK_HIGH 之上,也能触发 kswapd。
Boost 后 zone 标志置位 ZONE_BOOSTED_WATERMARK。在下一次 rmqueue() 返回路径检查该标志并唤醒 kswapd:
/* mm/page_alloc.c */ if ((alloc_flags & ALLOC_KSWAPD) && unlikely(test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags))) { clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags); wakeup_kswapd(zone, 0, 0, zone_idx(zone)); }
balance_pgdat() 再逐步消化 boost:每次迭代减去初始 zone_boosts[i] 快照,直到 boost 清零、zone 真正均衡。
9.3 控制 boost 行为
# 默认:15000(WMARK_HIGH 的 150%) cat /proc/sys/vm/watermark_boost_factor # 完全关闭 boosting echo 0 > /proc/sys/vm/watermark_boost_factor
在 THP 回退频繁、无谓的 kswapd 唤醒带来多余回收压力的系统上,可设为 0。
10. 如何观察 Zone 状态
10.1 /proc/zoneinfo
最权威的按 zone 诊断。关键字段:
Node 0, zone Normal pages free 524288 boost 0 ← 当前 watermark_boost min 8192 low 10240 high 12288 spanned 2097152 present 2097152 managed 1998765
重点看 free 低于 low 或接近 min 的 zone。
10.2 /proc/vmstat —— zone reclaim 计数器
这些计数器只在 CONFIG_NUMA=y 时存在:
grep zone_reclaim /proc/vmstat # zone_reclaim_success 42 ← node_reclaim() 释放了足够页 # zone_reclaim_failed 1831 ← node_reclaim() 跑了但没满足
启用了 zone_reclaim_mode 却 zone_reclaim_failed 很高,是强信号:工作集热页在本地不可回收,应该关闭 zone_reclaim_mode。
10.3 快速诊断流程
# 1. 看 zone 水线与空闲页 grep -A 15 "^Node" /proc/zoneinfo | grep -E "Node|zone|free|boost|min|low|high" # 2. 看是否启用了 zone reclaim cat /proc/sys/vm/zone_reclaim_mode # 3. 看回收成功/失败比(仅 NUMA) grep -E "zone_reclaim|pgscan" /proc/vmstat # 4. 看 DMA zone 是否耗尽 grep -A 10 "zone.*DMA" /proc/zoneinfo | grep free # 5. 看是否水线 boost 驱动了 kswapd grep -E "pageoutrun|kswapd" /proc/vmstat
10.4 调优指南
---------------------------------------------------------------------------------------------------------------------------------------------- 症状 可能原因 动作 ---------------------------------------------------------------------------------------------------------------------------------------------- zone_reclaim_failed >> zone_reclaim_success 工作集装不进本地节点 echo 0 > /proc/sys/vm/zone_reclaim_mode NUMA 系统延迟尖刺 zone_reclaim_mode 卡住分配者 关闭 zone reclaim 其他 zone 还有空闲却 OOM DMA zone 耗尽 减少 GFP_DMA / GFP_DMA32 消费者;查驱动内存占用 无内存压力 kswapd 却在跑 水线 boost 生效 看 /proc/zoneinfo 的 boost 字段;考虑调低 watermark_boost_factor 高 order 分配持续失败 Zone 碎片 看 /proc/buddyinfo;考虑 vm.compaction_proactiveness ----------------------------------------------------------------------------------------------------------------------------------------------
11. Further reading
11.1 内核源码
mm/vmscan.c —— node_reclaim()、balance_pgdat()、kswapd() 与 node_reclaim_mode 变量
mm/page_alloc.c —— get_page_from_freelist()、zone_watermark_fast()、zone_watermark_ok()、zone_allows_reclaim()、boost_watermark()
include/linux/mmzone.h —— struct zone 水线字段、enum zone_watermarks、enum zone_type 与水线内联访问函数
include/uapi/linux/mempolicy.h —— RECLAIM_ZONE、RECLAIM_WRITE、RECLAIM_UNMAP 位定义
11.2 内核文档
Documentation/admin-guide/sysctl/vm.rst —— zone_reclaim_mode、min_free_kbytes、watermark_boost_factor、watermark_scale_factor
/proc/sys/vm/zone_reclaim_mode —— 运行时开关;0 关闭,1 / 2 / 4 启用渐进更激进的回收
11.3 LWN 文章
Zone reclaim mode considered harmful —— 为什么 zone_reclaim_mode=1 损害大多数负载的吞吐、为什么默认关闭
Watermark boosting —— watermark_boost_factor 的动机,以及它如何缓解碎片导致的 kswapd 不活跃
DMA zone management —— DMA zone 为什么小、如何耗尽、跨 zone 回退的约束
posted on 2026-10-08 16:12 Hello-World3 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号