解析 ‘VFS Cache’ 的压力回收:当 Page Cache 占满内存时,内核如何决定先清理谁?

各位听众,下午好!

今天,我们将深入探讨Linux内核中一个既复杂又至关重要的主题:内存压力下的VFS缓存回收机制。当系统内存紧张,特别是当Page Cache占据了大量内存时,内核如何做出精明的决策,以确定哪些内存页或缓存对象应该被优先清理,以维持系统的稳定性和性能?这不仅仅是一个技术细节,更是理解Linux文件系统性能瓶颈和调优的关键。

我们将以一位编程专家的视角,剖析其底层原理、关键数据结构和核心算法,并通过代码示例来具象化这些抽象概念。

引言:内存管理的挑战与VFS缓存的压力

操作系统的一个核心职责是有效地管理有限的物理内存资源。在Linux中,文件系统相关的缓存占据了内存的相当大一部分,它们对于提升I/O性能至关重要。这些缓存主要分为两类:

  1. Page Cache(页缓存):这是最主要的文件数据缓存,存储着从磁盘读取的文件数据页以及即将写入磁盘的脏页。它的目的是减少对物理磁盘的访问,加速文件读写。
  2. VFS元数据缓存:这包括了Dentry Cache(目录项缓存)和Inode Cache(索引节点缓存)。它们缓存了文件系统的结构信息:
    • Dentry Cache (struct dentry):存储文件路径名与inode之间的映射关系。每次访问文件时,内核都会通过dentry cache来解析路径。
    • Inode Cache (struct inode):存储文件的元数据,如权限、所有者、大小、创建时间、修改时间以及指向数据块的指针等。

当系统内存充裕时,这些缓存可以尽情地增长,以提供最佳的性能。然而,一旦物理内存耗尽,系统就会面临巨大的压力。此时,内核必须启动内存回收(Reclaim)机制,来释放内存供新的内存分配请求使用。问题来了:面对庞大的Page Cache和同样重要的VFS元数据缓存,内核会如何权衡,决定“谁先被清理”?这个决策过程是动态的,并且受到多种因素的影响。

我们将从内存管理的基础开始,逐步深入到Page Cache和VFS元数据缓存的回收细节,最后揭示内核做出决策的内在逻辑。

第一章:Linux内存管理基础回顾

在深入缓存回收之前,我们有必要简要回顾一下Linux内存管理的一些基本概念。

1. 物理内存与虚拟内存

Linux采用虚拟内存管理。每个进程都有一个独立的虚拟地址空间,这个空间通过页表(Page Table)映射到物理内存。物理内存被划分为固定大小的内存页(通常是4KB)。内核管理的是这些物理内存页框(Page Frame)。

  • struct page:在内核中,每个物理内存页框都由一个struct page结构体来描述。这个结构体包含了页的状态(如是否脏、是否活跃)、引用计数、所属文件(如果它是文件页)、LRU列表节点等关键信息。

2. 内存区域与节点

为了更有效地管理内存,Linux将物理内存划分为不同的区域(Zone),例如ZONE_DMAZONE_DMA32ZONE_NORMAL。在NUMA(Non-Uniform Memory Access)架构下,内存还会被进一步划分为不同的节点(Node),每个节点有自己的内存区域集合。struct zone是描述一个内存区域的核心数据结构。

3. 内存回收的触发机制

内存回收可以由两种主要机制触发:

  • kswapd (后台回收):这是一个内核线程,在每个内存节点上运行(kswapd0, kswapd1等)。它会周期性地检查内存水位线(watermarks)。当空闲内存低于某个低水位线(min_free_pages)时,kswapd就会被唤醒,开始在后台回收内存,直到空闲内存达到高水位线(high_free_pages)。后台回收是非阻塞的,旨在提前释放内存,避免系统因内存不足而停顿。
  • Direct Reclaim (直接回收):当应用程序或内核代码尝试分配内存,但kswapd未能及时释放足够的内存,导致内存分配失败时,分配线程会暂停并直接进入内存回收流程。这称为直接回收。直接回收是阻塞的,会影响当前请求内存的进程的性能,因此应尽量避免。

无论是kswapd还是直接回收,它们的核心目标都是调用内存回收函数,如try_to_free_pages,来释放不再使用的物理内存页。

第二章:Page Cache的运作与回收机制

Page Cache是Linux文件I/O性能的基石。它缓存了文件系统中的文件数据。

