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

  • 不活跃匿名页面链表(LRU_INACTIVE_ANON )
  • 活跃匿名页面链表(LRU_ACTIVE_ANON )
  • 不活跃文件映射页面链表(LRU_INACTIVE_FILE )
  • 活跃文件映射页面链表(LRU_ACTIVE_FILE )
  • 不可回收页面链表(LRU_UNEVICTABLE )
  • 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 空间(add_to_swap

    写回原文件(pageout

    大页处理

    优先拆分(split_folio_to_list

    较少拆分,直接写回

    迁移策略

    不参与迁移(依赖 swap)

    可迁移到其他节点(demote_folio_list

    释放条件

    必须有 swap 备份

    数据写回或文件系统允许释放

    核心函数调用

    folio_free_swap

    folio_ref_freeze

    __remove_mapping

    filemap_release_folio

    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() // 输出跟踪日志
    

    #嵌入式##嵌入式笔面经分享##秋招提前批启动你开冲了吗##嵌入式软开##牛客创作赏金赛#
    全部评论

    相关推荐

    点赞 评论 收藏
    分享
    评论
    点赞
    1
    分享

    创作者周榜

    更多
    牛客网
    牛客网在线编程
    牛客网题解
    牛客企业服务