本页目录

页面回收与交换

覆盖: LRU (匿名/文件) → kswapd + direct reclaim → swap 机制 → 反向映射 (rmap) → page cache reclaim → compaction 内核版本: 2.6 ~ 6.x,重点标注 multi-gen LRU (6.1+)

概述

当物理内存不足时,内核必须收回一些页面。回收的对象分两类:

  • 文件页 (file-backed): 干净的直接丢弃(有磁盘副本),脏的写回
  • 匿名页 (anonymous): 必须写到 swap 才能回收

回收策略的核心是 LRU (Least Recently Used)——把冷页面换出,保留热页面。但"冷热"判断在几百 GB 的工作集中是极其困难的。Linux 从传统的双链表 LRU 演进到 6.1 引入的 Multi-Gen LRU (MGLRU),显著改善了对大工作集和扫描风暴的处理。


LRU 链表

传统双 LRU (2.6 ~ 5.x)

// mm/vmscan.c
// 每个 memcg 和每个 node 有一组 LRU:

enum lru_list {
    LRU_INACTIVE_ANON,  // 冷匿名页 (长时间未访问)
    LRU_ACTIVE_ANON,    // 热匿名页
    LRU_INACTIVE_FILE,  // 冷文件页
    LRU_ACTIVE_FILE,    // 热文件页
    LRU_UNEVICTABLE,    // 不可回收 (mlock, ramfs)
    NR_LRU_LISTS
};

// 页面在 active 和 inactive 之间移动:
//   首次映射 → inactive
//   被再次访问 → 提升到 active
//   长时间未被访问 → 降级到 inactive
//   inactive 中的页面 → 回收候选

回收优先级

// 回收顺序 (文件页优先于匿名页):
//   1. 干净文件页 (drop, 最快)
//   2. 脏文件页 (写回)
//   3. 匿名页 (swap out)
//
// 为什么文件页优先?
//   干净文件页: 直接丢弃, 0 IO
//   匿名页:     必须写入 swap → IO 开销
//   脏文件页:   必须写回 → IO 开销
//
// /proc/sys/vm/swappiness 控制这个优先级:
//   0:   尽量回收文件页,除非实在不够才 swap
//   100: 平等对待文件和匿名
//   默认 60

Multi-Gen LRU (6.1+)

传统 LRU 的问题:
  - active/inactive 只有两代 → 粒度太粗
  - 冷热判断依赖 referenced bit + 周期性扫描
  - 大工作集下扫描成本极高 ("scanning storm")

MGLRU 的方案:
  页面按代 (generation) 分组,每代有独立的 LRU
    新一代 = 活跃 (最近被访问)
    旧一代 = 冷 (候选回收)
    
  回收时从最老一代开始 → 自然排除热页面
  不需要扫描所有页面来判断冷热 → 扫描量大幅减少
  
  实现: mm/vmscan.c (MGLRU build) + /sys/kernel/mm/lru_gen
  
  Google 和 Meta 都报告了显著的 CPU 节省
  (扫描开销降低 50%~90%)

kswapd 与 Direct Reclaim

kswapd: 后台回收

// mm/vmscan.c
// 每个 NUMA node 都有自己的 kswapd 内核线程

// 唤醒条件:
//   空闲页 < high watermark → kswapd 醒来
//   kswapd 持续回收直到空闲页 > high watermark

static int kswapd(void *p) {
    while (!kthread_should_stop()) {
        // 1. 检查 watermark: 如果 > high → 睡眠
        // 2. 调用 balance_pgdat()
        //    ├─ 扫描 LRU 链表
        //    ├─ shrink_node() → shrink_lruvec()
        //    │   ├─ shrink_inactive_list()  → 回收冷页
        //    │   └─ shrink_active_list()    → 降级热页
        //    └─ 可能触发 compaction (如果碎片严重)

        // 3. 如果回收了足够页面 → 睡眠
        //    如果回收不够 → watermark 仍然低 → 加大扫描力度
    }
}

Direct Reclaim: 同步回收

// 当 kswapd 来不及回收 (内存分配太快), 分配者自己回收:
__alloc_pages_slowpath()__alloc_pages_direct_reclaim()__perform_reclaim()try_to_free_pages()do_try_to_free_pages()shrink_zones()shrink_node()shrink_lruvec()
          // 与 kswapd 相同的回收逻辑,但是同步执行
          // 分配者阻塞在这里直到有可用的页面

direct reclaim 会导致分配延迟突增——这也是为什么大家都关注"内存压力"指标。


Swap

Swap 分区/文件

# 配置
swapon /dev/sda2           # swap 分区
swapon /swapfile            # swap 文件 (5.x+ 支持 Btrfs swap)

# 优先级 (多个 swap 设备)
swapon -p 10 /dev/nvme0n1p2  # NVMe swap (快)
swapon -p 1  /dev/sda2        # SATA swap (慢)

