shrink_page_list() 是 Linux 内存回收机制中最关键的核心函数。它从 LRU 链表取出页面,逐个做出“回收还是保留”的判决,并将可回收页面真正释放到 buddy system。如果说 shrink_node 是调度回收,那么 shrink_page_list 就是真正的执行层——每一页的命运在这里决定。

文件位置: mm/vmscan.c

调用者

  • reclaim_clean_pages_from_list() — clean pages 快速回收

  • reclaim_pages() — 批量页面回收

全链路位置

get_page_from_freelist → __alloc_pages_slowpath → __alloc_pages_direct_reclaim
  → __perform_reclaim → try_to_free_pages → do_try_to_free_pages
  → shrink_zones → shrink_node → shrink_lruvec → shrink_list → shrink_inactive_list
  → shrink_page_list  ← 最终执行层

一、函数签名与数据结构

static unsigned int shrink_page_list(struct list_head *page_list,   // 待回收页面列表
                                     struct pglist_data *pgdat,   // 节点数据结构
                                     struct scan_control *sc,     // 回收控制(nr_to_reclaim/may_writepage等)
                                     struct reclaim_stat *stat,   // 统计信息
                                     bool ignore_references)      // 是否忽略引用检查

返回: 回收的页面总数(以基数页为单位)。

三个本地临时链表:

  • ret_pages:需要放回 LRU 的页面(未回收成功)

  • free_pages:已经回收、可直接释放给 buddy system 的页面

  • demote_pages:需要降级到其他 NUMA 节点的 THP 页面

初始化阶段

  • 清除 reclaim_stat 结构体(0 初始化各计数器)

  • 调用 can_demote(pgdat->node_id, sc) 判断是否开启 NUMA 降级通道

  • 调用 cond_resched() 释放 CPU 给其他进程


二、主循环:逐个页面回收决策(核心)

从 LRU 链表末尾取出页面,进入循环(retry 标签开始),对每个页面执行以下检查链:

2.1 预检查

获取页面锁

if (!trylock_page(page))
    goto keep;
  • trylock_page 尝试获取自旋锁,失败则跳过该页面,保持原位置不动

  • 防止并发竞争,确保页面操作的安全性

  • 后续有 activate_lockedkeep_locked 标签,统一处理解锁逻辑

活跃度断言

VM_BUG_ON_PAGE(PageActive(page), page);
  • 进入该函数时页面必须处于 inactive 状态,活跃页不在传入的列表中

  • 这是一个安全断言,生产环境也会检查

计算复合页基数

nr_pages = compound_nr(page);
  • 普通页 nr_pages = 1

  • THP(透明大页)nr_pages = 512(4MB)

  • 后续统计会按基数计算

页面可回收性检查

if (unlikely(!page_evictable(page)))
    goto activate_locked;
  • page_evictable() 检查页面是否可以被回收(不能回收的页面包括 mlocked 页、不可迁移页等)

  • 不满足条件的页面被激活并放回到 active 链表

映射检查

if (!sc->may_unmap && page_mapped(page))
    goto keep_locked;
  • 如果 scan_control.may_unmap == 0(不允许拆掉页表映射),且页面正在被用户空间映射,则不能回收,直接保持

  • 在直接回收时可能设置 may_unmap=1,允许拆除映射


三、脏页和回写分析

获取脏页/回写状态

page_check_dirty_writeback(page, &dirty, &writeback);
if (dirty || writeback)
    stat->nr_dirty++;
if (dirty && !writeback)
    stat->nr_unqueued_dirty++;
  • 统计脏页数量和未入队列的脏页

  • nr_unqueued_dirty 是关键指标:如果大量脏页都没有在回写队列中,意味着脏页回收策略可能失效

检查 BDI 设备阻塞

mapping = page_mapping(page);
if (((dirty || writeback) && mapping &&
     inode_write_congested(mapping->host)) ||
    (writeback && PageReclaim(page)))
    stat->nr_congested++;
  • 如果页面对应文件所在 inode 的写入设备(BDI)处于阻塞状态,记录 congested

  • 这是 shrink_zones 中决定是否需要 stall 的依据(PGDAT_WRITEBACK 标志)