1. Page Cache的本质

  • 文件数据页的缓存:当一个进程读取文件时,内核会将文件内容从磁盘加载到Page Cache中。后续对同一文件的读请求可以直接从内存中获取数据,无需再次访问磁盘。写入文件时,数据首先写入Page Cache,并被标记为“脏”,随后由内核异步写回磁盘。

  • struct page:核心数据结构:每个Page Cache中的页都是一个struct page实例。它包含了页的元数据,如:

    • flags:描述页的状态,如PG_dirty(脏页)、PG_active(活跃页)、PG_referenced(被引用过)等。
    • mapping:指向struct address_space,关联到哪个inode。
    • index:页在文件中的偏移量。
    • lru:用于将页连接到LRU列表的list_head
  • 文件支持页 (File-backed pages) 与匿名页 (Anonymous pages)

    • 文件支持页:对应于文件系统中的文件数据。它们在内存不足时,如果干净,可以直接丢弃;如果脏,则需要写回磁盘。
    • 匿名页:不与任何文件关联,通常是进程的堆栈、堆或者mmap映射的匿名内存。这些页在内存不足时通常需要被交换(swap out)到交换分区。Page Cache主要关注文件支持页。

2. LRU回收算法

Linux内核使用一种改进的LRU(Least Recently Used)算法来管理Page Cache的回收。它将文件支持页和匿名页都放在两个LRU列表中:

  • active_lru 列表:包含了最近频繁访问的内存页。这些页被认为是“热”的,不应轻易回收。
  • inactive_lru 列表:包含了最近较少访问的内存页。这些页被认为是“冷”的,是回收的优先目标。

页面在LRU列表间的移动逻辑

  1. 新分配或首次访问的页:通常被添加到inactive_lru列表的尾部。
  2. inactive_lru 中的页被访问:如果一个inactive_lru中的页被再次访问,它会被标记PG_referenced,并移动到active_lru列表的头部。
  3. active_lru 中的页老化kswapd或直接回收机制会周期性地扫描active_lru列表。如果一个active_lru中的页在扫描时发现PG_referenced标志未设置(即在上次扫描后未被访问),它会被移到inactive_lru列表的头部,并清除PG_referenced标志。如果该页在上次扫描后被访问过,则PG_referenced标志会被清除,但页仍保留在active_lru中。
  4. inactive_lru 中的页回收:当需要回收内存时,内核会从inactive_lru列表的尾部开始扫描,优先回收那些最久未被访问的页。

这个双LRU机制旨在防止“一次性”访问的文件数据(如视频流)污染active_lru列表,从而保护真正热点的数据。

3. Page Cache页面的状态

  • Clean Pages (干净页):页的内容与磁盘上的文件内容一致。如果这样的页被选中回收,它可以直接从内存中丢弃,无需写回磁盘。
  • Dirty Pages (脏页):页的内容已在内存中被修改,但尚未写回磁盘。这样的页在回收前必须被写回磁盘,这涉及到I/O操作,会增加回收的延迟和成本。

写回机制
内核有一套复杂的机制来管理脏页的写回:

  • bdi_writeback / Flusher threads:每个块设备都有一个bdi_writeback结构和一个对应的内核线程(如ext4-rsv-bdi-0),负责将脏页异步地写回磁盘。
  • 脏页限制:内核通过vm.dirty_background_bytes / vm.dirty_background_ratiovm.dirty_bytes / vm.dirty_ratio 等参数来控制系统中脏页的总量。当脏页达到背景阈值时,bdi_writeback线程会开始写回;当达到硬性阈值时,甚至会导致应用程序阻塞,直到脏页数量下降。

4. Page Cache回收流程(简化)

当内核需要回收Page Cache时,其简化流程如下:

  1. shrink_zone(struct zone *zone, struct scan_control *sc):这是区域回收的主入口。它会为指定内存区域执行回收。
  2. shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc):在区域内部,进一步细化到LRU向量(一个区域可以有多个LRU向量)。
  3. shrink_inactive_list(struct list_head *lru_list, struct scan_control *sc, bool file):这是实际扫描和回收inactive_lru列表的地方。
    • 函数会遍历inactive_lru列表的页。
    • 对于每个页:
      • 如果页被引用(PG_lru标志被设置且在active_lru中),或者被进程映射,则不能回收。
      • 如果页是脏的(PG_dirty),则尝试将其提交给写回机制。一旦写回完成,页的状态会变为干净。
      • 如果页是干净的,并且没有其他引用,则可以从Page Cache中移除并释放。
