AI智能摘要
在上一节整体梳理内存回收机制之后,本篇聚焦“内脏细节”,系统性拆解 Linux 内核中实际扫描与释放页面的关键链路。文章围绕 kswapd 后台回收与直接回收两大路径展开:一条从 kswapd() → balance_pgdat() → shrink_node(),解析内核回收线程何时被唤醒、在何种回收程度下停止,以及它与页面分配器之间如何协同保持内存水位;
此摘要由AI分析文章内容生成,仅供参考。

前言

紧接上文,我们从整体脉络上把握了内存回收机制的几种情况,但回收的"内脏"——实际扫描和释放页面的机制——还未深入!本文将从 kswapd 开始详细拆解内部细节:

方向

核心函数

分析重点

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

kswapd 是什么?

kswapd 是 Linux 内核为每个 NUMA 节点创建的后台内存回收线程。它的职责是在内存水位下降到 LOW 水位时被唤醒,异步回收页面,将水位提升到 HIGH 水位后重新睡眠。

核心定位: kswapd 是内存回收的"第一道防线",在分配器慢路径的直接回收(direct reclaim)之前工作,避免分配线程被阻塞在同步回收中。

整体架构

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                              分配路径                                                        │
│                                                                                             │
│   __alloc_pages_slowpath()                                                                  │
│   ├─ wake_all_kswapds()             ─────────────────────────────────────┐                  │
│   ├─ __alloc_pages_direct_compact()                                     │                  │
│   ├─ __alloc_pages_direct_reclaim()                                     │                  │
│   └─ __alloc_pages_may_oom()                                            │                  │
└─────────────────────────────────────────────────────────────────────────┼──────────────────┘
                                                                          │
                                                                          ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                              kswapd 后台回收                                                 │
│                                                                                             │
│   wakeup_kswapd()                                                                           │
│   │  传递 order / highest_zoneidx                                                           │
│   │  唤醒 pgdat->kswapd_wait                                                                │
│   ▼                                                                                         │
│   kswapd() 主循环                                                                            │
│   ├─ kswapd_try_to_sleep()         // 两阶段睡眠                                            │
│   │   ├─ 短睡 100ms(试探)                                                                 │
│   │   └─ 长睡眠(schedule)                                                                 │
│   └─ balance_pgdat()              // 回收主函数                                             │
│       ├─ priority 递减循环 (12→1)                                                           │
│       ├─ pgdat_balanced()         // 平衡检查                                               │
│       ├─ age_active_anon()        // 匿名页老化                                             │
│       ├─ mem_cgroup_soft_limit_reclaim()                                                    │
│       └─ kswapd_shrink_node()                                                               │
│           └─ shrink_node()        // ★节点级回收核心★                                      │
│                                                                                             │
│   回收完成后:                                                                               │
│   ├─ 唤醒被 throttle 的 direct reclaim 进程                                                │
│   └─ 唤醒 kcompactd 做碎片整理                                                              │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

唤醒链路详解

wake_all_kswapds

// mm/page_alloc.c:4752-4766
static void wake_all_kswapds(unsigned int order, gfp_t gfp_mask,
                             const struct alloc_context *ac)
{
    struct zoneref *z;
    struct zone *zone;
    pg_data_t *last_pgdat = NULL;
    enum zone_type highest_zoneidx = ac->highest_zoneidx;
 
    for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, highest_zoneidx,
                                    ac->nodemask) {
        if (last_pgdat != zone->zone_pgdat)
            wakeup_kswapd(zone, gfp_mask, order, highest_zoneidx);
        last_pgdat = zone->zone_pgdat;
    }
}

设计要点:

  • 遍历 zonelist 中所有 zone,每个 NUMA 节点(pgdat)只唤醒一次

  • 通过 last_pgdat 指针去重,避免同一节点多个 zone 重复唤醒

wakeup_kswapd

void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
                   enum zone_type highest_zoneidx)
{
    pg_data_t *pgdat;
    enum zone_type curr_idx;
 
    // 1. 前置检查
    if (!managed_zone(zone))          // zone 无管理页面
        return;
    if (!cpuset_zone_allowed(zone, gfp_flags))  // cpuset 限制
        return;
 
