内存管理-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)    收藏  举报

导航