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_locked和keep_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_RECLAIM或PAGEREF_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 时统计:
这些统计信息传递给 shrink_list,进一步决定是否需要扫描更多页或进入下一阶段。
十四、与其他函数的协作
总结
shrink_page_list 是 Linux 内存回收中最复杂的决策函数,它通过一系列条件检查和操作路径,对每个页面做出回收、激活或保持的判决。这个函数是整个内存回收机制的“执行引擎”,直接决定了回收效率和页面管理质量。