一、前言

承接上一篇,上一章我们介绍了:Memory Reclaim 整体架构

但是:有一个问题没有回答。
Memory Reclaim 到底是谁触发的?

这一篇则回答:

Linux内核在何时决定回收?

  graph TB
      subgraph "用户空间 / 系统调用层"
          APP[应用程序<br/>malloc/mmap/etc]
      end

      subgraph "页面分配入口 (mm/page_alloc.c)"
          alloc_pages["alloc_pages()"]
          __alloc_pages["__alloc_pages()<br/>gfp.h:529"]
          get_page["get_page_from_freelist()<br/>快速路径 - 从 freelist 获取页面"]
      end

      subgraph "慢速路径 (mm/page_alloc.c)"
          slowpath["__alloc_pages_slowpath()<br/>page_alloc.c:4988"]
          wake_all["wake_all_kswapds()<br/>page_alloc.c:4751"]
          direct_reclaim["__alloc_pages_direct_reclaim()<br/>page_alloc.c:4721"]
          perform_reclaim["__perform_reclaim()<br/>page_alloc.c:4693"]
          retry["should_reclaim_retry()<br/>page_alloc.c:4861"]
      end

      subgraph "Direct Reclaim 直接回收 (mm/vmscan.c)"
          try_free["try_to_free_pages()<br/>vmscan.c:3760"]
          do_try_free["do_try_to_free_pages()<br/>vmscan.c:3543"]
          throttle["throttle_direct_reclaim()<br/>节流 - 等待 kswapd"]
          shrink_zones_dr["shrink_zones()<br/>vmscan.c:3430"]
      end

      subgraph "kswapd 后台回收 (mm/vmscan.c)"
          kswapd["kswapd()<br/>vmscan.c:4462<br/>内核线程 - 每个 NUMA node"]
          balance_pgdat["balance_pgdat()<br/>vmscan.c:4119"]
          kswapd_shrink["kswapd_shrink_node()<br/>vmscan.c:4029"]
          prepare_sleep["prepare_kswapd_sleep()<br/>vmscan.c:3990"]
          allow_dr["allow_direct_reclaim()<br/>vmscan.c:3631"]
      end

      subgraph "Node/Zone 回收层 (mm/vmscan.c)"
          shrink_node["shrink_node()<br/>vmscan.c:3174"]
          shrink_node_memcgs["shrink_node_memcgs()<br/>vmscan.c:3112"]
      end

      subgraph "LRU 列表操作 (mm/vmscan.c)"
          shrink_lruvec["shrink_lruvec()<br/>vmscan.c:2914"]
          shrink_list["shrink_list()<br/>vmscan.c:2618"]
          shrink_active["shrink_active_list()<br/>vmscan.c:2445"]
          shrink_inactive["shrink_inactive_list()<br/>vmscan.c:2320"]
      end

      subgraph "Slab 回收 (mm/vmscan.c)"
          shrink_slab["shrink_slab()<br/>vmscan.c:908"]
          do_shrink_slab["do_shrink_slab()"]
          shrink_slab_memcg["shrink_slab_memcg()"]
      end

      subgraph "页面回收细节"
          isolate["isolate_lru_pages()<br/>从 LRU 隔离页面"]
          page_ref["page_referenced()<br/>检查页面引用 (rmap.c)"]
          add_swap["add_to_swap()<br/>swap_state.c:184"]
          try_unmap["try_to_unmap()<br/>取消页面映射 (rmap.c)"]
          try_release["try_to_release_page()<br/>释放 pagecache (filemap.c)"]
          free_pages["free_unref_page_list()<br/>释放页面"]
      end

      subgraph "Memcg 控制 (mm/memcontrol.c)"
          memcg_iter["mem_cgroup_iter()"]
          memcg_calc["mem_cgroup_calculate_protection()"]
          memcg_soft["mem_cgroup_soft_limit_reclaim()"]
          reclaim_high["reclaim_high()"]
          handle_over["mem_cgroup_handle_over_high()"]
      end

      subgraph "其他辅助"
          compaction["compaction 内存压缩<br/>mm/compaction.c"]
          vmpressure["vmpressure 虚拟内存压力<br/>mm/vmpressure.c"]
          psi["psi_memstall_enter/leave<br/>PSI 压力监控"]
      end

      %% 主要调用流
      APP --> alloc_pages
      alloc_pages --> __alloc_pages
      __alloc_pages --> get_page

      get_page -->|成功| APP
      get_page -->|失败 - 内存不足| slowpath

      slowpath --> wake_all
      slowpath --> direct_reclaim
      slowpath --> retry

      wake_all --> kswapd

      direct_reclaim --> perform_reclaim
      perform_reclaim --> try_free
      perform_reclaim --> psi

      try_free --> throttle
      try_free --> do_try_free
      do_try_free --> shrink_zones_dr

      kswapd --> balance_pgdat
      balance_pgdat --> kswapd_shrink
      balance_pgdat --> prepare_sleep
      balance_pgdat --> allow_dr
      balance_pgdat --> compaction

      kswapd_shrink --> shrink_node
      shrink_zones_dr --> shrink_node

      shrink_node --> shrink_node_memcgs
      shrink_node_memcgs --> memcg_iter
      shrink_node_memcgs --> memcg_calc
      shrink_node_memcgs --> shrink_lruvec
      shrink_node_memcgs --> shrink_slab

      shrink_lruvec --> shrink_list
      shrink_list --> shrink_active
      shrink_list --> shrink_inactive

      shrink_active --> isolate
      shrink_active --> page_ref
      shrink_active --> free_pages

      shrink_inactive --> isolate
      shrink_inactive --> add_swap
      shrink_inactive --> try_unmap
      shrink_inactive --> try_release
      shrink_inactive --> free_pages

      shrink_slab --> do_shrink_slab
      shrink_slab --> shrink_slab_memcg

      %% Memcg 回收路径
      handle_over --> reclaim_high
      reclaim_high --> shrink_zones_dr
      memcg_soft --> do_try_free

      retry -->|重试| get_page
      retry -->|失败| slowpath
      retry -->|OOM| APP

      %% 样式
      classDef entry fill:#e1f5fe
      classDef slowpath fill:#fff3e0
      classDef direct fill:#fce4ec
      classDef kswapd fill:#e8f5e9
      classDef node fill:#f3e5f5
      classDef lru fill:#fff8e1
      classDef slab fill:#e0f2f1
      classDef memcg fill:#f1f8e9

      class alloc_pages,__alloc_pages,get_page entry
      class slowpath,wake_all,direct_reclaim,perform_reclaim,retry slowpath
      class try_free,do_try_free,throttle,shrink_zones_dr direct
      class kswapd,balance_pgdat,kswapd_shrink,prepare_sleep,allow_dr kswapd
      class shrink_node,shrink_node_memcgs node
      class shrink_lruvec,shrink_list,shrink_active,shrink_inactive lru
      class shrink_slab,do_shrink_slab,shrink_slab_memcg slab
      class memcg_iter,memcg_calc,memcg_soft,reclaim_high,handle_over memcg

