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 in include/linux/shrinker.h and driven by do_shrink_slab() in mm/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.

The organising fact of this note, stated plainly: shrinkers exist because the kernel’s reclaim core cannot see inside subsystem caches, so it must ask them to free. Page reclaim works by inspection — the scanner walks an LRU list of struct folio, examines reference bits, and decides for itself which pages to evict. It can do that because every folio has the same shape and the same eviction rules. A dentry is not like that: whether it can be freed depends on its reference count, whether it is a mount point, whether its parent is pinned, whether the filesystem is being unmounted, and a dozen other conditions that only fs/dcache.c knows how to evaluate. The reclaim core has no business encoding those rules, and the VFS has no business polling for memory pressure. So the design delegates: the core owns how hard to press and the subsystem owns what to free. Every peculiarity of the shrinker API — the two-callback split, the seeks cost hint, the gfp_mask in the control block, the deferred-work carry-forward — falls out of that division of labour.

Where the boundary with its neighbours lies

This note owns the reclaim of slab memory: the callback interface, the quota arithmetic, and the in-tree shrinkers that implement it. It does not own slab allocation — The Slab Allocator and SLUB covers how objects are carved out of pages, how partial slabs are tracked, and how SLUB returns wholly-empty slabs to the buddy allocator on its own. The seam between the two is worth stating precisely, because it is the thing most often glossed over: SLUB frees a slab page only when every object on it is free. A slab page holding 63 free objects and one live dentry is 100 % unreclaimable from SLUB’s point of view. A shrinker is what evicts that last dentry so the page becomes free at all. Slab reclaim is therefore a two-stage process — the shrinker releases objects, and only then can the allocator release pages — and a workload that fragments its caches can hold gigabytes of SReclaimable that shrink slowly because the surviving objects are scattered one per page.

Similarly, Memory Reclaim Overview owns the reclaim loop as a whole and The LRU Lists / Multi-Generational LRU own the page scanner; this note picks up at the single line where that loop calls shrink_slab().

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 */
};

Field by field, with who writes each one:

FieldTypeWritten byMeaning
count_objectscallbackthe subsystemThe gauge. Returns how many objects could be freed right now. Called on every registered shrinker on every reclaim pass, so it must be cheap and must not take heavyweight locks. The header is explicit that no deadlock checks belong here: “No deadlock checks should be done during the count callback”
scan_objectscallbackthe subsystemThe valve. Called only when count_objects returned non-zero. Frees up to sc->nr_to_scan objects and returns how many it actually freed
batchlongthe subsystem, optionalScan chunk size. 0 means use SHRINK_BATCH (128). Larger values amortise per-call overhead; fs/super.c sets 1024
seeksintshrinker_alloc(), overridableCost-to-recreate hint, in notional disk seeks. Divides the scan quota, so a higher value means gentler reclaim. shrinker_alloc() pre-sets this to DEFAULT_SEEKS (2) — see the trap below
flagsunsignedshrinker_alloc() + internalCapability and state bits; table in the next section
refcountrefcount_tcoreStarts at 1 on registration. shrinker_try_get() / shrinker_put() bracket every call so the descriptor cannot be freed mid-scan
donestruct completioncoreshrinker_free() waits on this until refcount reaches 0
rcustruct rcu_headcoreThe descriptor is freed via call_rcu() so concurrent shrinker_list walkers stay safe
private_datavoid *the subsystemBack-pointer to whatever owns the cache. fs/super.c stores the struct super_block * here; the callbacks recover it with shrink->private_data
liststruct list_headcoreLinkage on the global shrinker_list, walked only for root-memcg reclaim
idint (CONFIG_MEMCG)shrinker_memcg_alloc()Index in shrinker_idr. Memcg-aware shrinkers are found through a per-cgroup bitmap keyed by this id, not by walking the list
debugfs_id, name, debugfs_entry(CONFIG_SHRINKER_DEBUG)coreBacking for <debugfs>/shrinker/<name>-<id>/
nr_deferredatomic_long_t *corePer-node array of work that could not be done on earlier passes. Only allocated for non-memcg-aware shrinkers — memcg-aware ones keep their deferred counts in the per-cgroup shrinker_info_unit instead

The seeks trap — a default that is not where you would look for it

fs/super.c never assigns s_shrink->seeks. Reading only that file, you would conclude the VFS superblock shrinker runs with seeks == 0 (it is kzalloced), which selects the aggressive delta = freeable / 2 branch of the quota formula. It does not. shrinker_alloc() sets shrinker->seeks = DEFAULT_SEEKS; immediately after kzalloc (mm/shrinker.c:687-696, v6.12), so every shrinker starts at 2 and a subsystem must lower it deliberately to get the aggressive branch. Read past the first definition you find.

The count_objects / scan_objects return-value contract

Both callbacks communicate through unsigned long return values with two reserved sentinels, and getting them wrong is a live source of bugs. SHRINK_STOP is (~0UL) and SHRINK_EMPTY is (~0UL - 1) (include/linux/shrinker.h).

CallbackReturnMeaning per the header’s own contractWhat the core does
count_objectsn > 0n objects are freeableComputes a quota and calls scan_objects
count_objects0“the number of freeable items cannot be determined or shrinker should skip this cache for this time (e.g., their number is below shrinkable limit)”do_shrink_slab() returns immediately — no deferred work is recorded
count_objectsSHRINK_EMPTY“there are no objects to free” — genuinely empty, as distinct from “don’t ask”do_shrink_slab() returns SHRINK_EMPTY; shrink_slab() converts it to 0 for the caller, but the memcg path uses the distinction to clear this shrinker’s bit in the cgroup bitmap so it is skipped until something is charged again
scan_objectsn ≥ 0n objects were actually freedAdds to freed, subtracts sc->nr_scanned from the remaining quota, loops
scan_objectsSHRINK_STOP“progress cannot be made due to potential deadlocks”breaks out of the batch loop. Per the header, “no further attempts to call the @scan_objects will be made from the current reclaim context” — and critically, the unfinished quota is carried into nr_deferred, so the pressure is not lost

One subtlety that the field’s name hides: sc->nr_scanned is an in/out parameter, and the core pre-sets it before each call (shrinkctl->nr_scanned = nr_to_scan; in do_shrink_slab()). The header says so directly: “This defaults to nr_to_scan before every call, but the callee should track its actual progress.” A callback that simply ignores nr_scanned is therefore treated as having scanned everything it was asked to, which is usually the right default; a callback that examined fewer objects must lower it, or the quota will be consumed faster than the work was done.

seeks is the cost-to-recreate hint, walked through in detail below. nr_deferred holds 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.

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)

The first two are internal state; the last three are the capability bits a subsystem passes to shrinker_alloc(). Each one changes something concrete about how the core invokes the callbacks.

