The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [PATCH v3 0/3] make unused huge shrinker memcg aware
@ 2026-08-03  8:46 Qi Zheng
  2026-08-03  8:46 ` [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible() Qi Zheng
                   ` (4 more replies)
  0 siblings, 5 replies; 11+ messages in thread
From: Qi Zheng @ 2026-08-03  8:46 UTC (permalink / raw)
  To: hughd, baolin.wang, usama.arif, brauner, akpm, david
  Cc: linux-mm, linux-kernel, Qi Zheng

From: Qi Zheng <zhengqi.arch@bytedance.com>

Changes in v3:
 - add a fix patch to fix missed removal of super_fs_objects_eligible()
 - move the original shrinklist addition logic after all checks are completed,
   and split it into a separate patch. (suggested by Baolin)
 - simplify the shmem_unused_huge_requeue() (suggested by Baolin)
 - keep the move_back label in shmem_unused_huge_shrink() (suggested by Baolin)
 - rebase onto the next-20260731

Changes in v2:
 - temporarily add the dependent patch from Usama to the series for review
   convenience
 - remove shrinklist_scan and shrinklist_isolated from struct shmem_inode_info,
   and re-implement the logic by resuing the same info->shrinklist
   (suggested by Baolin)
 - add more comments (suggested by Andrew)
 - fix missing initialization of info->shrinklist_memcg (pointed by sashiko)
 - rebase onto the next-20260717

Qi Zheng (3):
  fs: fix missed removal of super_fs_objects_eligible()
  mm: shmem: move unused huge shrinklist queuing past the truncation
    check
  mm: shmem: make unused huge shrinker memcg aware

 fs/super.c               |  18 +-
 include/linux/shmem_fs.h |  12 +-
 mm/shmem.c               | 361 +++++++++++++++++++++++++++++----------
 3 files changed, 283 insertions(+), 108 deletions(-)

-- 
2.54.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible()
  2026-08-03  8:46 [PATCH v3 0/3] make unused huge shrinker memcg aware Qi Zheng
