AI智能摘要
shrink_node 是 Linux 内存回收路径的核心枢纽:无论是 kswapd 后台回收,还是 direct reclaim / node_reclaim 这种前台、同步回收,最终都汇聚到这个函数,在节点维度完成真实的页面回收调度。文中用结构化示意图清晰展开 shrink_node 的调用链:上游由 kswapd_shrink_node(后台回收)、shrink_zones(try_to_free_pages 路径的 direct reclaim)以及 __node_reclaim(快速 node_reclaim 同步回收)等入口触发;下游则分流到 shrink_node_memcgs、
此摘要由AI分析文章内容生成,仅供参考。

一、总体定位

shrink_node 是 Linux 内存回收的核心枢纽——kswapd 后台回收和 direct reclaim 直接回收两条路径在此汇合。无论内存回收从哪条路径触发,最终都要经过 shrink_node 完成实际的页面回收工作。

                    ┌─────────────────────┐
                    │    shrink_node()     │  ← 核心枢纽
                    │   (节点级回收调度)    │
                    └──────────┬──────────┘
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
    shrink_node_memcgs()   vmpressure()   should_continue_reclaim()
              │
    ┌─────────┴─────────┐
    ▼                   ▼
shrink_lruvec()    shrink_slab()
    │
    ├─ get_scan_count()      ← 计算扫描比例
    ├─ shrink_list()
    │   ├─ shrink_active_list()      ← 活跃→不活跃降级
    │   └─ shrink_inactive_list()    ← 不活跃页回收
    └─ shrink_active_list() (尾部)   ← 匿名页老化

1.1 调用者(上游)

调用者

文件

场景

kswapd_shrink_node()

vmscan.c

kswapd 后台回收

shrink_zones()

vmscan.c

direct reclaim(try_to_free_pages 路径)

__node_reclaim()

vmscan.c

快路径 node_reclaim 同步回收

1.2 函数签名

// mm/vmscan.c:3174
static void shrink_node(pg_data_t *pgdat, struct scan_control *sc)

参数:

  • pgdat: 目标 NUMA 节点

  • sc: 回收控制结构(包含优先级、目标回收数、GFP 掩码等)


二、执行流程详解

2.1 完整流程图

shrink_node(pgdat, sc)
│
├─ ① 初始化阶段
│   ├─ target_lruvec = mem_cgroup_lruvec(sc->target_mem_cgroup, pgdat)
│   ├─ mem_cgroup_flush_stats()          ← 刷新 memcg 统计,确保读到准确数据
│   ├─ memset(&sc->nr, 0, ...)           ← 清零本轮扫描统计
│   └─ 读取 nr_reclaimed / nr_scanned 快照
│
├─ ② 获取回收成本
│   ├─ spin_lock_irq(&target_lruvec->lru_lock)
│   ├─ sc->anon_cost = target_lruvec->anon_cost   ← 匿名页回收成本
│   ├─ sc->file_cost = target_lruvec->file_cost   ← 文件页回收成本
│   └─ spin_unlock_irq()
│
├─ ③ 降级决策(may_deactivate)
│   ├─ force_deactivate == true?
│   │   └─ 是 → may_deactivate = DEACTIVATE_ANON | DEACTIVATE_FILE(强制降级)
│   └─ 否 → 分别判断 anon 和 file
│       ├─ 匿名页: refaults 变化 || inactive_is_low() → 允许降级
│       └─ 文件页: refaults 变化 || inactive_is_low() → 允许降级
│
├─ ④ cache_trim_mode 判断
│   ├─ file >> sc->priority && 无需降级文件页?
│   │   └─ 是 → cache_trim_mode = 1(优先回收文件缓存)
│   └─ 否 → cache_trim_mode = 0
│
├─ ⑤ file_is_tiny 保护(仅非 cgroup 回收)
│   ├─ 计算 free + file 总量 vs total_high_wmark
│   └─ file + free <= high_wmark && 无需降级 anon && anon 充足?
│       └─ 是 → file_is_tiny = 1(保护文件页,强制扫描匿名页)
│
├─ ⑥ 核心回收 ★
│   └─ shrink_node_memcgs(pgdat, sc)
│       └─ 遍历每个 memcg → shrink_lruvec() + shrink_slab()
│
├─ ⑦ 累加 slab 回收量
│   └─ sc->nr_reclaimed += reclaim_state->reclaimed_slab
│
├─ ⑧ vmpressure 评估
│   └─ vmpressure(gfp_mask, target_mem_cgroup, ...)
│
├─ ⑨ kswapd 专属:IO 拥塞处理
│   ├─ 全部 taken 页都在 writeback? → set PGDAT_WRITEBACK
│   ├─ 全部 taken 页都是 unqueued_dirty? → set PGDAT_DIRTY
│   └─ 有 immediate 页? → congestion_wait(100ms)
│
├─ ⑩ 拥塞标记(kswapd + cgroup)
│   └─ dirty 页全是 congested? → set LRUVEC_CONGESTED
│
├─ ⑪ direct reclaim 拥塞等待
│   └─ 非 kswapd && LRUVEC_CONGESTED? → wait_iff_congested(100ms)
│
├─ ⑫ 判断是否继续回收
│   └─ should_continue_reclaim() == true? → goto again
│
└─ ⑬ 清理
    └─ reclaimable? → pgdat->kswapd_failures = 0(重置失败计数器)

