跳转至

第三篇:页回收与 OOM Killer — kswapd、直接回收与 out_of_memory

源码:mm/vmscan.c, mm/oom_kill.c, mm/page_alloc.c | 头文件:include/linux/mmzone.h

系列目录:内存管理 内核源码深度解析


1. 概述

当 Buddy 分配器无法满足分配请求时,内核不会立即放弃。它首先尝试回收(Reclaim)——将可回收的页腾出内存。回收由两条路径驱动:

  1. kswapd(后台回收):每个 NUMA node 有一个内核线程,在空闲内存低于 LOW 水位时被唤醒,回收直到高于 HIGH 水位
  2. 直接回收(direct reclaim):分配者在慢速路径直接回收页,发生在 kswapd 来不及补充时

当所有回收努力都无法释放足够内存时,最终手段是 OOM Killer——选择并杀死内存消耗最大的进程。

核心文件: - mm/vmscan.c — 页回收核心逻辑(~8070 行) - mm/oom_kill.c — OOM Killer 实现(~1260 行) - include/linux/mmzone.h — LRU 链表枚举、zone watermark


2. LRU 链表架构

2.1 LRU 链表枚举

include/linux/mmzone.h:382-393

#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
    LRU_INACTIVE_ANON = LRU_BASE,              // 0: 不活跃匿名页
    LRU_ACTIVE_ANON   = LRU_BASE + LRU_ACTIVE, // 1: 活跃匿名页
    LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,    // 2: 不活跃文件页
    LRU_ACTIVE_FILE   = LRU_BASE + LRU_FILE + LRU_ACTIVE, // 3: 活跃文件页
    LRU_UNEVICTABLE,                            // 4: 不可回收页
    NR_LRU_LISTS                                // 5
};

LRU 分类逻辑

                          page 首次分配
                     ┌────────────────┐
                     │  LRU_INACTIVE  │
                     │  (不活跃链表)   │
                     └───────┬────────┘
                             │ 被访问 (folio_referenced)
                     ┌────────────────┐
                     │  LRU_ACTIVE    │
                     │  (活跃链表)     │
                     └───────┬────────┘
                             │ 长期未访问
                     ┌────────────────┐
                     │  LRU_INACTIVE  │  (回退到不活跃)
                     └───────┬────────┘
                             │ 仍被访问 → 可能再晋升
                             │ 未被访问 → 回收
                        回收 / swap out

LRU_UNEVICTABLE: 被 mlock() 锁定的页、被 VFIO pinned 的页

2.2 per-lruvec 层级结构

pglist_data (NUMA node)
└── node_mem_cgroup → lruvec
     ├── lists[LRU_INACTIVE_ANON]  → folio → folio → folio
     ├── lists[LRU_ACTIVE_ANON]    → folio → folio
     ├── lists[LRU_INACTIVE_FILE]  → folio → folio → folio → folio
     ├── lists[LRU_ACTIVE_FILE]    → folio
     └── lists[LRU_UNEVICTABLE]    → folio → folio

每个 folio 通过 folio->lru 链入对应 LRU 链表
folio 的状态通过 flags 中的 PG_lru、PG_active、PG_unevictable 等位表示

3. struct scan_control — 回收控制参数

mm/vmscan.c:74-145

struct scan_control {
    unsigned long nr_to_reclaim;    // 目标回收页数
    nodemask_t  *nodemask;          // 允许扫描的 NUMA 节点集

    struct mem_cgroup *target_mem_cgroup;  // 目标 memcg(memcg 回收)

    // 文件页 vs 匿名页扫描压力平衡
    unsigned long anon_cost;
    unsigned long file_cost;

    int *proactive_swappiness;      // 用户主动回收的 swappiness

    // 回收行为控制
    unsigned int may_writepage:1;   // 允许回写脏页
    unsigned int may_unmap:1;       // 允许解除页表映射
    unsigned int may_swap:1;        // 允许 swap out 匿名页
    unsigned int may_deactivate:2;  // 允许降级活跃页
    unsigned int force_deactivate:1;
    unsigned int skipped_deactivate:1;
    unsigned int no_cache_trim_mode:1;

    // 回收优先级(0=最高,DEF_PRIORITY=12=最低)
    int priority;

    // 统计
    struct reclaim_state reclaim_state;

    // 扫描/回收计数
    struct {
        unsigned int nr_dirty;      // 遇到脏页数
        unsigned int nr_unqueued_dirty;
        unsigned int nr_congested;
        unsigned int nr_writeback;  // 正在回写页数
        unsigned int nr_immediate;
        unsigned int nr_pageout;    // 发起写回数
        unsigned int nr_ref_keep;   // 保留引用的页数
        unsigned int nr_unmap_fail;
    } nr;
};

关键控制位: - may_swap:是否允许换出匿名页到 swap。若为 0,匿名页不能被回收 - may_writepage:是否允许回写脏文件页。若为 0,脏文件页跳过 - may_unmap:是否允许解除页表映射(try_to_unmap)。remap 文件页可能需要


4. kswapd — 后台回收守护线程

4.1 kswapd 主循环

mm/vmscan.c:7438-7510

static int kswapd(void *p)
{
    unsigned int alloc_order, reclaim_order;
    pg_data_t *pgdat = (pg_data_t *)p;
    struct task_struct *tsk = current;

    // kswapd 设为 PF_MEMALLOC:允许它使用保留内存
    tsk->flags |= PF_MEMALLOC | PF_KSWAPD;
    set_freezable();

    for ( ; ; ) {
        bool was_frozen;

        // 读取需要回收的 order 和 zone
        alloc_order = reclaim_order = READ_ONCE(pgdat->kswapd_order);
        highest_zoneidx = kswapd_highest_zoneidx(pgdat, highest_zoneidx);

kswapd_try_sleep:
        // 1. 休眠:直到内存水位低于 LOW 或被唤醒
        kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
                            highest_zoneidx);

        // 2. 重新读取 wakeup 设置的 order
        alloc_order = READ_ONCE(pgdat->kswapd_order);
        highest_zoneidx = kswapd_highest_zoneidx(pgdat, highest_zoneidx);

        // 3. 检查是否需要停止(freeze/suspend)
        if (kthread_freezable_should_stop(&was_frozen))
            break;
        if (was_frozen)
            continue;

        // 4. 执行回收
        reclaim_order = balance_pgdat(pgdat, alloc_order, highest_zoneidx);

        // 5. 如果回收的 order 小于请求的 order
        //    → 说明高 order 回收失败,降级为 order-0
        //    → 重新休眠(等 kcompactd 做 compaction)
        if (reclaim_order < alloc_order)
            goto kswapd_try_sleep;
    }

    tsk->flags &= ~(PF_MEMALLOC | PF_KSWAPD);
    return 0;
}

4.2 kswapd 唤醒与休眠条件

休眠条件:所有 zone 的空闲内存 > HIGH watermark
唤醒条件:
  ① 分配者调用 wakeup_kswapd()(page_alloc.c 慢速路径)
     → kswapd_order = max(kswapd_order, alloc_order)
     → 当 zone 空闲内存 < LOW watermark
  ② 外部事件:memory hotplug、compaction 失败等

kswapd 周期:
  ┌──────────┐    free > HIGH    ┌──────────┐
  │  SLEEP   │ ◄──────────────── │  ACTIVE  │
  │ (休眠)   │                  │  (回收)  │
  └──────────┘                  └──────────┘
       │                              │
       │ free < LOW                   │
       └──────────────────────────────┘
           被 wakeup_kswapd() 唤醒

4.3 balance_pgdat — 节点级回收循环

kswapd 调用 balance_pgdat(),对每个 zone 循环回收:

balance_pgdat(pgdat, alloc_order, highest_zoneidx) {
    // 从最高 zone 向下扫描
    for (i = 0; i <= end_zone; i++) {
        zone = pgdat->node_zones + i;

        // 水位检查:free > high 则跳过
        if (zone_watermark_ok_safe(zone, order, high_wmark_pages(zone), ...))
            continue;

        // 回收
        sc.nr_to_reclaim = max(SWAP_CLUSTER_MAX, high_wmark_pages(zone));
        shrink_node(pgdat, &sc);
    }

    // 检查是否达到目标
    if (sc.nr_reclaimed >= nr_to_reclaim && 水位恢复)
        break;  // 回收完成
}

5. shrink_node — 节点回收核心

mm/vmscan.c:6190-6260

static void shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
    unsigned long nr_reclaimed, nr_scanned, nr_node_reclaimed;
    struct lruvec *target_lruvec;

    // 1. 如果启用了 MGLRU(多代 LRU),走 MGLRU 路径
    if (lru_gen_enabled() || lru_gen_switching()) {
        memset(&sc->nr, 0, sizeof(sc->nr));
        lru_gen_shrink_node(pgdat, sc);
        return;
    }

    target_lruvec = mem_cgroup_lruvec(sc->target_mem_cgroup, pgdat);

again:
    // 2. 遍历所有 memcg(如果有目标 memcg 则只处理它)
    shrink_node_memcgs(pgdat, sc);

    // 3. 如果回收了页,标记 reclaimable
    if (nr_node_reclaimed)
        reclaimable = true;

    // 4. kswapd 特殊处理:回写检查
    if (current_is_kswapd()) {
        // 如果回收的页全是回写中的页 → 标记 PGDAT_WRITEBACK
        if (sc->nr.writeback && sc->nr.writeback == sc->nr.taken)
            set_bit(PGDAT_WRITEBACK, &pgdat->flags);

        // 如果遇到 immediate reclaim 标记的页 → 节流等待回写完成
        if (sc->nr.immediate)
            reclaim_throttle(pgdat, VMSCAN_THROTTLE_WRITEBACK);
    }

    // 5. 如果需要继续(还没达到目标)
    if (should_continue_reclaim(pgdat, nr_node_reclaimed, sc))
        goto again;
}

6. shrink_inactive_list — 不活跃链表回收

mm/vmscan.c:1951-2020

这是回收的"主力"函数,负责从不活跃 LRU 链表取出页进行回收:

static unsigned long shrink_inactive_list(unsigned long nr_to_scan,
        struct lruvec *lruvec, struct scan_control *sc,
        enum lru_list lru)
{
    LIST_HEAD(folio_list);
    unsigned long nr_scanned;
    unsigned int nr_reclaimed = 0;
    unsigned long nr_taken;
    struct reclaim_stat stat;
    bool file = is_file_lru(lru);
    struct pglist_data *pgdat = lruvec_pgdat(lruvec);
    bool stalled = false;

    // 1. 节流:如果太多页正在隔离,等待
    while (unlikely(too_many_isolated(pgdat, file, sc))) {
        if (stalled)
            return 0;
        stalled = true;
        reclaim_throttle(pgdat, VMSCAN_THROTTLE_ISOLATED);
        if (fatal_signal_pending(current))
            return SWAP_CLUSTER_MAX;
    }

    // 2. 从 LRU 链表隔离页(一次最多 32 页 = SWAP_CLUSTER_MAX)
    lru_add_drain();
    lruvec_lock_irq(lruvec);
    nr_taken = isolate_lru_folios(nr_to_scan, lruvec, &folio_list,
                                   &nr_scanned, sc, lru);
    lruvec_unlock_irq(lruvec);

    if (nr_taken == 0)
        return 0;

    // 3. 对隔离出的 folio 列表执行实际回收
    nr_reclaimed = shrink_folio_list(&folio_list, pgdat, sc, &stat, false,
                                     lruvec_memcg(lruvec));

    // 4. 未回收的 folio 放回 LRU
    move_folios_to_lru(&folio_list);

    return nr_reclaimed;
}

shrink_folio_list 回收决策

对每个 folio:
  ├─ 脏文件页 (PG_dirty) ────────────────────────────────────────
  │   ├─ may_writepage → 发起回写 → 页标记为回写中 → 放回 LRU
  │   └─ !may_writepage → 跳过,放回 LRU
  ├─ 干净文件页 ─────────────────────────────────────────────────
  │   └─ 直接释放 (folio_unlock + free)
  ├─ 匿名页 (anon) ──────────────────────────────────────────────
  │   ├─ may_swap → 添加到 swap cache → 解除页表映射 → 写入 swap
  │   └─ !may_swap → 无法回收,放回 LRU
  ├─ 正在回写 (PG_writeback) ────────────────────────────────────
  │   └─ 等待或跳过
  └─ 被引用 (PG_referenced) ─────────────────────────────────────
      └─ 放回活跃链表


7. shrink_active_list — 活跃链表降级

mm/vmscan.c:2068-2130

活跃链表的作用是保护频繁访问的页不被回收。shrink_active_list 将活跃页降级到不活跃链表:

static void shrink_active_list(unsigned long nr_to_scan,
                               struct lruvec *lruvec,
                               struct scan_control *sc,
                               enum lru_list lru)
{
    unsigned long nr_taken;
    LIST_HEAD(l_hold);    // 从活跃链表隔离出的页
    LIST_HEAD(l_active);  // 保留在活跃链表的页
    LIST_HEAD(l_inactive); // 降级到不活跃链表的页
    unsigned nr_deactivate, nr_activate;
    bool file = is_file_lru(lru);

    lru_add_drain();
    lruvec_lock_irq(lruvec);

    // 1. 从活跃 LRU 隔离 folio
    nr_taken = isolate_lru_folios(nr_to_scan, lruvec, &l_hold,
                                   &nr_scanned, sc, lru);
    lruvec_unlock_irq(lruvec);

    // 2. 逐个检查每个 folio 的引用状态
    while (!list_empty(&l_hold)) {
        folio = lru_to_folio(&l_hold);
        list_del(&folio->lru);

        // 不可回收页 → 放回
        if (unlikely(!folio_evictable(folio))) {
            folio_putback_lru(folio);
            continue;
        }

        // 3. 检查是否被引用 (folio_referenced)
        if (folio_referenced(folio, 0, sc->target_mem_cgroup,
                             &vm_flags) != 0) {
            // 仍被引用 → 保留在活跃链表
            nr_rotate++;
            list_add(&folio->lru, &l_active);
        } else {
            // 未被引用 → 降级到不活跃链表
            nr_deactivate++;
            list_add(&folio->lru, &l_inactive);
        }
    }

    // 4. 将页放回对应 LRU
    move_active_folios_to_lru(lruvec, &l_active, &l_inactive, lru);
}

活跃/不活跃迁移图示

                     folio_referenced() == 0 (最近未访问)
LRU_ACTIVE  ──────────────────────────────────────►  LRU_INACTIVE
  (活跃)                                               (不活跃)
    ▲                                                       │
    │                          folio_referenced() != 0       │
    │                          (被访问)                      │
    │                                                       │
    └───────────────────────────────────────────────────────┘
                    新分配页 → INACTIVE
                    INACTIVE 被再次访问 → ACTIVE


8. 直接回收 (Direct Reclaim)

8.1 __alloc_pages_direct_reclaim

mm/page_alloc.c:4409-4430

当 kswapd 回收速度跟不上分配速度,且慢速路径已经尝试过低水位分配后仍然失败:

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;

    // 1. 执行直接回收
    *did_some_progress = __perform_reclaim(gfp_mask, order, ac);

    if (unlikely(!(*did_some_progress)))
        return NULL;

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

    // 3. 如果没有 GFP_NORETRY,允许循环
    if (!page && !(gfp_mask & __GFP_NORETRY))
        goto retry;

    return page;
}

8.2 throttle_direct_reclaim — 限制并发直接回收

mm/vmscan.c:6640-6660

过多的直接回收者会相互竞争,内核会限制并发量:

static bool throttle_direct_reclaim(gfp_t gfp_mask, struct zonelist *zonelist,
                                    nodemask_t *nodemask)
{
    // 检查每个 zone 的空闲内存是否极度不足
    // 如果 PFMEMALLOC 保留内存也被大量消耗 → 需要节流
    // 让 kswapd 有 CPU 时间做后台回收
}

9. 回收优先级 (Scan Priority)

回收使用 12 级优先级(DEF_PRIORITY = 12,0 最高):

priority 12 (最低优先级):
  扫描少量页,只回收最不活跃的页
priority 8:
  扫描更多页,开始扫描活跃链表
priority 0 (最高优先级):
  扫描所有 LRU 页,不放过任何可回收的页
  尝试 flush 脏页,尝试 swap

扫描量与 priority 的关系

每个 zone 每次扫描的页数:
  scan_size = zone_total_pages >> priority

priority=12 → 扫描 zone_total_pages / 4096 页
priority=8  → 扫描 zone_total_pages / 256 页
priority=0  → 扫描 zone_total_pages 页(全部扫)

回收循环

do {
    shrink_node() → 遍历 LRU,递减 priority
} while (sc->nr_reclaimed < sc->nr_to_reclaim && --sc->priority >= 0);


10. 文件页 vs 匿名页的平衡

swappiness — 匿名页回收倾向

/proc/sys/vm/swappiness(默认 60):

swappiness 决定匿名页相对于文件页的回收倾向:

  swappiness ≈ 0   → 优先回收文件页,几乎不 swap
  swappiness = 60  → 文件页和匿名页差不多平等对待
  swappiness = 100 → 文件页和匿名页完全平等对待
  swappiness = 200 → 优先回收匿名页

anon_prio = sc->swappiness    (swappiness value, max 200)
file_prio = 200 - anon_prio + 1

scan_balance:
  anon_scan_ratio : file_scan_ratio ≈ anon_prio : file_prio

11. OOM Killer

11.1 out_of_memory — OOM 入口

mm/oom_kill.c:1103-1170

当直接回收也无法获取内存时,慢速路径调用 out_of_memory()

bool out_of_memory(struct oom_control *oc)
{
    unsigned long freed = 0;

    // 1. OOM killer 是否被禁用?
    if (oom_killer_disabled)
        return false;

    // 2. 非 memcg OOM:先通知 oom_notify_list(给注册者一次释放机会)
    if (!is_memcg_oom(oc)) {
        blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
        if (freed > 0 && !is_sysrq_oom(oc))
            return true;  // 有些内存被释放了
    }

    // 3. 如果 current 进程正被 SIGKILL 或正在退出
    //    → 标记为 OOM victim,让它尽快结束释放内存
    if (task_will_free_mem(current)) {
        mark_oom_victim(current);
        queue_oom_reaper(current);
        return true;
    }

    // 4. GFP_NOFS 且非 memcg OOM → 无法做更多,返回
    if (!(oc->gfp_mask & __GFP_FS) && !is_memcg_oom(oc))
        return true;

    // 5. 检查分配约束(cpuset/memory policy/memcg)
    oc->constraint = constrained_alloc(oc);
    check_panic_on_oom(oc);

    // 6. 如果设置了 oom_kill_allocating_task → 直接杀分配者
    if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task && ...) {
        get_task_struct(current);
        oc->chosen = current;
        oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)");
        return true;
    }

    // 7. 选择最坏的进程
    select_bad_process(oc);
    if (!oc->chosen) {
        // 没有可杀进程 → 系统即将 panic
        dump_header(oc);
        pr_warn("Out of memory and no killable processes...\n");
        panic("System is deadlocked on memory\n");
    }

    // 8. 杀死选中的进程
    if (oc->chosen && oc->chosen != (void *)-1UL)
        oom_kill_process(oc, !is_memcg_oom(oc) ?
                         "Out of memory" : "Memory cgroup out of memory");

    return !!oc->chosen;
}

11.2 oom_badness — 计算进程"坏度"

mm/oom_kill.c:199-237

long oom_badness(struct task_struct *p, unsigned long totalpages)
{
    long points;
    long adj;

    // 1. 跳过不可杀的任务(init、内核线程)
    if (oom_unkillable_task(p))
        return LONG_MIN;

    p = find_lock_task_mm(p);
    if (!p)
        return LONG_MIN;

    // 2. 跳过 oom_score_adj = -1000 或已被标记 OOM_SKIP 的进程
    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN ||   // -1000(不可被 OOM kill)
        mm_flags_test(MMF_OOM_SKIP, p->mm) ||
        in_vfork(p)) {
        return LONG_MIN;
    }

    // 3. 核心评分公式:
    //    points = RSS + Swap Entries + Page Tables
    points = get_mm_rss_sum(p->mm) +
             get_mm_counter_sum(p->mm, MM_SWAPENTS) +
             mm_pgtables_bytes(p->mm) / PAGE_SIZE;

    // 4. 应用 oom_score_adj:
    //    adj = oom_score_adj * totalpages / 1000
    //    points = points + adj
    adj *= totalpages / 1000;
    points += adj;

    return points;
}

评分详解

points = 常驻内存页(RSS)
       + swap entries (换出但未释放的页)
       + 页表页 (Page Table pages)
       + oom_score_adj * totalpages / 1000

其中 oom_score_adj:
  -1000 → 完全豁免 OOM kill(如 sshd)
  0     → 默认
  500   → 增加 50% 的"坏度",更容易被杀
  1000  → 总是被杀

可通过 /proc/<pid>/oom_score 查看当前分数,/proc/<pid>/oom_score_adj 调整偏移。

11.3 select_bad_process — 选出最坏进程

mm/oom_kill.c:362-380

static void select_bad_process(struct oom_control *oc)
{
    oc->chosen_points = LONG_MIN;

    if (is_memcg_oom(oc))
        // memcg OOM:只扫描该 cgroup 内的进程
        mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
    else {
        // 全局 OOM:遍历所有进程
        struct task_struct *p;
        rcu_read_lock();
        for_each_process(p)
            if (oom_evaluate_task(p, oc))
                break;  // 找到足够"坏"的进程
        rcu_read_unlock();
    }
}

oom_evaluate_task 选择策略

对每个进程:
  ① 计算 oom_badness(p)
  ② 如果分数 > oc->chosen_points → 更新 chosen
  ③ 额外启发式:
     - 优先杀子进程多的(释放更多内存)
     - 优先杀非 root 进程
     - 如果启用了 oom_reaper,优先杀已有已退出线程的进程(内存已被 reaper 标记)
     - 如果 oc->chosen_points > oc->totalpages → 宣布找到(足够"坏"了)

11.4 oom_kill_process — 执行杀死

mm/oom_kill.c:1008-1045

static void oom_kill_process(struct oom_control *oc, const char *message)
{
    struct task_struct *victim = oc->chosen;
    struct mem_cgroup *oom_group;

    // 1. 如果 victim 正在退出 → 标记 OOM victim 即可,让它自然死亡
    task_lock(victim);
    if (task_will_free_mem(victim)) {
        mark_oom_victim(victim);
        queue_oom_reaper(victim);  // 异步回收器会加速释放
        task_unlock(victim);
        return;
    }
    task_unlock(victim);

    // 2. 如果是 memcg oom,尝试发送 SIGKILL 到整个 cgroup
    if (is_memcg_oom(oc)) {
        oom_group = mem_cgroup_get_oom_group(victim, oc->memcg);
        if (oom_group) {
            // 杀整个 cgroup
            ...
        }
    }

    // 3. 打印 OOM 报告
    dump_header(oc);
    pr_err("%s: Killed process %d (%s) ...\n",
           message, task_pid_nr(victim), victim->comm);

    // 4. 发送 SIGKILL
    do_send_sig_info(SIGKILL, SEND_SIG_PRIV, victim, PIDTYPE_TGID);
}

11.5 oom_reaper — 异步内存回收

OOM reaper 是一个独立的内核线程,它不会等待 victim 进程自然退出,而是主动进行异步回收:

victim 被标记 OOM victim
  ├─ mark_oom_victim()  → MMF_OOM_VICTIM 标志
  ├─ queue_oom_reaper() → 唤醒 oom_reaper 线程
  │   │
  │   └─ oom_reaper:
  │       ├─ 获取 victim 的 mm
  │       ├─ mmap_write_lock(mm)
  │       ├─ 遍历所有 VMA
  │       │   ├─ 匿名页 → 解除页表映射 (unmap)
  │       │   └─ 文件页 → 释放
  │       ├─ mmap_write_unlock(mm)
  │       └─ 标记 MMF_OOM_SKIP (下次 OOM 跳过此进程)
  └─ victim 收到 SIGKILL → 退出 → free 剩余内存

12. 回收与 OOM 的完整决策链

alloc_pages(order, gfp)
  ├─[快速路径]────────────────────────────────
  │  ALLOC_WMARK_LOW
  │  get_page_from_freelist → 成功 → 返回
  └─[慢速路径] __alloc_pages_slowpath
       ├─ ① wake kswapd
       │     → 后台异步回收(不阻塞分配者)
       ├─ ② ALLOC_WMARK_MIN → 再试分配
       │     get_page_from_freelist → 成功 → 返回
       ├─ ③ compaction → 再试分配
       │     成功 → 返回
       ├─ ④ direct reclaim
       │     __alloc_pages_direct_reclaim()
       │       → __perform_reclaim()
       │         → try_to_free_pages()
       │           → do_try_to_free_pages()
       │             ├─ shrink_node(pgdat, &sc)
       │             │   └─ shrink_node_memcgs()
       │             │       └─ shrink_lruvec()
       │             │           ├─ shrink_active_list()  (活跃→不活跃)
       │             │           └─ shrink_inactive_list() (回收/swap)
       │             └─ 返回 reclaimed 页数
       │     回收了一些页 → 再试分配 → 成功 → 返回
       │     没有进展 → 视情况重试
       ├─ ⑤ 循环重试(最多 16 次,含 compaction 重试)
       │     should_compact_retry() → 再试 compaction
       │     should_reclaim_retry()  → 再试 reclaim
       │     成功 → 返回
       ├─ ⑥ __GFP_RETRY_MAYFAIL 且 order <= COMPACT_COSTLY →
       │     允许无限重试(但会 throttle)
       └─ ⑦ 所有手段耗尽
            ├─ __GFP_NOFAIL → goto retry (永远重试,可能死锁)
            └─ 触发 OOM:
                 __alloc_pages_may_oom()
                   → out_of_memory(&oc)
                     ├─ select_bad_process()
                     │   └─ oom_badness() 对每个进程评分
                     └─ oom_kill_process()
                         ├─ 发送 SIGKILL
                         ├─ queue_oom_reaper() → 异步回收内存
                         └─ dump_header() → dmesg 输出 OOM 报告

13. OOM 触发条件总结

OOM 不是任意条件下都会触发。内核会逐步升级:

条件                                                        动作
─────────────────────────────────────────────────────────────────────
zone free < WMARK_LOW                                     → wake kswapd
zone free < WMARK_MIN 且 kswapd 未及时补充                 → direct reclaim
direct reclaim 达到 priority=0 且 nr_reclaimed=0         → 考虑 OOM
非 ALLOC_OOM / ALLOC_NO_WATERMARKS 分配者                 → OOM
当前有 OOM victim 正在退出                                → 等待,不触发新 OOM
oom_killer_disabled (sysctl)                              → 直接返回失败
panic_on_oom (sysctl)                                     → panic 而不是 kill

抑制 OOM 的条件: - 已有 OOM victim 正在被 oom_reaper 回收中 - oom_killer_disabled 开启(通常在内存热插拔期间) - GFP_NOFS 且不是 memcg OOM(文件系统递归风险) - current 已被标记 OOM victim(让它自己释放内存)


14. /proc 监控接口

# 查看 LRU 链表大小
cat /proc/meminfo | grep -E "Active|Inactive|Unevictable"
Active:          4194304 kB
Inactive:        2097152 kB
Active(anon):    1048576 kB
Inactive(anon):   524288 kB
Active(file):    3145728 kB
Inactive(file):  1572864 kB
Unevictable:          0 kB

# 查看各 zone 水位线
cat /proc/zoneinfo | grep -E "min|low|high"

# 查看各进程 oom_score
cat /proc/$(pgrep -f process)/oom_score

# 调整 oom_score_adj(-1000 到 1000)
echo -500 > /proc/$(pgrep -f process)/oom_score_adj

# swappiness
cat /proc/sys/vm/swappiness  # 默认 60

# 是否有正在进行的 OOM
dmesg | grep -i "out of memory\|oom"

下一篇文章

第四篇:缺页异常与 mmap — page fault 处理、VMA 与 file-backed


💬 评论