4. Active与Inactive链表:双链LRU的工作模型

Linux内核在经典LRU(Least Recently Used)算法基础上,引入了双链表LRU机制,将页面分为Active(活跃)和Inactive(非活跃)两个链表。这种设计解决了传统单LRU链表在扫描大内存时效率低下、冷热页面混淆的问题,是内核页面置换的核心数据结构。

核心思想:页面在Active与Inactive链表之间通过“晋升”与“降级”机制动态调整,使得频繁访问的页面长期驻留Active链表,而一次性访问的页面快速被回收。

4.1 双链LRU的整体架构

每个内存管理区(zone)都维护一对LRU链表:active_list 和 inactive_list。页面根据其访问频率和最近使用情况在这两个链表之间迁移。

双链LRU工作模型 Inactive 链表 (非活跃页面) 页面 A 页面 B 页面 C 页面 D Active 链表 (活跃页面) 页面 X 页面 Y 页面 Z 晋升 (第二次访问) 降级 (扫描未访问) 页面回收 Inactive链表头部是回收候选,Active链表头部是长期驻留页面

4.2 页面晋升机制(Promotion)

当页面首次被访问时,它被加入Inactive链表的尾部。如果该页面在短时间内被第二次访问,内核认为它是“热”页面,将其晋升到Active链表的尾部。晋升操作由 mark_page_accessed() 和 activate_page() 函数完成。

关键判断:页面是否晋升取决于其 PG_referenced 标志位和 PG_active 标志位的组合状态。第一次访问设置 PG_referenced,第二次访问时若 PG_referenced 已置位,则触发晋升。
/* mm/swap.c - 页面晋升核心逻辑 */
void mark_page_accessed(struct page *page)
{
    if (!PageReferenced(page)) {
        SetPageReferenced(page);  /* 第一次访问:设置引用标志 */
    } else if (PageInactive(page)) {
        /* 第二次访问且页面在Inactive链表:晋升到Active */
        activate_page(page);
        ClearPageReferenced(page);
    }
    /* 已在Active链表:仅保留引用标志,不重复晋升 */
}

4.3 页面降级机制(Demotion)

当内核进行内存回收时,shrink_active_list() 函数扫描Active链表。对于每个页面,如果其 PG_referenced 标志位未被设置,说明该页面在最近一轮扫描中未被访问,则将其降级到Inactive链表尾部。如果标志位已被设置,则清除该标志位并保留在Active链表中,等待下一轮扫描。

扫描结果 PG_referenced 状态 操作 页面去向
页面被访问过 已设置 清除标志位,保留在Active链表 继续驻留
页面未被访问 未设置 移动到Inactive链表尾部 降级
Inactive页面被访问 已设置 晋升到Active链表尾部 晋升
/* mm/vmscan.c - Active链表扫描与降级 */
static void shrink_active_list(unsigned long nr_to_scan,
                                struct lruvec *lruvec,
                                struct scan_control *sc)
{
    struct page *page;
    LIST_HEAD(l_active);   /* 保留在Active的页面 */
    LIST_HEAD(l_inactive); /* 降级到Inactive的页面 */

    while (nr_to_scan--) {
        page = lru_to_page(&lruvec->lists[LRU_ACTIVE_ANON]);
        if (page_referenced(page, 0, sc->target_mem_cgroup,
                            &vm_flags)) {
            /* 页面被引用:保留在Active */
            list_move(&page->lru, &l_active);
        } else {
            /* 页面未被引用:降级到Inactive */
            list_move(&page->lru, &l_inactive);
            ClearPageActive(page);
            SetPageInactive(page);
        }
    }
    /* 将l_active和l_inactive合并回原链表 */
}

4.4 双链LRU的平衡策略

内核通过比例控制和动态调整维持两个链表的平衡,防止某一链表过度膨胀。

  • 默认比例:Active链表与Inactive链表的页面数量比例通常接近 2:1,但会根据系统负载动态调整。
  • 扫描优先级:回收时优先扫描Inactive链表,只有当Inactive链表不足时才会从Active链表降级页面补充。
  • 防止颠簸:如果系统频繁发生页面晋升和降级(颠簸),内核会调整 swappiness 参数或触发直接回收。
双链LRU平衡与回收流程 Inactive 链表 Active 链表 页面回收 kswapd 扫描 回收候选 扫描降级 降级补充 晋升 回收优先级:Inactive尾部 → Inactive头部 → Active降级 → Active头部

4.5 链表操作的关键数据结构

每个内存管理区(zone)的LRU链表通过 struct lruvec 结构体管理,该结构体包含五个LRU链表(匿名页和文件页各有两个,加上不可回收页)。

/* include/linux/mmzone.h - LRU链表枚举与向量结构 */
enum lru_list {
    LRU_INACTIVE_ANON = 0,  /* 非活跃匿名页 */
    LRU_ACTIVE_ANON   = 1,  /* 活跃匿名页 */
    LRU_INACTIVE_FILE = 2,  /* 非活跃文件页 */
    LRU_ACTIVE_FILE   = 3,  /* 活跃文件页 */
    LRU_UNEVICTABLE   = 4,  /* 不可回收页 */
    NR_LRU_LISTS
};

struct lruvec {
    struct list_head lists[NR_LRU_LISTS];  /* 五个LRU链表头 */
    spinlock_t          lru_lock;          /* 链表操作自旋锁 */
    unsigned long       anon_cost;         /* 匿名页回收代价 */
    unsigned long       file_cost;         /* 文件页回收代价 */
};
注意事项:双链LRU的晋升和降级操作必须在持有 lru_lock 自旋锁的情况下进行,以避免并发访问导致链表损坏。在SMP系统中,锁竞争可能成为性能瓶颈,内核通过per-CPU页向量(pagevec)批量操作来减少锁开销。

4.6 页面状态转换总结

当前状态 事件 新状态 触发函数
不存在 首次页面分配 Inactive链表尾部 lru_cache_add()
Inactive 第一次访问 Inactive(设置PG_referenced) mark_page_accessed()
Inactive(已引用) 第二次访问 Active链表尾部 activate_page()
Active kswapd扫描未访问 Inactive链表尾部 shrink_active_list()
Active kswapd扫描已访问 Active(保留) shrink_active_list()
Inactive 页面回收 释放 shrink_inactive_list()

双链LRU机制通过精细的晋升与降级策略,有效区分了“热”页面和“冷”页面,使得内核在内存回收时能够优先回收真正不再使用的页面,同时保护频繁访问的工作集不被错误回收。这一设计是Linux内存管理子系统高效运行的重要基石。