2.2 关键阶段详解

阶段③:降级决策(may_deactivate)

这是 shrink_node 最关键的决策之一——决定是否将活跃链表页面降级到不活跃链表。

两个判断条件:

  1. refaults 变化检测: WORKINGSET_ACTIVATE_ANON/FILE 计数器变化意味着有新的 workingset 正在建立,说明之前的降级决策可能有误,需要更激进地降级旧页面。

  2. inactive_is_low(): 检查 inactive:active 比例是否过低。

// mm/vmscan.c:2662
static bool inactive_is_low(struct lruvec *lruvec, enum lru_list inactive_lru)
{
    unsigned long inactive, active;
    unsigned long inactive_ratio;
    unsigned long gb;

    inactive = lruvec_page_state(lruvec, NR_LRU_BASE + inactive_lru);
    active = lruvec_page_state(lruvec, NR_LRU_BASE + active_lru);

    gb = (inactive + active) >> (30 - PAGE_SHIFT);  // 总量(GB)
    if (gb)
        inactive_ratio = int_sqrt(10 * gb);  // 比例随总量增长
    else
        inactive_ratio = 1;

    return inactive * inactive_ratio < active;  // inactive 太少?
}

inactive_ratio 的设计思想:

  • 内存越大,inactive 比例可以越低(因为绝对数量已经够用)

  • 1GB → ratio=3, 10GB→ratio=10, 100GB→ratio=31, 1TB→ratio=100

  • 这避免了大内存系统频繁做 active→inactive 降级

阶段④⑤:cache_trim_mode 与 file_is_tiny

这两个标志控制 anon/file 扫描比例:

标志

条件

效果

cache_trim_mode=1

不活跃文件页充足(file >> priority) 且无需降级文件页

只扫描文件页

file_is_tiny

文件页+空闲页 <= high_wmark 且匿名页充足

保护文件页,强制扫描匿名页

"缓存陷阱"防护: file_is_tiny 防止一个正反馈循环——文件缓存被大量回收 → 文件页变少 → 每次文件 fault 都触发 refault → 扫描比例偏向文件 → 文件页更少 → 恶性循环。file_is_tiny 在文件页极少时强制扫描匿名页,打破这个循环。

阶段⑨⑩⑪:IO 拥塞三级处理

              kswapd                    direct reclaim
                │                            │
    ┌───────────┴───────────┐                │
    ▼                       ▼                ▼
 PGDAT_WRITEBACK      PGDAT_DIRTY     LRUVEC_CONGESTED
 (全部页在回写)        (全部是脏页)      (脏页全拥塞)
    │                       │                │
    └───── congestion_wait(100ms) ───────────┘
  • PGDAT_WRITEBACK: 所有隔离页都在回写中,说明 IO 跟不上回收速度

  • PGDAT_DIRTY: 所有隔离页都是未排队的脏页,唤醒 flusher 线程

  • LRUVEC_CONGESTED: 脏页的 BDI 全部拥塞,direct reclaim 等待 IO 完成


三、核心子函数分析

