内存管理-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)    收藏  举报

导航