回写页面的三种处理策略

PageWriteback(page) 时,进入三重决策:

Case 1 — kswapd + 大量回写 + PGDAT_WRITEBACK 已设置

if (current_is_kswapd() && PageReclaim(page) &&
    test_bit(PGDAT_WRITEBACK, &pgdat->flags)) {
    stat->nr_immediate++;
    goto activate_locked;
}
  • kswapd 正在运行,并且该页已被标记为立即回收,且节点上已有很多页在回写

  • 说明磁盘回写系统已经不堪重负,不再等待此页,直接激活并跳过,继续扫描其他页面

  • 后续 shrink_zones 会看到 congested 标记并做 stall

Case 2 — 非 kswapd / 非 PageReclaim / 或无文件系统权限

} else if (writeback_throttling_sane(sc) ||
           !PageReclaim(page) || !may_enter_fs) {
    SetPageReclaim(page);
    stat->nr_writeback++;
    goto activate_locked;
}
  • 对非直接回收的线程(kswapd 或带 __GFP_FS 的分配器线程),且页面不在立即回收标记上

  • 将该页标记为 PageReclaim,激活并跳过,让其他干净页先回收

  • 下一次再遇到这个页时,PageReclaim 标志存在,会走 case 3

Case 3 — 其他所有情况(legacy memcg 或必须等待)

} else {
    unlock_page(page);
    wait_on_page_writeback(page);
    list_add_tail(&page->lru, page_list);
    continue;
}
  • 释放锁,原地睡眠等待回写完成

  • 将页面放回 LRU 末尾,然后 continue 处理下一个页面

  • 这是一个重阻塞操作,通常只在 memcg 回收中出现(memcg 没有脏页限速机制,必须等待)


四、引用检查与决策

if (!ignore_references)
    references = page_check_references(page, sc);

page_check_references()通过 page_referenced(page, sc) 来判定页面引用状态:

  • 使用 rmap 链 遍历页面在所有 VMA 中的映射,检查页表项中的 accessed/dirty 位

  • 如果页面在最近一次扫描期间被访问过,返回 PAGEREF_ACTIVATE

  • 如果页面未被引用但仍有保留价值,返回 PAGEREF_KEEP

  • 否则返回 PAGEREF_RECLAIMPAGEREF_RECLAIM_CLEAN

决策 switch

  • PAGEREF_ACTIVATE → 激活页面到 active 链表

  • PAGEREF_KEEP → 保持在 inactive 链表

  • PAGEREF_RECLAIM / RECLAIM_CLEAN → 继续回收


五、THP 降级通道

if (do_demote_pass &&
    (thp_migration_supported() || !PageTransHuge(page))) {
    list_add(&page->lru, &demote_pages);
    unlock_page(page);
    continue;
}
  • 如果开启了降级(can_demote 返回 true),且节点支持 THP 迁移或当前不是 THP

  • 页面被加入降级列表,稍后调用 demote_page_list 迁移到其他 NUMA 节点

  • 这避免了在单节点内存耗尽时被迫回收页面,先将大块页面迁走


六、匿名页面 Swap 分配