3.1 shrink_node_memcgs — memcg 遍历回收

// mm/vmscan.c:3112
static void shrink_node_memcgs(pg_data_t *pgdat, struct scan_control *sc)

职责: 遍历节点上的所有 memcg(内存控制组),对每个 memcg 执行 LRU 回收和 slab 回收。

shrink_node_memcgs(pgdat, sc)
│
├─ memcg = mem_cgroup_iter(target_memcg, NULL, NULL)  ← 获取首个 memcg
│
└─ do {
    ├─ cond_resched()                         ← 避免 soft lockup
    ├─ mem_cgroup_calculate_protection()      ← 计算保护阈值
    │
    ├─ mem_cgroup_below_min(memcg)?           ← 硬保护:跳过
    │   └─ continue
    ├─ mem_cgroup_below_low(memcg)?           ← 软保护:首次跳过
    │   └─ sc->memcg_low_skipped = 1; continue
    │
    ├─ shrink_lruvec(lruvec, sc)              ← LRU 页面回收
    ├─ shrink_slab(gfp_mask, nid, memcg, priority)  ← Slab 回收
    └─ vmpressure()                           ← 压力评估
    } while ((memcg = mem_cgroup_iter(...)))  ← 遍历下一个 memcg

memcg 保护机制:

条件

行为

含义

mem_cgroup_below_min

跳过

硬保护,绝不回收

mem_cgroup_below_low 且首次

跳过,设 memcg_low_skipped

软保护,先回收其他组

mem_cgroup_below_low 且二次

回收,发 MEMCG_LOW 事件

软保护被覆盖(其他组不够)

3.2 shrink_lruvec — LRU 扫描入口

// mm/vmscan.c:2914
static void shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)

职责: 对一个 lruvec(LRU 向量,包含 anon inactive/active 和 file inactive/active 五条链表)执行扫描和回收。

shrink_lruvec(lruvec, sc)
│
├─ get_scan_count(lruvec, sc, nr)     ← 计算每条链表扫描量
├─ memcpy(targets, nr, ...)           ← 保存原始目标(用于比例调整)
│
├─ scan_adjusted 判断:
│   └─ 非 cgroup && 非 kswapd && priority == DEF_PRIORITY?
│       → scan_adjusted = true(全局直接回收,回收够了也继续扫)
│
├─ blk_start_plug(&plug)              ← 合并 IO 请求
│
├─ while 循环(扫描 anon inactive、file active、file inactive)
│   ├─ for_each_evictable_lru(lru):
│   │   ├─ nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX)  ← 每次最多32页
│   │   └─ shrink_list(lru, nr_to_scan, lruvec, sc)
│   │       ├─ active? → shrink_active_list()   ← 降级到 inactive
│   │       └─ inactive? → shrink_inactive_list() ← 尝试回收
│   │
│   ├─ cond_resched()
│   │
│   └─ 回收够了 && !scan_adjusted?
│       └─ 比例调整:停掉较小的 LRU,按比例调整较大的 LRU
│
├─ blk_finish_plug(&plug)             ← 提交合并的 IO
├─ sc->nr_reclaimed += nr_reclaimed
│
└─ 尾部:匿名页老化
    └─ can_age_anon_pages() && inactive_is_low(LRU_INACTIVE_ANON)?
        └─ shrink_active_list(SWAP_CLUSTER_MAX, ..., LRU_ACTIVE_ANON)

关键设计 — scan_adjusted:

  • 当全局直接回收(非 kswapd、非 cgroup)且优先级为默认值时,即使回收够了也继续扫描

  • 原因:直接回收意味着 kswapd 已经跟不上,应该多做一些工作

  • 对于 kswapd 和 cgroup 回收,回收够了就按比例缩减,避免过度回收

关键设计 — 比例调整: 当一种 LRU(如 file)先被扫完,系统不会继续猛攻另一种(anon),而是:

  1. 停掉已扫完的 LRU

  2. 按已扫描百分比缩减另一种 LRU 的扫描量

  3. 这保证 anon/file 扫描比例大致符合 get_scan_count 的设计

3.3 get_scan_count — 扫描比例计算

// mm/vmscan.c:2698
static void get_scan_count(struct lruvec *lruvec, struct scan_control *sc,
                           unsigned long *nr)