__alloc_pages 正是分析Linux内核内存回收流程最核心、最合适的入口函数。

它是内核内存分配请求的“总枢纽”,几乎所有物理页面的分配请求最终都会调用它。从它开始分析,我们可以清晰地看到从“尝试快速分配”到“触发慢速路径回收”的完整决策链条。

  __alloc_pages()
    │
    ├─ get_page_from_freelist()    ← 快路径,本函数
    │   ├─ 水位检查通过 → 直接分配(无需回收)
    │   ├─ 水位不足 → node_reclaim() 同步回收(小规模)
    │   └─ 全部失败 → return NULL
    │
    └─ __alloc_pages_slowpath()    ← 慢路径(返回 NULL 后进入)
        ├─ direct reclaim(直接回收)
        ├─ direct compaction(直接压缩)
        ├─ OOM killer

从内存回收(reclaim)的角度来看,get_page_from_freelist 是分配路径上的第一道回收触发点。它在"快速分配"和"慢速回收"之间起到了关键的桥梁作用。

从内存回收角度看快速分配( get_page_from_freelist )

三级水位体系

  // include/linux/mmzone.h
  
#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)
#define wmark_pages(z, i) (z->_watermark[i] + z->watermark_boost)

每个 zone 维护三个水位线:

水位

含义

回收行为

WMARK_HIGH

高水位

kswapd 停止回收

WMARK_LOW

低水位

唤醒kswapd 开始回收

WMARK_MIN

最低水位

同步回收,分配者自己回收

get_page_from_freelist 使用的水位由 alloc_flags & ALLOC_WMARK_MASK 决定:

// page_alloc.c:4209
    mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
    ///返回true,表示满足最低水位,且满足分配要求
    ///条件分支内容,为处理分配失败

    if (!zone_watermark_fast(zone, order, mark,
               ac->highest_zoneidx, alloc_flags,
               gfp_mask)) {
               //...
  • 普通分配 → 使用 WMARK_LOW(kswapd 被唤醒后,分配者等待 kswapd 回收到低水位以上)

  • 紧急分配(ALLOC_HARDER)→ 使用 WMARK_MIN 甚至更低

/*
 * 检测判断zone是否大于最低水位,且根据order判断是否有足够大空闲内存
 * 水位值在初始化时,根据空闲页面设定,也可以在节点/proc/sys/vm/mim_free_kbytes设置,kswapd内核线程也会用到
 * 
 * z:检测是否满足请求的zone
 * order:分配2^order个物理页面
 * mark:要测试的水位标准;
 * highest_zoneidx:首选zone的编号
 * alloc_flags:属性标志位
 * fgp_mask:属性屏蔽位
 * */
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)
{
	long free_pages;

	///获取zone空闲页面数量
	free_pages = zone_page_state(z, NR_FREE_PAGES);

	/*
	 * Fast check for order-0 only. If this fails then the reserves
	 * need to be calculated.
	 */
	///针对申请单个page,做快速优化
	//lowmem_reserve是每个zone预留的内存,为防止高端zone在内存不足时,过度使用低端zone内存资源
	if (!order) {
		long fast_free;

		fast_free = free_pages;
		fast_free -= __zone_watermark_unusable_free(z, 0, alloc_flags);
		if (fast_free > mark + z->lowmem_reserve[highest_zoneidx])
			return true;
	}

	///进一步检查
	if (__zone_watermark_ok(z, order, mark, highest_zoneidx, alloc_flags,
					free_pages))
		return true;
	/*
	 * Ignore watermark boosting for GFP_ATOMIC order-0 allocations
	 * when checking the min watermark. The min watermark is the
	 * point where boosting is ignored so that kswapd is woken up
	 * when below the low watermark.
	 */
	///针对申请单个页面,进一步检查, 忽略临时水位
	if (unlikely(!order && (gfp_mask & __GFP_ATOMIC) && z->watermark_boost
		&& ((alloc_flags & ALLOC_WMARK_MASK) == WMARK_MIN))) {
		mark = z->_watermark[WMARK_MIN];
		return __zone_watermark_ok(z, order, mark, highest_zoneidx,
					alloc_flags, free_pages);
	}

	return false;
}

水位检查的降级策略(__zone_watermark_ok)

  // page_alloc.c:3918
  bool __zone_watermark_ok(struct zone *z, unsigned int order,
                           unsigned long mark, ...)
  {
      long min = mark;

      // ① ALLOC_HIGH(GFP_HIGHUSER 等)→ 水位降低 50%
      if (alloc_flags & ALLOC_HIGH)
          min -= min / 2;

      // ② ALLOC_HARDER(紧急分配)→ 水位再降低 25%
      if (unlikely(alloc_harder)) {
          if (alloc_flags & ALLOC_OOM)   // OOM 受害者
              min -= min / 2;            // 总共降低 75%
          else
              min -= min / 4;            // 总共降低 62.5%
      }

      // ③ 基本检查:空闲页面 > min + lowmem_reserve
      if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
          return false;

      // ④ 高阶分配:遍历 free_area 检查是否有足够大的连续块
      for (o = order; o < MAX_ORDER; o++) {
          // 检查 MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE
          // 以及 CMA, HIGHATOMIC 区域
      }
  }

设计思想:越是紧急的分配,水位要求越低——宁可让系统内存更紧张,也要保证关键分配成功。

get_page_from_freelist 中的三条回收路径

路径1: zone 快速水位检查失败 → 跳过该 zone

  // page_alloc.c:4213
  if (!zone_watermark_fast(zone, order, mark, ...)) {
      // 水位不满足,进入降级逻辑
  }

zone_watermark_fast 是一个快速预检:

  • order-0:直接比较 free_pages 和 mark + lowmem_reserve,避免进入完整的 __zone_watermark_ok

  • 高阶:委托给 __zone_watermark_ok 做完整检查

如果快速检查失败,有以下几种处理:

路径 2:ALLOC_NO_WATERMARKS —— 跳过水位检查

  // page_alloc.c:4230
  if (alloc_flags & ALLOC_NO_WATERMARKS)
      goto try_this_zone;  // 强制分配,不检查水位

用于 __GFP_MEMALLOC 等紧急场景(如 swap IO、网络收包),从系统预留内存中分配。

路径 3:node_reclaim() —— 同步节点回收(核心回收路径)

  // page_alloc.c:4237-4257
  if (!node_reclaim_enabled() ||
      !zone_allows_reclaim(ac->preferred_zoneref->zone, zone))
      continue;  // 不允许回收,跳到下一个 zone

  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, ...))
          goto try_this_zone;  // 水位满足,尝试分配
      continue;
  }

触发条件:

  1. zone_reclaim_mode 启用(/proc/sys/vm/zone_reclaim_mode,默认关闭)

  2. 当前 zone 允许回收(zone_allows_reclaim)

  3. 分配可以阻塞(gfpflags_allow_blocking)

node_reclaim 的内部逻辑

  // vmscan.c:4817
  int node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
  {
      // ① 快速判断:没有可回收的页面就直接返回
      if (node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages &&
          node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages)
          return NODE_RECLAIM_FULL;  // 没有可回收的

      // ② 不允许阻塞的分配不能回收
      if (!gfpflags_allow_blocking(gfp_mask) || (current->flags & PF_MEMALLOC))
          return NODE_RECLAIM_NOSCAN;

      // ③ 只在本地节点或无 CPU 的节点上回收
      if (node_state(pgdat->node_id, N_CPU) && pgdat->node_id != numa_node_id())
          return NODE_RECLAIM_NOSCAN;

      // ④ 加锁防止并发回收
      if (test_and_set_bit(PGDAT_RECLAIM_LOCKED, &pgdat->flags))
          return NODE_RECLAIM_NOSCAN;

      // ⑤ 实际回收
      ret = __node_reclaim(pgdat, gfp_mask, order);
      ...
  }

__node_reclaim 的回收策略:

  // vmscan.c:4763
  static int __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
  {
      struct scan_control sc = {
          .nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX),  // 至少回收这么多页
          .priority = NODE_RECLAIM_PRIORITY,  // 初始扫描优先级(较低,扫描量少)
          .may_writepage = !!(node_reclaim_mode & RECLAIM_WRITE),  // 是否写脏页
          .may_unmap = !!(node_reclaim_mode & RECLAIM_UNMAP),      // 是否 unmap
          .may_swap = 1,  // 允许 swap
      };

      // 从优先级 12 开始递减扫描,直到回收足够页面
      do {
          shrink_node(pgdat, &sc);  // 核心:扫描节点的 LRU 链表
      } while (sc.nr_reclaimed < nr_pages && --sc.priority >= 0);

      return sc.nr_reclaimed >= nr_pages;
  }

回收目标:unmapped file-backed pages(无映射的文件页)和 slab 可回收页面。这些页面回收代价低(不需要写回磁盘或
unmap)。

分配成功后的回收触发:rmqueue 中的 kswapd 唤醒

即使分配成功,rmqueue 中也有回收相关的逻辑

  // page_alloc.c:3803 (rmqueue 函数末尾)
  if (test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags)) {
      clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
      wakeup_kswapd(zone, 0, 0, zone_idx(zone));
  }

ZONE_BOOSTED_WATERMARK 标志:当伙伴系统从备份链表(fallback migratetype)借用页面时设置,表明发生了外碎片化。此时唤醒 kswapd,让它提前回收内存,为后续的大阶分配做准备。
这是一种预防性回收——分配成功了,但系统已经开始借用其他迁移类型的页面,说明内存紧张的苗头已经出现。

快速路径下的回收决策流程图

  get_page_from_freelist()
  │
  ├─ 遍历 zonelist 中的每个 zone
  │   │
  │   ├─ 水位检查 (zone_watermark_fast)
  │   │   │
  │   │   ├─ ✅ 通过 → try_this_zone → rmqueue() 分配
  │   │   │   └─ 分配成功后检查 ZONE_BOOSTED_WATERMARK
  │   │   │       └─ 有标志 → wakeup_kswapd() 预防性回收
  │   │   │
  │   │   └─ ❌ 失败
  │   │       │
  │   │       ├─ ALLOC_NO_WATERMARKS?→ 强制 try_this_zone
  │   │       │
  │   │       ├─ deferred_pages?→ 扩展 zone 容量后 try_this_zone
  │   │       │
  │   │       └─ zone_reclaim_mode 启用?
  │   │           │
  │   │           ├─ 不启用 → continue(跳到下一个 zone)
  │   │           │
  │   │           └─ 启用 → node_reclaim()
  │   │               │
  │   │               ├─ 无可回收 → NODE_RECLAIM_FULL → continue
  │   │               ├─ 不能扫描 → NODE_RECLAIM_NOSCAN → continue
  │   │               └─ 回收成功 → zone_watermark_ok 再检查
  │   │                   ├─ ✅ → try_this_zone
  │   │                   └─ ❌ → continue
  │   │
  │   └─ rmqueue() 失败 → 继续遍历下一个 zone
  │
  ├─ 全部 zone 遍历完,且有 ALLOC_NOFRAGMENT
  │   └─ 关闭碎片化避免,retry(最后尝试)
  │
  └─ return NULL → 进入 __alloc_pages_slowpath()
      ├─ direct_reclaim(直接回收,更激进)
      ├─ direct_compaction(直接压缩)
      ├─ __alloc_pages_may_oom(OOM killer)
      └─ ...

-get_page_from_freelist 内存回收策略.drawio.png

关键设计总结

设计点

说明

三级水位

high/low/min 控制回收的紧急程度,越急水位越低

watermark_boost

碎片化临时提高水位, 提前唤醒kswapd

node_reclaim是可选的

默认关闭(zone_reclaim_mode=0),NUMA 系统上更倾向跨节点分配

快速路径 vs 慢速路径

get_page_from_freelist 只做轻量回收(node_reclaim),重活留给 slowpath

预防性回收

rmqueue 中检测到借用备份链表时唤醒 kswapd,未雨绸缪

ALLOC_NOFRAGMENT 降级

宁可碎片化也要分配成功,最后的保底策略

从回收视角看,get_page_from_freelist 的核心哲学是:能不回收就不回收(水位够就不触发),能轻量回收就不重量回收(node_reclaim 而非 direct reclaim),能本地回收就不跨节点(zone_reclaim_mode)

从内存回收角度看慢速分配 ( __alloc_pages_slowpath )

当 get_page_from_freelist(快路径)返回 NULL 时进入。从内存回收视角看,这里集中了内核所有重量级回收手段。

慢路径下的回收策略

路径1: 调整分配标志 + 唤醒 kswapd

  // ① 降低水位要求:从 WMARK_LOW → WMARK_MIN
  alloc_flags = gfp_to_alloc_flags(gfp_mask);

  // ② 重新选择首选 zone
  ac->preferred_zoneref = first_zones_zonelist(...);

  // ③ 唤醒所有相关节点的 kswapd 线程
  if (alloc_flags & ALLOC_KSWAPD)
      wake_all_kswapds(order, gfp_mask, ac);

  // ④ 用新的低水位再次尝试分配
  page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

设计思想:快路径用 WMARK_LOW,失败后慢路径降级到 WMARK_MIN——降低 50% 的水位要求,同时唤醒 kswapd 后台回收。这是异步回收 + 放宽条件的组合策略。

阶段 2:直接压缩(Direct Compaction)—— 高阶分配优先

  if (can_direct_reclaim &&
      (costly_order || (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
      && !gfp_pfmemalloc_allowed(gfp_mask)) {

      page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                                          INIT_COMPACT_PRIORITY, &compact_result);
      if (page)
          goto got_pg;
  }
  // page_alloc.c:4993
  bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;

关键标志:

  • GFP_DIRECT_RECLAIM:允许直接回收(GFP_KERNEL 有,GFP_ATOMIC 没有

  • GFP_NORETRY:分配失败不重试

  • GFP_NOFAIL:必须分配成功,永不放弃

  • GFP_RETRY_MAYFAIL:可以重试,但最终允许失败

触发条件:

  • 允许直接回收

  • 高阶分配(order > PAGE_ALLOC_COSTLY_ORDER)或不可移动的高阶分配

  • 不是紧急分配(不能访问系统预留内存)

为什么先压缩后回收? 对于高阶分配,失败的原因往往不是内存总量不足,而是外碎片化——内存碎片导致没有连续的大块。压缩(compaction)通过迁移页面来合并碎片,比重回收费用更低。

__alloc_pages_direct_compact 内部流程:

  // page_alloc.c:4457
  static struct page *__alloc_pages_direct_compact(...)
  {
      // ① 只处理高阶分配
      if (!order) return NULL;

      // ② 执行压缩:try_to_compact_pages()
      *compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac, prio, &page);

      // ③ 压缩过程中可能直接捕获到页面
      if (page) {
          prep_new_page(page, order, gfp_mask, alloc_flags);
          return page;
      }

      // ④ 压缩完成后再次从 freelist 分配
      if (!page)
          page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

      return page;
  }   

内存规整本章节不过多叙述,待后面内存规整章节详细描述。

阶段 3:使用预留内存 + 再次尝试分配

  // 检查是否可以使用系统预留内存
  reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
  if (reserve_flags)
      alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags);

  // 如果有预留内存权限,忽略 cpuset 限制
  if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
      ac->nodemask = NULL;
      ac->preferred_zoneref = first_zones_zonelist(...);
  }

  // 再次尝试分配(可能已经使用预留内存)
  page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

__gfp_pfmemalloc_flags 返回值:

条件

返回值

含义

__GFP_MEMALLOC

ALLOC_NO_WATERMARKS

跳过所有水位检查

PF_MEMALLOC

ALLOC_NO_WATERMARKS

内核线程的紧急分配

OOM 受害者

ALLOC_OOM

水位低于 75%

其他

0

不能使用预留内存

阶段 4:直接回收(Direct Reclaim)—— 核心回收路径

  page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                                       &did_some_progress);

这是最重量级的同步回收。调用链:

  __alloc_pages_direct_reclaim()
    │
    ├─ __perform_reclaim()
    │   ├─ cpuset_memory_pressure_bump()  // 记录内存压力
    │   ├─ psi_memstall_enter()           // PSI 压力统计
    │   ├─ fs_reclaim_acquire()           // 锁依赖检查
    │   ├─ memalloc_noreclaim_save()      // 设置 NORECLAIM 防止递归
    │   │
    │   ├─ try_to_free_pages()            // ★ 核心回收函数
    │   │   ├─ shrink_zones()
    │   │   │   └─ shrink_node()
    │   │   │       ├─ shrink_lruvec()    // 扫描 LRU 链表
    │   │   │       │   ├─ shrink_active_list()  // 活性链表 → 不活性链表
    │   │   │       │   └─ shrink_inactive_list() // 不活性链表 → 回收
    │   │   │       │       ├─ shrink_page_list()
    │   │   │       │       │   ├─ pageout()     // 脏页写回
    │   │   │       │       │   └─ free_page()   // 回收干净页
    │   │   │       │       └─ ...
    │   │   │       └─ shrink_slab()      // 回收 slab 缓存
    │   │   └─ ...
    │   │
    │   ├─ memalloc_noreclaim_restore()
    │   └─ psi_memstall_leave()
    │
    ├─ 回收成功?→ get_page_from_freelist() 再次分配
    │
    └─ 分配仍失败?
        ├─ unreserve_highatomic_pageblock()  // 释放 HIGHATOMIC 预留
        ├─ drain_all_pages()                 // 排空所有 PCP 缓存
        └─ get_page_from_freelist() 再次尝试

关键设计:

  // page_alloc.c:4722
  static inline struct page *
  __alloc_pages_direct_reclaim(...)
  {
      // ① 执行回收
      *did_some_progress = __perform_reclaim(gfp_mask, order, ac);
      if (!(*did_some_progress))
          return NULL;  // 没有回收到任何页面,直接失败

  retry:
      // ② 回收后再次分配
      page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

      // ③ 分配失败的补救措施
      if (!page && !drained) {
          unreserve_highatomic_pageblock(ac, false);  // 释放紧急原子预留
          drain_all_pages(NULL);  // 排空 PCP 缓存,释放碎片化的页面
          drained = true;
          goto retry;
      }
      return page;
  }

drain_all_pages 的作用:每个 CPU 都有 per-cpu 页面缓存(per_cpu_pages),这些缓存中的页面不会出现在 zone 的空闲链表中。回收后可能有页面积压在 PCP 缓存中,排空它们可以释放出碎片化的页面,配合压缩使用。

PS: 不过多赘述 __perform_reclaim 的内部流程,之后的文章会叙述。

阶段 5:再次直接压缩

page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                                       compact_priority, &compact_result);

直接回收释放了页面后,再次尝试压缩——回收提供了更多空闲页面,压缩成功率更高。

重试决策

这是慢路径中最复杂的决策逻辑

  // 1. __GFP_NORETRY → 不重试
  if (gfp_mask & __GFP_NORETRY)
      goto nopage;

  // 2. 高阶分配 + 非 RETRY_MAYFAIL → 不重试
  if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
      goto nopage;

  // 3. 是否值得重试回收?
  if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                           did_some_progress > 0, &no_progress_loops))
      goto retry;

  // 4. 是否值得重试压缩?
  if (did_some_progress > 0 &&
      should_compact_retry(ac, order, alloc_flags,
                           compact_result, &compact_priority,
                           &compaction_retries))
      goto retry;

  // 5. cpuset 变化检查
  if (check_retry_cpuset(cpuset_mems_cookie, ac))
      goto retry_cpuset;

should_reclaim_retry 的核心逻辑(4862-4951):

  static inline bool should_reclaim_retry(...)
  {
      // 1 计数器管理
      if (did_some_progress && order <= PAGE_ALLOC_COSTLY_ORDER)
          *no_progress_loops = 0;  // 有进展,重置计数
      else
          (*no_progress_loops)++;  // 无进展,计数增加

      // 2 超过最大重试次数(16次)→ 放弃
      if (*no_progress_loops > MAX_RECLAIM_RETRIES) {
          return unreserve_highatomic_pageblock(ac, true);  // 最后尝试释放预留
      }

      // 3 关键判断:如果回收所有可回收页面,水位能否满足?
      for_each_zone_zonelist_nodemask(zone, z, ...) {
          available = reclaimable = zone_reclaimable_pages(zone);
          available += zone_page_state_snapshot(zone, NR_FREE_PAGES);

          // 假设回收了所有可回收页面,水位是否满足?
          wmark = __zone_watermark_ok(zone, order, min_wmark,
                                      ac->highest_zoneidx, alloc_flags, available);
          if (wmark) {
              // 水位能满足 → 值得继续回收
              // 但如果没进展且有大量脏页在写回,等待 IO
              if (!did_some_progress) {
                  write_pending = zone_page_state_snapshot(zone, NR_ZONE_WRITE_PENDING);
                  if (2 * write_pending > reclaimable) {
                      congestion_wait(BLK_RW_ASYNC, HZ/10);  // 等待 IO 完成
                      return true;
                  }
              }
              ret = true;
              goto out;
          }
      }
      // 所有 zone 的可回收页面加起来都不够 → 不值得重试
      return ret;
  }

核心判断:不是看"回收了多少",而是看"如果把所有可回收页面都回收掉,能否满足水位"。如果理论上都不够,就不再浪费时间重试。

阶段 7:OOM Killer —— 最后的手段

  // 所有回收手段都失败,启动 OOM Killer
  page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);

__alloc_pages_may_oom 的保护逻辑:

  static inline struct page *
  __alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
                        const struct alloc_context *ac, unsigned long *did_some_progress)
  {
      *did_some_progress = 0;

      // ① 获取 oom_lock,防止并发 OOM
      if (!mutex_trylock(&oom_lock)) {
          *did_some_progress = 1;  // 别人在处理 OOM,视为有进展
          return NULL;
      }

      // ② 再做一次高水位分配(可能有并发 OOM 释放了内存)
      page = get_page_from_freelist((gfp_mask | __GFP_HARDWALL) & ~__GFP_DIRECT_RECLAIM,
                                    order, ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);
      if (page) goto out;

      // ③ 跳过 OOM 的条件(保护机制)
      if (current->flags & PF_DUMPCORE)     goto out;  // coredump 不杀进程
      if (order > PAGE_ALLOC_COSTLY_ORDER)  goto out;  // 高阶分配不杀进程
      if (gfp_mask & (__GFP_RETRY_MAYFAIL | __GFP_THISNODE)) goto out;  // 可失败的分配
      if (ac->highest_zoneidx < ZONE_NORMAL) goto out;  // lowmem 区域
      if (pm_suspended_storage())            goto out;  // 存储挂起

      // ④ 执行 OOM Killer
      if (out_of_memory(&oc) || WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL)) {
          *did_some_progress = 1;
          // __GFP_NOFAIL 的分配,使用预留内存
          if (gfp_mask & __GFP_NOFAIL)
              page = __alloc_pages_cpuset_fallback(gfp_mask, order,
                                                   ALLOC_NO_WATERMARKS, ac);
      }
  }

OOM 后的重试:

  // OOM killer 成功杀死了进程 → 重试
  if (did_some_progress) {
      no_progress_loops = 0;
      goto retry;
  }

阶段 8:__GFP_NOFAIL 的死循环保底

  if (gfp_mask & __GFP_NOFAIL) {
      // 降低水位,忽略 cpuset,使用预留内存
      page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
      if (page) goto got_pg;

      cond_resched();
      goto retry;  // 永不放弃,循环重试
  }

慢路径下的流程图

  __alloc_pages_slowpath()
  │
  ├─ 阶段1: 降低水位(WMARK_MIN) + 唤醒kswapd + 分配尝试
  │   └─ 成功?→ 返回
  │
  ├─ 阶段2: 直接压缩(高阶分配优先,异步优先级)
  │   └─ 成功?→ 返回
  │
  ├─ 阶段3: 使用预留内存(ALLOC_NO_WATERMARKS/ALLOC_OOM) + 分配尝试
  │   └─ 成功?→ 返回
  │
  ├─ 阶段4: 直接回收(Direct Reclaim) ★核心★
  │   ├─ try_to_free_pages() → 扫描 LRU + 回收页面
  │   ├─ 分配尝试
  │   ├─ 失败 → drain_all_pages() + unreserve_highatomic + 再分配
  │   └─ 成功?→ 返回
  │
  ├─ 阶段5: 再次直接压缩(回收后有更多空闲页)
  │   └─ 成功?→ 返回
  │
  ├─ 阶段6: 重试决策
  │   ├─ should_reclaim_retry() → 值得回收?→ goto retry
  │   ├─ should_compact_retry() → 值得压缩?→ goto retry
  │   └─ cpuset 变化?→ goto retry_cpuset
  │
  ├─ 阶段7: OOM Killer
  │   ├─ 条件检查(保护机制)
  │   ├─ out_of_memory() → 杀死进程释放内存
  │   └─ 成功?→ goto retry
  │
  ├─ 阶段8: __GFP_NOFAIL 死循环
  │   └─ cond_resched() + retry(永不放弃)
  │
  └─ nopage: 返回 NULL

__alloc_pages_slowpath 内存回收策略流程图.png

慢路径下回收策略的设计哲学

  1. 渐进式降级:水位从 LOW→MIN→NO_WATERMARKS,回收从轻量→重量→OOM,层层递进

  2. 压缩优先于回收(高阶分配):碎片化比内存不足更可能是高阶分配失败的原因

  3. 回收后必排空:drain_all_pages 释放 PCP 缓存中积压的碎片化页面

  4. 理论上限判断:should_reclaim_retry 不是看"回收了多少",而是看"全部回收够不够"

  5. IO 感知:脏页写回中时 congestion_wait,避免盲目重试

  6. 永不放弃的底线:__GFP_NOFAIL 确保关键路径(如文件系统元数据)不会因内存分配失败而崩溃

快速路径和慢速路径下的回收策略对比

维度

快路径 get_page_from_freelist

慢路径 __alloc_pages_slowpath

水位

WMARK_LOW

WMARK_MIN

回收方式

node_reclaim (轻量同步)

try_to_free_pages (重量同步)

回收范围

仅本地节点

所有允许的节点

压缩

直接压缩 (sync/async)

kswapd

不唤醒

主动唤醒

预留内存

不使用

使用(ALLOC_NO_WATERMARKS)

OOM

不触发

触发 OOM Killer

PCP drain

不排空

排空所有 PCP 缓存

HIGHATOMIC

不释放

释放预留的 HIGHATOMIC 块

阻塞

可能阻塞(node_reclaim)

一定阻塞

重试

不重试