// 伪代码: 简化 Page Cache 回收的关键逻辑
long shrink_inactive_list(struct list_head *lru_list, struct scan_control *sc, bool file) {
    long freed_pages = 0;
    LIST_HEAD(pages_to_free); // 准备释放的页
    LIST_HEAD(pages_to_writeback); // 准备写回的脏页

    struct page *page;
    list_for_each_entry_reverse(page, lru_list, lru) { // 从 inactive LRU 尾部开始
        if (page_is_active(page)) { // 如果页又活跃了,移到 active LRU
            // move_page_to_active(page);
            continue;
        }

        if (PageDirty(page)) { // 脏页,需要写回
            // 尝试将页加入写回队列
            // add_page_to_writeback_queue(page);
            list_del(&page->lru);
            list_add(&page->lru, &pages_to_writeback);
            continue;
        }

        if (page_mapped(page) || page_has_private(page)) { // 页仍被映射或有私有数据
            // 清除 PG_referenced,给下次机会,但这次不回收
            ClearPageReferenced(page);
            continue;
        }

        // 干净且未被映射的页,可以释放
        list_del(&page->lru);
        list_add(&page->lru, &pages_to_free);
        freed_pages++;
    }

    // 执行写回操作
    // do_writeback(pages_to_writeback);

    // 释放干净的页
    // free_pages(pages_to_free);

    return freed_pages;
}

这个流程展示了内核在回收Page Cache时如何根据页的活跃度、脏状态和映射情况来做出决策。

第三章:VFS元数据缓存(Dentry与Inode Cache)的运作与回收

除了文件数据本身,文件系统的元数据也需要被高效缓存。Dentry Cache和Inode Cache就是为此而生。

1. VFS元数据缓存的作用

  • struct dentry (目录项缓存)

    • 作用:缓存文件路径名与对应inode之间的映射关系。例如,/home/user/file.txt 这个路径,dentry会缓存 //home/home/user/home/user/file.txt 这些路径组件与它们各自的inode。
    • 结构dentry结构体通常包含路径名、指向父dentry的指针、子dentry列表、指向其inode的指针以及引用计数等。
    • 性能影响:频繁的文件访问和目录遍历会极大地受益于Dentry Cache。
  • struct inode (索引节点缓存)

    • 作用:缓存文件或目录的所有元数据,包括文件类型、权限、所有者、组、大小、创建时间、修改时间、访问时间以及指向数据块的指针。每个文件或目录在文件系统上都有一个唯一的inode。
    • 结构inode结构体包含了文件系统相关的元数据,以及与Page Cache关联的address_space结构。
    • 性能影响:文件属性查询、文件读写操作都需要访问inode。
  • struct super_block (超级块)

    • 作用:缓存文件系统的全局信息,如文件系统类型、大小、块大小、inode数量等。每个挂载的文件系统都有一个超级块。
    • 回收:超级块通常在文件系统卸载时才会被释放,不属于常规内存回收的目标。

2. Dentry与Inode的生命周期

Dentry和Inode对象通常通过Slab分配器(现在更多是SLUB分配器)分配和管理。它们都有引用计数(d_count for dentry, i_count for inode)来跟踪其使用情况。

  • 引用计数:当一个dentry或inode被内核的某个部分(如一个打开的文件描述符、一个正在进行的路径查找)引用时,其引用计数会增加。只有当引用计数降到零时,该对象才可能被视为“未使用”并可以回收。
  • "Used" vs. "Unused" 状态
    • Used (in-use):引用计数大于0,或虽然引用计数为0但仍被添加到散列列表,可能很快被再次使用(这称为“负dentry”或“inode热身”)。这些对象不能被直接释放。
    • Unused (freeable):引用计数为0,且没有其他活跃引用。这些对象是回收的目标。

3. VFS元数据缓存的回收:Shrinker机制

