[Linux]学习笔记系列 -- mm/shrinker.c 内核缓存收缩器(Kernel Cache Shrinker) 响应内存压力的回调机制
文章目录
- mm/shrinker.c 内核缓存收缩器(Kernel Cache Shrinker) 响应内存压力的回调机制
- 历史与背景
- 这项技术是为了解决什么特定问题而诞生的?
- 它的发展经历了哪些重要的里程碑或版本迭代?
- 目前该技术的社区活跃度和主流应用情况如何?
- 核心原理与设计
- 它的核心工作原理是什么?
- 它的主要优势体现在哪些方面?
- 它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 使用场景
- 在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
- 是否有不推荐使用该技术的场景?为什么?
- 对比分析
- 请将其 与 其他相似技术 进行详细对比。
- mm/shrinker.c
- shrinker_alloc
- shrinker_register 将 shrinker 结构体注册到内核的 shrinker 子系统中

https://github.com/wdfk-prog/linux-study
mm/shrinker.c 内核缓存收缩器(Kernel Cache Shrinker) 响应内存压力的回调机制
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术以及其实现的“收缩器”(Shrinker)框架,是为了解决Linux内核中一个根本性的资源管理问题:如何在一个统一的框架下,回收由各种不同内核子系统所占用的内存缓存。
- 内核缓存的多样性:Linux内核不仅仅有用于缓存文件数据的页面缓存(Page Cache)。还有许多其他重要的缓存,例如用于加速路径查找的dcache、用于缓存文件元数据的inode缓存,以及Slab/Slub分配器自身为内核对象维护的缓存等。
- 缺乏统一回收接口:当系统物理内存(RAM)不足时,内存管理(MM)子系统需要释放一些内存。对于页面缓存,它有自己复杂的LRU(最近最少使用)算法来回收。但对于dcache、inode cache等“非页面缓存”的内存,MM子系统本身并不知道它们的内部结构,也不知道哪些对象是“可回收的”(例如,一个未被使用的dentry)。
- 解耦的需求:在没有Shrinker框架的情况下,内存管理代码将不得不硬编码对dcache、inode cache等特定子系统的了解,以便调用它们的清理函数。这会造成紧密的耦合,使得代码难以维护,并且无法支持新的内核子系统添加自己的缓存回收逻辑。
Shrinker框架的诞生就是为了解决这个问题。它提供了一个通用的、基于回调的“插件”机制。任何内核子系统只要实现了自己的缓存,就可以向MM子系统注册一个shrinker,从而参与到全局的内存回收过程中。
它的发展经历了哪些重要的里程碑或版本迭代?
Shrinker机制是随着Linux内存管理系统的成熟而逐步完善的。
- 早期概念:内核很早就有了回收dentry和inode缓存的机制,这可以看作是Shrinker思想的雏形。
- 标准化接口:
mm/shrinker.c
的出现,将这个过程标准化为一个正式的框架。register_shrinker()
和unregister_shrinker()
API的确立,使得任何子系统都可以动态地加入或退出内存回收体系。 - 引入两阶段回调 (
count
/scan
):早期的实现可能只有一个简单的“shrink”回调。现代的Shrinker框架演进为一个更智能的两阶段过程:count_objects
:一个快速的、通常无锁的回调,用于估算可回收对象的数量。scan_objects
:一个较重的回调,负责实际扫描和释放对象。
这种分离使得MM子系统可以先“探查”所有缓存的“胖瘦”,然后根据内存压力和每个缓存的可回收对象数量,按比例地、公平地分配回收任务。
- NUMA和Cgroup感知:为了适应现代多节点(NUMA)服务器和容器化(cgroups)环境,Shrinker框架被扩展为NUMA和内存cgroup感知的。这意味着内存回收可以更具针对性,只在内存压力高的NUMA节点或cgroup内部触发其关联的shrinker,提高了回收的效率和隔离性。
目前该技术的社区活跃度和主流应用情况如何?
Shrinker是Linux内存管理中一个基础、稳定且至关重要的组成部分。
- 社区活跃度:其核心框架非常稳定。相关的社区活动主要集中在:1) 新的内核子系统(如新的网络协议栈或驱动)注册自己的shrinker;2) 对现有shrinker(特别是VFS的)进行性能调优,使其在
scan_objects
阶段更高效。 - 主流应用:它是所有主要内核缓存进行自我清理的标准方式。
- VFS:最重要的使用者。
fs/dcache.c
和fs/inode.c
都注册了shrinker,用于在内存压力下修剪未使用的dentry和inode对象。 - Slab/Slub分配器:Slab分配器自身也注册了shrinker,用于将完全空闲的slab页面释放回页分配器。
- XArray/Radix Tree:这些内核中广泛使用的数据结构,也提供了shrinker来回收其内部节点所占用的内存。
- VFS:最重要的使用者。
核心原理与设计
它的核心工作原理是什么?
mm/shrinker.c
的核心是一个“发布-订阅”模型,MM子系统是发布者(发布内存压力事件),各种内核缓存是订阅者(订阅并响应事件)。
- 注册(Subscription):一个希望参与内存回收的内核子系统(如VFS)会创建一个
struct shrinker
对象。这个结构体中最关键的是两个函数指针:count_objects
和scan_objects
。然后,该子系统调用register_shrinker()
将这个对象注册到一个全局的shrinker链表中。 - 触发(Publication):当系统内存不足时,无论是后台的
kswapd
内核线程被唤醒,还是某个进程因分配内存失败而触发“直接回收”(direct reclaim),它们最终都会调用到shrink_slab()
函数。 - 协同回收过程:
shrink_slab()
会遍历所有已注册的shrinker
对象,并对每个对象执行以下两步操作:- 第一步:计数(Count):调用该shrinker的
count_objects
回调函数。这个函数需要快速地(通常是原子地读取一个计数器)返回该缓存中当前可回收对象的估算数量。例如,dcache的count
函数会返回LRU链表中未使用dentry的数量。MM子系统会汇总所有shrinker的计数值,从而了解当前整个内核缓存的可回收潜力。 - 第二步:扫描(Scan):根据当前的内存压力和第一步中得到的计数值,MM子系统会计算出一个需要该shrinker释放的对象数量(
nr_to_scan
)。然后,它调用该shrinker的scan_objects
回调函数,并传入这个数量。scan_objects
函数则负责执行实际的清理工作,例如,遍历LRU链表,释放最多nr_to_scan
个未使用的对象。最后,它返回实际释放的对象数量。
- 第一步:计数(Count):调用该shrinker的
它的主要优势体现在哪些方面?
- 模块化与解耦:MM子系统无需知道任何关于dcache或inode cache的内部实现。它只通过标准的回调接口与它们通信,这使得代码清晰、易于维护。
- 可扩展性:任何新的内核代码只要想实现一个可回收的缓存,都可以简单地通过实现两个回调函数并注册一个shrinker来集成到全局内存管理中。
- 公平与高效:通过
count
/scan
两阶段机制,MM子系统可以根据每个缓存的“可回收性”来按比例施加压力,避免了对某个缓存进行过度的、低效的扫描,从而更公平、更高效地回收内存。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 实现依赖:Shrinker框架的效率高度依赖于每个shrinker实现者的代码质量。一个 poorly-written 的
scan_objects
函数(例如,持有锁的时间过长,或者扫描效率低下)会拖慢整个系统的内存回收过程。 - 估算的局限性:
count_objects
返回的是一个估算值,可能与实际可回收的数量有偏差。这可能导致MM子系统的回收决策不是最优的。 - 不适用于页面缓存:这个框架是为基于对象的内核缓存设计的。它不适用于页面缓存(Page Cache)的回收。页面缓存有其自己的一套更复杂的、基于LRU和页面状态的回收机制。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
在内核中,任何子系统创建了一个动态的、非必需的、基于对象的内存缓存时,提供一个shrinker是标准且首选的做法。
- VFS dentry缓存回收:这是最经典的例子。当系统运行时,会创建大量的dentry对象来加速路径查找。但其中很多dentry在一段时间后就不再被使用。当内存紧张时,MM子系统会调用VFS注册的dentry shrinker。其
scan_objects
函数会扫描dentry的LRU链表,将那些引用计数为0的dentry从哈希表和父子关系中脱离,并释放其占用的slab对象。 - Slab空闲页面回收:内核通过
kmem_cache_create
创建了很多slab缓存。当某个缓存(例如dentry_cache
)中的大量对象被释放后,可能会出现整个slab页面都变为空闲的情况。Slab分配器注册的shrinker被调用时,就会寻找这些完全空闲的slab页面,并将它们返还给伙伴系统(Buddy System),供其他用途使用。
是否有不推荐使用该技术的场景?为什么?
- 页面缓存(Page Cache):如前所述,页面缓存的回收逻辑(Active/Inactive List等)比shrinker模型复杂得多,有其专门的处理路径。
- 必需的核心数据:Shrinker用于回收“缓存”——即那些为了性能而保留,但丢弃后系统仍能正常工作(尽管可能变慢)的数据。它绝不能用于释放那些正在被使用的、不可或缺的核心数据结构。
对比分析
请将其 与 其他相似技术 进行详细对比。
对比一:Shrinker机制 vs. 页面缓存回收(Page Reclaim)
特性 | Shrinker机制 | 页面缓存回收 |
---|---|---|
回收对象 | 通用内核对象,通常由Slab/Slub分配器管理(如dentries, inodes)。 | 内存页(struct page ),主要是文件数据页(File-backed pages)和匿名页(Anonymous pages)。 |
核心机制 | 基于回调的“请求-响应”模型。MM请求,子系统响应并释放。 | 基于LRU(最近最-少使用)链表的管理。MM直接操作Active/Inactive链表来决定回收哪些页面。 |
决策者 | 子系统本身。scan_objects 函数内部决定具体释放哪个对象。 | MM子系统。kswapd 直接决定哪个page将被回收、换出或丢弃。 |
工作范围 | 补充页面回收,清理内核数据结构占用的非页面缓存内存。 | 内存回收的主力,负责清理系统中最大头的内存消耗者——页面缓存。 |
关系 | 协同工作。一次完整的内存回收过程,通常会同时进行页面回收和调用shrinkers。 |
对比二:Shrinker机制 vs. drop_caches
特性 | Shrinker机制 | echo N > /proc/sys/vm/drop_caches |
---|---|---|
触发方式 | 自动、按需。由内核在检测到内存压力时自动、渐进地触发。 | 手动、强制。由管理员或脚本显式触发,是一次性的、全局性的、暴力的操作。 |
回收方式 | 智能、渐进。根据压力大小,回收适量的、最不常用的对象(LRU)。 | 暴力、全部。尽可能多地丢弃所有可丢弃的缓存(N=1 丢弃页面缓存,N=2 丢棄dentry/inode,N=3 丢弃全部)。 |
对性能的影响 | 旨在平滑系统性能,避免因内存不足而突然停顿。 | 会导致剧烈的性能下降。因为所有热缓存都被清空,后续的文件访问会触发大量的磁盘I/O风暴。 |
使用场景 | 内核正常的、持续的后台内存管理。 | 仅限于调试和测试。用于在可控环境下测试应用的冷启动性能,或临时释放内存以进行某些特殊操作。绝不应在生产环境中作为常规操作使用。 |
mm/shrinker.c
shrinker_alloc
struct shrinker *shrinker_alloc(unsigned int flags, const char *fmt, ...)
{struct shrinker *shrinker;unsigned int size;va_list ap;int err;shrinker = kzalloc(sizeof(struct shrinker), GFP_KERNEL);if (!shrinker)return NULL;va_start(ap, fmt);/* return 0; */err = shrinker_debugfs_name_alloc(shrinker, fmt, ap);va_end(ap);if (err)goto err_name;/* 已分配(SHRINKER_ALLOCATED) */shrinker->flags = flags | SHRINKER_ALLOCATED;/* (DEFAULT_SEEKS),表示默认的搜索次数 */shrinker->seeks = DEFAULT_SEEKS;/* 尝试分配与内存控制组相关的资源 */if (flags & SHRINKER_MEMCG_AWARE) {/* return -ENOSYS; */err = shrinker_memcg_alloc(shrinker);if (err == -ENOSYS) {/*Memcg不受支持,回退到非Memcg感知的收缩器. */shrinker->flags &= ~SHRINKER_MEMCG_AWARE;goto non_memcg;}if (err)goto err_flags;return shrinker;}
/* 非 memcg 模式 */
non_memcg:/** nr_deferred 在每个 memcg 层级上可用于支持 memcg 的 shrinkers,因此仅在以下情况下分配 nr_deferred:* - 非 memcg 支持的 shrinkers* - !CONFIG_MEMCG* - 通过内核命令行禁用 memcg*//* 分配延迟计数器(nr_deferred) */size = sizeof(*shrinker->nr_deferred);if (flags & SHRINKER_NUMA_AWARE)size *= nr_node_ids;shrinker->nr_deferred = kzalloc(size, GFP_KERNEL);if (!shrinker->nr_deferred)goto err_flags;return shrinker;err_flags:shrinker_debugfs_name_free(shrinker);
err_name:kfree(shrinker);return NULL;
}
EXPORT_SYMBOL_GPL(shrinker_alloc);
shrinker_register 将 shrinker 结构体注册到内核的 shrinker 子系统中
void shrinker_register(struct shrinker *shrinker)
{/* 检查 shrinker 是否通过 shrinker_alloc 动态分配 */if (unlikely(!(shrinker->flags & SHRINKER_ALLOCATED))) {pr_warn("Must use shrinker_alloc() to dynamically allocate the shrinker");return;}mutex_lock(&shrinker_mutex);list_add_tail_rcu(&shrinker->list, &shrinker_list);/* 表示 shrinker 已成功注册 */shrinker->flags |= SHRINKER_REGISTERED;shrinker_debugfs_add(shrinker);mutex_unlock(&shrinker_mutex);init_completion(&shrinker->done);/** 现在收缩器已完全设置好,首次引用它以表明现在可以通过* shrinker_try_get() 使用它进行查找操作。*/refcount_set(&shrinker->refcount, 1);
}
EXPORT_SYMBOL_GPL(shrinker_register);