if (PageAnon(page) && PageSwapBacked(page)) {

处理匿名页(没有文件映射,只存在于 swap 或物理内存)。

未分配 swap 的情况

if (!PageSwapCache(page)) {
  • 如果页面还没有被加入 swap cache(即首次遇到这个匿名页)

  • 检查 __GFP_IO 标志,如果没有则不能分配 swap,保持页面

  • 检查是否被 DMA 固定(page_maybe_dma_pinned),如果是则保持

  • 处理 THP:先尝试拆分,如果无法拆分就激活

  • 关键调用 add_to_swap(page)

    • add_to_swap() 调用 get_swap_page() 获取一个 swap slot

    • 然后调用 add_to_swap_cache() 将页加入 swap cache,设置 PageSwapCache 标志

    • 如果分配成功,后续 pageout() 会直接写回 swap

THP 拆分

  • 如果无法分配 swap(THP),先拆分 THP 到列表,再对拆分后的小页尝试 add_to_swap

  • 如果拆分成功且小页也分配了 swap,nr_pages 会减小为 1(行 1655-1658)


七、文件映射页面的处理

解除映射(page_mapped 检查)

if (page_mapped(page)) {
    try_to_unmap(page, TTU_BATCH_FLUSH);
    if (page_mapped(page)) {
        stat->nr_unmap_fail += nr_pages;
        goto activate_locked;
    }
}
  • 调用 try_to_unmap()mm/rmap.c)通过 rmap 遍历所有页表项,用 ptep_clear_flush() 拆掉映射

  • 如果拆除后页面仍然有映射,说明有未拆掉的映射,回收失败,激活页面

  • ttu_flags 使用 TTU_BATCH_FLUSH,可以批量 flush TLB,提高效率

脏文件页回写PageDirty):

  • 如果是匿名页且 dirty,已经在上面处理

  • 如果是文件映射的 dirty 页,则走 pageout() 路径

  • 先检查:如果不是 kswapd 或者不满足脏页阈值,直接激活

  • 否则调用 pageout(page, mapping)

    • 对 swap-backed 匿名页,直接写回 swap(page_mkwrite 检查)

    • 对文件映射页,调用 mapping->a_ops->writepage(page, &wbc),走文件系统回写

  • 返回值:

    • PAGE_KEEP:保持页面

    • PAGE_ACTIVATE:激活页面

    • PAGE_SUCCESS:回写成功

    • PAGE_CLEAN:页面变干净(回写前已 dirty 但现在干净),可以释放

页面私有 buffer

if (page_has_private(page)) {
    if (!try_to_release_page(page, sc->gfp_mask))
        goto activate_locked;
}
  • 一些页面有私有 buffer(如块设备页面)

  • 调用 try_to_release_page 尝试释放所有 buffer,释放成功后页面可以回收


八、页面释放(free_it 路径)

if (PageAnon(page) && !PageSwapBacked(page)) {
  • 如果匿名页清除了 PG_swapbacked 标志(lazyfree),则可以直接释放

  • 调用 page_ref_freeze(page, 1) 冻结页面引用计数,如果冻结失败则保持

  • 计数 PGLAZYFREED,然后进入 free_it 路径

通用释放

} else if (!mapping || !__remove_mapping(mapping, page, true, ...))
    goto keep_locked;
  • 调用 __remove_mapping(mapping, page, true, ...) 从 address_space 中移除页面

  • 返回 false 表示移除失败(mapping 非空且页面仍被 mapping 引用),需要保持

  • 否则进入释放流程

真正释放(free_it 标签):

free_it:
nr_reclaimed += nr_pages;
if (unlikely(PageTransHuge(page)))
    destroy_compound_page(page);
else
    list_add(&page->lru, &free_pages);
  • 计数回收的页面

  • THP 调用 destroy_compound_page 销毁复合页面

  • 普通页或 THP 拆分后的小页加入 free_pages 链表


九、激活路径(activate_locked)

activate_locked:
if (PageSwapCache(page) && (mem_cgroup_swap_full(page) || PageMlocked(page)))
    try_to_free_swap(page);
SetPageActive(page);
stat->nr_activate[page_is_file_lru(page)] += nr_pages;
  • 先清理 swap 缓存中已不需要的条目

  • 将页面标记为 Active

  • 按 page type(文件页/匿名页)分别统计激活数量

  • 后续会调用 lru_add_active_page() 将其加入 active 链表尾部

十、保持路径(keep/keep_locked)

keep_locked:
unlock_page(page);
keep:
list_add(&page->lru, &ret_pages);
  • 先解锁,然后将页面放回 ret_pages 链表

  • 最终会 list_splice(&ret_pages, page_list) 把所有需要保留的页面放回 LRU


十一、循环结束与批量操作

降级页面处理

nr_reclaimed += demote_page_list(&demote_pages, pgdat);
  • 如果降级成功,返回释放的页面数

  • 降级失败则回退到重试

释放 batch 页面

mem_cgroup_uncharge_list(&free_pages);
try_to_unmap_flush();
free_unref_page_list(&free_pages);
  • 解计费控制组计费

  • flush TLB(处理所有被拆除的映射)

  • 批量将 free_pages 中的页面释放给 buddy system

