[toc]

fs/drop_caches.c 内核缓存手动回收(Manual Kernel Cache Reclaiming) 提供清空页面、目录和inode缓存的接口

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了给系统管理员、性能测试工程师和内核开发者提供一个手动、强制性地清空内核主要缓存的手段,以解决以下特定问题:

  • 可重复的性能基准测试:在进行磁盘I/O或文件系统性能测试时,上一次运行的结果会“预热”内核缓存(Page Cache)。这导致下一次运行会直接从高速的内存中读取数据,而不是从慢速的磁盘读取,从而使得测试结果不准确,无法反映真实的“冷启动”性能。drop_caches 允许在每次测试前清空缓存,确保测试环境的一致性。
  • 模拟内存压力:开发者需要测试应用程序或内核本身在内存资源紧张时的行为。通过手动清空缓存,可以快速回收大量内存,从而模拟出系统突然面临内存压力的场景。
  • 诊断特定的内存问题:在极少数情况下,内核的slab分配器可能出现严重的内存碎片问题,或者某些驱动程序存在内存泄漏,导致缓存无法被正常回收。drop_caches 可以作为一种临时的、诊断性的工具,强制回收内存,观察系统行为的变化。

它并非设计为一个常规的系统维护工具,而是一个用于调试和测试的“后门”。

它的发展经历了哪些重要的里程碑或版本迭代?

drop_caches 的实现相对简单且稳定,其核心功能自引入以来没有发生根本性的变化。它的演进主要体现在:

  • 功能的引入:它作为sysctl接口的一部分被添加到内核中,路径为 /proc/sys/vm/drop_caches,提供了一个简单的、通过文件系统写入来触发内核操作的机制。
  • 参数的定义:定义了写入不同整数值(1, 2, 3)代表不同操作的规范,使其具有一定的灵活性,可以选择性地清空不同类型的缓存。
  • 安全性的增强:代码实现中强调只丢弃“干净的”(clean)缓存页和未被使用的对象。它不会丢弃“脏的”(dirty)数据,防止数据丢失。用户必须先运行sync命令将脏数据写回磁盘。

目前该技术的社区活跃度和主流应用情况如何?

fs/drop_caches.c 是一个非常稳定且成熟的内核组件,其代码极少变动。它在系统管理和运维领域是一个广为人知的功能,但也是一个极易被误用的功能。

  • 主流应用:主要用于性能测试自动化脚本、数据库性能调优前的环境准备,以及在特定场景下(如KVM宿主机上)为虚拟机回收内存。
  • 社区态度:内核社区和有经验的系统管理员普遍强调,不应在生产环境中以定时任务(cron job)的方式定期运行drop_caches。这种做法与Linux内核的内存管理哲学背道而驰,通常会降低而不是提升系统性能。

核心原理与设计

它的核心工作原理是什么?

fs/drop_caches.c 的核心是一个注册到sysctl框架的写处理函数 (drop_caches_sysctl_handler)。当用户空间程序(如echo命令)向 /proc/sys/vm/drop_caches 文件写入一个值时,该函数被触发。

其工作流程如下:

  1. 用户写入:用户执行 echo "N" > /proc/sys/vm/drop_caches
  2. Sysctl处理:内核的VFS层将写操作路由到procfs,最终调用drop_caches_sysctl_handler函数,并将写入的数字N作为参数。
  3. 解析参数:函数解析这个整数N
    • 如果N包含1 (即值为1或3): 它会调用iterate_supers()函数遍历系统中所有已挂载的文件系统,并对每个文件系统的super_block调用drop_pagecache_sb()。这个函数会进一步遍历与该文件系统相关的所有inode,并调用invalidate_mapping_pages()来释放这些inode关联的Page Cache中的干净页面。
    • 如果N包含2 (即值为2或3): 它会调用iterate_supers(),并对每个super_block调用shrink_dcache_sb()来收缩目录项缓存(dentry cache)。同时,它也会调用shrink_icache_sb()来收缩inode缓存。这些操作会释放未被使用的dentry和inode对象,归还内存给slab分配器。
  4. 操作隔离:这些操作只针对干净、可回收的对象。脏页、被锁定的页或正在被使用的dentry/inode对象不会被释放,保证了系统的稳定性和数据的一致性。

