3. 页面类型与LRU分类:匿名页与文件页

Linux内核将物理页面划分为两种基本类型:匿名页(Anonymous Page)和文件页(File-backed Page)。这两种页面在生命周期、回写方式、回收代价以及LRU链表管理策略上存在本质差异。理解它们的区别是掌握内存回收机制的核心前提。

3.1 匿名页(Anonymous Page)

匿名页是指没有关联任何磁盘文件的物理页面。它们通常用于进程的堆(heap)、栈(stack)、写时复制(COW)产生的页面以及共享内存(SHM)区域。由于没有后备存储,匿名页在回收时必须先写入交换分区(swap)或交换文件,否则数据会丢失。

  • 典型来源:malloc()/mmap(MAP_ANONYMOUS) 分配的堆内存、线程栈、共享内存段。
  • 回写路径:必须经过 swap-out 写入交换设备,代价较高。
  • 回收条件:页面内容被换出后,物理页框可被重用;再次访问时触发缺页中断换入。
  • 脏页状态:匿名页始终被视为“脏页”,因为其内容与磁盘无同步关系。
关键指标:匿名页的回收代价通常高于文件页,因为涉及磁盘I/O的换入换出。内核在回收时会优先选择文件页,以降低性能抖动。

3.2 文件页(File-backed Page)

文件页是映射了磁盘文件内容的物理页面,包括通过 mmap() 映射的文件区域、可执行程序的代码段(text segment)以及内核文件缓存(page cache)。文件页的内容与磁盘文件保持同步,回收时只需将脏页写回原文件,干净页则可直接丢弃。

  • 典型来源:mmap(MAP_SHARED) 文件映射、可执行文件加载、文件读写产生的 page cache。
  • 回写路径:脏页通过 flusher 线程写回原磁盘文件;干净页直接释放,无需I/O。
  • 回收条件:干净页可立即回收;脏页需先写回,再回收。
  • 脏页状态:文件页有明确的“干净/脏”区分,由硬件脏位(dirty bit)或软件脏位标记。
性能优势:文件页的干净页回收几乎零代价,这是内核优先回收文件页的重要原因。对于频繁读取的只读文件(如共享库),其页面可以快速丢弃并在需要时从磁盘重新加载。

3.3 匿名页与文件页的核心对比

对比维度 匿名页(Anonymous Page) 文件页(File-backed Page)
后备存储 交换分区/交换文件 原始磁盘文件
脏页定义 始终为脏(无磁盘同步) 仅修改后为脏
回收路径 必须换出到 swap 脏页写回文件,干净页直接丢弃
回收代价 高(需I/O换出,可能需I/O换入) 低(干净页零代价,脏页需写回)
典型用途 堆、栈、共享内存 文件缓存、可执行代码、mmap文件
LRU链表归属 匿名页LRU链表 文件页LRU链表

3.4 LRU链表中的分类管理

Linux内核从2.6.28开始引入双链LRU(Per-zone Two-List LRU)管理机制,将匿名页和文件页分别维护在不同的LRU链表中。每个内存管理区(zone)维护两组LRU链表:一组用于匿名页,一组用于文件页。每组又分为活跃(active)和非活跃(inactive)两个链表。

  • 匿名页LRU链表:管理所有匿名页面,包括进程堆栈、共享内存等。
  • 文件页LRU链表:管理所有文件映射页面和page cache页面。

这种分离设计使得内核可以针对不同类型的页面采用不同的回收策略。例如,在内存压力下,内核可以优先从文件页的非活跃链表回收页面,而保留匿名页以降低swap抖动。

注意:在早期内核版本(2.6.28之前)中,所有页面共享同一组LRU链表,导致回收策略无法区分页面类型,容易引发匿名页频繁换入换出的“颠簸”问题。双链分离是解决该问题的关键改进。

3.5 页面类型与LRU链表映射关系

每个物理页面通过其 page 结构体中的标志位确定类型,并挂载到对应的LRU链表。内核通过 page_lru() 宏和 lru_cache_add() 函数实现页面分类入链。

/* 内核源码:include/linux/page-flags.h 中页面类型判断 */
#define PG_swapbacked  14   /* 匿名页标志:PageSwapBacked */
#define PG_swapcache   15   /* 交换缓存页标志 */

/* 判断是否为匿名页 */
static inline int PageAnon(struct page *page)
{
    return PageSwapBacked(page) && !PageSwapCache(page);
}

/* 判断是否为文件页 */
static inline int PageFileCache(struct page *page)
{
    return !PageSwapBacked(page);
}

当页面被加入LRU时,内核根据页面类型选择对应的链表组:

/* 内核源码:mm/swap.c 中 lru_cache_add 简化逻辑 */
void lru_cache_add(struct page *page)
{
    int lru = LRU_INACTIVE_ANON;  /* 默认匿名非活跃 */

    if (PageSwapBacked(page))
        lru = PageActive(page) ? LRU_ACTIVE_ANON : LRU_INACTIVE_ANON;
    else
        lru = PageActive(page) ? LRU_ACTIVE_FILE : LRU_INACTIVE_FILE;

    __lru_cache_add(page, lru);
}

3.6 双链LRU结构总览

下图展示了每个内存管理区(zone)内部的LRU链表组织结构,清晰呈现匿名页与文件页的分离管理:

每个内存管理区(zone)的LRU链表组织结构 匿名页 LRU 链表组 活跃匿名页链表(active_anon) 最近访问过的匿名页,优先保留 非活跃匿名页链表(inactive_anon) 候选回收的匿名页,需换出到swap 文件页 LRU 链表组 活跃文件页链表(active_file) 最近访问过的文件页,优先保留 非活跃文件页链表(inactive_file) 候选回收的文件页,干净页直接丢弃 swap换出(I/O代价高) 干净页:直接丢弃 脏页:写回文件后丢弃 内核回收决策:优先回收文件页 降低swap I/O,减少性能抖动 每个zone维护4条LRU链表:active_anon、inactive_anon、active_file、inactive_file 回收扫描从各非活跃链表的尾部开始,优先回收文件页

3.7 页面类型转换与特殊页面

某些页面在生命周期中会发生类型转换,内核需要动态调整其LRU归属:

  • 共享内存页(SHM):使用 tmpfs 或 shmfs 的共享内存页被标记为 PG_swapbacked,归入匿名页LRU链表,回收时需换出到swap。
  • 交换缓存页(SwapCache):当匿名页被换出到swap后,其物理页框可能仍保留在交换缓存中,此时页面同时具有匿名页和文件页特征,但内核将其归入匿名页LRU。
  • KSM合并页:Kernel Same-page Merging 合并后的页面被标记为匿名页,但多个进程共享同一物理页框,回收时需特殊处理。
内核设计哲学:页面类型分类的核心依据是“是否有后备存储”以及“是否需要swap”。文件页有明确的后备文件,干净页可零代价回收;匿名页无后备文件,必须依赖swap。这种分类使得LRU回收策略可以针对不同I/O代价做出最优选择。

3.8 回收优先级与水位线影响

内核在回收页面时,会根据当前内存水位线(watermark)和页面类型决定回收顺序:

  1. 正常水位:优先回收非活跃文件页的干净页,避免swap I/O。
  2. 低压水位:开始回收非活跃文件页的脏页(写回后回收),同时少量回收非活跃匿名页。
  3. 中压水位:加大匿名页回收力度,触发swap换出。
  4. 高压水位:全面回收所有类型页面,包括活跃链表页面,可能触发OOM killer。

这种分级回收策略确保了系统在内存压力较轻时保持高性能,在压力严重时仍能通过swap维持基本运行。