Dentry和Inode缓存的回收与Page Cache的LRU机制有所不同。它们主要依赖于内核的Shrinker机制

  • struct shrinker:这是一个内核接口,允许各种内核子系统注册自己的回调函数,以便在内存压力下回收其私有缓存。

    // 简化后的 struct shrinker 定义
    struct shrinker {
        unsigned long (*count_objects)(struct shrinker *, struct shrink_control *); // 计算可回收对象数量
        unsigned long (*scan_objects)(struct shrinker *, struct shrink_control *);  // 扫描并回收对象
        int seeks; // 扫描的步长
        struct list_head list;
        // ... 其他字段
    };

    内核会为dentry_cacheinode_cache注册相应的shrinker

  • shrink_slab 函数:这是总体的slab缓存回收入口。当内存回收路径(如shrink_zone)被调用时,它会调用shrink_slab来尝试释放slab缓存中的对象。shrink_slab会遍历所有已注册的shrinker,并调用它们的count_objects方法来估计可回收的对象数量,然后调用scan_objects来实际回收。

  • Dentry Cache回收 (prune_dcache)

    • dentry_cache_shrinkerscan_objects函数最终会调用到prune_dcache
    • prune_dcache会遍历dentry的各个哈希链表,寻找那些引用计数为0的dentry。
    • 这些dentry会从哈希表中移除,并最终通过dput释放其内存。如果一个dentry的inode也是不活跃的,那么inode也可能随后被回收。
  • Inode Cache回收 (prune_icache)

    • inode_cache_shrinkerscan_objects函数最终会调用到prune_icache
    • prune_icache会扫描inode列表,查找那些引用计数为0的inode。
    • 与dentry类似,这些inode会被从inode表中移除,并通过iput释放。但是,一个inode的回收有一个额外的重要条件:如果该inode有任何脏页(inode->i_mapping->nrpages > 0 且包含脏页),它就不能被完全释放,因为它需要等待所有脏页写回磁盘。

4. vm.vfs_cache_pressure 参数的含义与影响

vm.vfs_cache_pressure 是一个sysctl参数,它直接影响内核回收Dentry Cache和Inode Cache的积极程度。

  • 默认值:100。
  • 含义:这个值是一个比例因子,用于调整VFS元数据缓存相对于其他slab缓存和Page Cache的回收优先级。
    • vm.vfs_cache_pressure > 100:内核会更积极地回收Dentry Cache和Inode Cache。这意味着在内存压力下,Dentry和Inode对象更容易被释放,从而减少Slab内存的使用。这可能会导致文件路径查找和元数据访问的缓存命中率下降,增加I/O。
    • vm.vfs_cache_pressure < 100:内核会更倾向于保留Dentry Cache和Inode Cache。这意味着在内存压力下,这些元数据缓存会更长时间地驻留在内存中。如果Page Cache中存在更容易回收的干净页,它们可能会优先被回收。这有助于保持文件系统元数据的缓存命中率,但可能导致Page Cache更早地被回收。

这个参数通过影响shrinkerseeks字段来发挥作用。在shrink_slab中,vfs_cache_pressure会与一个全局的per_cpu_pageset_high值相乘,从而决定在每次回收周期中扫描多少个dentry/inode对象。

// 伪代码: vm.vfs_cache_pressure 如何影响 shrinker 的扫描量
// 假设 'pressure' 是 vm_stat_item(NR_VFS_CACHE_PRESSURE) 的值
// 'nr_to_scan' 是 shrink_slab 传递进来的总扫描量
// 'shrinker->seeks' 是 shrinker 的扫描步长

// 在 shrink_slab 中,计算每个 shrinker 应该扫描多少对象时,会使用类似如下的逻辑:
long nr_to_scan_this_shrinker = nr_to_scan * (shrinker->seeks * pressure) / 100;

// 如果 pressure = 100 (默认),则 nr_to_scan_this_shrinker = nr_to_scan * shrinker->seeks
// 如果 pressure = 200,则 nr_to_scan_this_shrinker = nr_to_scan * shrinker->seeks * 2 (更积极)
// 如果 pressure = 50,则 nr_to_scan_this_shrinker = nr_to_scan * shrinker->seeks / 2 (不那么积极)

通过调整vm.vfs_cache_pressure,系统管理员可以在元数据缓存命中率和整体内存使用之间进行权衡。

第四章:内核如何决策:谁先被清理?

现在我们来到了问题的核心:当Page Cache占满内存时,内核如何决定先清理谁?这个决策并非简单地选择“只清理Page Cache”或“只清理Dentry Cache”,而是一个高度协调和动态的过程。

1. 统一的内存回收路径