    // 2. 传递参数给 kswapd(只增不减)
    pgdat = zone->zone_pgdat;
    curr_idx = READ_ONCE(pgdat->kswapd_highest_zoneidx);
    if (curr_idx == MAX_NR_ZONES || curr_idx < highest_zoneidx)
        WRITE_ONCE(pgdat->kswapd_highest_zoneidx, highest_zoneidx);
    if (READ_ONCE(pgdat->kswapd_order) < order)
        WRITE_ONCE(pgdat->kswapd_order, order);
 
    // 3. kswapd 没在等待?不唤醒
    if (!waitqueue_active(&pgdat->kswapd_wait))
        return;
 
    // 4. 绝望节点保护
    if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES ||
        (pgdat_balanced(pgdat, order, highest_zoneidx) &&
         !pgdat_watermark_boosted(pgdat, highest_zoneidx))) {
        // 碎片化场景,唤醒 kcompactd 代替
        if (!(gfp_flags & __GFP_DIRECT_RECLAIM))
            wakeup_kcompactd(pgdat, order, highest_zoneidx);
        return;
    }
 
    // 5. 唤醒 kswapd
    wake_up_interruptible(&pgdat->kswapd_wait);
}

关键设计:

设计点

说明

原子参数传递

READ_ONCE / WRITE_ONCE 保证与 kswapd 线程的并发安全

只增不减

kswapd_order 和 kswapd_highest_zoneidx 只往大了更新,高优先级请求不被覆盖

绝望节点保护

连续 16 次失败后不再唤醒 kswapd,避免无意义 CPU 消耗

碎片化降级

绝望 + 碎片化场景,唤醒 kcompactd 做碎片整理而非回收

kswapd 主循环

文件: mm/vmscan.c:4463-4549

线程初始化

static int kswapd(void *p)
{
    pg_data_t *pgdat = (pg_data_t *)p;
    struct task_struct *tsk = current;
 
    // 绑定到本节点 CPU
    set_cpus_allowed_ptr(tsk, cpumask_of_node(pgdat->node_id));
 
    // 设置关键标志
    tsk->flags |= PF_MEMALLOC | PF_SWAPWRITE | PF_KSWAPD;
    set_freezable();
}

标志

作用

PF_MEMALLOC

允许使用系统预留内存,kswapd 回收时自身也可能需要分配内存

PF_SWAPWRITE

允许写交换分区

PF_KSWAPD

标识身份,影响 get_scan_count 中的 swappiness 调节

主循环流程

for(;;) {
    // 1. 读取唤醒参数
    alloc_order = pgdat->kswapd_order;
    highest_zoneidx = kswapd_highest_zoneidx(pgdat, highest_zoneidx);
 
    // 2. 重置参数(已被取走)
    pgdat->kswapd_order = 0;
    pgdat->kswapd_highest_zoneidx = MAX_NR_ZONES;
 
    // 3. 尝试睡眠
kswapd_try_sleep:
    kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order, highest_zoneidx);
 
    // 4. 再次读取参数(睡眠期间可能被更新)
    alloc_order = pgdat->kswapd_order;
    highest_zoneidx = kswapd_highest_zoneidx(pgdat, highest_zoneidx);
    pgdat->kswapd_order = 0;
    pgdat->kswapd_highest_zoneidx = MAX_NR_ZONES;
 
    // 5. 冻结检查(休眠/挂起场景)
    if (try_to_freeze()) continue;
    if (kthread_should_stop()) break;
 
    // 6. 执行回收
    reclaim_order = balance_pgdat(pgdat, alloc_order, highest_zoneidx);
 
    // 7. 回收不足?继续睡眠等待下次唤醒
    if (reclaim_order < alloc_order)
        goto kswapd_try_sleep;
}

两阶段睡眠机制

文件: mm/vmscan.c:4360-4439
这是 kswapd 最精妙的设计之一。

流程