FlagSet byWhat it changes about invocationWhat the subsystem must guarantee
SHRINKER_REGISTERED (bit 0)shrinker_register()Internal. Marks the descriptor as live on shrinker_list; shrinker_free() uses it to decide whether an unregister is needed—
SHRINKER_ALLOCATED (bit 1)shrinker_alloc()Internal. shrinker_register() refuses a descriptor without it, warning “Must use shrinker_alloc() to dynamically allocate the shrinker” — this is how the old embedded-struct pattern is kept out—
SHRINKER_NUMA_AWARE (bit 2)the subsystemThe shrinker is invoked once per NUMA node, with sc->nid naming the node under pressure, instead of once globally. Also sizes the nr_deferred array to nr_node_ids entries rather than oneObjects must be tracked per node (in practice: use list_lru, which does this for you) so that freeing on nid actually relieves that node. See NUMA Memory Model
SHRINKER_MEMCG_AWARE (bit 3)the subsystemThe shrinker is reachable from shrink_slab_memcg(), which finds it by id through a per-cgroup bitmap rather than by walking shrinker_list; sc->memcg names the cgroup. Deferred counts move from the nr_deferred array into the cgroup’s shrinker_info_unit, and are reparented to the parent cgroup on cgroup destruction (reparent_shrinker_deferred())Objects must be charged to a memcg and countable per cgroup. If CONFIG_MEMCG is off or memcg is disabled on the command line, shrinker_alloc() silently clears this flag and falls back to the global path
SHRINKER_NONSLAB (bit 4)the subsystemMarks a memcg-aware shrinker whose objects are not slab allocations, so kmem accounting does not double-count them. The header restricts it: “It just makes sense when the shrinker is also MEMCG_AWARE for now, non-MEMCG_AWARE shrinker should not have this flag set”Must be combined with SHRINKER_MEMCG_AWARE

Corrected while writing this note

An earlier revision said SHRINKER_NONSLAB is used by “the hugetlb or some GPU shrinkers”. Grepping the v6.12 tree finds exactly one user: the transparent-huge-page deferred-split shrinker, thp-deferred_split, allocated with SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE | SHRINKER_NONSLAB in thp_shrinker_init() (mm/huge_memory.c:825-838, v6.12). That makes sense: the objects it reclaims are partially-unused huge pages queued for splitting, which come from the buddy allocator, not from slab. mm/workingset.c, mm/zswap.c, mm/list_lru.c, mm/memcontrol.c and fs/super.c all have zero occurrences.

The SHRINKER_MEMCG_AWARE distinction is more consequential than it first appears, because it decides which of two entirely separate dispatch functions will ever call your shrinker. That is the subject of the next section.

The Registration API and the Shrinker Lifecycle

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, so a subsystem that freed its own state while a reclaimer sat inside scan_objects() produced a use-after-free. The 6.7-era rework replaced it with three calls.

As of v6.12 the old API is gone, not merely deprecated. Grepping include/linux/shrinker.h and mm/shrinker.c at the v6.12 tag for register_shrinker returns zero matches for the old symbol; the only hits are shrinker_register, shrinker_alloc and shrinker_free (shrinker.h:135-137, shrinker.c:678, :738, :769). Out-of-tree code written against register_shrinker() will not compile.

__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 pattern is allocate → fill in callbacks → register, and the gap between the first two steps is the point of the design: between shrinker_alloc() and shrinker_register() the descriptor exists but is not yet on any list, so the subsystem can finish wiring itself up without any possibility of a reclaimer arriving early. fs/super.c exploits this gap directly — it allocates the shrinker, then hands it to list_lru_init_memcg() for both of its LRUs, and only registers it much later, at sget_fc()/sget() time, once the superblock is actually usable (fs/super.c:378-391 and :787/:870, v6.12):

/* fs/super.c, alloc_super(), v6.12 — verbatim */
s->s_shrink = shrinker_alloc(SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE,
			     "sb-%s", type->name);
if (!s->s_shrink)
	goto fail;
 
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;
 
if (list_lru_init_memcg(&s->s_dentry_lru, s->s_shrink))
	goto fail;
if (list_lru_init_memcg(&s->s_inode_lru, s->s_shrink))
	goto fail;
/* ... much later, in sget_fc()/sget(): */
shrinker_register(s->s_shrink);

Three details in that listing are worth pausing on, and two of them contradict what a plausible reconstruction of the code would say:

  • batch is 1024, not the 128 default. SHRINK_BATCH is 128, but the VFS superblock shrinker sets batch = 1024. Dentry and inode pruning takes the superblock lock and the LRU locks; doing it in 128-object chunks would mean eight times as many lock acquisitions for the same work. Caches with cheap per-call overhead leave batch at 0.
  • seeks is never assigned here. It is 2 anyway, because shrinker_alloc() set it — see the trap flagged earlier. This is the single most likely thing to get wrong when reading this code, because the obvious grep (grep s_shrink fs/super.c) shows no seeks line at all.
  • The name is a format string. "sb-%s" with the filesystem type produces the sb-btrfs:vda2-24-style names visible in <debugfs>/shrinker/, and shrinker_debugfs_rename() updates it once the device is known (fs/super.c:1591).

shrinker_alloc() itself (mm/shrinker.c:678-736) kzallocs the descriptor, allocates the debugfs name, ORs in SHRINKER_ALLOCATED, sets seeks = DEFAULT_SEEKS, and then branches: a memcg-aware shrinker goes to shrinker_memcg_alloc() for an IDR id and gets no nr_deferred array (its deferred counts live per cgroup); a non-memcg-aware one gets a nr_deferred array sized nr_node_ids if it is NUMA-aware and one entry otherwise. shrinker_register() then 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);

Teardown is the interesting half. shrinker_free() drops the initial reference, then blocks on the completion until every in-flight scan_objects() has returned, and only then frees the descriptor via call_rcu(). That is what closes the use-after-free window the old API had.

stateDiagram-v2
  direction TB
  [*] --> Allocated
  Allocated: ALLOCATED<br/>kzalloc'd, flags has SHRINKER_ALLOCATED<br/>seeks = DEFAULT_SEEKS (2)<br/>debugfs name allocated<br/>NOT on any list — unreachable by reclaim
  Configured: CONFIGURED<br/>callbacks, batch, private_data set<br/>list_lru's bound to it<br/>still unreachable by reclaim
  Registered: REGISTERED<br/>on shrinker_list (RCU)<br/>refcount = 1, completion armed<br/>memcg bitmap bit settable
  InUse: IN USE<br/>a reclaimer holds a reference<br/>via shrinker_try_get()
  Draining: DRAINING<br/>shrinker_free() called<br/>removed from shrinker_list<br/>initial reference dropped<br/>waiting on completion
  [*] --> Allocated: shrinker_alloc(flags, fmt, ...)
  Allocated --> Configured: subsystem fills in<br/>count_objects / scan_objects
  Allocated --> [*]: error path — shrinker_free()<br/>before registration, frees immediately
  Configured --> Registered: shrinker_register()<br/>refcount_set(1)
  Registered --> InUse: shrink_slab() or shrink_slab_memcg()<br/>shrinker_try_get() succeeds
  InUse --> Registered: shrinker_put()<br/>refcount back to 1
  Registered --> Draining: shrinker_free()
  InUse --> Draining: shrinker_free() races<br/>with a live scan
  Draining --> Draining: last shrinker_put() calls complete()
  Draining --> [*]: wait_for_completion() returns<br/>then call_rcu(shrinker_free_rcu_cb)

