Linux内核6.6 内存管理 (14)页面回收
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() // 输出跟踪日志#嵌入式##嵌入式笔面经分享##秋招提前批启动你开冲了吗##嵌入式软开##牛客创作赏金赛#