kswapd_try_to_sleep()
│
├─ prepare_to_wait(TASK_INTERRUPTIBLE)
│
├─ prepare_kswapd_sleep() 检查是否可以睡
│   ├─ 唤醒 pfmemalloc_wait 上的等待者
│   ├─ kswapd_failures >= 16 → return true(绝望,可以睡)
│   └─ pgdat_balanced() → return true(已平衡,可以睡)
│
├─ 可以睡 →
│   ├─ reset_isolation_suitable()  // 重置压缩跳过缓存
│   ├─ wakeup_kcompactd()          // 唤醒 kcompactd
│   └─ schedule_timeout(HZ/10)     // ★短睡 100ms★
│       └─ remaining != 0 → 被提前唤醒,更新参数
│
└─ 短睡完仍可睡?→
    ├─ 降低 vmstat 阈值
    ├─ schedule()                    // ★长睡眠★
    └─ 恢复 vmstat 阈值

为什么是两阶段

阶段

时长

目的

短睡

100ms

"试探"——如果 100ms 内又被唤醒,说明内存压力持续,跳过长睡眠直接回收

长睡眠

无限期

真正休息,等待 wake_up_interruptible 唤醒

对比传统设计: 如果只有一阶段长睡眠,kswapd 被唤醒后回收完成就长睡,下一次分配压力来临时才被唤醒。中间如果有持续的小压力,kswapd 会反复"醒来→回收→长睡→被唤醒",每次都有长睡眠的延迟。两阶段设计让 kswapd 在压力持续时保持"浅睡眠",响应更快。

vmstat 阈值调整

// 睡前:降低阈值,减少 per-cpu 计数偏差
set_pgdat_percpu_threshold(pgdat, calculate_normal_threshold);
 
schedule();  // 长睡眠
 
// 醒后:恢复正常阈值,减少 CPU 开销
set_pgdat_percpu_threshold(pgdat, calculate_pressure_threshold);

原因: vmstat 计数器(如 NR_FREE_PAGES)有 per-cpu 偏差,偏差量 ≈ nr_online_cpus × threshold。kswapd 醒着时降低阈值,确保水位检查更准确;睡眠时恢复,减少刷新频率。

balance_pgdat 回收主函数

文件: mm/vmscan.c:4120-4343

初始化

struct scan_control sc = {
    .gfp_mask = GFP_KERNEL,
    .order = order,
    .may_unmap = 1,
};

boost 统计

nr_boost_reclaim = 0;
for (i = 0; i <= highest_zoneidx; i++) {
    zone = pgdat->node_zones + i;
    if (!managed_zone(zone)) continue;
    nr_boost_reclaim += zone->watermark_boost;
    zone_boosts[i] = zone->watermark_boost;
}
boosted = nr_boost_reclaim;

watermark_boost 是碎片化触发的水位提升量。统计它是为了在回收完成后恢复。

核心循环

restart:
    set_reclaim_active(pgdat, highest_zoneidx);
    sc.priority = DEF_PRIORITY;  // 12
 
    do {
        // 1. buffer_heads 过多?扩展回收范围
        if (buffer_heads_over_limit)
            sc.reclaim_idx = 最高 zone;
 
        // 2. 平衡检查
        balanced = pgdat_balanced(pgdat, sc.order, highest_zoneidx);
        if (!balanced && nr_boost_reclaim) {
            nr_boost_reclaim = 0;   // 放弃 boost,全力回收
            goto restart;
        }
        if (!nr_boost_reclaim && balanced)
            goto out;  // 已平衡,退出
 
        // 3. 设置回收策略
        sc.may_writepage = !laptop_mode && !nr_boost_reclaim;
        sc.may_swap = !nr_boost_reclaim;
        // boost 模式:不写回、不 swap,只释放干净页
 
        // 4. 老化匿名页活跃链表
        age_active_anon(pgdat, &sc);
 
        // 5. cgroup 软限制回收
        mem_cgroup_soft_limit_reclaim(pgdat, sc.order, ...);
 
        // 6. ★真正回收★
        if (kswapd_shrink_node(pgdat, &sc))
            raise_priority = false;
 
        // 7. 唤醒被 throttle 的 direct reclaim 进程
        if (allow_direct_reclaim(pgdat))
            wake_up_all(&pgdat->pfmemalloc_wait);
 
        // 8. 优先级调整
        if (raise_priority || !nr_reclaimed)
            sc.priority--;
 
        // 9. boost 保护:无进展则退出
        if (nr_boost_reclaim && !nr_reclaimed)
            break;
 
    } while (sc.priority >= 1);