Linux内核的内存回收机制是统一的。无论是kswapd还是直接回收,它们最终都会调用try_to_free_pages(或其内部调用的shrink_zone等函数)来释放内存。这些函数会尝试回收各种类型的内存,包括:

  • 匿名页(需要交换到磁盘)。
  • 文件支持页(Page Cache,脏页需要写回,干净页可直接丢弃)。
  • Slab缓存(包括Dentry Cache和Inode Cache)。

这意味着在一次内存回收周期中,内核会同时考虑并尝试回收这些不同类型的内存。

2. 回收优先级与启发式算法

内核在回收过程中使用一系列复杂的启发式算法来做出决策,核心原则是:优先回收那些最容易、成本最低、对系统性能影响最小的内存

  • LRU的绝对优先级:对于Page Cache和匿名页,LRU算法是决定回收优先级的核心。那些长时间未被访问(位于inactive_lru列表尾部,且PG_referenced标志未设置)的页是首要目标。
  • 页面类型的偏好
    • 干净的文件支持页:这是最容易回收的。它们可以直接丢弃,无需I/O,对系统负载影响最小。因此,在内存压力下,这类页通常是优先回收的对象。
    • 脏页:无论是文件支持脏页还是匿名脏页(需要写到交换分区),它们都需要进行I/O操作才能被释放。这增加了回收的成本和时间。内核会尽量将脏页提交给后台写回机制,然后等待写回完成。如果内存压力极其严重,甚至可能阻塞进程等待写回。
    • 匿名页:通常需要交换到磁盘。交换I/O的成本相对较高,但对于释放进程私有内存至关重要。
  • Slab缓存的回收shrink_slab函数会被shrink_zone调用,负责回收包括dentry/inode在内的slab对象。
    • Dentry/Inode的“未使用”状态:只有当dentry或inode的引用计数为0,并且没有其他活跃引用时,它们才会被视为可回收。内核会优先回收这些真正“不活跃”的元数据对象。
    • 回收成本:回收一个不活跃的dentry或inode通常是低成本的,因为它只是释放一块slab内存。
  • VFS元数据与Page Cache的互锁
    • Dentry/Inode阻止Page Cache回收:如果一个文件或目录的dentry或inode仍然被引用(即处于“in-use”状态),那么与该文件关联的Page Cache页面可能无法被完全释放,或者至少其元数据页不能被释放。虽然文件数据页可以独立于dentry/inode被回收,但如果文件本身还在使用,其元数据缓存的清除会更谨慎。
    • Page Cache脏页阻止Inode回收:一个inode只有在它没有任何关联的脏页时才能被完全释放。如果一个inode仍有脏页尚未写回磁盘,那么即使它的引用计数为0,它也只能被标记为“待释放”,而不能真正从内存中移除。这是一种重要的依赖关系。
  • vm.vfs_cache_pressure 的作用:这个参数提供了管理员对VFS元数据缓存回收积极性的直接控制。
    • 高值:如果vm.vfs_cache_pressure很高,内核会更积极地回收Dentry和Inode对象。这意味着,即使Page Cache中存在一些活跃度相对较低的页,内核也可能优先清理VFS元数据缓存,以腾出slab内存。这可能导致缓存命中率下降,但能更快地释放出小对象内存。
    • 低值:如果vm.vfs_cache_pressure很低,内核会更倾向于保留Dentry和Inode对象。在这种情况下,当内存不足时,内核会更优先考虑回收Page Cache中的文件数据页(尤其是干净页),或者交换出匿名页,以满足内存需求,即使这意味着保留了大量不活跃的VFS元数据。

3. 内存压力下的回收协调

在内存压力下,kswapd或直接回收线程会执行balance_pgdat -> shrink_zoneshrink_zone函数内部会包含对不同内存类型的回收调用,以实现平衡:

// 伪代码: 简化 shrink_zone 的核心流程
long shrink_zone(struct zone *zone, struct scan_control *sc) {
    long freed_pages = 0;
    long nr_to_scan = sc->nr_to_scan; // 本次回收周期内计划扫描的页数

    // 1. 尝试回收匿名页 (Anonymous pages)
    // 从 inactive_anon_lru 列表的尾部开始扫描,如果页可以交换,则交换出去。
    // 这可能涉及到 I/O (swap out)。
    freed_pages += shrink_anon_lru(zone, sc);

    // 2. 尝试回收文件支持页 (File-backed pages, Page Cache)
    // 从 inactive_file_lru 列表的尾部开始扫描。
    // 优先回收干净页,脏页提交写回。
    freed_pages += shrink_file_lru(zone, sc);

    // 3. 尝试回收 Slab 缓存 (包括 dentry/inode)
    // 这会遍历所有注册的 shrinker,包括 dentry_cache_shrinker 和 inode_cache_shrinker。
    // nr_to_scan 参数会根据 vm.vfs_cache_pressure 等因素调整给每个 shrinker。
    freed_pages += shrink_slab(nr_to_scan, zone->node, sc->gfp_mask);

    // 循环执行上述步骤,直到达到回收目标或没有更多可回收的内存
    // ...
    return freed_pages;
}

从上述伪代码可以看出,shrink_zone在一次回收循环中会尝试回收所有类型的内存。它不是简单地“先清理Page Cache”或“先清理Dentry Cache”,而是在一个统一的框架下,根据各自的回收算法(LRU、Shrinker)和当前的状态(活跃度、脏状态、引用计数),并行地进行回收。具体的“谁先被清理”取决于:

  • 系统中哪种类型的内存有更多的“冷”或“未使用”对象。
  • 回收哪种类型内存的成本最低。
  • vm.vfs_cache_pressure 等参数如何调整了不同缓存的回收积极性。

通常情况下,如果存在大量长时间未访问的干净Page Cache页,它们会是首选的回收目标,因为回收成本最低。如果Page Cache中多是活跃页或脏页,那么内核可能会转向回收不活跃的Dentry/Inode,或者交换出匿名页。这是一个动态平衡的过程。

表格:不同缓存的回收特性对比

缓存类型 主要内容 回收机制 关键影响因素 回收成本 vm.vfs_cache_pressure 影响
Page Cache 文件数据、匿名内存页 LRU (active/inactive lists) 访问时间、脏页状态、引用计数、映射关系 低 (干净页) / 高 (脏页需写回) / 中 (匿名页需交换) 间接:如果VFS元数据保留,其关联的Page Cache也可能保留久一些
Dentry Cache 文件路径与inode映射 Slab Shrinker (prune_dcache) 引用计数、是否被使用 低 (未被使用的可直接释放) 直接:数值越高,回收越积极
Inode Cache 文件/目录元数据 Slab Shrinker (prune_icache) 引用计数、是否关联脏页、是否被使用 低 (未被使用的可直接释放) / 高 (关联脏页) 直接:数值越高,回收越积极
Anonymous Pages 进程堆栈、堆、匿名映射 LRU (active/inactive lists) 访问时间、是否可交换 中 (需交换到磁盘)

第五章:调试与优化

理解了回收机制,我们就可以在实际环境中进行观察和调优。

1. 观察内存使用情况

  • free -h

    $ free -h
                  total        used        free      shared  buff/cache   available
    Mem:           7.8G        4.0G        1.2G        500M        2.6G        3.0G
    Swap:          2.0G          0B        2.0G
    • buff/cache:显示Page Cache和Buffers的总量。
    • Slab:在/proc/meminfo中可以看到Slab的详细使用,包括SReclaimable(可回收的slab,如dentry/inode cache)和SUnreclaim(不可回收的slab)。
  • /proc/meminfo:提供更详细的内存信息。

    MemTotal:        8092240 kB
    MemFree:         1200000 kB
    Buffers:          150000 kB
    Cached:          2400000 kB  <-- Page Cache
    SwapCached:            0 kB
    Active:          3500000 kB
    Inactive:        1500000 kB
    Active(anon):    2000000 kB
    Inactive(anon):   500000 kB
    Active(file):    1500000 kB
    Inactive(file):  1000000 kB
    Slab:             800000 kB  <-- Slab 总量
    SReclaimable:     600000 kB  <-- 可回收的 Slab (Dentry/Inode cache大部分在这里)
    SUnreclaim:       200000 kB  <-- 不可回收的 Slab
    VmallocTotal:   34359738367 kB
    VmallocUsed:      210000 kB
    VmallocChunk:   34359526975 kB
    ...

    关注Cached (Page Cache)、SlabSReclaimable等指标。SReclaimable是Dentry Cache和Inode Cache的主要组成部分,如果这个值很高,说明系统中存在大量可回收的VFS元数据缓存。