Swap 出 (匿名页 → swap)

// mm/vmscan.c
// shrink_page_list() 回收匿名页时:

if (PageAnon(page) && !PageSwapCache(page)) {
    // 1. 分配 swap slot
    add_to_swap(page)get_swap_page()       // 在 swap 位图中找空闲 slot
add_to_swap_cache()   // 页面加入 swap cache (address_space)
    
    // 2. 写 swap
    //   如果 CONFIG_THP_SWAP=y:
    //     THP 被分裂为 4KB 页分别 swap (避免 2MB 连续 IO)
    //
    // 3. 标记 PTE 为 swap entry
    //    PTE = swp_entry_t (type + offset)
    //    硬件看到 Present=0 → page fault → do_swap_page()
}

// swap cache:
//   页面在 swap 和内存之间过渡时的缓存
//   如果 swap 出和 swap 入之间有 race → swap cache 提供一致性

Swap 入 (page fault → swap)

// mm/memory.c
// do_swap_page(): 进程访问了被 swap 出去的地址

do_swap_page()
  ├─ lookup_swap_cache()     // 检查 swap cache (可能在内存中)
  │   └─ 如果在: 直接映射 (不需要 IO!)
  │
  └─ swapin_readahead()      // 从 swap 设备预读
      └─ read_swap_cache_async()swap_readpage()  // 提交 IO
          → 页面入 swap cache
          → 映射 PTE
          → 用户进程继续执行

反向映射 (rmap)

// mm/rmap.c
// 反向映射回答: "给定一个物理页, 哪些进程的哪些虚拟地址映射了它?"

// 匿名页:
//   通过 anon_vma + VMA 的 interval tree 遍历所有映射
//   page->mapping → anon_vma → anon_vma_chain → VMA

// 文件页:
//   通过 address_space + page->index 查找
//   page->mapping → address_space → i_mmap (interval tree)

// rmap_walk(page, func):
//   遍历 page 的所有 PTE 映射
//   对每个映射调用 func → 如 try_to_unmap
//     → 将 PTE 标记为 swap entry 或清空
//     → flush TLB

// try_to_unmap() 是 swap 的前提:
//   必须找到所有 PTE 引用,全部解除后,页面才能回收

Compaction (内存压缩)

// mm/compaction.c
// 问题: buddy allocator 需要连续页面 (如 order 4 = 16 页 = 64KB)
//       但内存已经碎片化 → 无法分配连续块

// compaction: 将已使用的页面"搬走",腾出连续的空闲区域
//   1. 扫描: 找 free pages (目标) 和 used pages (源)
//   2. 迁移: 将 used page 内容复制到 free page
//   3. 更新页表: 修改所有 PTE 指向新位置
//   4. 重复: 直到腾出足够连续空间

// 触发方式:
//   1. kcompactd: 后台线程 (类似 kswapd)
//   2. direct compaction: 分配者在 slowpath 中主动压缩

// 查看 compaction 状态:
cat /proc/vmstat | grep compact

调试与观测

# 内存压力概览
cat /proc/meminfo
# SwapTotal/SwapFree: swap 使用
# Active/Inactive(anon/file): LRU 中的页面
# Dirty/Writeback: 待写回页面

# 回收统计
cat /proc/vmstat | grep -E 'pgsteal|pgscan|pswpin|pswpout'
# pgsteal_*: 回收了多少页
# pgscan_*:  扫描了多少页
#   scan >> steal → LRU 扫描效率低 (MGLRU 改善)

# 是否启用了 MGLRU
ls /sys/kernel/mm/lru_gen 2>/dev/null && echo "MGLRU enabled"

# kswapd 行为
ps aux | grep kswapd
cat /proc/<kswapd_pid>/status

# compaction
cat /proc/vmstat | grep compact
cat /sys/kernel/debug/extfrag/extfrag_index

# 触发手动回收 (测试用, 生产慎用)
echo 3 > /proc/sys/vm/drop_caches  # 清 page cache

参考与延伸

  • 内核文档⁠: Documentation/admin-guide/sysctl/vm.rst, Documentation/mm/multigen_lru.rst
  • LWN:
    • "Multi-generational LRU" (lwn.net/Articles/856932/)
    • "The case for MGLRU" (lwn.net/Articles/916777/)
    • "Swap and the page cache" (lwn.net/Articles/358953/)
  • 源码文件⁠:
    • mm/vmscan.c — kswapd + LRU 扫描 + direct reclaim
    • mm/swap_state.c — swap cache
    • mm/swapfile.c — swap 分区/文件管理
    • mm/rmap.c — 反向映射
    • mm/compaction.c — 内存压缩
    • mm/page_io.c — swap IO

关键词: LRU, MGLRU, kswapd, direct reclaim, swap, swappiness, rmap, try_to_unmap, compaction, page cache reclaim