/* 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 锁定的物理内存页)
二、核心源码走读:页面的晋升机制(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)
性能调优关键启示:
深入走读 Linux LRU 双链表的实现机制,才能在面对生产环境偶发的 Cache 剧烈抖动与 I/O 拥塞时,从容定位瓶颈并给出精准的内核调优方案。



