Shrinkers and Slab Reclaim
Most of the kernel’s reclaimable memory is not page-cache pages on the LRU — it is the contents of dozens of internal object caches: the dentry cache (cached directory-entry lookups), the inode cache, per-filesystem metadata caches, the dquota cache, and many more, all carved out of slab memory. These objects are not on the page LRU lists, so the normal reclaim path cannot see them. The shrinker API is the kernel’s uniform callback interface by which any such subsystem advertises “I hold N freeable objects” (
count_objects) and “please try to free some” (scan_objects), so that when the system is under memory pressure the reclaim core can shrink all these caches proportionally — driving each cache down in rough proportion to how hard it is scanning the page LRU at the same priority. The interface is defined ininclude/linux/shrinker.hand driven bydo_shrink_slab()inmm/shrinker.c(Linux v6.12 source).
A shrinker is the bridge that lets the direct-reclaim and kswapd paths apply pressure to memory that is structured as kernel objects rather than as user pages. Without it, a box with a 4 GiB dentry cache and almost no page cache would happily OOM while gigabytes of trivially-freeable dentry structures sat untouched.
Mental Model
Think of every reclaimable kernel cache as a tank with a float valve. The reclaim core does not know what is inside any tank; it only knows two operations per tank: read the gauge (how many objects could be freed right now) and open the valve a measured amount (try to free this many). On each reclaim pass the core walks every registered tank, reads its gauge, computes a release quota that scales with (a) how full the tank is and (b) how hard the core is currently pressing on the page LRU, then asks the tank to release that quota. A cheap-to-rebuild cache gets a wider valve; an expensive-to-rebuild one (objects that cost disk I/O to recreate) gets a narrower valve. This “press everything in proportion” design is what keeps one greedy cache from starving the rest.
flowchart TD PRESS["Memory pressure<br/>(kswapd or direct reclaim<br/>at scan priority P)"] --> SS["shrink_slab()<br/>iterate shrinker_list (RCU)"] SS --> DSS["do_shrink_slab() per shrinker"] DSS --> CNT["count_objects()<br/>= freeable (the gauge)"] CNT --> DELTA["delta = (freeable >> P) * 4 / seeks<br/>total_scan = deferred>>P + delta<br/>clamp to 2*freeable"] DELTA --> LOOP{"total_scan >=<br/>batch_size?"} LOOP -->|yes| SCAN["scan_objects(nr_to_scan)<br/>(open the valve)"] SCAN --> FREED["free dentries / inodes / ...<br/>back to slab → buddy"] FREED --> LOOP LOOP -->|no| DEFER["next_deferred = nr + delta - scanned<br/>(carry leftover to next pass)"]
How a single reclaim pass drives the shrinkers. What it shows: the reclaim core (shrink_slab) walks every registered shrinker, calls count_objects to read the freeable count, converts that into a scan quota total_scan that is divided by the shrinker’s seeks cost and shifted right by the same scan priority the page-LRU scanner is using, then calls scan_objects in batch_size chunks until the quota is exhausted; whatever it could not scan this pass is deferred and carried forward. The insight to take: the shrinker never decides “the system is low on memory” — it is a passive callback. All policy (how hard to press) lives in the core; the cache owner supplies only the count and the free mechanism.
The struct shrinker Interface
A shrinker is a small descriptor that a subsystem fills in and registers. As of Linux 6.12 it is (include/linux/shrinker.h):
struct shrinker {
unsigned long (*count_objects)(struct shrinker *,
struct shrink_control *sc);
unsigned long (*scan_objects)(struct shrinker *,
struct shrink_control *sc);
long batch; /* reclaim batch size, 0 = default */
int seeks; /* seeks to recreate an obj */
unsigned flags;
refcount_t refcount;
struct completion done; /* use to wait for refcount to reach 0 */
struct rcu_head rcu;
void *private_data;
struct list_head list;
#ifdef CONFIG_MEMCG
int id; /* ID in shrinker_idr */
#endif
atomic_long_t *nr_deferred; /* objs pending delete, per node */
};The two function pointers are the whole contract:
count_objects(shrinker, sc)returns the number of objects the cache could free right now — its “gauge.” Per the header’s contract, returning0tells the core to skip this cache this pass; returningSHRINK_EMPTY(~0UL - 1) signals the cache is genuinely empty (a memcg-aware distinction). It must be cheap — it is called on every shrinker on every pass.scan_objects(shrinker, sc)is called only whencount_objectsreturned non-zero. It should try to free up tosc->nr_to_scanobjects, setsc->nr_scannedto how many it examined, and return the number actually freed — orSHRINK_STOP(~0UL) if it cannot make progress (e.g. it would have to sleep but the gfp context forbids it), which tells the core to stop hammering this shrinker for the rest of the pass.
seeks is the cost-to-recreate hint, walked through in detail below. batch overrides the default scan-chunk size (SHRINK_BATCH, 128) when a cache wants larger chunks. nr_deferred is a per-node array holding work that could not be done on previous passes and is carried forward. The refcount/done/rcu machinery exists so the cache can be freed safely while a concurrent reclaimer may still hold a reference (added in the 6.7-era refactor described below).
struct shrink_control — the per-call context
Every callback receives a shrink_control describing this invocation:
struct shrink_control {
gfp_t gfp_mask; /* allocation context that triggered reclaim */
int nid; /* NUMA node to scan (if NUMA-aware) */
unsigned long nr_to_scan; /* how many to scan this call */
unsigned long nr_scanned; /* OUT: how many the callback examined */
struct mem_cgroup *memcg; /* cgroup to scan (if memcg-aware) */
};gfp_mask matters because it tells the callback what it is allowed to do: a shrinker invoked from an atomic or GFP_NOFS context must not perform filesystem I/O or sleep, so it must bail (SHRINK_STOP) rather than block. See GFP Flags and Allocation Contexts for what each mask permits.
Flags: NUMA- and memcg-awareness
#define SHRINKER_REGISTERED BIT(0)
#define SHRINKER_ALLOCATED BIT(1)
#define SHRINKER_NUMA_AWARE BIT(2)
#define SHRINKER_MEMCG_AWARE BIT(3)
#define SHRINKER_NONSLAB BIT(4)SHRINKER_NUMA_AWARE tells the core the cache tracks its objects per NUMA node, so the core will invoke the shrinker once per node with sc->nid set, letting reclaim target the node that is actually short of memory rather than blindly freeing objects on a healthy node (see NUMA Memory Model). SHRINKER_MEMCG_AWARE means the cache accounts objects per memory cgroup, so per-cgroup reclaim (memory.high/memory.max pressure) can shrink only the offending cgroup’s objects; such shrinkers get an id in the shrinker_idr and use a per-memcg deferred-count map. The dentry and inode caches set both flags — they are the canonical NUMA- and memcg-aware shrinkers. SHRINKER_NONSLAB marks a shrinker whose objects are not slab objects (e.g. the hugetlb or some GPU shrinkers) so memcg kmem accounting handles them correctly.
The Modern Registration API (6.7+)
Uncertain
Verify: that the
register_shrinker()/unregister_shrinker()API was fully replaced byshrinker_alloc()/shrinker_register()/shrinker_free()and removed as of 6.12 (the brief asked specifically aboutregister_shrinker). Reason: I confirmed the new API exists in the v6.12shrinker.h/shrinker.cI fetched, and the refactor is the 2023 “shrinker debugging and a new API” work, but I did not directly grep v6.12 for a survivingregister_shrinkersymbol. To resolve: grepinclude/linux/shrinker.handmm/shrinker.cat thev6.12tag forregister_shrinker. uncertain
Historically a subsystem embedded a struct shrinker in its own structure, filled in the callbacks, and called register_shrinker(&s). That pattern had a lifetime hole: a shrinker could be running concurrently while its owner was being torn down. The 6.7-era rework split registration into three calls (confirmed present in v6.12 shrinker.h):
__printf(2, 3)
struct shrinker *shrinker_alloc(unsigned int flags, const char *fmt, ...);
void shrinker_register(struct shrinker *shrinker);
void shrinker_free(struct shrinker *shrinker);The new pattern is allocate → fill in callbacks → register, with a refcount and a completion so shrinker_free() can wait for any in-flight scan_objects to drain before the memory goes away:
sb->s_shrink = shrinker_alloc(SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE,
"sb-%s", type->name); /* heap-allocated, named */
sb->s_shrink->seeks = DEFAULT_SEEKS; /* = 2 */
sb->s_shrink->count_objects = super_cache_count;
sb->s_shrink->scan_objects = super_cache_scan;
sb->s_shrink->private_data = sb;
shrinker_register(sb->s_shrink); /* now visible to reclaim */Uncertain
Verify: the exact
fs/super.ccode that allocatessb->s_shrink(the snippet above is reconstructed from the API shape and the documented dentry/inode-cache behavior, not quoted verbatim — a fetch offs/super.cat v6.12 was blocked during research). Specifically confirm the flag combination, thatseeks == DEFAULT_SEEKS, and thesuper_cache_count/super_cache_scannames. Reason: source fetch blocked (backend overload). To resolve: readfs/super.cat thev6.12tag. uncertain
shrinker_alloc() (mm/shrinker.c) kzallocs the descriptor, ORs in SHRINKER_ALLOCATED, and — for memcg-aware shrinkers — calls shrinker_memcg_alloc() to get an IDR id; non-memcg shrinkers get a nr_deferred array sized one slot per node. shrinker_register() does the RCU-safe publish:
list_add_tail_rcu(&shrinker->list, &shrinker_list);
shrinker->flags |= SHRINKER_REGISTERED;
init_completion(&shrinker->done);
refcount_set(&shrinker->refcount, 1);Mechanical Walk-through: do_shrink_slab() and the Proportional Formula
When reclaim runs, shrink_slab() iterates the global shrinker_list under RCU, taking a temporary reference on each shrinker so it cannot vanish mid-call:
rcu_read_lock();
list_for_each_entry_rcu(shrinker, &shrinker_list, list) {
if (!shrinker_try_get(shrinker))
continue;
rcu_read_unlock();
ret = do_shrink_slab(&sc, shrinker, priority);
rcu_read_lock();
shrinker_put(shrinker);
}
rcu_read_unlock();The heart is do_shrink_slab(). It first reads the gauge and the carried-over deferred work, then computes the scan quota (mm/shrinker.c, v6.12):
freeable = shrinker->count_objects(shrinker, shrinkctl);
nr = xchg_nr_deferred(shrinker, shrinkctl); /* deferred from past passes */
if (shrinker->seeks) {
delta = freeable >> priority;
delta *= 4;
do_div(delta, shrinker->seeks);
} else {
delta = freeable / 2; /* no IO to recreate → trim aggressively */
}
total_scan = nr >> priority;
total_scan += delta;
total_scan = min(total_scan, (2 * freeable)); /* never scan more than 2x */Walking this symbol by symbol:
freeableis whatcount_objectsreturned — the number of objects this cache could give up.priorityis the reclaim scan priority, an integer that starts atDEF_PRIORITY(12) and is decremented as reclaim grows more desperate; a smaller priority means a bigger shift-divisor inverse, i.e. more aggressive.freeable >> priorityis therefore “the fraction of the cache proportional to how hard we are pressing.” At the gentle starting priority of 12,freeable >> 12isfreeable / 4096— a tiny nibble; as priority falls toward 0 the shift shrinks and the nibble grows toward the whole cache.delta *= 4scales that nibble up by a constant factor (this4pairs with the page scanner’s own constants so slab and page reclaim move at comparable rates).do_div(delta, shrinker->seeks)divides by the cost-to-recreate. This is the seeks heuristic:seeksanswers “how many disk seeks would it take to recreate one of these objects?” An object that is expensive to rebuild (highseeks) yields a smallerdelta, so the core frees fewer of them per pass — it is reluctant to throw away things that cost I/O to regenerate.DEFAULT_SEEKSis2(“a good number if you don’t know better”), the value used by the dentry/inode caches: two seeks, roughly, to reload an inode from disk.- The
elsebranch handlesseeks == 0, meaning the objects cost no I/O to recreate (the header’s comment: “These objects don’t require any IO to create. Trim them aggressively”). Such caches getdelta = freeable / 2— a fixed, aggressive half-the-cache quota per pass, with no seeks-based damping. nr >> priorityfolds in the deferred backlog at the same priority scaling, and the finalmin(total_scan, 2 * freeable)caps a single pass at twice the current cache size so a huge deferred backlog cannot cause one shrinker to spin freeing forever.
The scan then proceeds in batches:
while (total_scan >= batch_size || total_scan >= freeable) {
unsigned long nr_to_scan = min(batch_size, total_scan);
shrinkctl->nr_to_scan = nr_to_scan;
ret = shrinker->scan_objects(shrinker, shrinkctl);
freed += ret;
total_scan -= shrinkctl->nr_scanned;
scanned += shrinkctl->nr_scanned;
}batch_size is shrinker->batch ?: SHRINK_BATCH (128). Splitting the work into batches bounds how long any single scan_objects call holds locks and lets the loop re-check signals/SHRINK_STOP. Finally, whatever quota was not scanned is deferred for next time:
next_deferred = max_t(long, (nr + delta - scanned), 0);This carry-forward is why a shrinker that keeps returning SHRINK_STOP (because the gfp context forbids the I/O it needs) does not lose the pressure — the deferred count accumulates and is discharged once a permissive context (e.g. kswapd with GFP_KERNEL) comes along.
The VFS Caches: the Canonical Shrinkers
The dentry and inode caches are where shrinkers matter most in practice; on a busy file server they routinely dominate kernel memory. Each superblock registers one shrinker (sb->s_shrink) that counts and scans both its dentry LRU (s_dentry_lru) and inode LRU (s_inode_lru) via the generic list_lru infrastructure. super_cache_count sums list_lru_shrink_count() over the two LRUs and then applies the vfs_cache_pressure knob; super_cache_scan apportions sc->nr_to_scan between prune_dcache_sb() and prune_icache_sb(). Dentries are pruned first because freeing a dentry can drop the last reference on its inode, making the inode freeable in turn.
vfs_cache_pressure
The vm.vfs_cache_pressure sysctl scales the count a superblock shrinker reports, biasing how aggressively the VFS caches are reclaimed relative to the page cache. Per the kernel admin guide:
- Default 100. At the default, “the kernel will attempt to reclaim dentries and inodes at a ‘fair’ rate with respect to pagecache and swapcache reclaim.” Effectively the reported freeable count is multiplied by
100/denominator, i.e. taken at face value. - Below 100 → the kernel “will prefer to retain dentry and inode caches” (the reported count is scaled down, so the proportional formula above frees fewer of them). Useful on metadata-heavy workloads where re-walking directory trees is costly.
- Above 100 → more aggressive reclaim of dentries/inodes, freeing them sooner relative to page cache.
- 0 is dangerous: “the kernel will never reclaim dentries and inodes due to memory pressure and this can easily lead to out-of-memory conditions” — because the gauge always reads zero, the shrinker is effectively disabled.
Uncertain
Verify: the v6.12 admin-guide phrases the default in terms of a configurable denominator — “
vfs_cache_pressure = vfs_cache_pressure_denom” — implying the simple “percentage, default 100” framing was generalized to a numerator/denominator pair in a recent release. Confirm whethervfs_cache_pressure_denomexists as a separate sysctl in 6.12/6.18 and what its default is, and restate the scaling formula (count * pressure / denom) precisely. Reason: the fetched doc text usedvfs_cache_pressure_denombut I did not fetchfs/dcache.c(wheresysctl_vfs_cache_pressureandvfs_pressure_ratio()live) to confirm the exact arithmetic. To resolve: readfs/dcache.cand the sysctl registration at thev6.12tag. uncertain
Failure Modes and Diagnosis
- A cache the shrinker cannot see. If a subsystem caches reclaimable objects but never registers a shrinker, those objects are invisible to reclaim and will sit pinned until the subsystem frees them itself. The symptom is high
Slab/SReclaimablein/proc/meminfothat never drops under pressure, ending in an OOM kill despite “reclaimable”-looking memory. - Returning the wrong count. A
count_objectsthat overstates freeable objects makes the core scan harder than warranted (wasted CPU); one that understates leaves memory pinned. Returning0permanently disables the shrinker (this is exactly whatvfs_cache_pressure=0engineers). scan_objectsthat blocks in the wrong context. If a callback sleeps or does I/O whensc->gfp_maskforbids it (!__GFP_FS/!__GFP_IO, or atomic context), it can deadlock against the very filesystem that is trying to reclaim. The contract is to returnSHRINK_STOP/0and let the deferred count carry the work to a permissive context.- Deferred-count runaway is bounded, not eliminated. The
min(total_scan, 2*freeable)cap stops a single pass from scanning forever, but a pathological cache that keeps deferring can still cause repeated heavy passes;/sys/kernel/debug/shrinker/(theCONFIG_SHRINKER_DEBUGinterface) exposes per-shrinker counts to diagnose this. - Diagnosis tools:
slabtopand/proc/slabinfoshow which slab caches are large;echo 2 > /proc/sys/vm/drop_cachesforce-runs the dentry/inode shrinkers (useful to confirm a cache is reclaimable); theshrinkerdebugfs directory exposescount/scanper shrinker for fine-grained inspection.
Alternatives and Boundaries
Shrinkers are specifically for object caches the reclaim core cannot otherwise see. They are not how ordinary file pages are reclaimed — those live on the page LRU and are handled by the LRU/MGLRU scanner in vmscan.c, which runs alongside the shrinkers at the same priority. They are also distinct from the SLUB allocator’s own internal freeing of empty slab pages back to the buddy allocator: SLUB returns wholly-empty slabs automatically, but a slab that is even partially occupied stays put until a shrinker (or explicit free) releases the objects pinning it. If your subsystem holds memory that is genuinely reclaimable on demand, a shrinker is the right tool; if it holds memory that is page-cache-backed, let the LRU handle it; if it holds memory it can never give back, no shrinker will help.
Production Notes
The seeks/vfs_cache_pressure knobs are real operational levers. Database and build-farm operators frequently lower vfs_cache_pressure (e.g. to 50) so that hot dentry/inode metadata survives memory pressure, trading some page cache for fewer cold-path directory walks. Conversely, hosts that churn through millions of short-lived files (CI runners, mail spools) sometimes raise it so the negative-dentry and inode caches don’t balloon. Container platforms rely on the memcg-aware dentry/inode shrinkers so that a single container thrashing its filesystem metadata triggers reclaim within its own cgroup rather than evicting a neighbor’s caches — a property that only works because the VFS shrinkers set SHRINKER_MEMCG_AWARE and account objects per cgroup. The 6.7-era shrinker_alloc refactor was motivated in part by a class of use-after-free bugs in subsystems (notably some GPU and filesystem drivers) that freed a shrinker’s owner while a reclaimer was mid-scan_objects; the refcount-plus-completion model closed that window.
See Also
- Memory Reclaim Overview — the umbrella reclaim path that invokes
shrink_slabalongside the page-LRU scan. - The Slab Allocator and SLUB — where the objects shrinkers free actually live; empty slabs return to the buddy allocator.
- Direct Reclaim and kswapd and Background Reclaim — the two contexts that call into the shrinkers, with different gfp permissions.
- The LRU Lists / Multi-Generational LRU — page reclaim, which runs at the same priority as the shrinkers.
- GFP Flags and Allocation Contexts — what
sc->gfp_maskpermits ascan_objectscallback to do. - The Memory Cgroup memcg — per-cgroup reclaim relies on
SHRINKER_MEMCG_AWARE. - NUMA Memory Model —
SHRINKER_NUMA_AWARElets reclaim target the short node. - UP: Linux Memory Management MOC (§8 Memory Reclaim and the LRU).