职责: 计算五条 LRU 链表各应该扫描多少页面。这是回收策略的核心决策函数。

scan_balance 枚举(决定 anon/file 扫描比例):

模式

条件

含义

SCAN_FILE

无 swap / cgroup 无 swappiness / cache_trim_mode

只扫描文件页

SCAN_ANON

file_is_tiny

保护文件页,扫描匿名页

SCAN_EQUAL

priority==0 且有 swappiness

OOM 边界,均等扫描

SCAN_FRACT

默认

按成本比例扫描

SCAN_FRACT 比例计算:

// 匿名页回收成本(含 swap IO)
anon_cost = total_cost + sc->anon_cost;
// 文件页回收成本(含磁盘 IO)
file_cost = total_cost + sc->file_cost;

// swappiness 调节(0~200,100 为平衡点)
ap = swappiness * (total_cost + 1) / (anon_cost + 1);
fp = (200 - swappiness) * (total_cost + 1) / (file_cost + 1);

设计思想:

  • anon_cost 包含 swap out 的 IO 代价,file_cost 包含磁盘读回的代价

  • swappiness=100 时 anon/file 等价;swappiness=0 时只扫文件(cgroup 场景)

  • 全局回收即使 swappiness=0 也会扫描匿名页(防止 OOM)

  • 至少 1/3 的压力施加给每种 LRU(通过 total_cost 的加法保证)

memcg 保护缩放:

// 如果有 memory.low/min 保护
scan = lruvec_size - lruvec_size * protection / (cgroup_size + 1);
scan = max(scan, SWAP_CLUSTER_MAX);  // 至少扫 32 页
scan >>= sc->priority;               // 优先级缩放

3.4 shrink_list — 分发器

// mm/vmscan.c:2618
static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
                                 struct lruvec *lruvec, struct scan_control *sc)

职责: 根据 LRU 类型分发到不同的处理函数。

shrink_list(lru, nr_to_scan, lruvec, sc)
│
├─ is_active_lru(lru)?
│   ├─ may_deactivate 允许? → shrink_active_list()  ← 降级,返回 0(不回收)
│   └─ 不允许? → sc->skipped_deactivate = 1, 返回 0
│
└─ inactive? → shrink_inactive_list()  ← 实际回收,返回回收数

关键点: shrink_active_list 永远返回 0——它只做降级(active→inactive),不做回收。真正的回收由 shrink_inactive_list 完成。

3.5 shrink_active_list — 活跃链表降级

// mm/vmscan.c:2445
static void shrink_active_list(unsigned long nr_to_scan,
                               struct lruvec *lruvec,
                               struct scan_control *sc,
                               enum lru_list lru)

职责: 从活跃链表取出页面,检查访问情况,将未访问的页面降级到不活跃链表。

shrink_active_list(nr_to_scan, lruvec, sc, lru)
│
├─ ① 准备
│   ├─ lru_add_drain()           ← 将 per-cpu pagevec 刷入 LRU
│   └─ 定义三个临时链表: l_hold, l_active, l_inactive
│
├─ ② 隔离(持锁)
│   ├─ spin_lock_irq(&lruvec->lru_lock)
│   ├─ isolate_lru_pages()       ← 从活跃链表尾部取出页面到 l_hold
│   └─ spin_unlock_irq()
│
├─ ③ 逐页判断(无锁)
│   └─ while (!list_empty(&l_hold)):
│       ├─ page_evictable(page)? → 否: putback_lru_page()
│       ├─ buffer_heads_over_limit? → try_to_release_page() 释放 buffer head
│       │
│       ├─ page_referenced(page)? ← 检查最近是否有 PTE 引用
│       │   ├─ 是 && VM_EXEC && file? → list_add(l_active)  ← 代码页,保活
│       │   └─ 否 → 继续降级
│       │
│       ├─ ClearPageActive(page)      ← 清除活跃标志
│       ├─ SetPageWorkingset(page)    ← 标记为 workingset
│       └─ list_add(l_inactive)       ← 放入不活跃链表
│
├─ ④ 回写(持锁)
│   ├─ spin_lock_irq(&lruvec->lru_lock)
│   ├─ move_pages_to_lru(lruvec, &l_active)    ← 保活页→活跃链表
│   ├─ move_pages_to_lru(lruvec, &l_inactive)  ← 降级页→不活跃链表
│   └─ spin_unlock_irq()
│
└─ ⑤ 清理
    ├─ free_unref_page_list()    ← 释放已成为空闲的页面
    └─ 统计 PGDEACTIVATE 事件