The shrinker registration lifecycle as a state machine. What it shows: there are two states before a shrinker is visible to reclaim (ALLOCATED and CONFIGURED) and two after it stops being visible (DRAINING, then the RCU grace period). Reclaim can only enter through shrinker_try_get(), which is refcount_inc_not_zero() — so once shrinker_free() has dropped the initial reference, no new reclaimer can get in, while existing ones are waited for. The insight to take: the old register_shrinker() API collapsed ALLOCATED+CONFIGURED into one step and had no DRAINING state at all, which is precisely why a subsystem could free its own state out from under a running scan_objects(). The refcount-plus-completion pair is not defensive programming; it is the whole reason the API was changed.

Mechanical Walk-through: do_shrink_slab() and the Proportional Formula

Where reclaim calls in, and the fork in the road

shrink_slab() is invoked from exactly one place per memory cgroup per reclaim pass, and there are two such call sites in mm/vmscan.c depending on which page scanner is in use:

  • Classic LRU: shrink_node_memcgs() calls shrink_lruvec(lruvec, sc) and then shrink_slab(sc->gfp_mask, pgdat->node_id, memcg, sc->priority) (mm/vmscan.c:5917-5920, v6.12).
  • MGLRU: shrink_one() calls try_to_shrink_lruvec(lruvec, sc) and then the identical shrink_slab(...) line (mm/vmscan.c:4822-4824).

This matters for anyone reading a trace on a modern box: MGLRU changes the page scanner completely but does not change slab reclaim at all. The same sc->priority feeds the same shrink_slab() from both. There is also a third caller — drop_slab_node() at vmscan.c:414 calls shrink_slab(GFP_KERNEL, nid, memcg, 0) with priority 0, the maximum-aggression setting, which is what echo 2 > /proc/sys/vm/drop_caches does.

The first thing shrink_slab() does is a dispatch that the “walk the global list” mental model completely omits (mm/shrinker.c:614-624, v6.12):

if (!mem_cgroup_disabled() && !mem_cgroup_is_root(memcg))
	return shrink_slab_memcg(gfp_mask, nid, memcg, priority);

The global shrinker_list walk happens only for the root cgroup. For every other cgroup — which is to say, for essentially all reclaim on a systemd or container host, because every service and every user session lives in its own cgroup — shrink_slab_memcg() runs instead, and it does not iterate a list at all. It looks up memcg->nodeinfo[nid]->shrinker_info, walks a bitmap of shrinker ids that have objects charged to this cgroup, and calls only those. A shrinker that is not memcg-aware is therefore invisible to cgroup reclaim: it has no id, no bit, and will never be called on behalf of a container under memory.max pressure. It only ever runs when the root cgroup itself reclaims.

flowchart TD
  CALL["shrink_slab(gfp_mask, nid, memcg, priority)"] --> Q{"memcg enabled AND<br/>memcg is not root?"}
  Q -->|no — root / memcg off| G["GLOBAL PATH"]
  Q -->|yes| M["MEMCG PATH"]

  G --> G1["rcu_read_lock()"]
  G1 --> G2["list_for_each_entry_rcu(shrinker, &shrinker_list, list)"]
  G2 --> G3{"shrinker_try_get()<br/>refcount_inc_not_zero"}
  G3 -->|fail — being freed| G2
  G3 -->|ok| G4["rcu_read_unlock()<br/>do_shrink_slab() — MAY SLEEP<br/>rcu_read_lock() again<br/>shrinker_put()"]
  G4 --> G2
  G2 --> GD["every registered shrinker,<br/>memcg-aware or not"]

  M --> M1["info = memcg->nodeinfo[nid]->shrinker_info"]
  M1 --> M2["for_each_set_bit(offset, unit->map, 64)"]
  M2 --> M3["shrinker_id = calc_shrinker_id(index, offset)<br/>idr_find(&shrinker_idr, id)"]
  M3 --> M4{"shrinker_try_get()"}
  M4 -->|ok| M5["do_shrink_slab()"]
  M5 --> M6{"returned SHRINK_EMPTY?"}
  M6 -->|yes| M7["clear_bit(offset, unit->map)<br/>skip this shrinker next time<br/>until something is charged"]
  M6 -->|no| M2
  M7 --> M2
  M2 --> MD["only SHRINKER_MEMCG_AWARE shrinkers<br/>that hold objects for THIS cgroup"]

The two dispatch paths inside shrink_slab(). What it shows: the familiar RCU list walk on the left is the root-cgroup path. The right-hand path is what actually runs for any cgroup-confined workload, and it is bitmap-driven — a shrinker is reached by its IDR id, and a shrinker that reports SHRINK_EMPTY has its bit cleared so it is skipped entirely on subsequent passes until set_shrinker_bit() re-arms it. The insight to take: this is why SHRINKER_MEMCG_AWARE is not a nice-to-have. On a container host, a shrinker without it is simply never called during the reclaim that matters, and its cache will grow until global pressure arrives.

The quota arithmetic

