[PATCH 2/2] mm/shrinker: allocate the per-memcg shrinker units on demand

From: Konstantin Khorenko

Date: Mon Oct 19 2026 - 13:46:03 EST


Instead of a shrinker_info_unit for every SHRINKER_UNIT_BITS ids up to
shrinker_nr_max on every NUMA node, give a memory cgroup only the units
of the shrinkers it holds a list_lru list of.

The rule the code relies on:

A non-root memcg that has a list on a list_lru has the unit of that
list's shrinker, on every node.

The unit is installed before the list is published and lives until the
memcg is freed, so set_shrinker_bit() always finds it. A reader that
finds none knows the memcg never held objects of that shrinker.

Two cases outside the rule:
- the root memcg, where global reclaim keeps the deferred work of
memcg-aware shrinkers; it gets every unit up front and is the only
memcg a shrinker registration still visits;
- under cgroup.memory=nokmem a list_lru is never per-memcg, so it gets
no shrinker id either and no bit can be set.

The install runs with the gfp of the list allocation and can sit under
the i_pages lock, which is taken from interrupts, so the xarray lock is
irq-safe and a node it could not get under the lock is allocated
outside of it, as list_lru does for its own xarray.

The units of a memcg now scale with the shrinkers it and its
descendants hold objects of, not with shrinker_nr_max.

Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@xxxxxxxxxxxxx>
---
include/linux/memcontrol.h | 2 +
mm/list_lru.c | 15 +++++--
mm/memcontrol.c | 7 +---
mm/shrinker.c | 98 +++++++++++++++++++++++++++++-----------------
4 files changed, 77 insertions(+), 45 deletions(-)

diff --git a/include/linux/memcontrol.h b/include/linux/memcontrol.h
index 52f3646e244ae..91641cf082a4a 100644
--- a/include/linux/memcontrol.h
+++ b/include/linux/memcontrol.h
@@ -1678,6 +1678,8 @@ static inline u64 mem_cgroup_get_socket_pressure(struct mem_cgroup *memcg)

void shrinker_unit_cache_init(void);
int alloc_shrinker_info(struct mem_cgroup *memcg);
+int memcg_shrinker_unit_alloc(struct mem_cgroup *memcg, int shrinker_id,
+ gfp_t gfp);
void free_shrinker_info(struct mem_cgroup *memcg);
void set_shrinker_bit(struct mem_cgroup *memcg, int nid, int shrinker_id);
void reparent_shrinker_deferred(struct mem_cgroup *memcg);
diff --git a/mm/list_lru.c b/mm/list_lru.c
index 36662d02ff963..73480d8edf641 100644
--- a/mm/list_lru.c
+++ b/mm/list_lru.c
@@ -597,6 +597,12 @@ static int __memcg_list_lru_alloc(struct mem_cgroup *memcg,
parent = parent_mem_cgroup(pos);
}

