内存管理-77-kernel-internals系列-Memory专题-Caching & Reclaim-6-Shrinker API
一、Shrinker API 章节翻译
注: 翻译自 https://kernel-internals.org/mm/shrinker/
Shrinker API 一种回调机制,让各子系统参与内存回收 —— 当内核需要内存时,会请已注册的 shrinker 释放它们的缓存。
1. 什么是 Shrinker?
内核维护着很多内存缓存:目录项(dentry)缓存、inode 缓存、DMA-buf 堆、驱动私有对象池、zswap 池等等。这些缓存很有价值 —— 能避免昂贵的重新计算或 I/O —— 但在系统承压时必须让出内存。
shrinker API 就是各子系统向内存管理核心注册、表示“我有你可以要求释放的对象”的方式。回收运行时,每一轮会对每个已注册的 shrinker 调用两次:先问能释放多少(counting pass),再真正释放一部分(scanning pass)。
源码树内 shrinker 的例子:
fs/super.c —— 每个 superblock 的 shrinker,覆盖 dentry LRU 和 inode LRU(super_cache_scan / super_cache_count) mm/huge_memory.c —— 延迟拆分 THP 的 shrinker mm/zswap.c —— zswap 池 shrinker mm/workingset.c —— workingset shadow 表项 shrinker fs/xfs/xfs_icache.c —— XFS inode GC shrinker mm/vmalloc.c —— vmap node shrinker
2. struct shrinker
struct shrinker { //include/linux/shrinker.h
unsigned long (*count_objects)(struct shrinker *, struct shrink_control *sc);
unsigned long (*scan_objects)(struct shrinker *, struct shrink_control *sc);
long batch; /* 回收批大小,0 = 默认 */
int seeks; /* 重建一个对象的 seeks 代价 */
unsigned flags;
/*
* 该 shrinker 的引用计数。已注册的 shrinker 初始 refcount 为 1,之后查找操作可经 shrinker_try_get() 使用它。
* 注销时丢掉初始 refcount,refcount 到 0 后经 RCU 异步释放。
*/
refcount_t refcount;
struct completion done; /* 用于等待 refcount 到 0 */
struct rcu_head rcu;
void *private_data;
struct list_head list; /* 内部使用 */
#ifdef CONFIG_MEMCG
int id; /* 在 shrinker_idr 中的 ID */
#endif
#ifdef CONFIG_SHRINKER_DEBUG
int debugfs_id;
const char *name;
struct dentry *debugfs_entry;
#endif
/* 待删除对象,按节点 */
atomic_long_t *nr_deferred;
};
#define DEFAULT_SEEKS 2
2.1 字段指南
----------------------------------------------------------------------------------------------------------------------------------------------- 字段 说明 ----------------------------------------------------------------------------------------------------------------------------------------------- count_objects 返回可能被释放的对象数。只统计,不真正释放 scan_objects 释放最多 sc->nr_to_scan 个对象。返回释放数、SHRINK_STOP 或 SHRINK_EMPTY batch 每次 scan_objects 调用扫描多少对象。0 用内部默认 128。batch 越大调用开销越小;越小延迟越好 seeks 重建一个对象的相对代价(以磁盘 seek 为单位)。用于优先级缩放公式。不确定就用 DEFAULT_SEEKS(2)。重建不需要 I/O 的对象设为 0 —— 会被更激进地裁剪 flags SHRINKER_NUMA_AWARE、SHRINKER_MEMCG_AWARE、SHRINKER_NONSLAB —— 见下文 private_data 给 shrinker 实现用的不透明指针,例如 fs/super.c 中的 super_block * nr_deferred 算出来但还没做完(因 batch 取整或竞争)的扫描工作,按节点计数。结转到下一次调用。由核心管理 -----------------------------------------------------------------------------------------------------------------------------------------------
3. struct shrink_control
每次调用时,内核向两个回调都传入 struct shrink_control:
struct shrink_control {
gfp_t gfp_mask;
/* 正在收缩的节点(给 NUMA aware 的 shrinker 用) */
int nid;
/* scan_objects 应该扫描并尝试回收多少对象。每次调用前重置。 */
unsigned long nr_to_scan;
/* scan_objects 处理了多少对象?默认等于 nr_to_scan;被调方应更新以反映实际进度。*/
unsigned long nr_scanned;
/* 正在收缩的 memcg(给 memcg aware 的 shrinker 用) */
struct mem_cgroup *memcg;
};
字段语义:
gfp_mask: 触发回收的分配上下文。Shrinker 必须遵守。如果 __GFP_FS 被清除,shrinker 不得进入文件系统(例如 super_cache_scan 此时直接返回 SHRINK_STOP)。如果 __GFP_IO 被清除,避免发起 I/O。
nid: 正在收缩的 NUMA 节点。只有设置了 SHRINKER_NUMA_AWARE 时才有意义;其他 shrinker 永远只看到 node 0。
nr_to_scan: 调用 scan_objects 前由核心设置。shrinker 应尝试释放这么多对象。改它是安全的(核心每次调用前都会重置)。
nr_scanned: 核心预填为 nr_to_scan。shrinker 应更新它以反映实际检查了多少对象(不只是释放了多少)。核心用它统计已做功、更新 nr_deferred。
memcg: 正在针对的内存 cgroup。走按 memcg shrinker 时由核心设置;全局(root memcg)路径为 NULL。只有设置了 SHRINKER_MEMCG_AWARE 时才有意义。
4. count_objects vs scan_objects
这两个回调实现两阶段契约:
4.1 count_objects —— 估算可释放容量
unsigned long (*count_objects)(struct shrinker *, struct shrink_control *sc);
返回缓存中可能被释放的对象数。返回值决定是否会调用 scan_objects:
----------------------------------------------------------------------------------------------------------------------------- 返回值 含义 ----------------------------------------------------------------------------------------------------------------------------- SHRINK_EMPTY(~0UL - 1) 缓存完全空了。核心会清除该 shrinker 在按 memcg 位图中的位,加入新对象前跳过它. 0 现在没什么可释放的(例如对象都被 pin 住,或计数低于阈值)。本轮不会调用 scan_objects N > 0 约有 N 个可释放对象。核心用它计算经 nr_to_scan 请求多少对象 -----------------------------------------------------------------------------------------------------------------------------
无锁要求。 count_objects 不得拿可能与回收死锁的锁。计数可以是旧的 —— 核心容忍 count_objects 与 scan_objects 实际释放数不一致。这是设计使然:计数用于按比例缩放,不是硬限制。
4.2 scan_objects —— 真正释放对象
unsigned long (*scan_objects)(struct shrinker *, struct shrink_control *sc);
遍历你的缓存,释放最多 sc->nr_to_scan 个对象,更新 sc->nr_scanned,返回释放的对象数。
------------------------------------------------------------------------------------------------------------------------------------------------- 返回值 含义 ------------------------------------------------------------------------------------------------------------------------------------------------- N >= 0 本次调用释放的对象数 SHRINK_STOP(~0UL) 现在无法推进 —— 需要的锁在当前回收上下文中可能导致死锁。当前回收调用中核心不会再对该 shrinker 调用 scan_objects SHRINK_EMPTY(~0UL - 1) 缓存中没有对象了 -------------------------------------------------------------------------------------------------------------------------------------------------
定义在:
/* include/linux/shrinker.h */ #define SHRINK_STOP (~0UL) #define SHRINK_EMPTY (~0UL - 1)
5. Shrinker Flags
#define SHRINKER_NUMA_AWARE BIT(2) #define SHRINKER_MEMCG_AWARE BIT(3) #define SHRINKER_NONSLAB BIT(4)
SHRINKER_NUMA_AWARE —— 该 shrinker 按 NUMA 节点各调一次。sc->nid 设为正在回收的节点。当缓存按节点划分时用(例如按节点 LRU)。没有该标志时,无论哪个节点(node)承压,shrinker 只被调一次且 nid = 0。
SHRINKER_MEMCG_AWARE —— 该 shrinker 参与按 memcg 回收。当某个 memcg 超限时,核心调用带 MEMCG_AWARE 的 shrinker,并把 sc->memcg 设为目标 cgroup。shrinker 此时只允许释放记账到该 memcg 的对象。这要求你的 LRU 用 list_lru_init_memcg() 初始化。没有该标志时,shrinker 只在全局(root cgroup)回收时被调用。
SHRINKER_NONSLAB —— 只与 SHRINKER_MEMCG_AWARE 联合使用。标记该 shrinker 管理的是非 slab 内存。当 memcg kmem 记账关闭时,非 NONSLAB 的 shrinker 在按 memcg 回收时会被跳过;NONSLAB 的 shrinker 总是被调用。用于 DMA-buf attachment 这类不是 slab 分配、但仍应按 memcg 回收的对象。
6. 内核如何调用 Shrinker
6.1 调用路径
内存压力(分配失败或越过水线)
│
├── kswapd(后台)─────────────────────────────┐
│ │
└── 直接回收(分配进程阻塞)──────────────────────┤
│
▼
shrink_node() in mm/vmscan.c
│
▼
shrink_slab(gfp_mask, nid, memcg, priority) in mm/shrinker.c
│
┌─────────────────────────┴──────────────────────┐
│(非 root memcg) │(root)
▼ ▼
shrink_slab_memcg() list_for_each_entry_rcu 遍历 shrinker_list
按 memcg 位图迭代 │
│ │
└────────────────────────┬───────────────────────┘
▼
do_shrink_slab(sc, shrinker, priority)
│
│ 1. 调 count_objects()
│ 2. 按 priority 算 nr_to_scan
│ 3. 循环:按 batch 调 scan_objects(),直到 total_scan 用完或返回 SHRINK_STOP
6.2 do_shrink_slab 中的优先级缩放
priority 参数(来自 mm/vmscan.c 的 sc->priority)控制 shrinker 被调用的激进程度。priority 越高表示内存压力越大。
/* 来自 mm/shrinker.c: do_shrink_slab() */
if (shrinker->seeks) {
delta = freeable >> priority; /* priority 12 时:delta 很小 */
delta *= 4;
do_div(delta, shrinker->seeks);
} else {
delta = freeable / 2; /* seeks=0:一律裁一半 */
}
total_scan = nr >> priority; /* 延迟工作,低优先级时按比例缩小 */
total_scan += delta;
total_scan = min(total_scan, (2 * freeable));
在 priority = DEF_PRIORITY(12,低压力)时:freeable >> 12 很小 —— shrinker 几乎不被碰。随着 priority 趋近 0(极端压力):完整的 deferred 计数和更大的 delta 生效,每次调用扫描更多对象。total_scan 上限为 2 * freeable,防止过度扫描。
do_shrink_slab 接着循环调用 scan_objects,每次 batch_size 个对象,直到 total_scan 用完或返回 SHRINK_STOP。因 batch 取整没做完的、算出来了的工作存入 nr_deferred,累加到下一次调用。
6.3 并发与锁
shrinker 链表变更(注册/注销)受 shrinker_mutex 保护,遍历受 RCU 保护。单个 shrinker 查找用 shrinker_try_get()(refcount_inc_not_zero)在 do_shrink_slab 运行期间持有引用。shrinker_free() 调 wait_for_completion(&shrinker->done) 排空所有在途引用后才释放内存。
这意味着 scan_objects 可能在不同 CPU 上并发运行(针对不同 node 或 memcg)。Shrinker 实现必须能承受并发调用。
7. 注册与生命周期
7.1 三步生命周期
/* 1. Allocate and configure */
struct shrinker *shrinker;
shrinker = shrinker_alloc(SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE, "mydriver-%s", device_name);
if (!shrinker)
return -ENOMEM;
shrinker->count_objects = mydriver_count_objects;
shrinker->scan_objects = mydriver_scan_objects;
shrinker->seeks = DEFAULT_SEEKS;
shrinker->private_data = my_private_ctx;
/* 2. Register (shrinker becomes visible to reclaim) */
shrinker_register(shrinker);
/* ... device/module in use ... */
/* 3. Free (unregisters, drains references, then frees asynchronously via RCU) */
shrinker_free(shrinker);
7.2 shrinker_alloc(flags, fmt, ...)
分配并初始化 struct shrinker。fmt 是 printf 风格的名字,用于 /sys/kernel/debug/shrinker/(CONFIG_SHRINKER_DEBUG 开启时)。如果请求了 SHRINKER_MEMCG_AWARE 但运行时 memcg 被禁用,该标志会被静默清除,shrinker 回退到非 memcg 模式。分配失败返回 NULL。
7.3 shrinker_register(shrinker)
把 shrinker 加到 shrinker_list,置位 SHRINKER_REGISTERED,refcount 初始化为 1(对回收可见)。只能对 shrinker_alloc() 返回的 shrinker 调用。
7.4 shrinker_free(shrinker)
注销 + 释放一次搞定:
若已注册:丢掉初始 refcount 并等待(wait_for_completion)所有在途 do_shrink_slab 调用结束。
在 shrinker_mutex 下从 shrinker_list 摘除。
若 SHRINKER_MEMCG_AWARE,从 memcg IDR 摘除。
经 call_rcu 对结构体本身做 kfree。
shrinker_free() 返回后,容纳该 shrinker 的结构(例如 super_block、驱动的 device 结构)就可以安全释放了。
7.5 何时调用哪个
---------------------------------------------------------------------------------------------------- 场景 何时注册 何时释放 ---------------------------------------------------------------------------------------------------- 模块全局缓存 module_init/subsys_initcall module_exit 按设备缓存 probe(),缓存初始化后 remove()/probe() 错误路径 per-superblock cache sget()/alloc_super(),mount 完成前 kill_sb()/deactivate_super() ----------------------------------------------------------------------------------------------------
8. 真实例子:Superblock Shrinker
每个 mount 的文件系统都有一个 per-superblock 的 shrinker,同时覆盖 dentry LRU 和 inode LRU。在 alloc_super()(fs/super.c)中搭建:
/* fs/super.c: alloc_super() */ s->s_shrink = shrinker_alloc(SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE, "sb-%s", type->name); s->s_shrink->scan_objects = super_cache_scan; s->s_shrink->count_objects = super_cache_count; s->s_shrink->batch = 1024; s->s_shrink->private_data = s;
shrinker 稍后注册,此时 superblock 已完全建好并置位 SB_BORN。
/* fs/super.c: vfs_get_tree() / mount_fs() path */ shrinker_register(s->s_shrink);
super_cache_count 用 list_lru_shrink_count() 查 dentry 和 inode LRU,加上 s_op->nr_cached_objects() 的文件系统私有对象数,总数为 0 时返回 SHRINK_EMPTY。
super_cache_scan 按比例把 sc->nr_to_scan 分给 dentry、inode 和 FS 私有对象。先调 prune_dcache_sb()(dentry pin 住 inode),再调 prune_icache_sb(),最后调 s_op->free_cached_objects()。如果 sc->gfp_mask 没有 __GFP_FS,直接返回 SHRINK_STOP,避免重入可能持有锁的文件系统。
9. 常见错误
9.1 不检查 gfp_mask 就在 scan_objects 里阻塞或睡眠
scan_objects 可能在任意上下文的直接回收中被调用。如果调用者持有的锁正是你的 shrinker 也要拿的,或回收正发生在文件系统锁之下,再拿该锁就会死锁。进文件系统或发起 I/O 前务必检查 sc->gfp_mask 的 __GFP_FS 和 __GFP_IO。不能安全推进就返回 SHRINK_STOP。
9.2 当缓存实际已空时却从 count_objects 返回 0
0 表示“现在没什么可释放”(例如对象被 pin 住)。SHRINK_EMPTY 表示“缓存完全空了”。两者不可互换。全局回收路径下返回 0 代价不大,但在按 memcg 路径下会阻止内核清除 memcg 位图中该 shrinker 的位,导致以后无谓的唤醒。
9.3 nr_scanned 处理不当
sc->nr_scanned 预填为 sc->nr_to_scan。如果你的 scan_objects 实际检查的对象少于请求数(例如遇到 trylock 跳过了一部分),要把 sc->nr_scanned 更新为实际检查数。核心用它算 next_deferred。少扫了却不改,会把没做的工作记成做完了,少报 deferred 工作。
9.4 错误路径忘记调 shrinker_free
如果已经调过注册、之后设备或模块初始化失败,返回错误前必须调 shrinker_free。指向已释放或半初始化数据的已注册 shrinker,会在下一轮回收时被调用,导致 use-after-free 或空指针解引用。shrinker_free 对未注册和已注册两种情况都安全,所以 shrinker_alloc 成功后的任何清理路径都可以无条件调用。
9.5 scan_objects 的并发调用
内核可能并发调用 scan_objects(SHRINKER_NUMA_AWARE 按不同 node,SHRINKER_MEMCG_AWARE 按不同 memcg)。你的 LRU 遍历和计数器必须并发安全。用内置按节点、按 memcg 锁的 list_lru,别自己造轮子。
9.6 无意中用了 seeks = 0
seeks 为 0 会触发 do_shrink_slab 的激进分支:delta = freeable / 2。意味着任何优先级下,一次就要求扫描一半可释放对象。对重建便宜(无 I/O)的缓存这是故意的,但对昂贵缓存误用会在压力下导致过度驱逐。
10. 关键源文件
include/linux/shrinker.h: struct shrinker、struct shrink_control、SHRINK_STOP、SHRINK_EMPTY、flags、shrinker_alloc / shrinker_register / shrinker_free 声明.
mm/shrinker.c: shrinker_alloc、shrinker_register、shrinker_free、do_shrink_slab、shrink_slab、shrink_slab_memcg、memcg 位图管理.
mm/vmscan.c: shrink_node、shrink_lruvec —— 回收中调用 shrink_slab() 的位置.
fs/super.c: super_cache_scan、super_cache_count、alloc_super 中的按 superblock shrinker 搭建.
fs/dcache.c: prune_dcache_sb —— super_cache_scan 调它收缩 dentry LRU
fs/inode.c: prune_icache_sb —— super_cache_scan 调它收缩 inode LRU
include/linux/list_lru.h: list_lru_init_memcg、list_lru_shrink_count、list_lru_shrink_walk —— 与 shrinker 配合的 NUMA/memcg 感知 LRU
11. 延伸阅读:
mm/shrinker.c 中 shrinker_alloc()、shrinker_register()、shrinker_free()、do_shrink_slab()(优先级缩放与 batch 循环)、shrink_slab() 与带按 memcg 位图的 shrink_slab_memcg();
mm/vmscan.c 中调用 shrink_slab() 的 shrink_node() 与 shrink_lruvec();
LWN《Smarter shrinkers》两阶段 count_objects / scan_objects 取代旧单回调模型的动机与设计;
LWN《Shrinker reorganization》shrinker 代码从 mm/vmscan.c 拆到专用 mm/shrinker.c 及 RCU 引用计数生命周期的引入;
该站 reclaim 篇展示 shrink_slab() 在 LRU 扫描、kswapd、直接回收大图中的位置;
memcg 篇解释何时设置 sc->memcg 以及 SHRINKER_MEMCG_AWARE shrinker 与 cgroup 内存限制的交互。
posted on 2026-10-08 17:39 Hello-World3 阅读(1) 评论(0) 收藏 举报
浙公网安备 33010602011771号