priority 递减机制

priority

扫描量 (LRU 总页数 >> priority)

百分比

12

1/4096

0.02%

11

1/2048

0.05%

10

1/1024

0.1%

8

1/256

0.4%

6

1/64

1.6%

4

1/16

6.25%

2

1/4

25%

1

1/2

50%

设计思想: 从最轻量开始,大多数情况下低优先级就能回收够。只有在极端压力下才逐步加大力度,避免全量扫描的 CPU 和 IO 开销。

boost 模式详解

watermark_boost > 0(碎片化触发)时:

策略

boost 模式

正常模式

may_writepage

0(不写回)

1(可写回)

may_swap

0(不 swap)

1(可 swap)

priority 上限

DEF_PRIORITY - 2(最多扫 1/1024)

1(扫 1/2)

回收目标

释放干净文件缓存页

全面回收

目的: 碎片化场景的"快速止血"——只做最轻量的回收,释放页面后交给 kcompactd 做碎片整理。避免在碎片化场景下触发昂贵的 IO(写脏页/swap)。

kswapd_shrink_node

文件: mm/vmscan.c:4030-4065

static bool kswapd_shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
    // 计算目标回收量
    sc->nr_to_reclaim = 0;
    for (z = 0; z <= sc->reclaim_idx; z++) {
        zone = pgdat->node_zones + z;
        if (!managed_zone(zone)) continue;
        sc->nr_to_reclaim += max(high_wmark_pages(zone), SWAP_CLUSTER_MAX);
    }
 
    // ★调用 shrink_node★
    shrink_node(pgdat, sc);
 
    // 高阶回收成功,降为 order-0 继续
    if (sc->order && sc->nr_reclaimed >= compact_gap(sc->order))
        sc->order = 0;
 
    return sc->nr_scanned >= sc->nr_to_reclaim;
}

关键点:

  • nr_to_reclaim = 各 zone 的 HIGH 水位之和(至少 SWAP_CLUSTER_MAX = 32)

  • shrink_node() 是真正的回收执行者,下一层分析的重点

  • 高阶回收成功后降为 order-0,避免过度回收

prepare_kswapd_sleep 与 pgdat_balanced

prepare_kswapd_sleep

文件: mm/vmscan.c:3991-4020

static bool prepare_kswapd_sleep(pg_data_t *pgdat, int order, int highest_zoneidx)
{
    // 唤醒被 throttle 的 direct reclaim 进程
    if (waitqueue_active(&pgdat->pfmemalloc_wait))
        wake_up_all(&pgdat->pfmemalloc_wait);
 
    // 绝望节点
    if (pgdat->kswapd_failures >= MAX_RECLAIM_RETRIES)
        return true;
 
    // 已平衡
    if (pgdat_balanced(pgdat, order, highest_zoneidx)) {
        clear_pgdat_congested(pgdat);
        return true;
    }
 
    return false;
}

pgdat_balanced

文件: mm/vmscan.c:3940-3973

static bool pgdat_balanced(pg_data_t *pgdat, int order, int highest_zoneidx)
{
    // 从低 zone 到高 zone 逐个检查
    for (i = 0; i <= highest_zoneidx; i++) {
        zone = pgdat->node_zones + i;
        if (!managed_zone(zone)) continue;
 
        mark = high_wmark_pages(zone);
        // 检查:空闲页 > HIGH 水位 + 能分配 2^order 连续页
        if (zone_watermark_ok_safe(zone, order, mark, highest_zoneidx))
            return true;  // 有一个 zone 达标即可
    }
 
    if (mark == -1) return true;  // 无 populated zone
    return false;
}

关键: 只要有一个 zone 达到 HIGH 水位,kswapd 就认为节点已平衡。这是"最低标准"策略——不需要所有 zone 都完美,只要有一个够用就行。

kswapd 与其他组件的协作

与 kcompactd

