Linux内核6.6 内存管理 (12)页面回收
1.页面回收算法
- 最优页面置换算法。
- 在页面中记录这个页面会在多久后被再次访问到,这样优先置换一个很长时间才能被访问的页面理想算法,不能实现。
- 先进先出页面置换算法。
- 维护一个链表,淘汰最先进入链表的页面容易理解,实现简单,性能不理想
- 最近未使用页面置换算法。
- 利用页表中的 PTE_YONG 和 PTE_DIRTY 比特位来进行对页面分类容易实现,效果一般
- LRU 算法
- (最近最少使用页面置换算法 Least Recently Used),其要点有利用局部性原理、维护链表、新页面及被访问页面加入链表头部、置换时从链表尾部取页面
- 第二次机会法
- 要点是给页面第二次可被访问的机会。对 FIFO 算法的改进,为每个页面增加一个 “访问位”(Accessed Bit),当页面被访问时置为 1;回收时先检查访问位:
- 若访问位为 0,直接回收;
- 若访问位为 1,将其置为 0 并移到队列尾部(给予 “第二次机会”),继续检查下一个页面。
- 时钟算法
- 第二次机会算法的优化版,将页面组织成环形链表(类似时钟表盘),用一个 “指针” 指向当前待检查的页面:指针指向的页面若访问位为 0,直接回收并移动指针;
- 若访问位为 1,重置为 0 并移动指针(继续检查下一个)。
2.Linux内核中使用的页面回收算法
数据结构:
https://elixir.bootlin.com/linux/v6.6/source/include/linux/mmzone.h#L263
Linux 内核的页面回收机制结合了多种算法的思想,核心逻辑基于 LRU 和时钟算法的变种,主要特点如下:
1. LRU 链表分级
- 内核将页面分为活跃链表(Active LRU) 和非活跃链表(Inactive LRU):
- 活跃链表:最近被访问的页面,暂时不回收;
- 非活跃链表:可能被回收的页面,内存不足时优先从这里回收。
- 页面首次进入内存时加入非活跃链表;被访问时移到活跃链表(或重置访问位保留在非活跃链表)。
2. 访问位与第二次机会
- 每个页面有
Accessed标志(访问位),被访问时置 1; - 回收扫描时,若非活跃链表的页面访问位为 1,将其置 0 并移到活跃链表(给予第二次机会);若为 0,则回收。
3. 针对文件页和匿名页的优化
- 内核区分文件页(如磁盘缓存) 和匿名页(如进程堆 / 栈),分别维护独立的 LRU 链表:
- 文件页可直接释放(数据可从磁盘重新读取);
- 匿名页需先写入交换区(swap),回收成本更高。
- 通过
swappiness参数调整两者的回收优先级(值越高,越倾向回收匿名页)。
回收流程
2.1 从alloc_pages/kswapd来了解一下页面回收(reclaim是回收的意思)
https://elixir.bootlin.com/linux/v6.6/source/mm/page_alloc.c#L4390
伙伴系统空闲链表快速分配
如果不行,就走到慢速分配
https://elixir.bootlin.com/linux/v6.6/source/mm/page_alloc.c#L3899
alloc_pages() → 快速回收 → __alloc_pages_slowpath() // 快速路径失败,进入慢速路径 → __alloc_pages_direct_reclaim() // 触发直接回收 → __peform_reclaim() → try_to_free_pages()
→ do_try_to_free_pages() → shrink_zones() // 收缩内存区域
2.1 从kswpad了解一下页面回收
在走慢速分配的时候
https://elixir.bootlin.com/linux/v6.6/source/mm/page_alloc.c#L3954
如果激活了ALLOC_KSWAPD标志,会唤醒kswapds进程
2.2内存区域扫描与LRU链表操作
shrink_zones(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist)
//遍历内存域列表(zonelist)中的每个内存域(zone)
→ shrink_zone(zone, gfp_mask, order) // 尝试从单个内存域回收
// 若该内存域属于 NUMA 节点
→ shrink_node(node, gfp_mask, order)
→ shrink_node_memcgs(node, sc) // 回收节点下所有内存控制组(memcg)
//遍历节点下每个 memcg
→ shrink_lruvec(lruvec, sc) // 回收 memcg 的 LRU 向量
→ lru_gen_shrink_node(lruvec, sc) // 基于页面世代(generation)的回收
→ try_to_shrink_lruvec(lruvec, sc) // 扫描 LRU 向量
→ evict_folios(lruvec, sc, swappiness) // 核心回收逻辑
→ isolate_folios(lruvec, sc, swappiness, &type, &list) // 隔离可回收页面
2.3页面评估与标记处理(evict_folios)
evict_folios(struct lruvec *lruvec, struct scan_control *sc, int swappiness)
→ 初始化变量与链表:
LIST_HEAD(list); // 待回收页面链表
LIST_HEAD(clean); // 待重试页面链表
→ 锁定 LRU 链表:
spin_lock_irq(&lruvec->lru_lock);
→ 从 LRU 隔离可回收页面:
scanned = isolate_folios(lruvec, sc, swappiness, &type, &list);
→ 更新页面世代:
scanned += try_to_inc_min_seq(lruvec, swappiness);
→ 解锁 LRU 链表:
spin_unlock_irq(&lruvec->lru_lock);
→ 若无可回收页面,直接返回:
if (list_empty(&list))
return scanned;
retry:
→ 尝试回收页面:
reclaimed = shrink_folio_list(&list, pgdat, sc, &stat, false);
→ 遍历未回收的页面:
list_for_each_entry_safe_reverse(folio, next, &list, lru) {
→ 不可回收页面放回 LRU:
if (!folio_evictable(folio)) {
list_del(&folio->lru);
folio_putback_lru(folio);
}
→ 标记为回收的脏页/写回页:
else if (folio_test_reclaim(folio) && (folio_test_dirty(folio) || folio_test_writeback(folio))) {
if (folio_test_workingset(folio))
folio_set_referenced(folio);
}
→ 活跃/引用/映射/锁定/脏页标记为活跃:
else if (skip_retry || folio_test_active(folio) || ...) {
set_mask_bits(&folio->flags, ..., BIT(PG_active));
}
→ 可重试页面移至 clean 链表:
else {
list_move(&folio->lru, &clean);
sc->nr_scanned -= folio_nr_pages(folio);
}
}
→ 锁定 LRU 链表:
spin_lock_irq(&lruvec->lru_lock);
→ 将未回收页面放回 LRU:
move_folios_to_lru(lruvec, &list);
→ 统计回收事件:
__count_vm_events(PGSTEAL_KSWAPD + reclaimer_offset(), reclaimed);
__count_memcg_events(memcg, PGSTEAL_KSWAPD + reclaimer_offset(), reclaimed);
→ 解锁 LRU 链表:
spin_unlock_irq(&lruvec->lru_lock);
→ 释放无引用页面:
free_unref_page_list(&list);
→ 将 clean 链表合并到 list:
INIT_LIST_HEAD(&list);
list_splice_init(&clean, &list);
→ 若 list 非空,跳回 retry(仅重试一次):
if (!list_empty(&list)) {
skip_retry = true;
goto retry;
}
→ 返回扫描的页面数:
return scanned;
2.4页面回收执行(shrink_folio_list)
匿名页/文件页
函数操作 | 匿名页 | 文件页 |
脏数据处理 | 写入 swap 空间( | 写回原文件( |
大页处理 | 优先拆分( | 较少拆分,直接写回 |
迁移策略 | 不参与迁移(依赖 swap) | 可迁移到其他节点( |
释放条件 | 必须有 swap 备份 | 数据写回或文件系统允许释放 |
核心函数调用 |
|
|
3.页面回收详细流程图
https://elixir.bootlin.com/linux/v6.6/source/mm/vmscan.c#L6273
从shrink_lruvec往下捋
shrink_active_list()
→ spin_lock_irq() // 锁定 LRU 链表
→ isolate_lru_folios() // 从活跃 LRU 隔离页面
→ __mod_node_page_state() // 更新统计
→ __count_vm_events(PGREFILL) // 记录扫描事件
→ spin_unlock_irq() // 解锁 LRU 链表
→ while (!list_empty(&l_hold)): // 遍历隔离的页面
→ folio = lru_to_folio() // 取出页面
→ if (!folio_evictable()): // 不可回收?
→ folio_putback_lru() // 放回 LRU
→ if (buffer_heads_over_limit): // buffer_head 超限?
→ filemap_release_folio() // 释放资源
→ if (folio_referenced()): // 页面被引用?
→ list_add(&l_active) // 保持活跃*******************
→ else:
→ folio_clear_active() // 清除活跃标记
→ folio_set_workingset() // 标记为工作集
→ list_add(&l_inactive) // 降级为不活跃******************
→ spin_lock_irq() // 再次锁定 LRU 链表
→ move_folios_to_lru(&l_active) // 放回活跃 LRU
→ move_folios_to_lru(&l_inactive) // 放回不活跃 LRU
→ __count_vm_events(PGDEACTIVATE) // 记录降级事件
→ __mod_node_page_state() // 恢复统计
→ spin_unlock_irq() // 解锁 LRU 链表
→ lru_note_cost() // 记录旋转成本
→ mem_cgroup_uncharge_list() // 解除内存控制组记账
→ free_unref_page_list() // 释放无引用页面
→ trace_mm_vmscan_lru_shrink_active() // 输出跟踪日志
#嵌入式##嵌入式笔面经分享##秋招提前批启动你开冲了吗##嵌入式软开##牛客创作赏金赛#秋招之旅结束大半,目前也拿到了一些公司的offer,各种方向的: 芯片公司:兆芯,联发科,大普微等 产品公司:小米,联想,影石,虹软,诺瓦等 汽车公司:长安,博世,经纬恒润等 国央企研究所:32所,52所,712,星网,航空工业上电所 接下来会分享一些面试经验和学习经验
