欢迎光临
我们一直在努力

深入剖析 Linux LRU 链表:Active 与 Inactive 页面老化与移动算法源码

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

深入剖析 Linux LRU 链表:Active 与 Inactive 页面老化与移动算法源码

封面信息图

在操作系统内存管理子系统中,页面回收(Page Reclaim)的效率直接决定了系统的吞吐量与稳定性。当物理内存紧张时,内核必须决定“哪些页面应该留在内存中,哪些页面应该被丢弃或换出到磁盘”。

最经典的淘汰算法是单链表 LRU(Least Recently Used,最近最少使用)。但如果直接在操作系统中应用朴素的单链表 LRU,会遭遇一个毁灭性的缺陷:缓存污染(Cache Thrashing)。

试想一下:某一个后台进程突然执行了 grep "error" 50GB.log。随着大文件被顺序读入 Page Cache,朴素 LRU 链表会被这 50GB 仅被访问一次的冷数据迅速填满,导致数据库和核心业务常驻的活跃热点内存被无辜驱逐,引发全站剧烈的次级缺页异常(Major Page Fault)。

为了解决这个根本矛盾,Linux 内核设计了一套精妙的双 LRU 链表架构(Active / Inactive Lists)与两次访问激活机制。

本文将深入内核源码(基于 Linux 5.x / 6.x 内核 mm/swap.c 与 mm/vmscan.c),全面剖析页面的老化(Aging)、晋升(Promotion)与降级(Demotion)算法实现。


一、Linux 双 LRU 链表体系

在现代 Linux 内核中,每个 NUMA 节点(pg_data_t)以及每个 Memory Cgroup 都会维护 5 条核心 LRU 链表:

NUMA 节点 / MemCG LRU 体系
│
┌───────────────────────┴───────────────────────┐
▼ ▼
[文件页链表 (File-backed)] [匿名页链表 (Anonymous)]
(由可执行文件、mmap 文件、Page Cache 构成) (由进程堆栈、malloc、匿名 mmap 构成)
┌─────────────────┐ ┌─────────────────┐
│ Active File │ │ Active Anon │
│ (高频访问热点页)│ │ (高频访问热点页)│
└────────┬────────┘ └────────┬────────┘
│ ▲ (老化降级 / 两次访问晋升) │ ▲
▼ │ ▼ │
┌────────┴────────┐ ┌────────┴────────┐
│ Inactive File │ │ Inactive Anon │
│ (候选待回收冷页)│ │ (候选待回收冷页)│
└─────────────────┘ └─────────────────┘
│
▼
[不可回收链表 (LRU_UNEVICTABLE)]
(由 mlock 锁定的物理内存页)

  • Active 链表:存放最近频繁访问的热点页面,内核默认不扫描这些页面进行回收;
  • Inactive 链表:存放冷数据或仅被访问过一次的候选回收页面。当系统触发内存回收时,kswapd 主要从 Inactive 链表尾部抓取页面进行释放;
  • 分流管理:文件页(File)在回收时只需丢弃(若为脏页则刷盘),匿名页(Anon)回收时必须写入 Swap 分区,两者成本不同,因此物理链表完全独立。

  • 二、核心源码走读:页面的晋升机制(Promotion)

    页面是如何从 Inactive 链表晋升到 Active 链表的?

    1. 两次访问激活策略:mark_page_accessed() (mm/swap.c)

    Linux 采取了极其克制的晋升策略:一个文件页被初次读入时,默认直接放入 Inactive 链表。只有当它在短时间内被访问第二次时,才会被移入 Active 链表。

    void mark_page_accessed(struct page *page)
    {
    page = compound_head(page);

    // 状态机流转:
    // 1. 如果页面处于非 Active 状态,且未设置 Referenced 标志位:
    // -> 设置 PG_referenced 标志位 (记录第一次访问)
    // 2. 如果页面已经设置了 PG_referenced 标志位 (说明是第二次被访问):
    // -> 清除 PG_referenced 标志位
    // -> 调用 activate_page() 将页面晋升到 Active 链表

    if (!PageActive(page) && !PageUnevictable(page) &&
    PageReferenced(page)) {

    // 第二次访问:触发晋升
    activate_page(page);
    ClearPageReferenced(page);
    workingset_activation(page);
    } else if (!PageReferenced(page)) {
    // 第一次访问:仅打标记
    SetPageReferenced(page);
    }
    }

    通过这一简单的状态机,单次批量扫描的大文件只会将页面标记为 PG_referenced 并留在 Inactive 链表,随着后续扫描,它们会在 Inactive 尾部被无痛释放,绝对不会污染 Active 热点链表!


    三、核心源码走读:页面的老化与降级机制(Demotion)

    Active 链表不能无限膨胀。内核必须保持 Active 与 Inactive 链表之间的比例平衡(通常默认目标比例为 1:1 或 2:1)。

    1. 扫描与降级:shrink_active_list() (mm/vmscan.c)

    当 Inactive 链表中的页面不足时,内核会调用 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)
    {
    unsigned long nr_taken;
    LIST_HEAD(l_hold);
    LIST_HEAD(l_active);
    LIST_HEAD(l_inactive);

    // 1. 从 Active 链表尾部批量隔离一批页面
    nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
    sc, 0, lru);

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

    // 2. 检查硬件页表访问位 (PTE_ACCESSED / PTE_YOUNG)
    if (page_referenced(page, 0, sc->target_mem_cgroup, &vm_flags)) {
    // 如果最近又有进程通过虚拟地址访问了该页面,保留在 Active
    list_add(&page->lru, &l_active);
    continue;
    }

    // 3. 确实变冷:清除 Active 标志,加入降级链表
    ClearPageActive(page);
    list_add(&page->lru, &l_inactive);
    }

    // 4. 将降级页面归还至 Inactive LRU 链表
    move_pages_to_lru(lruvec, &l_inactive);
    move_pages_to_lru(lruvec, &l_active);
    }

    2. 硬件页表联动:page_referenced()

    操作系统如何知道用户态进程是否通过虚拟地址读取了某个内存页?

    • x86 / ARM 的 MMU 硬件在发生物理内存访问时,会自动将页表项(PTE)中的 Accessed 标志位置 1;
    • 内核在执行 page_referenced() 时,会遍历所有映射了该物理页的反向映射(RMAP – Reverse Mapping),检查并清除该硬件位。若硬件位为 1,则证明页面在上次扫描后仍在使用。

    四、生产环境观察与内核指标解读

    通过 /proc/meminfo 可以实时观测系统 LRU 链表的分布:

    # 查看全局 LRU 内存分布
    cat /proc/meminfo | grep -E "Active|Inactive"

    典型输出解析:

    Active: 18294520 kB # 全局活跃内存 (热点,受保护)
    Inactive: 14120340 kB # 全局非活跃内存 (可优先回收)
    Active(anon): 6241080 kB # 活跃匿名页 (进程堆栈,需 swap)
    Inactive(anon): 2104520 kB # 非活跃匿名页 (冷进程堆栈,候选换出)
    Active(file): 12053440 kB # 活跃文件缓存 (高频读写热点文件)
    Inactive(file): 12015820 kB # 非活跃文件缓存 (可瞬间丢弃释放的 Page Cache)

    性能调优关键启示:
  • Inactive(file) 过低:如果系统的 Inactive(file) 长期不足几百 MB,而业务又在频繁进行突发读写,系统极易触发 Direct Reclaim 导致性能抖动;
  • vm.vfs_cache_pressure:该参数(默认 100)控制内核回收目录项(Dentry)与索引节点(Inode)缓存相对于普通 Page Cache 的积极程度。调高此值可加速释放元数据缓存,调低可保留更多文件系统目录缓存。
  • 深入走读 Linux LRU 双链表的实现机制,才能在面对生产环境偶发的 Cache 剧烈抖动与 I/O 拥塞时,从容定位瓶颈并给出精准的内核调优方案。

    赞(0)
    未经允许不得转载:171主机测评 » 深入剖析 Linux LRU 链表:Active 与 Inactive 页面老化与移动算法源码
    分享到: 更多 (0)

    评论 抢沙发

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