@ 2026-08-03  8:46 ` Qi Zheng
  2026-08-03 21:20   ` Andrew Morton
  2026-08-03  8:46 ` [PATCH v3 2/3] mm: shmem: move unused huge shrinklist queuing past the truncation check Qi Zheng
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 11+ messages in thread
From: Qi Zheng @ 2026-08-03  8:46 UTC (permalink / raw)
  To: hughd, baolin.wang, usama.arif, brauner, akpm, david
  Cc: linux-mm, linux-kernel, Qi Zheng

From: Qi Zheng <zhengqi.arch@bytedance.com>

Commit 0ef8faff490be ("fs: push nr_cached_objects memcg gating into
individual filesystems") forgot to drop the super_fs_objects_eligible(),
just fix it.

Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
---
 fs/super.c | 18 ++----------------
 1 file changed, 2 insertions(+), 16 deletions(-)

diff --git a/fs/super.c b/fs/super.c
index 5feecf5d9038d..36b249f002a9a 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -171,19 +171,6 @@ static void super_wake(struct super_block *sb, unsigned int flag)
 	wake_up_var(&sb->s_flags);
 }
 
-/*
- * The s_op->nr_cached_objects hooks (used for example by btrfs and xfs)
- * operate on filesystem-global state and ignore sc->memcg. Driving them
- * from per-memcg shrink_slab_memcg() invocations only burns CPU walking
- * per-cpu counters and queueing duplicate work: the actual reclaim happens on
- * the global path (kswapd or root direct reclaim) regardless. Restrict them
- * to that path.
- */
-static inline bool super_fs_objects_eligible(struct shrink_control *sc)
-{
-	return !sc->memcg || mem_cgroup_is_root(sc->memcg);
-}
-
 /*
  * One thing we have to be careful of with a per-sb shrinker is that we don't
  * drop the last active reference to the superblock from within the shrinker.
@@ -213,7 +200,7 @@ static unsigned long super_cache_scan(struct shrinker *shrink,
 	if (!super_trylock_shared(sb))
 		return SHRINK_STOP;
 
-	if (sb->s_op->nr_cached_objects && super_fs_objects_eligible(sc))
+	if (sb->s_op->nr_cached_objects)
 		fs_objects = sb->s_op->nr_cached_objects(sb, sc);
 
 	inodes = list_lru_shrink_count(&sb->s_inode_lru, sc);
@@ -274,8 +261,7 @@ static unsigned long super_cache_count(struct shrinker *shrink,
 		return 0;
 	smp_rmb();
 
-	if (sb->s_op && sb->s_op->nr_cached_objects &&
-	    super_fs_objects_eligible(sc))
+	if (sb->s_op && sb->s_op->nr_cached_objects)
 		total_objects = sb->s_op->nr_cached_objects(sb, sc);
 
 	total_objects += list_lru_shrink_count(&sb->s_dentry_lru, sc);
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 2/3] mm: shmem: move unused huge shrinklist queuing past the truncation check
  2026-08-03  8:46 [PATCH v3 0/3] make unused huge shrinker memcg aware Qi Zheng
  2026-08-03  8:46 ` [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible() Qi Zheng
@ 2026-08-03  8:46 ` Qi Zheng
  2026-08-04  7:03   ` Qi Zheng
  2026-08-03  8:46 ` [PATCH v3 3/3] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 11+ messages in thread
From: Qi Zheng @ 2026-08-03  8:46 UTC (permalink / raw)
  To: hughd, baolin.wang, usama.arif, brauner, akpm, david
  Cc: linux-mm, linux-kernel, Qi Zheng

From: Qi Zheng <zhengqi.arch@bytedance.com>

The shmem_get_folio_gfp() adds the inode to the unused huge shrinker list
at the alloced label, but a subsequent truncation check may still fail and
remove the folio, leaving the inode on the list with a stale folio.

The original code works because the shrinker re-looks-up the folio and
drops stale entries, but it is cleaner to queue the inode only after all
checks that might remove the folio have passed.

So just make the pure structural move with no functional change, and it
serves as preparation for the memcg-aware shrinker conversion.

Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
---
 mm/shmem.c | 48 +++++++++++++++++++++++++++---------------------
 1 file changed, 27 insertions(+), 21 deletions(-)

diff --git a/mm/shmem.c b/mm/shmem.c
index 74dc3d900067a..bc0b768135113 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -2535,27 +2535,6 @@ static int shmem_get_folio_gfp(struct inode *inode, pgoff_t index,
 
 alloced:
 	alloced = true;
-	if (folio_test_large(folio) &&
-	    DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE) <
-					folio_next_index(folio)) {
-		struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
-		struct shmem_inode_info *info = SHMEM_I(inode);
-		/*
-		 * Part of the large folio is beyond i_size: subject
-		 * to shrink under memory pressure.
-		 */
-		spin_lock(&sbinfo->shrinklist_lock);
-		/*
-		 * _careful to defend against unlocked access to
-		 * ->shrink_list in shmem_unused_huge_shrink()
-		 */
-		if (list_empty_careful(&info->shrinklist)) {
-			list_add_tail(&info->shrinklist,
-				      &sbinfo->shrinklist);
-			sbinfo->shrinklist_len++;
-		}
-		spin_unlock(&sbinfo->shrinklist_lock);
-	}
 
 	if (sgp == SGP_WRITE)
 		folio_set_referenced(folio);
@@ -2585,6 +2564,33 @@ static int shmem_get_folio_gfp(struct inode *inode, pgoff_t index,
 		error = -EINVAL;
 		goto unlock;
 	}
+
+	/*
+	 * Queue the inode on the shrink list only after all checks that might
+	 * remove the folio have passed. Otherwise the inode could be left on
+	 * the shrinker list with a stale folio.
+	 */
+	if (alloced && folio_test_large(folio) &&
+	    DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE) < folio_next_index(folio)) {
+		struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
+		struct shmem_inode_info *info = SHMEM_I(inode);
+		/*
+		 * Part of the large folio is beyond i_size: subject
+		 * to shrink under memory pressure.
+		 */
+		spin_lock(&sbinfo->shrinklist_lock);
+		/*
+		 * _careful to defend against unlocked access to
+		 * ->shrink_list in shmem_unused_huge_shrink()
+		 */
+		if (list_empty_careful(&info->shrinklist)) {
+			list_add_tail(&info->shrinklist,
+				      &sbinfo->shrinklist);
+			sbinfo->shrinklist_len++;
+		}
+		spin_unlock(&sbinfo->shrinklist_lock);
+	}
+
 out:
 	*foliop = folio;
 	return 0;
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH v3 3/3] mm: shmem: make unused huge shrinker memcg aware
  2026-08-03  8:46 [PATCH v3 0/3] make unused huge shrinker memcg aware Qi Zheng
  2026-08-03  8:46 ` [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible() Qi Zheng
  2026-08-03  8:46 ` [PATCH v3 2/3] mm: shmem: move unused huge shrinklist queuing past the truncation check Qi Zheng
@ 2026-08-03  8:46 ` Qi Zheng
  2026-08-03 12:21 ` [PATCH v3 0/3] " David Hildenbrand (Arm)
  2026-08-03 21:24 ` Andrew Morton
  4 siblings, 0 replies; 11+ messages in thread
From: Qi Zheng @ 2026-08-03  8:46 UTC (permalink / raw)
  To: hughd, baolin.wang, usama.arif, brauner, akpm, david
  Cc: linux-mm, linux-kernel, Qi Zheng

From: Qi Zheng <zhengqi.arch@bytedance.com>

The shmem unused huge shrinker keeps a per-superblock list of inodes whose
tail huge folio extends beyond i_size. Since that list is not memcg aware,
reclaim triggered by one memcg can scan inodes from the whole superblock
and split shmem huge folios charged to unrelated memcgs.

Convert the shrink list to a memcg-aware list_lru. Queue each inode on the
list_lru sublist matching the memcg and node of the current tail huge
folio, so non-root memcg reclaim only walks candidates charged to the
reclaiming memcg. Global reclaim, root memcg reclaim and shmem quota
reclaim keep global semantics.

The list_lru still tracks inodes while the actual split target is the
current tail huge folio, so validate the folio memcg/node during scan. If
the folio no longer matches the reclaim context or splitting cannot
proceed, requeue the inode according to the current tail folio; if the
inode is no longer shrinkable, drop the scan entry.

This can be tested with the shrinker debugfs interface by allocating 32
tmpfs tail THPs in each of two memcgs, then scanning the sb-tmpfs shrinker
with memcg A's cgroup id:

               before A scan    after A scan
  base         A=64M, B=64M     A=64M, B=64M (per-memcg count is skipped)
  patched      A=64M, B=64M     A=0,   B=64M

Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
---
 include/linux/shmem_fs.h |  12 +-
 mm/shmem.c               | 341 +++++++++++++++++++++++++++++----------
 2 files changed, 268 insertions(+), 85 deletions(-)

diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
index 5663dff53186e..aeb59901a840c 100644
--- a/include/linux/shmem_fs.h
+++ b/include/linux/shmem_fs.h
@@ -11,6 +11,7 @@
 #include <linux/fs_parser.h>
 #include <linux/userfaultfd_k.h>
 #include <linux/bits.h>
+#include <linux/list_lru.h>
 
 /* inode in-kernel data */
 
@@ -54,6 +55,11 @@ struct shmem_inode_info {
 	struct dquot __rcu	*i_dquot[MAXQUOTAS];
 #endif
 	struct inode		vfs_inode;
+
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+	struct mem_cgroup	*shrinklist_memcg;
+	int			shrinklist_nid;
+#endif
 };
 
 #define SHMEM_FL_USER_VISIBLE		(FS_FL_USER_VISIBLE | FS_CASEFOLD_FL)
@@ -83,9 +89,9 @@ struct shmem_sb_info {
 	ino_t next_ino;		    /* The next per-sb inode number to use */
 	ino_t __percpu *ino_batch;  /* The next per-cpu inode number to use */
 	struct mempolicy *mpol;     /* default memory policy for mappings */
-	spinlock_t shrinklist_lock;   /* Protects shrinklist */
-	struct list_head shrinklist;  /* List of shinkable inodes */
-	unsigned long shrinklist_len; /* Length of shrinklist */
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+	struct list_lru shrinklist; /* List of shrinkable inodes */
+#endif
 	struct shmem_quota_limits qlimits; /* Default quota limits */
 	struct simple_xattr_cache xa_cache;
 };
diff --git a/mm/shmem.c b/mm/shmem.c
index bc0b768135113..704353852ae26 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -724,51 +724,237 @@ static const char *shmem_format_huge(int huge)
 }
 #endif
 
-static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
-		struct shrink_control *sc, unsigned long nr_to_free)
+static bool is_shmem_unused_huge_isolated(struct shmem_inode_info *info)
 {
-	LIST_HEAD(list), *pos, *next;
-	struct inode *inode;
+
+	return info->shrinklist_nid == -1;
+}
+
+static void set_shmem_unused_huge_isolated(struct shmem_inode_info *info)
+{
+	info->shrinklist_nid = -1;
+}
+
+static struct mem_cgroup *shmem_get_and_clear_memcg(struct shmem_inode_info *info)
+{
+	struct mem_cgroup *memcg = info->shrinklist_memcg;
+
+	info->shrinklist_memcg = NULL;
+
+	return memcg;
+}
+
+#ifdef CONFIG_MEMCG
+static struct mem_cgroup *
+shmem_unused_huge_alloc_lru(struct shmem_sb_info *sbinfo, struct folio *folio,
+			    gfp_t gfp)
+{
+	struct mem_cgroup *memcg;
+	int ret;
+
+	memcg = get_mem_cgroup_from_folio(folio);
+	if (!memcg)
+		return NULL;
+
+	ret = memcg_list_lru_alloc(memcg, &sbinfo->shrinklist, gfp);
+	if (ret) {
+		mem_cgroup_put(memcg);
+		return ERR_PTR(ret);
+	}
+
+	return memcg;
+}
+#else
+static struct mem_cgroup *
+shmem_unused_huge_alloc_lru(struct shmem_sb_info *sbinfo, struct folio *folio,
+			    gfp_t gfp)
+{
+	return NULL;
+}
+#endif
+
+static void shmem_unused_huge_add(struct inode *inode, struct folio *folio,
+				  gfp_t gfp)
+{
+	struct shmem_inode_info *info = SHMEM_I(inode);
+	struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
+	int nid = folio_nid(folio);
+	struct mem_cgroup *memcg = NULL, *old_memcg = NULL;
+
+	memcg = shmem_unused_huge_alloc_lru(sbinfo, folio, gfp);
+	if (IS_ERR(memcg))
+		return;
+
+	spin_lock(&info->lock);
+	if (!list_empty(&info->shrinklist)) {
+		/* isolated on scan list, let shrink handle it */
+		if (is_shmem_unused_huge_isolated(info))
+			goto unlock;
+
+		if (info->shrinklist_nid == nid &&
+		    info->shrinklist_memcg == memcg)
+			goto unlock;
+
+		list_lru_del(&sbinfo->shrinklist, &info->shrinklist,
+			     info->shrinklist_nid, info->shrinklist_memcg);
+		old_memcg = shmem_get_and_clear_memcg(info);
+	}
+
+	info->shrinklist_memcg = memcg;
+	info->shrinklist_nid = nid;
+	list_lru_add(&sbinfo->shrinklist, &info->shrinklist, nid, memcg);
+	memcg = NULL;
+unlock:
+	spin_unlock(&info->lock);
+	mem_cgroup_put(old_memcg);
+	mem_cgroup_put(memcg);
+}
+
+static void shmem_unused_huge_del(struct inode *inode)
+{
+	struct shmem_inode_info *info = SHMEM_I(inode);
+	struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
+	struct mem_cgroup *memcg = NULL;
+
+	spin_lock(&info->lock);
+	if (!list_empty(&info->shrinklist)) {
+		list_lru_del(&sbinfo->shrinklist, &info->shrinklist,
+			     info->shrinklist_nid, info->shrinklist_memcg);
+		memcg = shmem_get_and_clear_memcg(info);
+	}
+	spin_unlock(&info->lock);
+
+	mem_cgroup_put(memcg);
+}
+
+struct shmem_unused_huge_scan {
+	struct list_head list;
+	struct shrink_control *sc;
+};
+
+static enum lru_status shmem_unused_huge_isolate(struct list_head *item,
+						 struct list_lru_one *lru,
+						 void *arg)
+{
+	struct shmem_unused_huge_scan *scan = arg;
 	struct shmem_inode_info *info;
-	struct folio *folio;
-	unsigned long batch = sc ? sc->nr_to_scan : 128;
-	unsigned long split = 0, freed = 0;
+	struct inode *inode;
+	struct mem_cgroup *memcg = NULL;
 
-	if (list_empty(&sbinfo->shrinklist))
-		return SHRINK_STOP;
+	info = list_entry(item, struct shmem_inode_info, shrinklist);
 
-	spin_lock(&sbinfo->shrinklist_lock);
-	list_for_each_safe(pos, next, &sbinfo->shrinklist) {
-		info = list_entry(pos, struct shmem_inode_info, shrinklist);
+	/*
+	 * Use trylock to avoid ABBA deadlock: add/del path takes info->lock
+	 * before the list_lru bucket lock, while here the order is reversed.
+	 */
+	if (!spin_trylock(&info->lock))
+		return LRU_SKIP;
 
-		/* pin the inode */
-		inode = igrab(&info->vfs_inode);
+	/* pin the inode */
+	inode = igrab(&info->vfs_inode);
+	/* inode is about to be evicted */
+	if (!inode) {
+		list_lru_isolate(lru, item);
+		memcg = shmem_get_and_clear_memcg(info);
+		spin_unlock(&info->lock);
+		mem_cgroup_put(memcg);
+		return LRU_REMOVED;
+	}
 
-		/* inode is about to be evicted */
-		if (!inode) {
-			list_del_init(&info->shrinklist);
-			goto next;
-		}
+	list_lru_isolate(lru, item);
+	memcg = shmem_get_and_clear_memcg(info);
+	set_shmem_unused_huge_isolated(info);
+	list_add_tail(&info->shrinklist, &scan->list);
+	spin_unlock(&info->lock);
+	mem_cgroup_put(memcg);
 
-		list_move(&info->shrinklist, &list);
-next:
-		sbinfo->shrinklist_len--;
-		if (!--batch)
-			break;
+	return LRU_REMOVED;
+}
+
+static bool is_shmem_unused_huge_match(struct folio *folio,
+				       struct shrink_control *sc)
+{
+	struct mem_cgroup *memcg = NULL;
+	bool match;
+
+	/*
+	 * Only non-root memcg reclaim needs to match the folio charge against
+	 * sc->memcg. Skip the folio memcg check for the following cases:
+	 * 1. shmem quota reclaim (sc == NULL)
+	 * 2. global shrinker reclaim
+	 * 3. root memcg reclaim
+	 */
+	if (!sc || !sc->memcg || mem_cgroup_is_root(sc->memcg))
+		return true;
+
+	if (folio_nid(folio) != sc->nid)
+		return false;
+
+	memcg = get_mem_cgroup_from_folio(folio);
+	match = memcg == sc->memcg;
+	mem_cgroup_put(memcg);
+
+	return match;
+}
+
+static void shmem_unused_huge_drop(struct inode *inode)
+{
+	struct shmem_inode_info *info = SHMEM_I(inode);
+
+	spin_lock(&info->lock);
+	list_del_init(&info->shrinklist);
+	spin_unlock(&info->lock);
+}
+
+static void shmem_unused_huge_requeue(struct inode *inode, struct folio *folio)
+{
+	struct shmem_inode_info *info = SHMEM_I(inode);
+	struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
+	struct mem_cgroup *memcg;
+	int nid = folio_nid(folio);
+
+	memcg = shmem_unused_huge_alloc_lru(sbinfo, folio, GFP_NOWAIT);
+	if (IS_ERR(memcg)) {
+		shmem_unused_huge_drop(inode);
+		return;
 	}
-	spin_unlock(&sbinfo->shrinklist_lock);
 
-	list_for_each_safe(pos, next, &list) {
-		pgoff_t next, end;
+	spin_lock(&info->lock);
+	/* Requeue the inode to shrinklist */
+	list_del_init(&info->shrinklist);
+	list_lru_add(&sbinfo->shrinklist, &info->shrinklist, nid, memcg);
+	info->shrinklist_memcg = memcg;
+	info->shrinklist_nid = nid;
+	spin_unlock(&info->lock);
+}
+
+static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
+		struct shrink_control *sc, unsigned long nr_to_free)
+{
+	struct shmem_unused_huge_scan scan;
+	struct inode *inode;
+	struct shmem_inode_info *info;
+	struct folio *folio;
+	struct list_head *pos, *next;
+	unsigned long split = 0, freed = 0;
+
+	INIT_LIST_HEAD(&scan.list);
+	scan.sc = sc;
+	if (sc)
+		list_lru_shrink_walk(&sbinfo->shrinklist, sc,
+				     shmem_unused_huge_isolate, &scan);
+	else
+		list_lru_walk(&sbinfo->shrinklist, shmem_unused_huge_isolate,
+			      &scan, 128);
+
+	list_for_each_safe(pos, next, &scan.list) {
+		pgoff_t folio_end, end;
 		loff_t i_size;
 		int ret;
 
 		info = list_entry(pos, struct shmem_inode_info, shrinklist);
 		inode = &info->vfs_inode;
 
-		if (nr_to_free && freed >= nr_to_free)
-			goto move_back;
-
 		i_size = i_size_read(inode);
 		folio = filemap_get_entry(inode->i_mapping, i_size / PAGE_SIZE);
 		if (!folio || xa_is_value(folio))
@@ -781,13 +967,19 @@ static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
 		}
 
 		/* Check if there is anything to gain from splitting */
-		next = folio_next_index(folio);
+		folio_end = folio_next_index(folio);
 		end = shmem_fallocend(inode, DIV_ROUND_UP(i_size, PAGE_SIZE));
-		if (end <= folio->index || end >= next) {
+		if (end <= folio->index || end >= folio_end) {
 			folio_put(folio);
 			goto drop;
 		}
 
+		if (!is_shmem_unused_huge_match(folio, scan.sc))
+			goto move_back;
+
+		if (nr_to_free && freed >= nr_to_free)
+			goto move_back;
+
 		/*
 		 * Move the inode on the list back to shrinklist if we failed
 		 * to lock the page at this time.
@@ -795,35 +987,30 @@ static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
 		 * Waiting for the lock may lead to deadlock in the
 		 * reclaim path.
 		 */
-		if (!folio_trylock(folio)) {
-			folio_put(folio);
+		if (!folio_trylock(folio))
+			goto move_back;
+
+		if (!is_shmem_unused_huge_match(folio, scan.sc)) {
+			folio_unlock(folio);
 			goto move_back;
 		}
 
 		ret = split_folio(folio);
 		folio_unlock(folio);
-		folio_put(folio);
 
 		/* If split failed move the inode on the list back to shrinklist */
 		if (ret)
 			goto move_back;
 
-		freed += next - end;
+		freed += folio_end - end;
 		split++;
+		folio_put(folio);
 drop:
-		list_del_init(&info->shrinklist);
+		shmem_unused_huge_drop(inode);
 		goto put;
 move_back:
-		/*
-		 * Make sure the inode is either on the global list or deleted
-		 * from any local list before iput() since it could be deleted
-		 * in another thread once we put the inode (then the local list
-		 * is corrupted).
-		 */
-		spin_lock(&sbinfo->shrinklist_lock);
-		list_move(&info->shrinklist, &sbinfo->shrinklist);
-		sbinfo->shrinklist_len++;
-		spin_unlock(&sbinfo->shrinklist_lock);
+		shmem_unused_huge_requeue(inode, folio);
+		folio_put(folio);
 put:
 		iput(inode);
 	}
@@ -836,7 +1023,7 @@ static long shmem_unused_huge_scan(struct super_block *sb,
 {
 	struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
 
-	if (!READ_ONCE(sbinfo->shrinklist_len))
+	if (!list_lru_shrink_count(&sbinfo->shrinklist, sc))
 		return SHRINK_STOP;
 
 	return shmem_unused_huge_shrink(sbinfo, sc, 0);
@@ -847,21 +1034,21 @@ static long shmem_unused_huge_count(struct super_block *sb,
 {
 	struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
 
-	/*
-	 * The per-superblock shrinklist is filesystem-global and does not
-	 * honour sc->memcg, so it is only meaningful on the global (kswapd or
-	 * root direct reclaim) shrink path. Skip the per-memcg iterations of
-	 * shrink_slab_memcg() to avoid queueing duplicate global work.
-	 */
-	if (!mem_cgroup_shrink_is_root(sc))
-		return 0;
-
-	return READ_ONCE(sbinfo->shrinklist_len);
+	return list_lru_shrink_count(&sbinfo->shrinklist, sc);
 }
 #else /* !CONFIG_TRANSPARENT_HUGEPAGE */
 
 #define shmem_huge SHMEM_HUGE_DENY
 
+static void shmem_unused_huge_add(struct inode *inode, struct folio *folio,
+				  gfp_t gfp)
+{
+}
+
+static void shmem_unused_huge_del(struct inode *inode)
+{
+}
+
 static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
 		struct shrink_control *sc, unsigned long nr_to_free)
 {
@@ -1417,14 +1604,7 @@ static void shmem_evict_inode(struct inode *inode)
 		inode->i_size = 0;
 		mapping_set_exiting(inode->i_mapping);
 		shmem_truncate_range(inode, 0, (loff_t)-1);
-		if (!list_empty(&info->shrinklist)) {
-			spin_lock(&sbinfo->shrinklist_lock);
-			if (!list_empty(&info->shrinklist)) {
-				list_del_init(&info->shrinklist);
-				sbinfo->shrinklist_len--;
-			}
-			spin_unlock(&sbinfo->shrinklist_lock);
-		}
+		shmem_unused_huge_del(inode);
 		while (!list_empty(&info->swaplist)) {
 			/* Wait while shmem_unuse() is scanning this inode... */
 			wait_var_event(&info->stop_eviction,
@@ -2572,25 +2752,12 @@ static int shmem_get_folio_gfp(struct inode *inode, pgoff_t index,
 	 */
 	if (alloced && folio_test_large(folio) &&
 	    DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE) < folio_next_index(folio)) {
-		struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
-		struct shmem_inode_info *info = SHMEM_I(inode);
 		/*
 		 * Part of the large folio is beyond i_size: subject
 		 * to shrink under memory pressure.
 		 */
-		spin_lock(&sbinfo->shrinklist_lock);
-		/*
-		 * _careful to defend against unlocked access to
-		 * ->shrink_list in shmem_unused_huge_shrink()
-		 */
-		if (list_empty_careful(&info->shrinklist)) {
-			list_add_tail(&info->shrinklist,
-				      &sbinfo->shrinklist);
-			sbinfo->shrinklist_len++;
-		}
-		spin_unlock(&sbinfo->shrinklist_lock);
+		shmem_unused_huge_add(inode, folio, gfp);
 	}
-
 out:
 	*foliop = folio;
 	return 0;
@@ -3067,6 +3234,10 @@ static struct inode *__shmem_get_inode(struct mnt_idmap *idmap,
 	if (info->fsflags)
 		shmem_set_inode_flags(inode, info->fsflags, NULL);
 	INIT_LIST_HEAD(&info->shrinklist);
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+	info->shrinklist_memcg = NULL;
+	info->shrinklist_nid = -1;
+#endif
 	INIT_LIST_HEAD(&info->swaplist);
 	cache_no_acl(inode);
 	if (sbinfo->noswap)
@@ -4933,6 +5104,9 @@ static void shmem_put_super(struct super_block *sb)
 #endif
 	free_percpu(sbinfo->ino_batch);
 	percpu_counter_destroy(&sbinfo->used_blocks);
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+	list_lru_destroy(&sbinfo->shrinklist);
+#endif
 	mpol_put(sbinfo->mpol);
 #ifdef CONFIG_TMPFS_XATTR
 	simple_xattr_cache_cleanup(&sbinfo->xa_cache);
@@ -5026,8 +5200,11 @@ static int shmem_fill_super(struct super_block *sb, struct fs_context *fc)
 	raw_spin_lock_init(&sbinfo->stat_lock);
 	if (percpu_counter_init(&sbinfo->used_blocks, 0, GFP_KERNEL))
 		goto failed;
-	spin_lock_init(&sbinfo->shrinklist_lock);
-	INIT_LIST_HEAD(&sbinfo->shrinklist);
+
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+	if (list_lru_init_memcg(&sbinfo->shrinklist, sb->s_shrink))
+		goto failed;
+#endif
 
 	sb->s_maxbytes = MAX_LFS_FILESIZE;
 	sb->s_blocksize = PAGE_SIZE;
-- 
2.54.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 0/3] make unused huge shrinker memcg aware
  2026-08-03  8:46 [PATCH v3 0/3] make unused huge shrinker memcg aware Qi Zheng
                   ` (2 preceding siblings ...)
  2026-08-03  8:46 ` [PATCH v3 3/3] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
@ 2026-08-03 12:21 ` David Hildenbrand (Arm)
  2026-08-04  3:51   ` Qi Zheng
  2026-08-03 21:24 ` Andrew Morton
  4 siblings, 1 reply; 11+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-03 12:21 UTC (permalink / raw)
  To: Qi Zheng, hughd, baolin.wang, usama.arif, brauner, akpm
  Cc: linux-mm, linux-kernel, Qi Zheng

On 8/3/26 10:46, Qi Zheng wrote:
> From: Qi Zheng <zhengqi.arch@bytedance.com>

I'm missing description and motivation here.

This is only about shrinking huge pages that span end of shmem files.

Is this really a problem? And if so, why?

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible()
  2026-08-03  8:46 ` [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible() Qi Zheng
@ 2026-08-03 21:20   ` Andrew Morton
  2026-08-04  6:18     ` Qi Zheng
  0 siblings, 1 reply; 11+ messages in thread
From: Andrew Morton @ 2026-08-03 21:20 UTC (permalink / raw)
  To: Qi Zheng
  Cc: hughd, baolin.wang, usama.arif, brauner, david, linux-mm,
	linux-kernel, Qi Zheng

On Mon,  3 Aug 2026 16:46:33 +0800 Qi Zheng <qi.zheng@linux.dev> wrote:

> Commit 0ef8faff490be ("fs: push nr_cached_objects memcg gating into
> individual filesystems") forgot to drop the super_fs_objects_eligible(),
> just fix it.

What are the userspace-visible runtime effects of this bug?  Please
include this info with all bugfix patches.

Should we cc:stable?

Thanks.

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 0/3] make unused huge shrinker memcg aware
  2026-08-03  8:46 [PATCH v3 0/3] make unused huge shrinker memcg aware Qi Zheng
                   ` (3 preceding siblings ...)
  2026-08-03 12:21 ` [PATCH v3 0/3] " David Hildenbrand (Arm)
@ 2026-08-03 21:24 ` Andrew Morton
  2026-08-04  6:36   ` Qi Zheng
  4 siblings, 1 reply; 11+ messages in thread
From: Andrew Morton @ 2026-08-03 21:24 UTC (permalink / raw)
  To: Qi Zheng
  Cc: hughd, baolin.wang, usama.arif, brauner, david, linux-mm,
	linux-kernel, Qi Zheng

On Mon,  3 Aug 2026 16:46:32 +0800 Qi Zheng <qi.zheng@linux.dev> wrote:

> 

What David said.

Also, AI review might have found a couple of things:
	https://sashiko.dev/#/patchset/cover.1785746588.git.zhengqi.arch@bytedance.com

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 0/3] make unused huge shrinker memcg aware
  2026-08-03 12:21 ` [PATCH v3 0/3] " David Hildenbrand (Arm)
@ 2026-08-04  3:51   ` Qi Zheng
  0 siblings, 0 replies; 11+ messages in thread
From: Qi Zheng @ 2026-08-04  3:51 UTC (permalink / raw)
  To: David Hildenbrand (Arm), hughd, baolin.wang, usama.arif, brauner,
	akpm, Qi Zheng
  Cc: linux-mm, linux-kernel

Hi David,

On 8/3/26 8:21 PM, David Hildenbrand (Arm) wrote:
> On 8/3/26 10:46, Qi Zheng wrote:
>> From: Qi Zheng <zhengqi.arch@bytedance.com>
> 
> I'm missing description and motivation here.

My bad, since v1 was just a single patch, I got lazy and didn't bother
adding a cover letter description later on.

> 
> This is only about shrinking huge pages that span end of shmem files.
> 
> Is this really a problem? And if so, why?

Yes, this is a real-world problem that we encountered in production.

The root cause is that shmem unused shrinker used to be non-memcg-aware.
This could lead to a scenario where reclaim triggered by one memcg A
reclaims the shmem of another memcg B, causing unexpected impact on it.

Even worse, memcg A might have no reclaimable shmem at all, making this
completely useless work and incurring some performance overhead.

such as:

tid 11340 comm scanner locked a page for 182264 us! kstack:
         unlock_page+1
         split_huge_page_to_list+3135
         shmem_unused_huge_shrink+767
         super_cache_scan+329
         do_shrink_slab+291
         shrink_slab+533
         shrink_node+400
         do_try_to_free_pages+206
         try_to_free_mem_cgroup_pages+262
         try_charge_memcg+591
         mem_cgroup_charge+136
         __handle_mm_fault+2431
         handle_mm_fault+194
         do_user_addr_fault+462
         __do_page_fault+176
         do_page_fault+48
         page_fault+62

Later, with Usama's patch [1], the shmem unused shrinker is no longer
triggered during memcg-level reclaim. But this actually doesn't make
sense either, since we can clearly just reclaim from memcg A individuallty.

[1]. 
https://lore.kernel.org/all/20260715103516.2410175-1-usama.arif@linux.dev/

Thanks,
Qi


> 


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible()
  2026-08-03 21:20   ` Andrew Morton
@ 2026-08-04  6:18     ` Qi Zheng
  0 siblings, 0 replies; 11+ messages in thread
From: Qi Zheng @ 2026-08-04  6:18 UTC (permalink / raw)
  To: Andrew Morton, brauner
  Cc: hughd, baolin.wang, usama.arif, david, linux-mm, linux-kernel,
	Qi Zheng



On 8/4/26 5:20 AM, Andrew Morton wrote:
> On Mon,  3 Aug 2026 16:46:33 +0800 Qi Zheng <qi.zheng@linux.dev> wrote:
> 
>> Commit 0ef8faff490be ("fs: push nr_cached_objects memcg gating into
>> individual filesystems") forgot to drop the super_fs_objects_eligible(),
>> just fix it.
> 
> What are the userspace-visible runtime effects of this bug?  Please
> include this info with all bugfix patches.

It's seems this part was missed during the push to the linux-next
branch, see 
https://lore.kernel.org/all/9c7efd5f-f8d3-4926-acb4-34c326ffb1c3@linux.dev/.

I expect Christian to help fix this. Including this patch in the
current series is just to provide a complete picture and make
reviewing easier.

Thanks,
Qi

> 
> Should we cc:stable?
> 
> Thanks.


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 0/3] make unused huge shrinker memcg aware
  2026-08-03 21:24 ` Andrew Morton
@ 2026-08-04  6:36   ` Qi Zheng
  0 siblings, 0 replies; 11+ messages in thread
From: Qi Zheng @ 2026-08-04  6:36 UTC (permalink / raw)
  To: Andrew Morton, baolin.wang
  Cc: hughd, usama.arif, brauner, david, linux-mm, linux-kernel,
	Qi Zheng

Hi Andrew,

On 8/4/26 5:24 AM, Andrew Morton wrote:
> On Mon,  3 Aug 2026 16:46:32 +0800 Qi Zheng <qi.zheng@linux.dev> wrote:
> 
>>
> 
> What David said.
> 
> Also, AI review might have found a couple of things:
> 	https://sashiko.dev/#/patchset/cover.1785746588.git.zhengqi.arch@bytedance.com

I saw it, and it sounds reasonable.

 > > +static struct mem_cgroup *
 > > +shmem_unused_huge_alloc_lru(struct shmem_sb_info *sbinfo, struct 
folio *folio,
 > > +			    gfp_t gfp)
 > > +{
 > > +	struct mem_cgroup *memcg;
 > > +	int ret;
 > > +
 > > +	memcg = get_mem_cgroup_from_folio(folio);
 > > +	if (!memcg)
 > > +		return NULL;
 > Could this leak the mem_cgroup structure if a file is truncated?
 > When shmem_unused_huge_add() calls this function, it stores the memcg
 > reference in info->shrinklist_memcg.
 > If an unprivileged user truncates the file to 0 bytes, 
shmem_truncate_range()
 > frees the folio, but it does not remove the inode from the shrinklist 
or drop
 > this memcg reference.
 > Without memory pressure to trigger the shrinker, the mem_cgroup might 
remain
 > pinned indefinitely until the file is deleted, potentially allowing 
zombie
 > memcgs to accumulate on the system.

Indeed, will also call shmem_unused_huge_del() in shmem_truncate_range()
to take the inode off the shrinklist in time.

 > [ ... ]
 > > +static bool is_shmem_unused_huge_match(struct folio *folio,
 > > +				       struct shrink_control *sc)
 > > +{
 > > +	struct mem_cgroup *memcg = NULL;
 > > +	bool match;
 > > +
 > > +	/*
 > > +	 * Only non-root memcg reclaim needs to match the folio charge 
against
 > > +	 * sc->memcg. Skip the folio memcg check for the following cases:
 > > +	 * 1. shmem quota reclaim (sc == NULL)
 > > +	 * 2. global shrinker reclaim
 > > +	 * 3. root memcg reclaim
 > > +	 */
 > > +	if (!sc || !sc->memcg || mem_cgroup_is_root(sc->memcg))
 > > +		return true;
 > > +
 > > +	if (folio_nid(folio) != sc->nid)
 > > +		return false;
 > Does this early return bypass the NUMA node check for global and root 
memcg
 > reclaim?
 > The comment indicates this is only intended to skip the folio memcg 
check.
 > However, returning true immediately also bypasses the sc->nid check.
 > If a huge folio is migrated to a different NUMA node, kswapd or direct
 > reclaim might incorrectly split folios on the wrong node instead of
 > requeuing the inode to the correct node's list.

Indeed, root memcg reclaim also needs to check if the nid matches.

Will move the nid check earlier in v4:

       if (!sc)
               return true;

       if (folio_nid(folio) != sc->nid)
               return false;

       if (!sc->memcg || mem_cgroup_is_root(sc->memcg))
               return true;

Thanks,
Qi





^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH v3 2/3] mm: shmem: move unused huge shrinklist queuing past the truncation check
  2026-08-03  8:46 ` [PATCH v3 2/3] mm: shmem: move unused huge shrinklist queuing past the truncation check Qi Zheng
@ 2026-08-04  7:03   ` Qi Zheng
  0 siblings, 0 replies; 11+ messages in thread
From: Qi Zheng @ 2026-08-04  7:03 UTC (permalink / raw)
  To: baolin.wang
  Cc: linux-mm, linux-kernel, hughd, usama.arif, akpm, brauner, david



On 8/3/26 4:46 PM, Qi Zheng wrote:
> From: Qi Zheng <zhengqi.arch@bytedance.com>
> 
> The shmem_get_folio_gfp() adds the inode to the unused huge shrinker list
> at the alloced label, but a subsequent truncation check may still fail and
> remove the folio, leaving the inode on the list with a stale folio.
> 
> The original code works because the shrinker re-looks-up the folio and
> drops stale entries, but it is cleaner to queue the inode only after all
> checks that might remove the folio have passed.
> 
> So just make the pure structural move with no functional change, and it
> serves as preparation for the memcg-aware shrinker conversion.
> 
> Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>


missed adding:

Suggested-by: Baolin Wang <baolin.wang@linux.alibaba.com>

^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-08-04  7:03 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-03  8:46 [PATCH v3 0/3] make unused huge shrinker memcg aware Qi Zheng
2026-08-03  8:46 ` [PATCH v3 1/3] fs: fix missed removal of super_fs_objects_eligible() Qi Zheng
2026-08-03 21:20   ` Andrew Morton
2026-08-04  6:18     ` Qi Zheng
2026-08-03  8:46 ` [PATCH v3 2/3] mm: shmem: move unused huge shrinklist queuing past the truncation check Qi Zheng
2026-08-04  7:03   ` Qi Zheng
2026-08-03  8:46 ` [PATCH v3 3/3] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
2026-08-03 12:21 ` [PATCH v3 0/3] " David Hildenbrand (Arm)
2026-08-04  3:51   ` Qi Zheng
2026-08-03 21:24 ` Andrew Morton
2026-08-04  6:36   ` Qi Zheng

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox