AI智能摘要
直接回收是 Linux 内存分配慢路径中的同步回收机制:当 kswapd 的后台回收跟不上分配需求、快路径已失败时,由分配线程“自己动手”回收页面,作为介于 kswapd 与 OOM 之间的第二道防线。整体流程发生在 __alloc_pages_slowpath 中:先尝试唤醒 kswapd 和常规分配,再进行内存压缩和预留内存使用,关键的第四阶段是 __alloc_pages_direct_reclaim,通过 __perform_reclaim 调用 try_to_free_pages 做实际回收,然后再用 get_page_from_freelist 重试分配;若仍失败,则一次性释放 H
此摘要由AI分析文章内容生成,仅供参考。

一、直接回收是什么

直接回收是分配器慢路径中的同步回收机制。当 kswapd 后台回收来不及跟上分配需求、且快路径分配失败时,分配线程自己动手回收页面,然后重试分配。

核心定位: 直接回收是内存回收的"第二道防线",在 kswapd 之后、OOM 之前工作。与 kswapd 的异步回收不同,直接回收是同步阻塞的——分配线程会暂停,直到回收到足够页面或确认无法回收。

二、整体架构

__alloc_pages_slowpath() 慢路径
│
├─ 阶段1: wake_all_kswapds() + get_page_from_freelist()
│   └─ 成功?→ 返回页面
│
├─ 阶段2: __alloc_pages_direct_compact()
│   └─ 成功?→ 返回页面
│
├─ 阶段3: 使用预留内存 + get_page_from_freelist()
│   └─ 成功?→ 返回页面
│
├─ 阶段4: __alloc_pages_direct_reclaim() ★直接回收★
│   │
│   ├─ __perform_reclaim()
│   │   ├─ cpuset_memory_pressure_bump()  // 通知 cpuset 内存压力
│   │   ├─ psi_memstall_enter()           // PSI 内存停顿追踪
│   │   ├─ memalloc_noreclaim_save()      // 允许使用预留内存
│   │   └─ try_to_free_pages()            // ★核心回收函数★
│   │       │
│   │       ├─ throttle_direct_reclaim()  // 节流保护
│   │       │   └─ 等待 kswapd 有进展
│   │       │
│   │       └─ do_try_to_free_pages()
│   │           └─ priority 循环 (12→0)
│   │               └─ shrink_zones()
│   │                   └─ shrink_node()  // 节点级回收(下一篇详析)
│   │
│   ├─ did_some_progress > 0?
│   │   └─ 否 → return NULL
│   │
│   └─ get_page_from_freelist()  // 回收后重试分配
│       └─ 失败?
│           ├─ unreserve_highatomic_pageblock()
│           ├─ drain_all_pages()
│           └─ 再次 get_page_from_freelist()
│
├─ 阶段5: __alloc_pages_direct_compact()(回收后压缩)
│
├─ 阶段6: 重试决策 (should_reclaim_retry / should_compact_retry)
│
├─ 阶段7: __alloc_pages_may_oom()  // OOM Killer
│
└─ 阶段8: __GFP_NOFAIL 死循环

三、入口函数:__alloc_pages_direct_reclaim

文件: mm/page_alloc.c:4722-4750

static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
        unsigned int alloc_flags, const struct alloc_context *ac,
        unsigned long *did_some_progress)
{
    struct page *page = NULL;
    bool drained = false;

    // ① 执行回收
    *did_some_progress = __perform_reclaim(gfp_mask, order, ac);
    if (unlikely(!(*did_some_progress)))
        return NULL;  // 回收失败,放弃

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

    // ③ 分配失败?排空 PCP 缓存 + 释放 HIGHATOMIC 预留后重试
    if (!page && !drained) {
        unreserve_highatomic_pageblock(ac, false);
        drain_all_pages(NULL);
        drained = true;
        goto retry;
    }

    return page;
}

设计要点:

  1. 先回收再分配: __perform_reclaim 回收完成后,用 get_page_from_freelist 重新分配

  2. drain_all_pages 补救: PCP 缓存中可能有碎片化的 order-0 页面,排空后配合压缩使用

  3. unreserve_highatomic 释放: 高阶原子分配预留块在紧急时可以释放

  4. 只重试一次: drained 标志确保最多做一次 drain + retry,避免死循环


四、回收包装:__perform_reclaim

文件: mm/page_alloc.c:4694-4719

static unsigned long
__perform_reclaim(gfp_t gfp_mask, unsigned int order,
                  const struct alloc_context *ac)
{
    unsigned int noreclaim_flag;
    unsigned long pflags, progress;

    cond_resched();  // ① 让出 CPU,给其他进程机会

    cpuset_memory_pressure_bump();  // ② 通知 cpuset 内存压力
    psi_memstall_enter(&pflags);    // ③ PSI 内存停顿追踪
    fs_reclaim_acquire(gfp_mask);   // ④ lockdep 检查
    noreclaim_flag = memalloc_noreclaim_save();  // ⑤ 允许使用预留内存

    // ⑥ 调用核心回收函数
    progress = try_to_free_pages(ac->zonelist, order, gfp_mask, ac->nodemask);

    memalloc_noreclaim_restore(noreclaim_flag);
    fs_reclaim_release(gfp_mask);
    psi_memstall_leave(&pflags);

    cond_resched();  // ⑦ 回收后再次让出 CPU

    return progress;
}

各步骤作用:

步骤

函数

作用

cond_resched()

检查 TIF_NEED_RESCHED,让出 CPU 避免长时间占用

cpuset_memory_pressure_bump()

通知 cpuset 进入内存压力模式,可能放宽限制

psi_memstall_enter()

Pressure Stall Information 追踪,记录内存停顿时间

fs_reclaim_acquire()

lockdep 检查,确保不会在持有 FS 锁时回收导致死锁

memalloc_noreclaim_save()

设置 PF_MEMALLOC,允许使用系统预留内存

try_to_free_pages()

核心回收函数

cond_resched()

回收可能耗时很长,再次让出 CPU


五、核心回收函数:try_to_free_pages

文件: mm/vmscan.c:3761-3802

unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
                gfp_t gfp_mask, nodemask_t *nodemask)
{
    unsigned long nr_reclaimed;
    struct scan_control sc = {
        .nr_to_reclaim = SWAP_CLUSTER_MAX,  // 32 页
        .gfp_mask = current_gfp_context(gfp_mask),
        .reclaim_idx = gfp_zone(gfp_mask),
        .order = order,
        .nodemask = nodemask,
        .priority = DEF_PRIORITY,           // 12
        .may_writepage = !laptop_mode,      // laptop_mode 下不写回
        .may_unmap = 1,                     // 允许 unmap
        .may_swap = 1,                      // 允许 swap
    };

    // ① 节流检查:等待 kswapd 有进展
    if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
        return 1;  // 返回 1 避免触发 OOM

    // ② 设置回收状态
    set_task_reclaim_state(current, &sc.reclaim_state);
    trace_mm_vmscan_direct_reclaim_begin(order, sc.gfp_mask);

    // ③ 执行回收
    nr_reclaimed = do_try_to_free_pages(zonelist, &sc);

    trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);
    set_task_reclaim_state(current, NULL);

    return nr_reclaimed;
}

关键参数:

参数

含义

nr_to_reclaim

SWAP_CLUSTER_MAX (32)

目标回收量,只回收 32 页就够了

priority

DEF_PRIORITY (12)

初始优先级,从最轻量开始

may_writepage

!laptop_mode

默认允许写回,laptop 模式下禁止

may_unmap

1

允许 unmap 映射页

may_swap

1

允许 swap 匿名页

与 kswapd 的区别:

维度

kswapd

直接回收

nr_to_reclaim

Σ high_wmark_pages

SWAP_CLUSTER_MAX (32)

触发方式

异步唤醒

同步阻塞

目标

恢复到 HIGH 水位

只要能分配成功

may_writepage

boost 模式下禁止

默认允许

may_swap

boost 模式下禁止

默认允许


六、节流机制:throttle_direct_reclaim

文件: mm/vmscan.c:3681-3759

这是直接回收最重要的保护机制之一。

6.1 为什么需要节流

当系统内存极度紧张时,大量分配线程同时进入直接回收会:

  1. 竞争回收资源: 多个线程扫描同一个 LRU 链表,浪费 CPU

  2. 阻塞关键线程: 如果文件系统线程(如 kjournald)被阻塞,可能导致回收本身无法进展

  3. OOM 误杀: 在 kswapd 正在回收的过程中,直接回收线程可能误判为 OOM

6.2 节流流程

throttle_direct_reclaim(gfp_mask, zonelist, nodemask)
│
├─ ① 跳过条件
│   ├─ 内核线程 (PF_KTHREAD) → 不节流(可能是回收的关键参与者)
│   └─ 有致命信号 (fatal_signal_pending) → 不节流(让进程快速退出释放内存)
│
├─ ② 检查 pfmemalloc 储备
│   └─ 遍历 zonelist,找到第一个 ZONE_NORMAL 或更低的 zone
│       └─ allow_direct_reclaim(pgdat) 检查
│           ├─ free_pages > pfmemalloc_reserve / 2 → 不节流
│           └─ 否则 → 需要节流
│
├─ ③ 节流方式
│   ├─ 有 __GFP_FS?→ wait_event_killable (无限等待 kswapd 唤醒)
│   └─ 无 __GFP_FS?→ wait_event_interruptible_timeout (最多等 1 秒)
│   └─ 无 __GFP_FS 的原因:可能持有 FS 锁,无限等待会死锁
│
└─ ④ 返回
    ├─ fatal_signal_pending → return true(分配器不触发 OOM)
    └─ 正常唤醒 → return false(继续直接回收)

6.3 allow_direct_reclaim 判断

static bool allow_direct_reclaim(pg_data_t *pgdat)
{
    // 绝望节点:kswapd 失败 16 次,不再节流
    if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
        return true;

    // 计算 pfmemalloc 储备和空闲页
    for (i = 0; i <= ZONE_NORMAL; i++) {
        pfmemalloc_reserve += min_wmark_pages(zone);
        free_pages += zone_page_state(zone, NR_FREE_PAGES);
    }

    wmark_ok = free_pages > pfmemalloc_reserve / 2;

    // 水位不足?唤醒 kswapd
    if (!wmark_ok && waitqueue_active(&pgdat->kswapd_wait)) {
        WRITE_ONCE(pgdat->kswapd_highest_zoneidx, ZONE_NORMAL);
        wake_up_interruptible(&pgdat->kswapd_wait);
    }

    return wmark_ok;
}

关键设计:

  • 储备阈值: pfmemalloc_reserve / 2,即 MIN 水位的一半

  • 自动唤醒 kswapd: 节流时顺便唤醒 kswapd,让后台线程先干活

  • 绝望不节流: kswapd 连续失败 16 次后,不再节流,让直接回收放手干


七、回收主循环:do_try_to_free_pages

文件: mm/vmscan.c:3544-3630

do_try_to_free_pages(zonelist, sc)
│
├─ 统计 ALLOCSTALL 事件
│
├─ do {  // priority 从 12 递减到 0
│   │
│   ├─ vmpressure_prio()  // 内存压力通知
│   │
│   ├─ shrink_zones(zonelist, sc)  // ★回收所有 zone★
│   │   └─ for_each_zone(zone, zonelist)
│   │       ├─ cpuset_zone_allowed() 检查
│   │       ├─ compaction_ready()?→ 跳过,留给压缩
│   │       ├─ mem_cgroup_soft_limit_reclaim()
│   │       └─ shrink_node(pgdat, sc)  // ★节点级回收★
│   │
│   ├─ nr_reclaimed >= nr_to_reclaim?→ break(够了)
│   │
│   ├─ compaction_ready?→ break(压缩可用,停止回收)
│   │
│   ├─ priority < 10?→ may_writepage = 1(困难时允许写回)
│   │
│   └─ priority--
│
│  } while (priority >= 0)
│
├─ snapshot_refaults()  // 记录 refault 统计
│
├─ 回收成功?→ return nr_reclaimed
│
├─ compaction_ready?→ return 1(避免 OOM)
│
├─ skipped_deactivate?→ 强制 deactivate,retry
│
├─ memcg_low_skipped?→ 强制回收 memcg low 保护内存,retry
│
└─ return 0(回收失败)

7.1 priority 递减 vs kswapd

维度

kswapd (balance_pgdat)

直接回收 (do_try_to_free_pages)

priority 范围

12 → 1

12 → 0

目标量

Σ high_wmark_pages

SWAP_CLUSTER_MAX (32)

停止条件

平衡或 priority=1

回收够或 priority=0

may_writepage

boost 模式禁止

priority<10 时强制开启

7.2 nr_to_reclaim 的差异

kswapd 的 nr_to_reclaim 是各 zone HIGH 水位之和(可能几百页),目标是全面恢复水位

直接回收的 nr_to_reclaimSWAP_CLUSTER_MAX = 32 页,目标是最小化阻塞时间——只要能分配成功就行。

7.3 两个重试条件

skipped_deactivate 重试:

  • reclaim_idx 限制或 memory.low cgroup 设置导致大量内存不可回收

  • LRU 扫描时跳过了活跃页降级(deactivate)

  • 解决方案:强制 force_deactivate = 1,强制降级所有活跃页

memcg_low_skipped 重试:

  • cgroup 的 memory.low 保护了一些内存,回收时跳过了

  • 解决方案:设置 memcg_low_reclaim = 1,强制回收被保护的内存

  • 这是最后手段,避免 OOM


八、shrink_zones — zone 级回收调度

文件: mm/vmscan.c:3431-3514

static void shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
{
    // buffer_heads 过多?扩展到 highmem
    if (buffer_heads_over_limit) {
        sc->gfp_mask |= __GFP_HIGHMEM;
        sc->reclaim_idx = gfp_zone(sc->gfp_mask);
    }

    for_each_zone_zonelist_nodemask(zone, z, zonelist, sc->reclaim_idx, ...) {
        // cpuset 检查
        if (!cpuset_zone_allowed(zone, ...))
            continue;

        // 高阶分配 + 压缩已就绪 → 跳过回收
        if (sc->order > PAGE_ALLOC_COSTLY_ORDER && compaction_ready(zone, sc)) {
            sc->compaction_ready = true;
            continue;
        }

        // 同一节点只回收一次
        if (zone->zone_pgdat == last_pgdat)
            continue;
        last_pgdat = zone->zone_pgdat;

        // cgroup 软限制回收
        mem_cgroup_soft_limit_reclaim(zone->zone_pgdat, ...);

        // ★调用 shrink_node★
        shrink_node(zone->zone_pgdat, sc);
    }
}

设计要点:

  1. 每个节点只回收一次: 通过 last_pgdat 去重

  2. 压缩优先: 高阶分配场景下,如果压缩已就绪,跳过回收

  3. buffer_heads 特殊处理: 32 位系统上 highmem 页可能 pin 住 lowmem


九、直接回收 vs kswapd 对比

维度

kswapd

直接回收

触发

水位降到 LOW

分配失败,慢路径

执行上下文

kswapd 内核线程

分配线程本身

同步/异步

异步(不阻塞分配)

同步(阻塞分配线程)

回收目标

恢复到 HIGH 水位

最小化(32 页)

priority 范围

12 → 1

12 → 0

may_writepage

boost 模式禁止

默认允许

may_swap

boost 模式禁止

默认允许

节流机制

throttle_direct_reclaim

OOM 触发

不触发

可触发

失败后果

kswapd_failures++

进入 OOM 阶段


十、直接回收的时序

时间线 ──────────────────────────────────────────────────────►

分配线程                          kswapd
   │                                │
   │ __alloc_pages_slowpath()       │
   │                                │
   │ wake_all_kswapds() ──────────►│ (异步唤醒)
   │                                │
   │ get_page_from_freelist()       │
   │ └─ 失败                        │
   │                                │
   │ __alloc_pages_direct_compact() │
   │ └─ 失败                        │
   │                                │
   │ __alloc_pages_direct_reclaim() │
   │ │                              │
   │ ├─ __perform_reclaim()         │
   │ │   ├─ cond_resched()          │
   │ │   ├─ throttle_direct_reclaim │
   │ │   │   └─ 检查水位            │
   │ │   │       ├─ 水位 OK → 继续  │
   │ │   │       └─ 水位不足        │
   │ │   │           ├─ 唤醒 kswapd─┤
   │ │   │           └─ 等待...     │ ◄── kswapd 回收中...
   │ │   │               │          │
   │ │   │           kswapd 唤醒 ──►│
   │ │   │               │          │
   │ │   ├─ try_to_free_pages()     │
   │ │   │   └─ do_try_to_free_pages│
   │ │   │       └─ shrink_zones()  │
   │ │   │           └─ shrink_node()│
   │ │   │                          │
   │ │   └─ 回收完成                 │
   │ │                              │
   │ ├─ get_page_from_freelist()    │
   │ │   └─ 成功 ✓                  │
   │ │                              │
   │ └─ 返回页面                    │