它的主要优势体現在哪些方面?

  • 简单直接:提供了一个非常简单明了的接口来执行一个复杂的内核操作。
  • 控制力强:允许用户在需要时立即、强制性地回收内存,为测试和诊断提供了极大的便利。
  • 相对安全:设计上避免了丢弃脏数据,降低了误操作导致数据丢失的风险。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能“悬崖”:执行drop_caches会立即导致系统性能急剧下降。因为所有后续的文件访问都必须从慢速的磁盘重新读取,系统会经历一个高I/O负载的“预热”期,直到常用数据被重新缓存。
  • “治标不治本”:如果一个系统需要频繁地手动清空缓存来维持运行,这通常表明存在更深层次的问题,如内存不足(RAM太小)、应用程序存在内存泄漏或配置不当。drop_caches只是掩盖了症状,而不是解决根本原因。
  • 与内核设计冲突:“缓存是用来被使用的”,Linux内核会尽可能地利用空闲内存作为缓存来提升性能。手动清空缓存是在和内核高效的内存管理器“对着干”。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 磁盘I/O基准测试:在运行fio, dd, iozone等工具测试磁盘或文件系统吞吐量和延迟之前,执行sync; echo 3 > /proc/sys/vm/drop_caches,可以确保测试结果不受Page Cache的影响,测量的是真实的硬件性能。
  • 应用程序冷启动性能分析:想要分析一个数据库或Web服务器在重启后第一次加载数据时的性能,可以通过清空缓存来模拟一个“冷”的系统环境,而无需真的重启整个操作系统。
  • 作为极端情况下的应急手段:在极少数情况下,如果系统由于内核bug或严重的内存碎片导致无法自动回收内存而濒临崩溃,手动执行drop_caches可能可以释放出足够的内存来执行诊断命令或正常关机,避免硬重启。

是否有不推荐使用该技术的场景?为什么?

  • 在生产服务器上定时执行:这是最经典的反模式。定时清空缓存会导致周期性的性能抖动,服务器需要反复从磁盘加载热点数据,整体性能会下降。正确的做法是让内核的LRU(Least Recently Used)算法来自动换出冷数据。
  • 为了让free -m命令中的free字段看起来更大:Linux中,available(可用内存)比free(空闲内存)更有意义。大量的cached内存是系统健康的标志,说明系统正在高效地利用内存。为了虚高的free值而牺牲性能是完全没有必要的。

对比分析

请将其 与 其他相似技术 进行详细对比。

drop_caches vs. 内核自动内存回收 (kswapd/直接回收)

特性 drop_caches (手动) 内核自动回收 (Automatic)
触发机制 用户通过sysctl接口显式、主动触发。 内核根据内存水位线(watermarks),由kswapd内核线程在后台自动或在内存分配失败时**同步地(直接回收)**触发。
回收策略 “野蛮”/非选择性:丢弃所有文件系统中的所有干净缓存。 “智能”/选择性:基于LRU(最近最少使用)等算法,优先回收那些最长时间未被访问的“冷”页面,力求保留“热”数据。
执行时机 立即执行。 持续、渐进地在后台执行,或在需要时才执行。
对性能的影响 导致剧烈的、全局性的性能下降。 平滑地管理内存,旨在维持系统整体性能的稳定。
主要目的 用于测试、诊断和极端情况下的应急。 作为操作系统正常运行的核心内存管理机制。

drop_caches vs. sync 命令

这两者经常被一起使用,但功能完全不同,理解其区别至关重要。

特性 echo N > /proc/sys/vm/drop_caches sync
数据流向 从内存中丢弃 (Discard)。不涉及磁盘写入。 从内存写入到磁盘 (Write-back)
操作对象 干净的 (Clean) 缓存数据。 脏的 (Dirty) 缓存数据(已被修改但尚未写入磁盘)。
操作结果 缓存被清空,内存被释放。 缓存内容与磁盘内容同步,数据持久化。缓存中的数据(现在是干净的)仍然保留在内存中。
常用组合 sync; echo 3 > ... 这是安全的操作顺序:首先确保所有修改都已安全写入磁盘,然后才能安全地丢弃缓存。 sync 本身是一个独立的数据一致性保障命令。