kswapd 回收完成
  ├─ kswapd_try_to_sleep() → wakeup_kcompactd()
  └─ balance_pgdat() out → wakeup_kcompactd()

kswapd 负责释放页面,kcompactd 负责整理碎片。两者形成流水线:

  • kswapd 释放页面后,空闲页增多,kcompactd 有更多空间做迁移

  • kcompactd 整理碎片后,高阶分配成功率提升

与 direct reclaim

balance_pgdat()
  └─ if (allow_direct_reclaim(pgdat))
      wake_up_all(&pgdat->pfmemalloc_wait)

当 kswapd 回收使水位恢复到 LOW 以上时,唤醒被 throttle 的 direct reclaim 进程。这些进程在 throttle_direct_reclaim() 中等待 kswapd 有了进展再继续。

与 OOM

kswapd_failures >= MAX_RECLAIM_RETRIES (16)
  → kswapd 放弃该节点
  → 交给 direct reclaim + OOM Killer

kswapd 不会触发 OOM。它只是回收失败后"投降",让分配路径的 direct reclaim 和 OOM Killer 接管。

scan_control 结构体速查

文件: mm/vmscan.c:65-175

struct scan_control {
    unsigned long nr_to_reclaim;    // 目标回收量
    unsigned long nr_reclaimed;     // 已回收量
    unsigned long nr_scanned;       // 已扫描量
    s8            order;            // 分配阶数
    s8            priority;         // 扫描优先级 (12→1)
    s8            reclaim_idx;      // 最高回收 zone
    gfp_t         gfp_mask;         // 分配掩码
    unsigned int  may_writepage:1;  // 允许写回脏页
    unsigned int  may_unmap:1;      // 允许 unmap 映射页
    unsigned int  may_swap:1;       // 允许 swap
    unsigned int  may_deactivate:2; // 允许降级活跃页
    unsigned int  cache_trim_mode:1;// 文件缓存过多模式
    unsigned int  file_is_tiny:1;   // 文件页过少保护
    struct mem_cgroup *target_mem_cgroup;  // 目标 cgroup
};

关键数据结构关系

pg_data_t (per NUMA node)
├─ kswapd_wait          // 等待队列,wakeup_kswapd 唤醒
├─ kswapd_order         // 请求的分配阶数
├─ kswapd_highest_zoneidx  // 请求的最高 zone
├─ kswapd_failures      // 连续失败次数
├─ pfmemalloc_wait      // direct reclaim 等待队列
├─ node_zones[]         // zone 数组
│   ├─ _watermark[3]    // MIN/LOW/HIGH 水位
│   ├─ watermark_boost  // 碎片化 boost 量
│   └─ flags            // ZONE_RECLAIM_ACTIVE 等
└─ kswapd               // kswapd task_struct 指针

完整时序图

设计思想总结

#

设计思想

体现位置

解决的问题

1

两阶段睡眠

kswapd_try_to_sleep

压力持续时快速响应,避免反复长睡眠

2

只增不减的参数传递

wakeup_kswapd

高优先级请求不被低优先级覆盖

3

priority 递减

balance_pgdat

从轻量扫描开始,避免全量扫描开销

4

boost 轻量模式

balance_pgdat

碎片化场景快速止血,不做昂贵 IO

5

绝望保护

wakeup_kswapd / prepare_kswapd_sleep

连续失败后停止无意义空转

6

与 kcompactd 流水线

kswapd_try_to_sleep / balance_pgdat out

回收 + 压缩分工协作

7

唤醒 throttle 进程

balance_pgdat

kswapd 有进展后让等待的分配线程继续

8

vmstat 阈值调整

kswapd_try_to_sleep

减少 per-cpu 计数偏差对水位检查的影响

下一篇预告

kswapd 分析完成后,两条路汇聚到同一个函数:

kswapd 路径:  balance_pgdat() → kswapd_shrink_node() → shrink_node()
direct reclaim 路径:  try_to_free_pages() → shrink_zones() → shrink_node()

所以我们下一篇就分析 direct reclaim 的路径,然后再下一批深入 shrink_node ,节点级回收调度,它是 kswapd 和 direct reclaim 的共同下游。