欢迎光临
我们一直在努力

Linux 物理内存冷热分离机制:深入剖析 active 与 inactive 双链表 LRU 页置换内核源码

封面信息图

在Linux内核的内存管理子系统(MM)中,页面回收(Page Reclaim)机制直接决定了系统在高内存负载下的吞吐底线。如果操作系统简单地采用教科书式的单链表LRU(Least Recently Used,最近最少使用)算法,系统将面临灾难性的“缓存污染”(Cache Pollution):一次偶发的全盘扫描(如grep -r或tar -czf流式打包备份),就会瞬间将整机DRAM塞满一次性文件页,并将数据库或核心服务长期常驻的热点物理页全部踢出,引发系统级剧烈颠簸(Thrashing)与致命抖动。

为了彻底解决缓存污染并实现纳秒级冷热分离,Linux内核在每个内存节点(pg_data_t)与内存控制组(memcg)的lruvec中,构建了一套精妙的active_list(活跃链表)与inactive_list(非活跃链表)双链表LRU置换架构。

为什么朴素LRU无法胜任现代通用内核

朴素LRU的基本假设是:越是最近被访问的页面,在未来被访问的概率越高。然而在真实的复杂生产负荷下,访存模式存在本质分化:

  • 周期性热点:进程的代码段、堆栈、B-Tree索引页,每秒被反复读取数万次。
  • 一次性流式访问:大文件拷贝、日志流水写入、视频流推拉,数据只在几毫秒内经过CPU一次,随后永远沉睡。

在单链表LRU中,流式数据会盲目占据链表头部,将周期性热点强制向后挤出并最终逐出物理内存。为了实现对“瞬时突发访问”与“持续高频访问”的精准区分,Linux引入了两次访问原则(Two-Touch Strategy):新分配或缺页载入的页面默认只能进入冷区(inactive链表),只有当其在冷区驻留期间被再次访问时,才有资格晋升为热区(active链表)。

双链表拓扑与页面标志位状态机