/proc/sys/vm/drop_caches 实现:手动回收内核缓存内存

本代码片段实现了Linux内核中一个著名的、供系统管理员使用的调优接口:/proc/sys/vm/drop_caches。其核心功能是提供一个手动触发机制,用于强制内核丢弃干净的(clean)页缓存(Page Cache)、dentry缓存和inode缓存(它们都属于Slab缓存)。这通常用于在特定的性能测试场景下创建一个可复现的、干净的内存环境,或者在某些特殊情况下,当管理员确信缓存中的数据不再需要时,用于立即回收内存。

实现原理分析

该功能的实现基于内核的sysctl框架,通过一个自定义的处理函数来响应用户空间的写操作。

  1. Sysctl 接口注册 (init_vm_drop_caches_sysctls):

    • 通过register_sysctl_init/proc/sys/vm/目录下创建了一个名为drop_caches的文件。
    • 该文件的权限被设置为0200(仅所有者可写),意味着只有root用户才能触发此操作,并且该文件不可读(因为读取一个触发器没有意义)。
    • 关键在于.proc_handler指向了自定义的drop_caches_sysctl_handler函数,而不是标准的整数处理函数。
  2. 写操作处理 (drop_caches_sysctl_handler):

    • 当用户执行echo N > /proc/sys/vm/drop_caches时,这个处理函数被调用。
    • 它首先使用proc_dointvec_minmax来安全地将用户写入的整数值存入全局变量sysctl_drop_caches
    • 位掩码逻辑: 函数的核心是检查sysctl_drop_caches变量的特定比特位:
      • Bit 0 (& 1): 如果设置为1(用户写入1或3),则触发页缓存的清理。它首先调用lru_add_drain_all()来处理per-CPU的页面列表,然后通过iterate_supers(drop_pagecache_sb, NULL)来遍历所有已挂载的文件系统,并对每个文件系统执行drop_pagecache_sb
      • Bit 1 (& 2): 如果设置为2(用户写入2或3),则触发Slab缓存的清理,通过调用drop_slab()来收缩dentry和inode等对象的缓存。
      • Bit 2 (& 4): 如果设置为4,则会设置一个静态变量stfu(”Shut The F* Up”的缩写),用于在后续的清理操作中抑制内核日志的打印。
  3. 页缓存清理工作函数 (drop_pagecache_sb):

    • 这是实际执行页缓存清理的核心。它遍历指定文件系统(super_block)中的所有inode
    • 安全性: 它会跳过正在被创建或销毁的inode(检查i_state),以避免竞态条件。
    • 引用计数: 在处理一个inode之前,它必须通过__iget()增加其引用计数。这是因为后续操作需要临时释放保护inode链表的自旋锁,__iget确保了即使在锁释放期间,inode对象也不会被其他进程销毁。
    • 锁释放与抢占: 这是一个经典的内核长循环模式。它在循环中会释放自旋锁 (spin_unlock(&sb->s_inode_list_lock)),然后执行耗时操作,之后再重新获取锁。这样做是为了避免长时间持有自旋锁。同时,它会调用cond_resched(),主动让出CPU,防止因遍历大量文件而导致系统无响应或触发软锁死(soft lockup)。
    • 核心操作: invalidate_mapping_pages(inode->i_mapping, 0, -1)是最终的执行者。它会找到与该inode关联的所有页缓存页面,并将它们从LRU链表中移除。如果页面是脏的(dirty),它会先被写回到磁盘。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互与MMU

此功能是纯粹的内存管理软件逻辑,它操作的对象是内核在RAM中维护的数据结构(页、inode、dentry)。它与具体的硬件外设无关,也完全独立于MMU的存在。

单核环境影响

  • cond_resched() 的重要性: 在单核的STM32H750上,cond_resched() 依然至关重要。虽然没有其他CPU核心可以并行执行,但系统上可能运行着多个任务。如果drop_pagecache_sb在一个包含成千上万个文件的文件系统上运行,而没有cond_resched(),这个操作将会独占CPU很长时间,导致其他所有任务(包括高优先级的实时任务、网络栈等)都无法运行,从而“冻结”整个系统。
  • 锁机制: 自旋锁(spin_lock)在单核可抢占内核上会退化为禁止本地中断,这仍然是保护数据结构免受中断上下文干扰的正确方式。