2. slabtop:查看slab缓存的详细使用情况

slabtop工具可以实时显示各种slab缓存(包括dentryinode_cache)的使用情况、对象数量、内存占用和可回收性。

$ slabtop -s c
Active / Total Objects (% used)    : 1673895 / 1735118 (96.5%)
Active / Total Slabs (% used)      : 23158 / 23158 (100.0%)
Active / Total Caches (% used)     : 89 / 138 (64.5%)
Active / Total Size (% used)       : 649692.64K / 685375.40K (94.8%)
Minimum / Average / Maximum Object Size: 0.01K / 0.40K / 8192.00K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
1048576 1048576 100%    0.19K  26214       4096   1024.0M dentry
 196608  196608 100%    0.50K   6144         32    256.0M inode_cache
  ...

slabtop中,我们可以清晰地看到dentryinode_cache占用的内存大小、对象数量。如果dentryinode_cacheACTIVE接近OBJS,且占用内存巨大,同时SReclaimable也很高,那么可能意味着这些元数据缓存正在消耗大量内存。

3. vmstat:观察内存交换和I/O情况

vmstat可以提供实时的系统活动报告,包括内存(swpdfreebuffcache)、交换(siso)、I/O(bibo)等。

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0      0 1200000 150000 2400000    0    0     0     0  100  200  1  1 98  0  0
 0  0      0 1100000 150000 2500000    0    0     0     0  120  250  2  2 96  0  0
...

如果si(swap in)和so(swap out)值很高,说明系统正在频繁地进行内存交换,这通常是内存紧张的信号。如果bi(blocks in)和bo(blocks out)也很高,可能与脏页写回或文件I/O有关。

4. 调整 vm.vfs_cache_pressure

如果你发现VFS元数据缓存(dentryinode_cache)占用了过多的SReclaimable内存,而你希望系统更积极地回收它们,可以尝试调高vm.vfs_cache_pressure

  • 查看当前值

    sysctl vm.vfs_cache_pressure
    # vm.vfs_cache_pressure = 100
  • 临时修改

    sudo sysctl -w vm.vfs_cache_pressure=200

    这会使内核以两倍的积极性回收VFS元数据缓存。观察系统行为,特别是slabtop中的dentryinode_cache大小以及free输出中的SReclaimable

  • 永久修改:将 vm.vfs_cache_pressure = 200 添加到 /etc/sysctl.conf 文件中,然后运行 sudo sysctl -p

注意事项

  • 不宜过高:过高的vfs_cache_pressure值会导致VFS元数据缓存频繁被回收,降低缓存命中率,从而增加文件路径查找和元数据访问的I/O开销,可能反过来影响性能。
  • 不宜过低:过低的vfs_cache_pressure值(如0)意味着内核几乎不回收VFS元数据缓存,这可能导致这些缓存无限增长,最终挤占其他重要内存,甚至引发OOM(Out Of Memory)。
  • 权衡:最佳值取决于具体的工作负载。通常,默认值100是一个合理的起点。只有在明确观察到问题且理解其影响时才进行调整。

5. 其他相关参数

  • vm.dirty_background_bytes / vm.dirty_background_ratio:控制后台写回脏页的阈值。
  • vm.dirty_bytes / vm.dirty_ratio:控制阻塞进程写回脏页的硬性阈值。
  • vm.swappiness:影响内核回收匿名页(倾向于交换出)和文件页(倾向于丢弃)的倾向性。高值倾向于交换匿名页,低值倾向于保留匿名页而回收文件页。

持续演进的内存管理

Linux内核的内存管理是一个持续演进的领域。随着硬件和应用场景的变化,新的算法和机制不断被引入。例如:

  • Cgroup v2 的内存管理:提供了更精细的内存资源隔离和控制,允许为不同的工作负载设置独立的内存回收策略。
  • DAMON (Data Access MONitor):一种新的数据访问监控机制,旨在更准确地识别内存页的冷热程度,从而优化LRU回收效率。

这些新特性都在努力使内核在内存资源有限时,能够做出更智能、更高效的回收决策。

理解Linux内核如何管理和回收VFS缓存,是每个系统管理员和开发者优化系统性能,特别是在I/O密集型或内存受限环境中,不可或缺的知识。通过深入学习其内部机制,并结合实际的系统监控工具,我们就能更好地诊断和解决性能问题,确保系统运行的流畅与稳定。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注