关键设计 — 代码页保护:

if ((vm_flags & VM_EXEC) && page_is_file_lru(page)) {
    nr_rotated += thp_nr_pages(page);
    list_add(&page->lru, &l_active);  // 放回活跃链表
    continue;
}
  • 可执行文件的代码页(VM_EXEC)即使最近被引用过一次也保活

  • 这保证程序代码在中等内存压力下不会被回收

  • 匿名页不享受此待遇(JVM 会创建大量 VM_EXEC 匿名页)

3.6 shrink_inactive_list — 不活跃链表回收

// mm/vmscan.c:2320
static unsigned long
shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
                     struct scan_control *sc, enum lru_list lru)

职责: 从不活跃链表取出页面,调用 shrink_page_list 尝试回收。

shrink_inactive_list(nr_to_scan, lruvec, sc, lru)
│
├─ ① 防抖保护
│   └─ too_many_isolated()? → msleep(100)   ← 太多进程在回收,避免 OOM
│
├─ ② 隔离(持锁)
│   ├─ lru_add_drain()
│   ├─ spin_lock_irq()
│   ├─ isolate_lru_pages()     ← 从不活跃链表尾部取出页面
│   └─ spin_unlock_irq()
│
├─ ③ 回收(无锁)★
│   └─ shrink_page_list(&page_list, pgdat, sc, &stat, false)
│       ├─ 干净页 → 直接释放
│       ├─ 脏页 → pageout() 写回
│       ├─ 映射页 → unmap + pageout
│       └─ 匿名页 → swap out
│
├─ ④ 回写(持锁)
│   ├─ spin_lock_irq()
│   ├─ move_pages_to_lru()     ← 未回收的页面放回不活跃链表头部
│   └─ spin_unlock_irq()
│
├─ ⑤ 统计
│   ├─ PGSCAN_KSWAPD / PGSCAN_DIRECT   ← 扫描量
│   ├─ PGSTEAL_KSWAPD / PGSTEAL_DIRECT ← 回收量
│   └─ PGSCAN_ANON / PGSCAN_FILE       ← 按类型统计
│
├─ ⑥ 脏页唤醒
│   └─ 全部隔离页都是未排队脏页? → wakeup_flusher_threads()
│
└─ ⑦ 累加拥塞统计到 sc->nr

关键设计 — too_many_isolated 防抖:

while (unlikely(too_many_isolated(pgdat, file, sc))) {
    msleep(100);   // 等待其他回收进程完成
    if (fatal_signal_pending(current))
        return SWAP_CLUSTER_MAX;  // 进程即将死亡,返回让 OOM 处理
}
  • 防止大量进程同时做 direct reclaim 导致内存抖动

  • 最多等一次 100ms,避免死锁

3.7 shrink_slab — Slab 回收

// mm/vmscan.c:908
static unsigned long shrink_slab(gfp_t gfp_mask, int nid,
                                 struct mem_cgroup *memcg, int priority)

职责: 回收内核 slab 缓存(dentry cache、inode cache 等)。

shrink_slab(gfp_mask, nid, memcg, priority)
│
├─ memcg 非 root? → shrink_slab_memcg()    ← 走 memcg 专属路径
│
├─ down_read_trylock(&shrinker_rwsem)       ← 获取 shrinker 读锁
│   └─ 失败? → goto out(不等待,避免阻塞)
│
├─ list_for_each_entry(shrinker, &shrinker_list):
│   ├─ do_shrink_slab(&sc, shrinker, priority)
│   │   └─ shrinker->scan_objects() × (1 << priority) 次
│   ├─ freed += ret
│   └─ rwsem_is_contended()? → break  ← 有新 shrinker 注册,让步
│
└─ up_read(&shrinker_rwsem)

shrinker 机制:

  • 各子系统(VFS、inode、dentry 等)通过 register_shrinker() 注册回收回调

  • do_shrink_slab 调用 shrinker->scan_objects() 回收对象

  • down_read_trylock 而非 down_read:回收路径不能阻塞等待注册

  • rwsem_is_contended 检查:如果有进程在等待注册 shrinker,尽早让步


四、关键数据结构

4.1 struct reclaim_stat — 回收统计

// mm/vmscan.c (shrink_page_list 的输出)
struct reclaim_stat {
    unsigned nr_dirty;           // 隔离出的脏页数
    unsigned nr_unqueued_dirty;  // 未排队的脏页数
    unsigned nr_congested;       // 拥塞 BDI 的脏页数
    unsigned nr_writeback;       // 正在回写的页数
    unsigned nr_immediate;       // 需要立即回收的页数
    unsigned nr_taken;           // 隔离的总页数
    unsigned nr_file_taken;      // 隔离的文件页数
    unsigned nr_pageout;         // 执行 pageout 的页数
};

这些统计从 shrink_inactive_list 累加到 sc->nr,最终在 shrink_node 中用于 IO 拥塞判断。

4.2 scan_balance 枚举

// mm/vmscan.c:2680
enum scan_balance {
    SCAN_EQUAL,    // 均等扫描(OOM 边界)
    SCAN_FRACT,    // 按成本比例扫描(默认)
    SCAN_ANON,     // 只扫描匿名页(file_is_tiny)
    SCAN_FILE,     // 只扫描文件页(无 swap / cache_trim_mode)
};

4.3 DEACTIVATE 标志

#define DEACTIVATE_ANON  (1 << LRU_INACTIVE_ANON)  // = 1
#define DEACTIVATE_FILE  (1 << LRU_INACTIVE_FILE)   // = 4

五、核心设计思想

5.1 分层回收架构

shrink_node          ← 节点级:IO 拥塞控制、vmpressure、继续回收判断
  └─ shrink_node_memcgs  ← memcg 级:保护阈值、遍历 cgroup
      └─ shrink_lruvec       ← LRU 级:扫描比例、比例调整
          └─ shrink_list         ← 链表级:active/inactive 分发
              ├─ shrink_active_list    ← 页面级:访问检测、降级
              └─ shrink_inactive_list  ← 页面级:回收决策
                  └─ shrink_page_list      ← 单页级:unmap、writeback、free

每一层都有自己的策略和保护机制,上层控制"扫多少",下层决定"怎么回收"。

5.2 两阶段回收

  • shrink_active_list: 不回收页面,只做降级(active→inactive)

  • shrink_inactive_list: 实际回收页面

这保证了"二次机会"——被降级的页面如果在不活跃链表中再次被访问,会被重新激活,而不是直接被回收。

5.3 anon/file 平衡

系统通过三个机制维持 anon/file 扫描平衡:

  1. get_scan_count: 根据 swappiness 和回收成本计算初始比例

  2. shrink_lruvec 比例调整: 回收够了之后按比例缩减

  3. cache_trim_mode / file_is_tiny: 极端情况下的保护

5.4 拥塞感知

shrink_node 是回收路径中唯一做 IO 拥塞处理的函数:

  • kswapd: 检测 PGDAT_WRITEBACK/PGDAT_DIRTY,主动 congestion_wait

  • direct reclaim: 检测 LRUVEC_CONGESTED,wait_iff_congested

  • 这避免了回收速度超过 IO 带宽时的无效工作

5.5 继续回收判断(should_continue_reclaim)

// mm/vmscan.c:3059
static inline bool should_continue_reclaim(pgdat, nr_reclaimed, sc)

判断条件:

  1. 在 reclaim/compaction 模式

  2. 本轮有回收成果(nr_reclaimed > 0)

  3. 所有 zone 都不适合直接压缩(COMPACT_PARTIAL

  4. 不活跃页总量 > 压缩所需空闲页

这确保在"回收+压缩"联合模式下,回收为压缩提供足够的空闲页面。


六、关键函数调用表

函数

文件:行

作用

shrink_node()

vmscan.c:3174

节点级回收入口

shrink_node_memcgs()

vmscan.c:3112

遍历 memcg 回收

shrink_lruvec()

vmscan.c:2914

LRU 链表扫描

get_scan_count()

vmscan.c:2698

计算扫描比例

shrink_list()

vmscan.c:2618

active/inactive 分发

shrink_active_list()

vmscan.c:2445

活跃链表降级

shrink_inactive_list()

vmscan.c:2320

不活跃链表回收

shrink_slab()

vmscan.c:908

Slab 缓存回收

inactive_is_low()

vmscan.c:2662

判断 inactive 比例

should_continue_reclaim()

vmscan.c:3059

判断是否继续回收

do_shrink_slab()

vmscan.c:864

执行单个 shrinker

shrink_page_list()

vmscan.c:1394

单页回收决策(待分析)

isolate_lru_pages()

vmscan.c:1696

从 LRU 隔离页面

move_pages_to_lru()

vmscan.c:1529

页面批量迁回 LRU

page_referenced()

rmap.c

检查 PTE 引用

mem_cgroup_iter()

memcontrol.c

遍历 memcg 树

vmpressure()

vmpressure.c

内存压力评估


七、流程图描述

7.1 shrink_node 总体流程

┌────────────────────────────────────────────────────────────────────┐
│                        shrink_node()                               │
│                                                                    │
│  ┌──────────┐  ┌──────────────┐  ┌─────────────────────────────┐  │
│  │ 初始化    │→│ 降级决策      │→│ shrink_node_memcgs()        │  │
│  │ (①②)    │  │ (③④⑤)      │  │  ┌───────────────────────┐  │  │
│  └──────────┘  └──────────────┘  │  │ 遍历 memcg            │  │  │
│                                  │  │ ┌───────────────────┐ │  │  │
│                                  │  │ │ shrink_lruvec()   │ │  │  │
│                                  │  │ │  ┌─────────────┐  │ │  │  │
│                                  │  │ │  │get_scan_count│  │ │  │  │
│                                  │  │ │  └──────┬──────┘  │ │  │  │
│                                  │  │ │         ▼         │ │  │  │
│                                  │  │ │  ┌─────────────┐  │ │  │  │
│                                  │  │ │  │ shrink_list │  │ │  │  │
│                                  │  │ │  └──┬──────┬──┘  │ │  │  │
│                                  │  │ │     ▼      ▼     │ │  │  │
│                                  │  │ │  active  inactive │ │  │  │
│                                  │  │ │  (降级)  (回收)   │ │  │  │
│                                  │  │ └───────────────────┘ │  │  │
│                                  │  │ shrink_slab()         │  │  │
│                                  │  └───────────────────────┘  │  │
│                                  └─────────────────────────────┘  │
│                          ┌─────────────────────────────┐          │
│                          │ IO 拥塞处理 + 继续回收判断   │          │
│                          │ (⑨⑩⑪⑫)                    │          │
│                          └─────────────────────────────┘          │
└────────────────────────────────────────────────────────────────────┘

八、与上游的衔接

8.1 kswapd 路径

balance_pgdat()
  └─ kswapd_shrink_node()
      ├─ sc->nr_to_reclaim = Σ max(high_wmark_pages, SWAP_CLUSTER_MAX)
      └─ shrink_node(pgdat, sc)     ← 本报告分析的函数

8.2 direct reclaim 路径

try_to_free_pages()
  └─ do_try_to_free_pages()
      └─ shrink_zones()
          └─ shrink_node(pgdat, sc)     ← 同一个函数

8.3 node_reclaim 路径

__node_reclaim()
  └─ shrink_node(pgdat, sc)     ← 同一个函数

九、后续分析路线

当前位置: shrink_node ✅
    │
    ▼
┌─────────────────────────────────────────────────────────┐
│  下一步: shrink_page_list() — 单页回收决策(核心)       │
│                                                          │
│  这是真正决定"一个页面能不能回收"的函数:                 │
│  - 干净页 → 直接释放                                     │
│  - 脏页 → pageout() 写回                                │
│  - 映射页 → try_to_unmap() 解除映射                     │
│  - 匿名页 → add_to_swap() + pageout() swap out          │
│  - 正在回写 → 等待 / 立即回收                            │
│  - 锁定页 → 跳过                                        │
│                                                          │
│  文件: mm/vmscan.c:1394-1633                             │