实际意义与应用场景

在像STM32H750这样内存资源极其宝贵的嵌入式平台上,drop_caches功能比在大型服务器上更为实用和直接

  • 确定性内存控制: 嵌入式系统常常需要可预测的、确定性的行为。在一个关键任务执行之前,应用程序或启动脚本可以通过echo 1 > /proc/sys/vm/drop_caches主动回收由前一个任务(例如,解压固件更新包)所产生的、现在已无用的页缓存,从而为关键任务确保一个已知的、最大的可用内存量。
  • 避免内存不足: 在一个长时间运行的设备上,即使没有内存泄漏,页缓存也可能会逐渐增长,挤占应用程序可用的内存。虽然内核会在内存压力下自动回收,但在某些场景下,开发者可能希望在系统进入低功耗模式或空闲时,主动进行一次“大扫除”,而不是被动等待。
  • 调试: 当怀疑系统因缓存问题而行为异常时,drop_caches是一个简单有效的诊断工具,可以用来判断问题是否与缓存状态有关。

结论drop_caches为STM32H750平台上的开发者提供了一个强有力的、用于显式内存管理的工具,这对于在资源受限的环境中构建稳定、可预测的系统非常有价值。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// drop_pagecache_sb: 针对单个文件系统,丢弃其所有inode的页缓存。
static void drop_pagecache_sb(struct super_block *sb, void *unused)
{
struct inode *inode, *toput_inode = NULL;

// 获取保护inode链表的锁。
spin_lock(&sb->s_inode_list_lock);
// 遍历该文件系统的所有inode。
list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
spin_lock(&inode->i_lock);
// 安全性检查:跳过正在被创建/销毁的inode。
if ((inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) ||
// 优化:如果inode没有缓存页,并且系统不需要调度,则跳过。
(mapping_empty(inode->i_mapping) && !need_resched())) {
spin_unlock(&inode->i_lock);
continue;
}
// 增加inode引用计数,防止它在后续锁释放期间被销毁。
__iget(inode);
spin_unlock(&inode->i_lock);
// 释放inode链表锁,以执行耗时操作。
spin_unlock(&sb->s_inode_list_lock);

// 核心操作:使该inode的所有页缓存无效。
invalidate_mapping_pages(inode->i_mapping, 0, -1);
// 释放上一个循环中持有的inode引用。
iput(toput_inode);
toput_inode = inode;

// 主动让出CPU,防止系统无响应。
cond_resched();
// 重新获取锁,继续下一个循环。
spin_lock(&sb->s_inode_list_lock);
}
spin_unlock(&sb->s_inode_list_lock);
// 释放最后一个持有的inode引用。
iput(toput_inode);
}

// drop_caches_sysctl_handler: /proc/sys/vm/drop_caches的自定义处理函数。
static int drop_caches_sysctl_handler(const struct ctl_table *table, int write,
void *buffer, size_t *length, loff_t *ppos)
{
int ret;

// 使用标准函数处理整数写入和验证。
ret = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (ret)
return ret;
// 只在写操作时执行。
if (write) {
// Bit 0: 清理页缓存。
if (sysctl_drop_caches & 1) {
lru_add_drain_all();
iterate_supers(drop_pagecache_sb, NULL);
count_vm_event(DROP_PAGECACHE);
}
// Bit 1: 清理slab缓存(dentries, inodes等)。
if (sysctl_drop_caches & 2) {
drop_slab();
count_vm_event(DROP_SLAB);
}
// ... (日志和stfu标志处理) ...
}
return 0;
}

// drop_caches_table: 定义sysctl表项。
static const struct ctl_table drop_caches_table[] = {
{
.procname = "drop_caches",
.data = &sysctl_drop_caches,
.maxlen = sizeof(int),
.mode = 0200, // 只写权限
.proc_handler = drop_caches_sysctl_handler,
// ... (范围限制) ...
},
};

// init_vm_drop_caches_sysctls: 初始化函数。
static int __init init_vm_drop_caches_sysctls(void)
{
// 将sysctl表注册到"vm"目录下。
register_sysctl_init("vm", drop_caches_table);
return 0;
}
// 在文件系统初始化阶段调用。
fs_initcall(init_vm_drop_caches_sysctls);