多轮重试 + 死循环(NOFAIL)

总结

这次我们从页面分配器出发,沿着两条路深入分析了内存回收的触发机制:

快路径:get_page_from_freelist

定位:分配的第一道尝试,能不回收就不回收。
核心逻辑:遍历 zone → 过滤检查 → 水位判断 → 分配或轻量回收

环节

回收策略

水位满足

直接分配,无需回收

水位不足

node_reclaim() 同步回收本地节点(file-backed 页 + slab)

分配成功但借用备份链表

wakeup_kswapd() 预防性回收

全部 zone 失败

清除 ALLOC_NOFRAGMENT 重试,放弃碎片化避免

设计哲学:快速、轻量、尽量不阻塞。水位是核心判据,node_reclaim 是唯一的同步回收手段。

慢路径:__alloc_pages_slowpath

定位:快路径失败后的全面回收,层层降级直到成功或放弃。
核心逻辑:降低水位 → 压缩 → 预留内存 → 直接回收 → 再压缩 → 重试决策 → OOM

阶段

策略

重量级

1

降低水位 WMARK_MIN + 唤醒 kswapd

2

直接压缩(高阶分配优先)

★★

3

使用预留内存(ALLOC_NO_WATERMARKS)

4

直接回收(try_to_free_pages)

★★★★

5

再次直接压缩(回收后空闲页更多)

★★★

6

重试决策(理论上限判断)

★★

7

OOM Killer(杀死进程释放内存)

★★★★★

8

__GFP_NOFAIL 死循环保底

设计哲学:不惜代价保证分配成功。先做性价比高的事(压缩),再做昂贵的事(直接回收),最后动用终极手段(OOM)

全景对比

                      ┌─────────────────────────────────────────────┐
                      │         __alloc_pages_nodemask()            │
                      │              分配入口                        │
                      └──────────────────┬──────────────────────────┘
                                         │
                      ┌──────────────────▼──────────────────────────┐
                      │     get_page_from_freelist (快路径)          │
                      │                                            │
                      │  水位检查 ──→ node_reclaim (轻量同步回收)    │
                      │  分配成功 ──→ wakeup_kswapd (预防性回收)     │
                      │                                            │
                      │  返回 page?──→ 是 ──→ 分配完成              │
                      │      │                                     │
                      │      否                                    │
                      └──────┼─────────────────────────────────────┘
                             │
                      ┌──────▼─────────────────────────────────────┐
                      │     __alloc_pages_slowpath (慢路径)         │
                      │                                            │
                      │  ① 降低水位 + kswapd                        │
                      │  ② 直接压缩 (compaction)                    │
                      │  ③ 预留内存                                │
                      │  ④ 直接回收 (direct reclaim) ★核心★         │
                      │  ⑤ 再次压缩                                │
                      │  ⑥ 重试决策                                │
                      │  ⑦ OOM Killer                              │
                      │  ⑧ NOFAIL 死循环                           │
                      │                                            │
                      │  返回 page?──→ 是 ──→ 分配完成              │
                      │      │                                     │
                      │      否 ──→ return NULL (分配彻底失败)       │
                      └────────────────────────────────────────────┘

核心回收手段总结

回收手段

触发位置

方式

回收目标

阻塞

wakeup_kswapd

快路径 rmqueue 后

异步

LRU 页面

node_reclaim

快路径水位不足

同步

file-backed + slab

try_to_compact_pages

慢路径

同步

碎片整理(不回收)

try_to_free_pages

慢路径直接回收

同步

LRU + slab + 脏页写回

out_of_memory

慢路径最后手段

同步

杀死进程释放全部内存

内存回收的后续章节

本篇分析覆盖了回收的触发点和调度逻辑,但回收的"内脏"——实际扫描和释放页面的机制——还未深入。后续可以从以下方向继续:

方向

核心函数

分析重点

kswapd 后台回收

kswapd()balance_pgdat()shrink_node()

内核线程如何被唤醒、回收到什么程度停止、与分配器的协作

直接回收

try_to_free_pages()shrink_zones()shrink_node()

同步回收的完整链路、优先级递减扫描、脏页回写决策

LRU 链表扫描

shrink_lruvec()shrink_active_list() + shrink_inactive_list()

页面在 active/inactive 链表间的流转、二次机会算法、refs 位

页面回收细节

shrink_page_list()

单页回收决策:干净页直接释放、脏页 pageout()、匿名页 swap

Slab 回收

shrink_slab()shrinker->scan_objects()

dentry/inode 缓存回收、注册 shrinker 机制

内存压缩

try_to_compact_pages()compact_zone()

页面迁移、碎片整理、与回收的配合

水位管理

__setup_per_zone_wmarks() / watermark_boost

水位如何计算、boost 机制、与 kswapd 的关系

OOM 策略

out_of_memory()select_bad_process()oom_kill_process()

OOM 评分算法、选择牺牲者的策略、OOM reaper