放回 LRU 并统计激活

list_splice(&ret_pages, page_list);
count_vm_events(PGACTIVATE, pgactivate);
  • 将需要保留的页面放回原 LRU 链表

  • 更新虚拟内存统计


十二、决策流程图

开始:遍历 page_list(LRU inactive 链)
    │
    ├── trylock_page 失败?→ keep (保留在原位)
    │
    ├── page_evictable 失败?→ activate_locked (激活,放 active 链)
    │
    ├── may_unmap=0 && page_mapped?→ keep_locked (保持)
    │
    ├── dirty/writeback?
    │   ├── dirty 页 + 脏 BDI → congested++,继续
    │   └── PageWriteback?
    │       ├── kswapd + 回写阻塞 → activate_locked (跳过)
    │       ├── 非kswapd/无fs权限 → SetPageReclaim, activate_locked
    │       └── 其他(memcg)→ sleep + 放回尾部
    │
    ├── page_check_references 检查
    │   ├── PAGEREF_ACTIVATE → activate_locked
    │   ├── PAGEREF_KEEP → keep_locked
    │   └── PAGEREF_RECLAIM/RECLAIM_CLEAN → 继续
    │
    ├── 可降级?→ demote_pages 降级 (跨 NUMA 迁移)
    │
    ├── PageAnon && PageSwapBacked && !PageSwapCache?
    │   ├── add_to_swap (分配 swap slot + swap cache)
    │   └── 失败?→ activate_locked
    │
    ├── page_mapped?
    │   └── try_to_unmap → 仍被映射?→ activate_locked
    │
    ├── PageDirty 脏页?
    │   ├── 非kswapd 且脏页阈值不满足 → activate_locked
    │   ├── pageout() 回写 (swap 或 file writepage)
    │   │   ├── PAGE_KEEP → keep_locked
    │   │   ├── PAGE_ACTIVATE → activate_locked
    │   │   └── PAGE_SUCCESS/CLEAN → 继续释放
    │   └── try_to_release_page (私有 buffer) → 失败则激活
    │
    ├── PageAnon && !PageSwapBacked (lazyfree)?
    │   └── page_ref_freeze + count PGLAZYFREED
    │
    ├── __remove_mapping 失败?→ keep_locked
    │
    └── free_it: 加入 free_pages 链
        └── 最终 free_unref_page_list 释放给 buddy

循环结束 → demote + free batch → 放回 ret_pages 到 LRU

十三、关键统计字段

reclaim_stat 在每次调用 shrink_page_list 时统计:

字段

含义

nr_dirty

被检查到的脏页总数

nr_unqueued_dirty

脏页但未进入回写队列的数量

nr_congested

遇到阻塞设备的页

nr_immediate

需要立即激活的页(回写阻塞)

nr_writeback

标记 PageReclaim 后激活的页

nr_ref_keep

引用检查后保持的页

nr_unmap_fail

映射拆除失败的页

nr_lazyfree_fail

lazyfree 释放失败的页

nr_pageout

回写成功的页

nr_activate[0/1]

按类型(文件/匿名)激活的页

这些统计信息传递给 shrink_list,进一步决定是否需要扫描更多页或进入下一阶段。


十四、与其他函数的协作

函数

职责

与 shrink_page_list 的关系

page_referenced (rmap.c)

通过 rmap 检查页面引用

page_check_references 调用

try_to_unmap (rmap.c)

拆除页表映射

直接处理被映射页

add_to_swap (swap_state.c)

分配 swap slot

为匿名页分配 swap

pageout (vmscan.c)

回写 dirty 页

对 dirty 页执行 writepage

can_demote (vmscan.c)

判断是否开启降级

初始化阶段判断

page_evictable (internal.h)

判断页面可回收性

预检查的第一关


总结

shrink_page_list 是 Linux 内存回收中最复杂的决策函数,它通过一系列条件检查和操作路径,对每个页面做出回收、激活或保持的判决。这个函数是整个内存回收机制的“执行引擎”,直接决定了回收效率和页面管理质量。