Whichever path was taken, the per-shrinker work 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:

  • freeable is what count_objects returned — the number of objects this cache could give up.
  • priority is the reclaim scan priority, an integer that starts at DEF_PRIORITY (12) and is decremented as reclaim grows more desperate; a smaller priority means a bigger shift-divisor inverse, i.e. more aggressive. freeable >> priority is therefore “the fraction of the cache proportional to how hard we are pressing.” At the gentle starting priority of 12, freeable >> 12 is freeable / 4096 — a tiny nibble; as priority falls toward 0 the shift shrinks and the nibble grows toward the whole cache.
  • delta *= 4 scales that nibble up by a constant factor (this 4 pairs 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: seeks answers “how many disk seeks would it take to recreate one of these objects?” An object that is expensive to rebuild (high seeks) yields a smaller delta, so the core frees fewer of them per pass — it is reluctant to throw away things that cost I/O to regenerate. DEFAULT_SEEKS is 2 (“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 else branch handles seeks == 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 get delta = freeable / 2 — a fixed, aggressive half-the-cache quota per pass, with no seeks-based damping.
  • nr >> priority folds in the deferred backlog at the same priority scaling, and the final min(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. Note the loop condition is a disjunction — total_scan >= batch_size || total_scan >= freeable — and the source explains the second clause at length: normally you do not want to disturb a cache for fewer than batch_size objects, but when total_scan has grown past freeable the core must be “really tight on memory” and should “try to reclaim all available objects, otherwise we can end up failing allocations although there are plenty of reclaimable objects spread over several slabs with usage less than the batch_size”. That is the slab-fragmentation case described in the boundary section at the top of this note, handled explicitly.

Finally, whatever quota was not scanned is deferred for next time — and the carry-forward is capped, which the obvious one-line quotation of this code omits:

next_deferred = max_t(long, (nr + delta - scanned), 0);
next_deferred = min(next_deferred, (2 * freeable));
new_nr = add_nr_deferred(next_deferred, shrinker, shrinkctl);

Both clamps matter. The max_t(..., 0) stops the backlog going negative when a shrinker over-delivers. The min(..., 2 * freeable) — the same cap applied to total_scan earlier — stops an unbounded backlog accumulating in a cache that is persistently unable to make progress. The comment states the invariant directly: “And it is capped to two times of the freeable items.” So the deferred mechanism is a bounded memory of unfinished work, not an unbounded debt. 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 count accumulates and is discharged once a permissive context (e.g. kswapd with GFP_KERNEL) comes along — but it also cannot grow without limit while that context fails to arrive.

The arithmetic worked through on real numbers

Formulas with four terms are much easier to trust once you have watched them produce numbers. Take a superblock shrinker holding freeable = 200,000 dentries plus inodes — roughly what the measurement later in this note produced — with seeks = 2 (the default) and batch = 1024 (the fs/super.c value), and no deferred backlog (nr = 0). Reclaim starts gently at DEF_PRIORITY and decrements as it fails to make progress:

priorityfreeable >> prioritydelta = (that × 4) / seekstotal_scan = (nr >> priority) + delta, capped at 2 × freeableBatches of 1024Fraction of the cache scanned
12 (DEF_PRIORITY)4896960 — below batch_size, and 96 < freeable, so the loop body never runs0 % — the whole quota is deferred
101953903900 — still below 10240 %, deferred grows
87811,5621,56210.5 %
63,1256,2506,25063.1 %
412,50025,00025,0002412.5 %
250,000100,000100,0009750 %
1100,000200,000200,000195100 %
0 (drop_caches)200,000400,000400,000 (exactly the 2 × freeable cap)390200 % — every object scanned twice

Three things fall out of the table that prose alone does not convey:

  1. At gentle priorities the shrinker does nothing at all. With batch = 1024, the first three or four priority levels produce a quota below the batch size, so do_shrink_slab() computes a delta, enters no batches, and defers the whole thing. The VFS caches genuinely are not touched until reclaim is already struggling. This is the arithmetic behind the observation, in the measurement section below, that slab_reclaimable sat rock-steady at 133 MiB through seven ballooning steps and then collapsed in one.
  2. The scaling is geometric, not linear. Each two-step drop in priority multiplies the quota by four. Between “barely touching it” and “scanning the whole cache twice” there are only twelve levels, so slab reclaim goes from invisible to total across a narrow band of pressure.
  3. seeks is a plain divisor of the whole curve. Setting seeks = 1 doubles every row; seeks = 0 replaces the formula entirely with delta = freeable / 2 — a flat 100,000 objects at every priority, including the gentlest. That is why seeks = 0 is reserved for caches that cost no I/O to rebuild: mm-shadow, the workingset shadow-node shrinker, is the in-tree example (mm/workingset.c:833, v6.12), and its comment — /* ->count reports only fully expendable nodes */ — explains why that is safe: it only ever counts nodes it is certain are disposable, so aggression costs nothing.

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. Note the granularity: there is no global dentry shrinker. Each mounted superblock registers one shrinker (sb->s_shrink) that covers both of that filesystem’s caches — its dentry LRU (s_dentry_lru) and its inode LRU (s_inode_lru) — through the generic list_lru infrastructure. A host with forty mounts has forty superblock shrinkers, which is why a <debugfs>/shrinker/ listing is dominated by sb-* entries.

super_cache_count() is short enough to read in full (fs/super.c:234-270, v6.12), and every line is load-bearing:

if (!(sb->s_flags & SB_BORN))          /* mid-mount: the fs may be half-built */
	return 0;                      /*   -> "skip me", NOT "I am empty"    */
smp_rmb();                             /* pairs with the barrier in mount_fs() */
 
if (sb->s_op && sb->s_op->nr_cached_objects)
	total_objects = sb->s_op->nr_cached_objects(sb, sc);   /* fs-private cache */
 
total_objects += list_lru_shrink_count(&sb->s_dentry_lru, sc);
total_objects += list_lru_shrink_count(&sb->s_inode_lru, sc);
 
if (!total_objects)
	return SHRINK_EMPTY;           /* genuinely empty -> clear the memcg bit */
 
total_objects = vfs_pressure_ratio(total_objects);   /* apply the sysctl */
return total_objects;

The SB_BORN guard is the mid-mount case, and the comment above it explains why the function deliberately does not take the superblock lock: super_trylock_shared() “is a scalability bottleneck”, so the count runs lock-free against a possibly-inconsistent view and accepts that “counts can change between super_cache_count and super_cache_scan”. The two sentinel returns are used correctly and differently — 0 for “not ready, ask again”, SHRINK_EMPTY for “nothing here”, which as the contract table showed is what lets shrink_slab_memcg() clear this shrinker’s bit.

super_cache_scan() is where the delegation becomes concrete (fs/super.c:177-232, v6.12). It opens with two bail-outs and then divides the quota proportionally:

if (!(sc->gfp_mask & __GFP_FS))
	return SHRINK_STOP;             /* we may hold FS locks; recursing deadlocks */
if (!super_trylock_shared(sb))
	return SHRINK_STOP;             /* someone is unmounting or freezing */
 
inodes   = list_lru_shrink_count(&sb->s_inode_lru, sc);
dentries = list_lru_shrink_count(&sb->s_dentry_lru, sc);
total_objects = dentries + inodes + fs_objects + 1;
 
/* proportion the scan between the caches */
dentries   = mult_frac(sc->nr_to_scan, dentries,   total_objects);
inodes     = mult_frac(sc->nr_to_scan, inodes,     total_objects);
fs_objects = mult_frac(sc->nr_to_scan, fs_objects, total_objects);
 
sc->nr_to_scan = dentries + 1;   freed  = prune_dcache_sb(sb, sc);
sc->nr_to_scan = inodes + 1;     freed += prune_icache_sb(sb, sc);
if (fs_objects) {
	sc->nr_to_scan = fs_objects + 1;
	freed += sb->s_op->free_cached_objects(sb, sc);
}

Four things to notice. First, __GFP_FS is checked before anything else, and the comment gives the exact hazard: “We may hold various FS locks, and we don’t want to recurse into the FS that called us in clear_inode() and friends”. This is the canonical example of the SHRINK_STOP contract — the shrinker refuses the work rather than risking the deadlock, and the quota survives in nr_deferred until kswapd comes along with GFP_KERNEL. Second, the quota is split between the three caches in proportion to their current sizes via mult_frac, so a superblock with a huge dentry cache and a small inode cache is pressed on mostly where the objects are. Third, the + 1 on each nr_to_scan is not rounding slop; the comment says it is deliberate — “Ensure that we always scan at least one object — memcg kmem accounting uses this to fully empty the caches” — because a cgroup being torn down needs its caches to reach exactly zero, and a proportional split that rounds to zero would leave a permanent residue. Fourth, the order is fixed: “prune the dcache first as the icache is pinned by it”. A dentry holds a reference on its inode, so pruning dentries is what makes inodes freeable; doing it the other way round would find every inode pinned and free nothing.

list_lru: how a memcg- and NUMA-aware cache is actually organised

The flags table said a SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE shrinker must be able to count and free objects per node and per cgroup. list_lru is the generic machinery that makes that practical, and it is why fs/super.c can implement both flags in four lines. It was introduced alongside the two-callback shrinker API in 2013 — the header credits Dave Chinner and Glauber Costa — and LWN’s “Smarter shrinkers” covers the design argument of that whole series.

A struct list_lru is not one list. It is a two-dimensional grid of them (include/linux/list_lru.h:31-59, v6.12):

flowchart TB
  LRU["struct list_lru<br/>(e.g. sb->s_dentry_lru)<br/>memcg_aware = true<br/>shrinker_id = 24"]
  LRU --> NODES["struct list_lru_node *node<br/>one per NUMA node<br/>each has its OWN spinlock<br/>____cacheline_aligned_in_smp"]
  LRU --> XA["struct xarray xa<br/>keyed by memcg id"]

  NODES --> N0["node[0]<br/>lock + list_lru_one lru<br/>(root cgroup's list)<br/>nr_items"]
  NODES --> N1["node[1]<br/>lock + list_lru_one lru<br/>nr_items"]

  XA --> MC1["list_lru_memcg for memcg A<br/>list_lru_one node[0], node[1], ...<br/>each: struct list_head + nr_items"]
  XA --> MC2["list_lru_memcg for memcg B<br/>list_lru_one node[0], node[1], ..."]

  MC1 -.->|"locked by"| N0
  MC2 -.->|"locked by"| N0

  SHR["super_cache_count(sc)<br/>list_lru_shrink_count(lru, sc)"] -->|"sc->nid picks the row<br/>sc->memcg picks the column"| CELL["exactly ONE list_lru_one<br/>read its nr_items"]

The list_lru grid that backs a memcg- and NUMA-aware shrinker. What it shows: the rows are NUMA nodes and the columns are cgroups, and list_lru_shrink_count(lru, sc) uses sc->nid and sc->memcg from the shrink_control to select one cell — a struct list_lru_one, which is just a list_head plus an nr_items counter. That is why counting is O(1) rather than a list walk, and why the count can be exact per (node, cgroup) pair. The insight to take: the lock lives on the node, not the cell — struct list_lru_node.lock “protects all lists on the node, including per cgroup”. So cgroups on the same NUMA node contend with each other, but nodes never contend. That is the deliberate trade: per-cgroup isolation of accounting without per-cgroup proliferation of locks.

Two details reward attention. The comment on list_lru_one.nr_items reads /* may become negative during memcg reparenting */ — when a cgroup is destroyed its objects are moved to the parent by memcg_reparent_list_lrus(), and the counters are transiently inconsistent, which is a specific reason count_objects() is documented as best-effort rather than exact. And list_lru_add()’s comment warns that the “already on a list” check is valid “for a list, not this list” — an object cannot be on two list_lrus at once without the caller explicitly removing it first, which is why a dentry is on the dentry LRU or in use, never both.

list_lru_walk_cb callbacks return one of six enum lru_status values — LRU_REMOVED, LRU_REMOVED_RETRY, LRU_ROTATE, LRU_SKIP, LRU_RETRY, LRU_STOP — which is the fine-grained vocabulary prune_dcache_sb() uses to say “freed it”, “give this one another pass because it was referenced”, “could not lock it, skip”, or “stop walking”. The shrinker’s coarse SHRINK_STOP is the aggregate of many such decisions.

The major in-tree shrinkers

There are on the order of a hundred shrinker_alloc() call sites in the tree. The ones below are the ones that show up in practice, with the flags they actually pass, read from the v6.12 source rather than from memory.

Shrinker (debugfs name)WhereFlagsWhat it freesNotes
sb-<fstype>[:<dev>]fs/super.c:378NUMA_AWARE | MEMCG_AWAREdentries, inodes, and the filesystem’s own nr_cached_objectsOne per mounted superblock. batch = 1024. The dominant consumer on almost every host
mm-shadowmm/workingset.c:820NUMA_AWARE | MEMCG_AWAREXArray nodes that hold only shadow entries, from the page cache and the swap cacheseeks = 0 — the aggressive branch, because shadow nodes cost no I/O to rebuild
thp-deferred_splitmm/huge_memory.c:831NUMA_AWARE | MEMCG_AWARE | NONSLABpartially-used transparent huge pages queued for splittingThe only SHRINKER_NONSLAB user in v6.12
thp-zeromm/huge_memory.c:827nonethe shared huge zero pageGlobal, single object
mm-zswapmm/zswap.cNUMA_AWARE | MEMCG_AWAREcompressed pages in the zswap pool, written back to the real swap deviceNot registered on this host — zswap is disabled
dquota-cachefs/quota/dquot.cnone (flags 0)cached struct dquot disk-quota structuresGlobal, not per-superblock and not memcg-aware
mbcache-shrinkerfs/mbcache.cnonethe extended-attribute block cache shared by ext4 and friends
ext4-es:<dev>fs/ext4/extents_status.cnoneext4 extent-status tree entriesPer-superblock but not memcg-aware
jbd2-journal:<maj>:<min>fs/jbd2/journal.cnonejournal revoke/checkpoint buffers
xfs-buf:<dev>, xfs-inodegc:<dev>fs/xfs/xfs_buf.cNUMA_AWAREthe XFS buffer cache; deferred inode-inactivation workXFS registers several shrinkers of its own on top of the generic sb-xfs:*
f2fs-shrinkerfs/f2fs/super.cnonef2fs NAT/extent cachesGlobal across all f2fs mounts
nfs-aclfs/nfs/super.cnonecached NFS access-control entries
x86-mmuarch/x86/kvm/mmu/mmu.cnoneKVM shadow page tablesA reminder that shrinkers are not a VFS-only mechanism
drm-i915_gemdrivers/gpu/drm/i915/gem/i915_gem_shrinker.cnonepurgeable and swappable GPU buffer objects
android-binderdrivers/android/binder_alloc.cnoneunused pages in binder transaction buffers
md-bcache:<uuid>drivers/md/bcache/btree.cnonecached bcache btree nodes

Reading across the Flags column is the most useful thing in this table. The large majority of in-tree shrinkers pass 0 — they are neither NUMA-aware nor memcg-aware. Per the dispatch diagram above, that means none of them are ever called during cgroup-confined reclaim. A container hammering ext4 extent-status entries or NFS ACLs will not shrink those caches by hitting its own memory.max; only global pressure reaches them. The VFS superblock shrinker, the shadow-node shrinker, the THP split queue and zswap are the memcg-aware exceptions, and that set is essentially the list of caches that container platforms can actually contain.

The example listing in the kernel’s own Shrinker Debugfs Interface documentation shows what this looks like on a live system — dquota-cache-16, mm-shadow-18, rcu-kfree-0, sb-btrfs:vda2-24, thp-deferred_split-10, xfs-buf:vda1-37 and a long tail of sb-tmpfs-* — one directory per shrinker, each containing count and scan files that invoke the callbacks directly per memcg and per NUMA node.

vfs_cache_pressure: how the count is biased

vfs_cache_pressure is the one shrinker knob an administrator actually turns, and it is worth being precise about where it acts, because the common description (“controls how aggressively the kernel reclaims dentries and inodes”) hides the mechanism. It does not scale the scan. It scales the count — the last line of super_cache_count(), total_objects = vfs_pressure_ratio(total_objects). The shrinker lies about how many objects it has, and the proportional formula, working from the lie, produces a correspondingly smaller or larger quota. Everything else in the reclaim core is untouched.

In v6.12 the function is a one-liner in the header (include/linux/dcache.h:511-516), with the denominator hard-coded:

extern int sysctl_vfs_cache_pressure;   /* = 100, fs/dcache.c:76 */
 
static inline unsigned long vfs_pressure_ratio(unsigned long val)
{
	return mult_frac(val, sysctl_vfs_cache_pressure, 100);
}

mult_frac(val, num, denom) is the kernel’s overflow-avoiding way of writing val * num / denom. So at the default of 100 the count passes through unchanged; at 50 the shrinker reports half of what it holds and is pressed on half as hard; at 200, double.

SettingReported countEffect, per Documentation/admin-guide/sysctl/vm.rst, v6.12When you would want it
0always 0“the kernel will never reclaim dentries and inodes due to memory pressure and this can easily lead to out-of-memory conditions”Never, in production. Note how it breaks: a count of 0 means “skip this cache”, so the shrinker is disabled — but SHRINK_EMPTY is never returned either, so nothing signals “empty”. The cache simply becomes invisible and grows without bound
1 – 99scaled down“Decreasing vfs_cache_pressure causes the kernel to prefer to retain dentry and inode caches”Metadata-heavy workloads where re-walking directory trees is expensive: build farms, databases with many data files, file servers
100 (default)unchanged“the kernel will attempt to reclaim dentries and inodes at a ‘fair’ rate with respect to pagecache and swapcache reclaim”The default, and correct for most systems
> 100scaled up“causes the kernel to prefer to reclaim dentries and inodes”Hosts churning through millions of short-lived files — CI runners, mail spools — where negative dentries accumulate
≫ 100 (e.g. 1000)scaled up 10דmay have negative performance impact. Reclaim code needs to take various locks to find freeable directory and inode objects. With vfs_cache_pressure=1000, it will look for ten times more freeable objects than there are”Essentially never. The shrinker spends its time taking locks and finding nothing

That last row is the failure mode people do not anticipate: an inflated count does not make reclaim find more objects, it makes do_shrink_slab() request batches that prune_dcache_sb() cannot fill, so the cost is paid in lock traffic with no memory returned.

The vfs_cache_pressure_denom addition — and a source trap

A note-writing trap lives here, and it caught an earlier revision of this note. docs.kernel.org serves the latest documentation, not the pinned release’s. Fetching the vfs_cache_pressure section from docs.kernel.org in 2026 returns text phrased in terms of a vfs_cache_pressure_denom, which does not exist at v6.12 at all — leading to a confident-but-wrong claim that v6.12 had a numerator/denominator pair. Existence-checking include/linux/dcache.h and fs/dcache.c across tags settles it:

Tagvfs_pressure_ratio lives inFormulavfs_cache_pressure_denom?
v6.12, v6.13, v6.14include/linux/dcache.h (static inline)mult_frac(val, sysctl_vfs_cache_pressure, 100)no
v6.15fs/dcache.c (out-of-line, EXPORT_SYMBOL_GPL)mult_frac(val, sysctl_vfs_cache_pressure, 100)no
v6.16 – v7.1fs/dcache.cmult_frac(val, sysctl_vfs_cache_pressure, sysctl_vfs_cache_pressure_denom)yes, default 100

The knob was added by commit e7b9cea718ee, “vfs: Add sysctl vfs_cache_pressure_denom for bulk file operations” (Yafang Shao, dated 11 May 2025, merged for v6.16 via the vfs-6.16-rc1.misc tag). The motivating story is worth quoting because it is a real production constraint that the old knob could not express:

On our HDFS servers with 12 HDDs per server, a HDFS datanode startup involves scanning all files and caching their metadata (including dentries and inodes) in memory. Each HDD contains approximately 2 million files, resulting in a total of ~20 million cached dentries after initialization. To minimize dentry reclamation, we set vfs_cache_pressure to 1. Despite this configuration, memory pressure conditions can still trigger reclamation of up to 50% of cached dentries […] The current minimum reclaim ratio (1/100 of total dentries) remains too aggressive for our workload.

The problem is a floor, not a slope: with a fixed denominator of 100, the gentlest possible setting is 1/100, and one percent of twenty million dentries is still two hundred thousand objects per pass. The fix does not change the formula’s shape, only its resolution — [vfs_cache_pressure=1, vfs_cache_pressure_denom=10000] gives a ratio of 1/10000 and, per the commit, “effectively maintains the full 20 million dentry cache under memory pressure”.

On this host (kernel 7.1.13), both files exist and read the defaults:

$ cat /proc/sys/vm/vfs_cache_pressure /proc/sys/vm/vfs_cache_pressure_denom
100
100

Neither is writable without privilege here (mode 0644, owner root), so the effect of varying them was not measured — only the arithmetic was read from source.

Measured: 133 MiB of Dentries, Then One Reclaim Episode

The formula above predicts a very particular shape: nothing, nothing, nothing, then a collapse. This section is that prediction measured on a live host, unprivileged.

What could not be instrumented, and why

Start with the negative result, because it changes which tools the rest of this section can use. The standard advice — read /proc/slabinfo, run slabtop -o, poke <debugfs>/shrinker/ — is unavailable to an unprivileged user on a modern Fedora host:

$ ls -l /proc/slabinfo
-r--------. 1 root root 0 Sep 12 13:49 /proc/slabinfo
 
$ head -3 /proc/slabinfo
head: cannot open '/proc/slabinfo' for reading: Permission denied
 
$ slabtop -o
slabtop: Unable to create slabinfo structure: Permission denied
 
$ ls /sys/kernel/debug/shrinker
ls: cannot access '/sys/kernel/debug/shrinker': Permission denied
 
$ ls -l /proc/sys/vm/drop_caches
--w-------. 1 root root 0 Sep 11 23:12 /proc/sys/vm/drop_caches

/proc/slabinfo is mode 0400, owner root — a deliberate hardening measure, since per-cache object counts are a usable side channel. slabtop is a /proc/slabinfo reader and fails with it. /sys/kernel/debug is 0700, so the per-shrinker count/scan files are out of reach, and drop_caches is write-only to root, so the usual “force the shrinkers and watch” trick is unavailable too. Every demonstration below therefore uses different instruments than the conventional ones, and no claim here rests on slabinfo.

Three unprivileged substitutes exist, and two of them are better than what they replace:

InstrumentPathScopeWhat it gives
slab_reclaimable / slab_unreclaimable<cgroup>/memory.statper cgroupBytes of slab charged to this cgroup. Strictly better than /proc/slabinfo for an experiment, because it isolates the test from everything else on a busy host
slabs_scanned/proc/vmstatglobal, monotonicTotal objects passed to scan_objects() since boot — count_vm_events(SLABS_SCANNED, shrinkctl->nr_scanned) at mm/shrinker.c:442, v6.12. This is the direct shrinker activity counter and it is world-readable
SReclaimable / KReclaimable / Slab/proc/meminfoglobalCoarse totals, no per-cache breakdown
pginodesteal / kswapd_inodesteal / drop_slab/proc/vmstatglobalInodes reclaimed by the legacy invalidate_inodes steal path, and drop_caches slab invocations — useful for telling which path freed something

The experiment

Inside a systemd-run --user --scope cgroup capped at MemoryMax=400M with swap disabled for the scope, the test creates 120,000 empty files across 120 directories on btrfs, walks them all with find to pull every dentry and inode onto the superblock LRUs, then allocates anonymous memory in 32 MiB steps, sampling memory.stat and /proc/vmstat after each step. Because the files are created inside the cgroup, their dentries and inodes are charged to it, so the memcg-aware sb-btrfs:* shrinker is the one under test — this is the shrink_slab_memcg() bitmap path, not the global list walk.

$ systemd-run --user --scope -q -p MemoryMax=400M -p MemorySwapMax=0 python3 shrinkexp2.py
 
cgroup /sys/fs/cgroup/.../shrinkexp2-1996371.scope  max 419430400
00-baseline        current=    6M slab_reclaim=    0M anon=  5M file=   0M slabs_scanned=25609575
01-created-120k    current=  144M slab_reclaim=  132M anon=  5M file=   0M slabs_scanned=25609575
02-walked          current=  144M slab_reclaim=  132M anon=  5M file=   0M slabs_scanned=25609575
03-balloon-  32M   current=  176M slab_reclaim=  132M anon=  5M file=  32M slabs_scanned=25609575
03-balloon-  64M   current=  208M slab_reclaim=  133M anon=  5M file=  64M slabs_scanned=25609575
03-balloon-  96M   current=  240M slab_reclaim=  133M anon=  5M file=  96M slabs_scanned=25609575
03-balloon- 128M   current=  272M slab_reclaim=  133M anon=  5M file= 128M slabs_scanned=25609575
03-balloon- 160M   current=  304M slab_reclaim=  133M anon=  5M file= 160M slabs_scanned=25609575
03-balloon- 192M   current=  337M slab_reclaim=  133M anon=  5M file= 192M slabs_scanned=25609575
03-balloon- 224M   current=  369M slab_reclaim=  133M anon=  5M file= 224M slabs_scanned=25609575
03-balloon- 256M   current=  278M slab_reclaim=   12M anon=  5M file= 256M slabs_scanned=25990503
03-balloon- 288M   current=  299M slab_reclaim=    2M anon=  5M file= 288M slabs_scanned=25990503
03-balloon- 320M   current=  330M slab_reclaim=    0M anon=  5M file= 320M slabs_scanned=25990503
03-balloon- 352M   current=  362M slab_reclaim=    0M anon=  5M file= 352M slabs_scanned=25990503
03-balloon- 384M   current=  394M slab_reclaim=    0M anon=  5M file= 384M slabs_scanned=25990503

Read it line by line.

Creating and walking 120,000 empty files charged 132 MiB of reclaimable slab to the cgroup — about 1.1 KiB per file, which is a dentry plus a btrfs_inode plus their list_lru linkage. Note that slabs_scanned did not move during creation: filling a cache is not shrinker activity.

Through seven ballooning steps, slab_reclaimable does not move. From 32 MiB to 224 MiB of ballooned memory, memory.current climbs from 176 MiB to 369 MiB against a 400 MiB cap, and the dentry cache sits at 132–133 MiB, untouched. This is exactly the top rows of the arithmetic table: at gentle priorities the quota for a batch = 1024 shrinker is smaller than one batch, so do_shrink_slab() computes a delta, runs zero batches, and defers everything. The shrinkers are being called and are doing nothing, which is the correct behaviour and is invisible to any metric except slabs_scanned.

At the next step, everything happens at once. memory.current goes down from 369 MiB to 278 MiB despite 32 MiB more being allocated, slab_reclaimable collapses from 133 MiB to 12 MiB — a 91 % reduction in a single reclaim episode — and slabs_scanned jumps from 25,609,575 to 25,990,503, +380,928 objects scanned. That single number is the most direct evidence in this note: 380,928 is the sum of shrinkctl->nr_scanned across every scan_objects() call the core made, and it is roughly twice the object count the cache held, which is precisely the min(total_scan, 2 * freeable) ceiling being reached. Reclaim went from priority 12 to a priority low enough to scan the cache twice over, inside one allocation.

Two more steps finish the job, 12 MiB → 2 MiB → 0 MiB, with slabs_scanned unchanged — the remaining objects were freed by the deferred backlog discharging, not by new scans.

pginodesteal and kswapd_inodesteal never moved (both flat at 0 and 17,088 respectively across the whole run). That rules out the legacy invalidate_inodes path and confirms the freeing came through super_cache_scan() → prune_dcache_sb() / prune_icache_sb() as described above.

Uncertain

Verify: the attribution of the 380,928 scanned objects specifically to the sb-btrfs:* shrinker. slabs_scanned is a global counter with no per-shrinker breakdown available unprivileged, and the host was not idle — other cgroups’ reclaim could in principle contribute. The attribution rests on the timing (the counter is frozen for the entire run and jumps only at the step where this cgroup’s slab_reclaimable collapses) and on the magnitude (≈2× the object count this cgroup held), which is strong circumstantial evidence but not proof. Reason: /sys/kernel/debug/shrinker/<name>/scan is mode-0700-blocked, and /proc/slabinfo is root-only, so neither per-shrinker nor per-cache attribution was possible. To resolve: read <debugfs>/shrinker/sb-btrfs:*/count and /scan before and after, as root. uncertain

A note on the host

FactResolving commandValue
Kerneluname -r7.1.13-200.fc44.x86_64 (a stable release; this note’s source pin is v6.12, and the relevant mm/shrinker.c logic is unchanged between them except where noted)
Test filesystemdf -T /homebtrfs — chosen deliberately: /tmp on this host is tmpfs, whose dentries and inodes behave differently and whose pages are shmem, which would have silently changed what was being measured
Cgroup controllerscat <user@.service>/cgroup.controllerscpu io memory pids — the memory controller is delegated to the user session, which is what makes an unprivileged MemoryMax scope possible at all
vfs_cache_pressurecat /proc/sys/vm/vfs_cache_pressure100 (default — the counts above are unscaled)
MGLRUcat /sys/kernel/mm/lru_gen/enabled0x0007 — on, so shrink_slab() was reached via shrink_one(), not shrink_node_memcgs()

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/SReclaimable in /proc/meminfo that never drops under pressure, ending in an OOM kill despite “reclaimable”-looking memory.
  • Returning the wrong count. A count_objects that overstates freeable objects makes the core scan harder than warranted (wasted CPU); one that understates leaves memory pinned. Returning 0 permanently disables the shrinker (this is exactly what vfs_cache_pressure=0 engineers).
  • scan_objects that blocks in the wrong context. If a callback sleeps or does I/O when sc->gfp_mask forbids it (!__GFP_FS/!__GFP_IO, or atomic context), it can deadlock against the very filesystem that is trying to reclaim. The contract is to return SHRINK_STOP/0 and let the deferred count carry the work to a permissive context.
  • Deferred-count runaway is bounded twice. Both total_scan and next_deferred are clamped to 2 * freeable, so neither a single pass nor the accumulated backlog can grow without limit. What can still happen is repeated heavy passes when a cache reports a large freeable and then consistently fails to free anything — the cap scales with the reported count, so a cache that lies large stays expensive. This is the same mechanism as the vfs_cache_pressure=1000 pathology, arriving from the other direction.
  • A non-memcg-aware cache in a container. The quietest failure of all, and the one the flags table exists to make visible. A shrinker registered with flags 0 is unreachable from shrink_slab_memcg(), so a containerised workload hitting its own memory.max will never shrink it. The symptom is a container that OOMs while the host shows gigabytes of SReclaimable belonging to, say, ext4-es or nfs-acl. The container cannot free it; only global pressure can.
  • Fragmentation, not object count, is what pins the memory. A shrinker that reports freeing 90 % of its objects may release almost no pages, because the surviving 10 % can be spread one per slab page. slab_reclaimable in memory.stat counts the bytes of the pages, not the live objects, so the two can diverge sharply. See the boundary discussion with The Slab Allocator and SLUB at the top of this note.
  • Diagnosis tools — and their permissions. The conventional advice does not survive contact with a hardened distribution. On the Fedora 44 host measured above, /proc/slabinfo is mode 0400 root-only (so slabtop fails outright), /sys/kernel/debug is 0700 (so the CONFIG_SHRINKER_DEBUG per-shrinker count/scan files are unreachable), and /proc/sys/vm/drop_caches is write-only to root (so echo 2 > drop_caches is unavailable). What is readable unprivileged: slabs_scanned in /proc/vmstat, which is the direct global shrinker-activity counter and the single most useful number for “are the shrinkers doing anything at all”; slab_reclaimable / slab_unreclaimable in any delegated cgroup’s memory.stat, which is per-cgroup and therefore better than slabinfo for isolating one workload; and SReclaimable / KReclaimable in /proc/meminfo for coarse totals. Reach for the debugfs interface only when you have root and need per-shrinker attribution.

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 by the lifetime hazard the state diagram above makes explicit: an owner freed while a reclaimer sat inside scan_objects(). The refcount-plus-completion model closes that window by construction.

Uncertain

Verify: the claim that the pre-6.7 lifetime hole produced use-after-free bugs “notably in GPU and filesystem drivers” — an earlier revision of this note asserted specific subsystems. The mechanism is confirmed from the v6.12 source (shrinker_try_get() is refcount_inc_not_zero(), shrinker_free() waits on a completion, and shrinker_register() refuses a non-shrinker_alloc() descriptor with an explicit warning), but no specific bug report was located. Reason: lore.kernel.org returns HTTP 403 to curl here, and the LWN article an earlier revision cited for this refactor — lwn.net/Articles/945226/ — is a 404 (“404 - Not Found”, 3,906 bytes), so that citation has been removed rather than kept as a dead link. To resolve: search the linux-mm archive via a working public-inbox mirror for the cover letter of the shrinker_alloc() series. uncertain

A documented production case. The most concrete real-world shrinker-tuning story in the tree is the one behind vfs_cache_pressure_denom: a fleet of HDFS DataNodes with 12 HDDs each, caching metadata for roughly 20 million files, where vfs_cache_pressure=1 — the most conservative setting the old knob could express — still permitted memory pressure to evict half the dentry cache, and every DataNode restart then paid a long cache-rebuild penalty. The operator’s fix was not a workaround but a kernel change: extend the ratio’s denominator so 1/10000 becomes expressible (commit e7b9cea718ee, merged for v6.16). It is a good illustration of the general lesson: the shrinker knobs are ratios applied to a reported count, so on a cache large enough, even the gentlest ratio is a large absolute number of objects.

What the measurement section above adds operationally. The 133 MiB → 12 MiB collapse in a single allocation step is worth internalising, because it means dentry-cache size is not a smooth function of memory pressure. Monitoring slab_reclaimable at 30-second intervals will show a cache that appears stable and then, between two samples, is gone. If cache warmth matters to your latency (the HDFS case above, or any metadata-heavy service), the metric to alert on is not the cache size but slabs_scanned in /proc/vmstat — it moves before the collapse is visible in the size, because the early passes defer rather than free.

See Also

  • Memory Reclaim Overview — the umbrella reclaim path that invokes shrink_slab alongside 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_mask permits a scan_objects callback to do.
  • The Memory Cgroup memcg — per-cgroup reclaim relies on SHRINKER_MEMCG_AWARE.
  • NUMA Memory Model — SHRINKER_NUMA_AWARE lets reclaim target the short node.
  • The Swap Cache — shares the mm-shadow shrinker: swap-cache XArray nodes that hold only shadow entries are reclaimed by the same seeks = 0 callback that reclaims the page cache’s.
  • The Page Cache — the other producer of shadow nodes, and the reclaim target that slab reclaim runs alongside at the same priority.
  • The Buddy Allocator — where freed slab pages ultimately return, and the reason object-level reclaim is only half the job.
  • zswap — registers mm-zswap, one of the few memcg-aware non-VFS shrinkers.
  • The OOM Killer — what happens when shrink_slab() plus the page scanner still cannot meet the allocation.
  • The Page Fault Handler — the other side of the __GFP_FS constraint: an allocation made while holding filesystem locks is why super_cache_scan() must be able to refuse.
  • UP: Linux Memory Management MOC (§8 Memory Reclaim and the LRU).