+ if (!css_is_dying(&pos->css) &&
+ memcg_shrinker_unit_alloc(pos, lru_shrinker_id(lru), gfp)) {
+ kfree(mlru);
+ return -ENOMEM;
+ }
+
if (!mlru) {
mlru = memcg_init_list_lru_one(lru, gfp);
if (!mlru)
@@ -666,13 +672,14 @@ int __list_lru_init(struct list_lru *lru, bool memcg_aware, struct shrinker *shr
int i;

#ifdef CONFIG_MEMCG
- if (shrinker)
+ if (mem_cgroup_kmem_disabled())
+ memcg_aware = false;
+
+ /* A list that is not per-memcg sets no memcg's shrinker bit. */
+ if (shrinker && memcg_aware)
lru->shrinker_id = shrinker->id;
else
lru->shrinker_id = -1;
-
- if (mem_cgroup_kmem_disabled())
- memcg_aware = false;
#endif

lru->node = kzalloc_objs(*lru->node, nr_node_ids);
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index a13f3984f0e09..07075bb220719 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -4110,7 +4110,7 @@ static bool alloc_mem_cgroup_per_node_info(struct mem_cgroup *memcg, int node)
INIT_LIST_HEAD(&pn->objcg_list);

lruvec_init(&pn->lruvec);
- xa_init(&pn->shrinker_units);
+ xa_init_flags(&pn->shrinker_units, XA_FLAGS_LOCK_IRQ);
pn->memcg = memcg;

memcg->nodeinfo[node] = pn;
@@ -4277,11 +4277,6 @@ static int mem_cgroup_css_online(struct cgroup_subsys_state *css)

memcg_online_kmem(memcg);

- /*
- * A memcg must be visible for expand_shrinker_info()
- * by the time the maps are allocated. So, we allocate maps
- * here, when mem_cgroup_iter() can't skip it.
- */
if (alloc_shrinker_info(memcg))
goto offline_kmem;

diff --git a/mm/shrinker.c b/mm/shrinker.c
index 74fc6f64459f0..cd9441f149363 100644
--- a/mm/shrinker.c
+++ b/mm/shrinker.c
@@ -42,31 +42,40 @@ static struct shrinker_info_unit *shrinker_unit_get(struct mem_cgroup *memcg,
}

/*
- * A memcg is visible to expand_shrinker_info() before it goes online, so
- * a unit may already be there from either side.
+ * Installs run from the list_lru allocation path with the caller's gfp,
+ * possibly under the i_pages lock, which is taken from interrupts: the
+ * xarray lock is irq-safe and a node it could not get under the lock is
+ * allocated outside of it.
*/
-static int shrinker_unit_install(struct mem_cgroup *memcg, int nid, int index)
+static int shrinker_unit_install(struct mem_cgroup *memcg, int nid, int index,
+ gfp_t gfp)
{
- struct xarray *units = &memcg->nodeinfo[nid]->shrinker_units;
+ XA_STATE(xas, &memcg->nodeinfo[nid]->shrinker_units, index);
struct shrinker_info_unit *unit;
- int ret;
+ unsigned long flags;

- if (xa_load(units, index))
+ if (xa_load(xas.xa, index))
return 0;

- unit = kmem_cache_alloc_node(shrinker_unit_cachep,
- GFP_KERNEL | __GFP_ZERO, nid);
+ unit = kmem_cache_alloc_node(shrinker_unit_cachep, gfp | __GFP_ZERO,
+ nid);
if (!unit)
return -ENOMEM;

- ret = xa_insert(units, index, unit, GFP_KERNEL);
- if (ret) {
+ do {
+ xas_lock_irqsave(&xas, flags);
+ if (!xas_load(&xas)) {
+ xas_store(&xas, unit);
+ if (!xas_error(&xas))
+ unit = NULL;
+ }
+ xas_unlock_irqrestore(&xas, flags);
+ } while (xas_nomem(&xas, gfp));
+
+ if (unit)
kmem_cache_free(shrinker_unit_cachep, unit);
- if (ret == -EBUSY)
- ret = 0;
- }

- return ret;
+ return xas_error(&xas);
}

static int shrinker_units_install(struct mem_cgroup *memcg, int start, int end)
@@ -75,7 +84,8 @@ static int shrinker_units_install(struct mem_cgroup *memcg, int start, int end)

for_each_node(nid) {
for (index = start; index < end; index++) {
- ret = shrinker_unit_install(memcg, nid, index);
+ ret = shrinker_unit_install(memcg, nid, index,
+ GFP_KERNEL);
if (ret)
return ret;
}
@@ -108,10 +118,17 @@ void free_shrinker_info(struct mem_cgroup *memcg)
mutex_unlock(&shrinker_mutex);
}

+/*
+ * Global reclaim keeps the deferred work of memcg-aware shrinkers in the
+ * root's units, and root has no list_lru allocation to install them from.
+ */
int alloc_shrinker_info(struct mem_cgroup *memcg)
{
int ret;

+ if (!mem_cgroup_is_root(memcg))
+ return 0;
+
mutex_lock(&shrinker_mutex);
ret = shrinker_units_install(memcg, 0,
shrinker_id_to_index(shrinker_nr_max));
@@ -124,26 +141,15 @@ int alloc_shrinker_info(struct mem_cgroup *memcg)

static int expand_shrinker_info(int new_id)
{
- int ret = 0;
int new_nr_max = round_up(new_id + 1, SHRINKER_UNIT_BITS);
- struct mem_cgroup *memcg;
-
- if (!root_mem_cgroup)
- goto out;
+ int ret = 0;

lockdep_assert_held(&shrinker_mutex);

- memcg = mem_cgroup_iter(NULL, NULL, NULL);
- do {
- ret = shrinker_units_install(memcg,
+ if (root_mem_cgroup)
+ ret = shrinker_units_install(root_mem_cgroup,
shrinker_id_to_index(shrinker_nr_max),
shrinker_id_to_index(new_nr_max));
- if (ret) {
- mem_cgroup_iter_break(NULL, memcg);
- goto out;
- }
- } while ((memcg = mem_cgroup_iter(NULL, memcg, NULL)) != NULL);
-out:
if (!ret)
shrinker_nr_max = new_nr_max;

@@ -211,7 +217,8 @@ static long xchg_nr_deferred_memcg(int nid, struct shrinker *shrinker,
struct shrinker_info_unit *unit;

unit = shrinker_unit_get(memcg, nid, shrinker->id);
- if (WARN_ON_ONCE(!unit))
+ /* No unit: this memcg never held objects of this shrinker. */
+ if (!unit)
return 0;

return atomic_long_xchg(&unit->nr_deferred[shrinker_id_to_offset(shrinker->id)], 0);
@@ -223,12 +230,35 @@ static long add_nr_deferred_memcg(long nr, int nid, struct shrinker *shrinker,
struct shrinker_info_unit *unit;

unit = shrinker_unit_get(memcg, nid, shrinker->id);
- if (WARN_ON_ONCE(!unit))
+ if (!unit)
return 0;

return atomic_long_add_return(nr, &unit->nr_deferred[shrinker_id_to_offset(shrinker->id)]);
}

+/*
+ * Called before @memcg gets a list on a list_lru of shrinker @shrinker_id:
+ * a map bit is only ever set on a memcg that has such a list.
+ */
+int memcg_shrinker_unit_alloc(struct mem_cgroup *memcg, int shrinker_id,
+ gfp_t gfp)
+{
+ int nid, ret;
+
+ if (shrinker_id < 0 || mem_cgroup_is_root(memcg))
+ return 0;
+
+ for_each_node(nid) {
+ ret = shrinker_unit_install(memcg, nid,
+ shrinker_id_to_index(shrinker_id),
+ gfp);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
void reparent_shrinker_deferred(struct mem_cgroup *memcg)
{
struct mem_cgroup *parent = parent_mem_cgroup(memcg);
@@ -237,14 +267,13 @@ void reparent_shrinker_deferred(struct mem_cgroup *memcg)
int nid, offset;
long nr;

- /* Prevent from concurrent shrinker_info expand */
- mutex_lock(&shrinker_mutex);
for_each_node(nid) {
struct xarray *parent_units = &parent->nodeinfo[nid]->shrinker_units;

xa_for_each(&memcg->nodeinfo[nid]->shrinker_units, index, child_unit) {
parent_unit = xa_load(parent_units, index);
- if (WARN_ON_ONCE(!parent_unit))
+ /* Installed but never listed: nothing deferred in it. */
+ if (!parent_unit)
continue;
for (offset = 0; offset < SHRINKER_UNIT_BITS; offset++) {
nr = atomic_long_read(&child_unit->nr_deferred[offset]);
@@ -252,7 +281,6 @@ void reparent_shrinker_deferred(struct mem_cgroup *memcg)
}
}
}
- mutex_unlock(&shrinker_mutex);
}
#else
static int shrinker_memcg_alloc(struct shrinker *shrinker)

--
2.43.0