十一、关键设计思想

11.1 最小化阻塞

直接回收只回收 32 页(SWAP_CLUSTER_MAX),而不是像 kswapd 那样恢复整个水位。这是最小化阻塞时间的设计——分配线程只需要"够用"就行,剩下的交给 kswapd 慢慢恢复。

11.2 节流保护

throttle_direct_reclaim 是防止"回收风暴"的关键:

  • 内存紧张时,大量线程同时进入直接回收会加剧竞争

  • 节流让它们等待 kswapd 有进展后再继续

  • 避免了"越回收越紧张"的恶性循环

11.3 渐进式加力

priority 从 12 到 0,扫描量从 1/4096 到 100%。大多数情况下低优先级就够了,只有极端情况才全量扫描。

11.4 两层重试保护

  1. skipped_deactivate 重试: 强制降级活跃页

  2. memcg_low_skipped 重试: 强制回收 cgroup 保护内存

这两层重试确保在最终放弃(进入 OOM)之前,已经尝试了所有可能的回收手段。

11.5 与 kswapd 的协作

直接回收不是独立工作的:

  • 节流时唤醒 kswapd: 让后台线程先干活

  • kswapd 唤醒被 throttle 的进程: 回收有进展后通知等待的分配线程

  • compaction_ready 跳过: 如果压缩已就绪,直接回收可以停止,交给压缩


十二、与慢路径的衔接

回到 __alloc_pages_slowpath 的阶段 4:

// 阶段4: 直接回收
page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                                    &did_some_progress);
if (page)
    goto got_pg;

did_some_progress 的作用:

  • > 0: 回收了一些页面,后续的 OOM Killer 判断会考虑这一点

  • 0: 回收失败,分配器可能进入 OOM 阶段

  • should_reclaim_retry 中:如果 did_some_progress 且理论上回收够,值得重试


十三、关键数据结构

13.1 scan_control 在直接回收中的初始化

struct scan_control sc = {
    .nr_to_reclaim = SWAP_CLUSTER_MAX,  // 32(vs kswapd 的 Σ high_wmark)
    .gfp_mask = current_gfp_context(gfp_mask),
    .reclaim_idx = gfp_zone(gfp_mask),  // 由 GFP 标志决定
    .order = order,
    .nodemask = nodemask,
    .priority = DEF_PRIORITY,           // 12
    .may_writepage = !laptop_mode,      // 默认允许
    .may_unmap = 1,
    .may_swap = 1,
};

13.2 throttle_direct_reclaim 中的关键判断

// 内核线程不节流
if (current->flags & PF_KTHREAD) goto out;

// 致命信号不节流
if (fatal_signal_pending(current)) goto out;

// 水位判断:free_pages > MIN 水位 / 2
wmark_ok = free_pages > pfmemalloc_reserve / 2;

// 无 __GFP_FS → 最多等 1 秒(避免死锁)
// 有 __GFP_FS → 无限等待 kswapd 唤醒

十四、总结

#

要点

说明

1

同步回收

分配线程自己动手,阻塞等待

2

最小目标

只回收 32 页,最小化阻塞

3

节流保护

等待 kswapd 有进展,避免回收风暴

4

渐进加力

priority 12→0,从轻量到全量

5

两层重试

skipped_deactivate + memcg_low_skipped

6

与 kswapd 协作

节流时唤醒 kswapd,kswapd 唤醒被 throttle 的进程

7

压缩优先

compaction_ready 时跳过回收

8

不触发 OOM

回收失败后交给慢路径的 OOM 阶段


十五、待深入的下一步

直接回收和 kswapd 两条路最终汇聚到同一个函数:

kswapd:      balance_pgdat() → kswapd_shrink_node() → shrink_node()
direct:      try_to_free_pages() → do_try_to_free_pages() → shrink_zones() → shrink_node()

下一篇: shrink_node() — 节点级回收核心,kswapd 和 direct reclaim 的共同下游。