在Linux内核源码中(以mm/swap.c与mm/vmscan.c为核心),物理页(struct page或现代内核的struct folio)被细分为五种LRU链表类型:

  • LRU_INACTIVE_ANON:非活跃匿名页(堆、栈、匿名mmap,对应Swap候选)
  • LRU_ACTIVE_ANON:活跃匿名页
  • LRU_INACTIVE_FILE:非活跃文件页(Page Cache,对应直接清退或写回)
  • LRU_ACTIVE_FILE:活跃文件页
  • LRU_UNEVICTABLE:被mlock()锁定的不可回收页
  • 冷热状态的流转完全依托于页描述符中的两个标志位:

    • PG_active:该页当前处于active链表还是inactive链表。
    • PG_referenced:该页近期是否被引用(软件逻辑访问或硬件MMU置位)。

    在实际运转中,物理页面在冷热双链表之间形成了清晰的晋升与降级闭环:

    • 冷区准入与末位淘汰(Inactive 链表):新分配或缺页载入的页面默认插入 Inactive 链表头部。若后续没有二次访问,页面会随推进滑至链表尾部,在内存紧张时由 shrink_inactive_list() 直接回收、丢弃或换出。
    • 二次命中晋升(Inactive → Active):当驻留冷区的页面迎来二次访问时,触发 mark_page_accessed(),内核将其标志位翻转并移入 Active 链表头部,免遭瞬时淘汰。
    • 收缩回退降级(Active → Inactive):当内存水位吃紧触发回收时,内核通过 shrink_active_list() 扫描 Active 尾部较久未访问的页面,清除激活状态并降级打回 Inactive 链表重新考察。

    内核源码核心函数行为剖析

    为了彻底看清上述双向流转在底层的运转细节,我重点跟踪了内核源码中负责驱动状态流转的几个核心函数:

    1. 页面访问判定:mark_page_accessed()

    当用户态通过系统调用访问文件页,或者内核建立内存映射时,会触发mark_page_accessed()。其精简核心逻辑如下(源自内核mm/swap.c):

    // 内核源码逻辑概念化提炼
    void mark_page_accessed(struct page *page) {
    page = compound_head(page);

    // 状态一:页面处于非活跃状态且未被引用
    if (!PageActive(page) && !PageReferenced(page)) {
    // 第一次访问:仅置位referenced标志,依然留在inactive链表
    SetPageReferenced(page);
    }
    // 状态二:页面在inactive链表,且referenced已置位(第二次被访问!)
    else if (!PageActive(page) && PageReferenced(page)) {
    // 两次访问原则满足:清除referenced,正式激活晋升至active链表
    ClearPageReferenced(page);
    activate_page(page); // 从inactive_list迁移到active_list头部
    }
    // 状态三:页面已经在active链表上
    else if (PageActive(page)) {
    // 热区页面再次命中:仅刷新referenced,保持其热度
    SetPageReferenced(page);
    }
    }

    这一精简状态机彻底封死了流式数据污染热区的可能。一次性读取的页面虽然进入inactive链表并打上referenced,但由于再也不会发生第二次访问,随着后续内存申请推进,它将顺理成章地滑落到inactive尾部并被内核光速抹除。

    2. 热区向冷区的回降:shrink_active_list()

    active链表不能无限膨胀。内核通过维持active与inactive页面的动态比例(通常为1:1至3:1),在内存吃紧触发kswapd或直接回收(Direct Reclaim)时,调用shrink_active_list()将过热的active尾部页面扫描降级至inactive链表:

    static void shrink_active_list(unsigned long nr_to_scan,
    struct lruvec *lruvec,
    struct scan_control *sc,
    enum lru_list lru) {
    LIST_HEAD(l_hold);
    LIST_HEAD(l_active);
    LIST_HEAD(l_inactive);

    // 1. 从active链表尾部批量隔离 nr_to_scan 个页面
    isolate_lru_pages(nr_to_scan, lruvec, &l_hold, …);

    while (!list_empty(&l_hold)) {
    struct page *page = lru_to_page(&l_hold);
    list_del(&page->lru);

    // 检查硬件MMU页表项中的 _PAGE_ACCESSED 访问位
    int referenced = page_referenced(page, 0, sc->target_mem_cgroup, …);

    // 若页面在热区内依然有硬件级别的持续访问,且非匿名页,允许其继续留在active
    if (referenced && !page_is_file_lru(page)) {
    SetPageReferenced(page);
    list_add(&page->lru, &l_active);
    continue;
    }

    // 否则:清除PG_active标志,将其降级并推入inactive链表!
    ClearPageActive(page);
    ClearPageReferenced(page);
    list_add(&page->lru, &l_inactive);
    }

    // 将处理后的页面分别重新挂回全局active与inactive链表
    move_pages_to_lru(lruvec, &l_active);
    move_pages_to_lru(lruvec, &l_inactive);
    }

    3. 冷区最终抹杀:shrink_inactive_list()

    回收器执行shrink_inactive_list()时,仅针对inactive尾部的页面进行最终审判:

    • 文件未脏页(Clean File Page):无需I/O回写,直接解除页表映射并归还物理伙伴系统(Buddy System)。
    • 文件脏页(Dirty File Page):提交给后台刷盘线程,待写回磁盘完成后回收。
    • 匿名页(Anonymous Page):若系统开启了Swap分区,将其压缩或写入Swap,清空物理页表项并填入换出条目(Swap Entry)。

    双链表LRU的现代局限与MGLRU演进

    双链表LRU历经数十年迭代,支撑了Linux从小型嵌入式设备到超算集群的平稳运行。但在现代TB级DRAM、数万并发容器的服务器上,传统双链表暴露出两项底层瓶颈:

  • 全局自旋锁竞争(lru_lock Contention):每个lruvec只有一把lru_lock。在数百个CPU核心频繁并发分配和访问内存时,链表节点的移动导致该锁成为全系统的吞吐瓶颈。
  • 粗粒度代际划分:只有“冷”和“热”两个状态,无法精准识别“五分钟前访问的页”与“一小时前访问的页”之间的温差。在内存极其紧张时容易引发误杀。
  • 这正是Linux 6.1+正式合入主线的**MGLRU(Multi-Gen LRU,多代LRU)**旨在颠覆的现状。MGLRU通过引入代际(Generation)序列号并借助硬件页表脏位(Accessed Bit)的增量位图轮询,彻底解除了传统双链表频繁移动节点的自旋锁负担。

    理解active与inactive双链表的流转机制,是深入理解Linux内核虚存管理、精准定位生产环境内存抖动与调优Page Cache水位线(vm.vfs_cache_pressure、vm.min_free_kbytes)不可或缺的技术基石。

    赞(0)
    未经允许不得转载:171主机测评 » Linux 物理内存冷热分离机制:深入剖析 active 与 inactive 双链表 LRU 页置换内核源码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址