* [PATCH] mm: shmem: make unused huge shrinker memcg aware
@ 2026-07-14 3:19 Qi Zheng
2026-07-15 4:32 ` Andrew Morton
2026-07-15 9:37 ` Baolin Wang
0 siblings, 2 replies; 5+ messages in thread
From: Qi Zheng @ 2026-07-14 3:19 UTC (permalink / raw)
To: hughd, baolin.wang, akpm, usama.arif; +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=0, B=0
patched A=64M, B=64M A=0, B=64M
Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
---
This patch is based on next-20260701 because it doesn't include this patch:
https://lore.kernel.org/all/20260609123047.1948242-1-usama.arif@linux.dev/
Later on, Usama will consider moving this restriction down into the fs callback.
include/linux/shmem_fs.h | 14 +-
mm/shmem.c | 379 ++++++++++++++++++++++++++++++---------
2 files changed, 310 insertions(+), 83 deletions(-)
diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
index e729b9b0e38d4..0b14de1e890a9 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>
struct swap_iocb;
@@ -56,6 +57,13 @@ struct shmem_inode_info {
struct dquot __rcu *i_dquot[MAXQUOTAS];
#endif
struct inode vfs_inode;
+
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+ struct list_head shrinklist_scan;
+ struct mem_cgroup *shrinklist_memcg;
+ int shrinklist_nid;
+ bool shrinklist_isolated;
+#endif
};
#define SHMEM_FL_USER_VISIBLE (FS_FL_USER_VISIBLE | FS_CASEFOLD_FL)
@@ -85,9 +93,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 b06c1ae2f50c3..a17150e6107c6 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -724,50 +724,252 @@ static const char *shmem_format_huge(int huge)
}
#endif
+static inline struct mem_cgroup *shmem_get_and_clear_memcg(struct shmem_inode_info *info)
+{
+ struct mem_cgroup *memcg = info->shrinklist_memcg;
+
+ info->shrinklist_memcg = NULL;
+ info->shrinklist_nid = -1;
+
+ 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)) {
+ 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);
+ }
+
+ list_lru_add(&sbinfo->shrinklist, &info->shrinklist, nid, memcg);
+ info->shrinklist_memcg = memcg;
+ info->shrinklist_nid = nid;
+ 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 inode *inode;
+ struct mem_cgroup *memcg = NULL;
+
+ info = list_entry(item, struct shmem_inode_info, shrinklist);
+ if (!spin_trylock(&info->lock))
+ return LRU_SKIP;
+
+ if (info->shrinklist_isolated) {
+ spin_unlock(&info->lock);
+ return LRU_SKIP;
+ }
+
+ inode = igrab(&info->vfs_inode);
+ if (!inode) {
+ /*
+ * This inode is being evicted, just drop its stale active LRU
+ * entry.
+ */
+ list_lru_isolate(lru, item);
+ memcg = shmem_get_and_clear_memcg(info);
+ spin_unlock(&info->lock);
+ mem_cgroup_put(memcg);
+ return LRU_REMOVED;
+ }
+
+ info->shrinklist_isolated = true;
+ list_lru_isolate(lru, item);
+ memcg = shmem_get_and_clear_memcg(info);
+ list_add_tail(&info->shrinklist_scan, &scan->list);
+ spin_unlock(&info->lock);
+ mem_cgroup_put(memcg);
+
+ return LRU_REMOVED;
+}
+
+static bool is_shmem_unused_huge_readded(struct inode *inode)
+{
+ struct shmem_inode_info *info = SHMEM_I(inode);
+ bool readded;
+
+ spin_lock(&info->lock);
+ readded = info->shrinklist_isolated && !list_empty(&info->shrinklist);
+ spin_unlock(&info->lock);
+
+ return readded;
+}
+
+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_scan);
+ info->shrinklist_isolated = false;
+ 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))
+ goto drop;
+
+ spin_lock(&info->lock);
+ if (!info->shrinklist_isolated)
+ goto unlock;
+
+ list_del_init(&info->shrinklist_scan);
+ info->shrinklist_isolated = false;
+ if (!list_empty(&info->shrinklist))
+ goto unlock;
+
+ list_lru_add(&sbinfo->shrinklist, &info->shrinklist, nid, memcg);
+ info->shrinklist_memcg = memcg;
+ info->shrinklist_nid = nid;
+ memcg = NULL;
+
+unlock:
+ spin_unlock(&info->lock);
+ mem_cgroup_put(memcg);
+ return;
+
+drop:
+ shmem_unused_huge_drop(inode);
+}
+
static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
struct shrink_control *sc, unsigned long nr_to_free)
{
- LIST_HEAD(list), *pos, *next;
+ struct shmem_unused_huge_scan scan;
struct inode *inode;
struct shmem_inode_info *info;
struct folio *folio;
- unsigned long batch = sc ? sc->nr_to_scan : 128;
+ struct list_head *pos, *next;
unsigned long split = 0, freed = 0;
- if (list_empty(&sbinfo->shrinklist))
- return SHRINK_STOP;
-
- spin_lock(&sbinfo->shrinklist_lock);
- list_for_each_safe(pos, next, &sbinfo->shrinklist) {
- info = list_entry(pos, struct shmem_inode_info, shrinklist);
-
- /* pin the inode */
- inode = igrab(&info->vfs_inode);
-
- /* inode is about to be evicted */
- if (!inode) {
- list_del_init(&info->shrinklist);
- goto next;
- }
-
- list_move(&info->shrinklist, &list);
-next:
- sbinfo->shrinklist_len--;
- if (!--batch)
- break;
- }
- spin_unlock(&sbinfo->shrinklist_lock);
+ 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, &list) {
+ list_for_each_safe(pos, next, &scan.list) {
pgoff_t next, end;
loff_t i_size;
int ret;
- info = list_entry(pos, struct shmem_inode_info, shrinklist);
+ info = list_entry(pos, struct shmem_inode_info, shrinklist_scan);
inode = &info->vfs_inode;
- if (nr_to_free && freed >= nr_to_free)
- goto move_back;
+ if (is_shmem_unused_huge_readded(inode))
+ goto drop;
i_size = i_size_read(inode);
folio = filemap_get_entry(inode->i_mapping, i_size / PAGE_SIZE);
@@ -788,6 +990,18 @@ static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
goto drop;
}
+ if (!is_shmem_unused_huge_match(folio, scan.sc)) {
+ shmem_unused_huge_requeue(inode, folio);
+ folio_put(folio);
+ goto put;
+ }
+
+ if (nr_to_free && freed >= nr_to_free) {
+ shmem_unused_huge_requeue(inode, folio);
+ folio_put(folio);
+ goto put;
+ }
+
/*
* Move the inode on the list back to shrinklist if we failed
* to lock the page at this time.
@@ -796,34 +1010,39 @@ static unsigned long shmem_unused_huge_shrink(struct shmem_sb_info *sbinfo,
* reclaim path.
*/
if (!folio_trylock(folio)) {
+ shmem_unused_huge_requeue(inode, folio);
folio_put(folio);
- goto move_back;
+ goto put;
+ }
+
+ if (is_shmem_unused_huge_readded(inode)) {
+ folio_unlock(folio);
+ folio_put(folio);
+ goto drop;
+ }
+
+ if (!is_shmem_unused_huge_match(folio, scan.sc)) {
+ folio_unlock(folio);
+ shmem_unused_huge_requeue(inode, folio);
+ folio_put(folio);
+ goto put;
}
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;
+ if (ret) {
+ shmem_unused_huge_requeue(inode, folio);
+ folio_put(folio);
+ goto put;
+ }
freed += next - end;
split++;
+ folio_put(folio);
drop:
- list_del_init(&info->shrinklist);
- 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_drop(inode);
put:
iput(inode);
}
@@ -836,7 +1055,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);
@@ -846,12 +1065,22 @@ static long shmem_unused_huge_count(struct super_block *sb,
struct shrink_control *sc)
{
struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
- 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)
{
@@ -1405,14 +1634,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,
@@ -2510,28 +2732,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);
/*
@@ -2560,6 +2760,15 @@ static int shmem_get_folio_gfp(struct inode *inode, pgoff_t index,
error = -EINVAL;
goto unlock;
}
+
+ if (alloced && folio_test_large(folio) &&
+ DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE) < folio_next_index(folio)) {
+ /*
+ * Part of the large folio is beyond i_size: subject
+ * to shrink under memory pressure.
+ */
+ shmem_unused_huge_add(inode, folio, gfp);
+ }
out:
*foliop = folio;
return 0;
@@ -3036,6 +3245,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
+ INIT_LIST_HEAD(&info->shrinklist_scan);
+ info->shrinklist_nid = -1;
+#endif
INIT_LIST_HEAD(&info->swaplist);
cache_no_acl(inode);
if (sbinfo->noswap)
@@ -4902,6 +5115,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);
@@ -4995,8 +5211,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] 5+ messages in thread* Re: [PATCH] mm: shmem: make unused huge shrinker memcg aware
2026-07-14 3:19 [PATCH] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
@ 2026-07-15 4:32 ` Andrew Morton
2026-07-15 6:49 ` Qi Zheng
2026-07-15 9:37 ` Baolin Wang
1 sibling, 1 reply; 5+ messages in thread
From: Andrew Morton @ 2026-07-15 4:32 UTC (permalink / raw)
To: Qi Zheng; +Cc: hughd, baolin.wang, usama.arif, linux-mm, linux-kernel, Qi Zheng
On Tue, 14 Jul 2026 11:19:18 +0800 Qi Zheng <qi.zheng@linux.dev> wrote:
> 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=0, B=0
> patched A=64M, B=64M A=0, B=64M
Sashiko said a thing:
https://sashiko.dev/#/patchset/20260714031918.308-1-qi.zheng@linux.dev
> This patch is based on next-20260701 because it doesn't include this patch:
>
> https://lore.kernel.org/all/20260609123047.1948242-1-usama.arif@linux.dev/
>
> Later on, Usama will consider moving this restriction down into the fs callback.
I'm not understanding. Was Usama's above patch ever included in
anything?
> - 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 b06c1ae2f50c3..a17150e6107c6 100644
> --- a/mm/shmem.c
> +++ b/mm/shmem.c
> @@ -724,50 +724,252 @@ static const char *shmem_format_huge(int huge)
> }
> #endif
>
> +static inline struct mem_cgroup *shmem_get_and_clear_memcg(struct shmem_inode_info *info)
> +{
> + struct mem_cgroup *memcg = info->shrinklist_memcg;
> +
> + info->shrinklist_memcg = NULL;
> + info->shrinklist_nid = -1;
> +
> + return memcg;
> +}
Explicit inlining shouldn't be needed - gcc will figure it out.
> +
> +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)) {
> + 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);
> + }
> +
> + list_lru_add(&sbinfo->shrinklist, &info->shrinklist, nid, memcg);
> + info->shrinklist_memcg = memcg;
> + info->shrinklist_nid = nid;
Totally pointless style thing: it's nice to fully initialize the object
before adding it to the list.
> + memcg = NULL;
> +unlock:
> + spin_unlock(&info->lock);
> + mem_cgroup_put(old_memcg);
> + mem_cgroup_put(memcg);
> +}
>
> ..,
>
> +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 inode *inode;
> + struct mem_cgroup *memcg = NULL;
> +
> + info = list_entry(item, struct shmem_inode_info, shrinklist);
> + if (!spin_trylock(&info->lock))
> + return LRU_SKIP;
Why the trylock? Please add comment explaining its use.
> + if (info->shrinklist_isolated) {
> + spin_unlock(&info->lock);
> + return LRU_SKIP;
> + }
> +
> + inode = igrab(&info->vfs_inode);
> + if (!inode) {
> + /*
> + * This inode is being evicted, just drop its stale active LRU
> + * entry.
> + */
> + list_lru_isolate(lru, item);
> + memcg = shmem_get_and_clear_memcg(info);
> + spin_unlock(&info->lock);
> + mem_cgroup_put(memcg);
> + return LRU_REMOVED;
> + }
> +
> + info->shrinklist_isolated = true;
> + list_lru_isolate(lru, item);
> + memcg = shmem_get_and_clear_memcg(info);
> + list_add_tail(&info->shrinklist_scan, &scan->list);
> + spin_unlock(&info->lock);
> + mem_cgroup_put(memcg);
> +
> + return LRU_REMOVED;
> +}
> +
> +static bool is_shmem_unused_huge_readded(struct inode *inode)
> +{
> + struct shmem_inode_info *info = SHMEM_I(inode);
> + bool readded;
> +
> + spin_lock(&info->lock);
> + readded = info->shrinklist_isolated && !list_empty(&info->shrinklist);
> + spin_unlock(&info->lock);
> +
> + return readded;
> +}
The return value is out of date as soon as the lock is dropped? A
comment explaining why this is OK would be helpful.
> +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;
> +}
>
> ...
>
Generally speaking, the code seems a bit thin on comments explaining
why functions exist, why the code is doing a partucular thing, etc.
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: [PATCH] mm: shmem: make unused huge shrinker memcg aware
2026-07-15 4:32 ` Andrew Morton
@ 2026-07-15 6:49 ` Qi Zheng
0 siblings, 0 replies; 5+ messages in thread
From: Qi Zheng @ 2026-07-15 6:49 UTC (permalink / raw)
To: Andrew Morton
Cc: hughd, baolin.wang, usama.arif, linux-mm, linux-kernel, Qi Zheng
Hi Andrew,
On 7/15/26 12:32 PM, Andrew Morton wrote:
> On Tue, 14 Jul 2026 11:19:18 +0800 Qi Zheng <qi.zheng@linux.dev> wrote:
>
>> 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=0, B=0
>> patched A=64M, B=64M A=0, B=64M
>
> Sashiko said a thing:
> https://sashiko.dev/#/patchset/20260714031918.308-1-qi.zheng@linux.dev
Got it. I noticed that too. shrinklist_memcg and shrinklist_isolated
require explicit initalization. I'll fix this in the next version once
the following patch is merged into linux-next:
https://lore.kernel.org/all/20260714101454.1202449-1-usama.arif@linux.dev/
>
>> This patch is based on next-20260701 because it doesn't include this patch:
>>
>> https://lore.kernel.org/all/20260609123047.1948242-1-usama.arif@linux.dev/
>>
>> Later on, Usama will consider moving this restriction down into the fs callback.
>
> I'm not understanding. Was Usama's above patch ever included in
> anything?
It's included in linux-next, e.g. next-20260710.
>
>> - 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 b06c1ae2f50c3..a17150e6107c6 100644
>> --- a/mm/shmem.c
>> +++ b/mm/shmem.c
>> @@ -724,50 +724,252 @@ static const char *shmem_format_huge(int huge)
>> }
>> #endif
>>
>> +static inline struct mem_cgroup *shmem_get_and_clear_memcg(struct shmem_inode_info *info)
>> +{
>> + struct mem_cgroup *memcg = info->shrinklist_memcg;
>> +
>> + info->shrinklist_memcg = NULL;
>> + info->shrinklist_nid = -1;
>> +
>> + return memcg;
>> +}
>
> Explicit inlining shouldn't be needed - gcc will figure it out.
Got it.
>
>> +
>> +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)) {
>> + 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);
>> + }
>> +
>> + list_lru_add(&sbinfo->shrinklist, &info->shrinklist, nid, memcg);
>> + info->shrinklist_memcg = memcg;
>> + info->shrinklist_nid = nid;
>
> Totally pointless style thing: it's nice to fully initialize the object
> before adding it to the list.
Got it.
>
>> + memcg = NULL;
>> +unlock:
>> + spin_unlock(&info->lock);
>> + mem_cgroup_put(old_memcg);
>> + mem_cgroup_put(memcg);
>> +}
>>
>> ..,
>>
>> +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 inode *inode;
>> + struct mem_cgroup *memcg = NULL;
>> +
>> + info = list_entry(item, struct shmem_inode_info, shrinklist);
>> + if (!spin_trylock(&info->lock))
>> + return LRU_SKIP;
>
> Why the trylock? Please add comment explaining its use.
This is to prevent an ABBA deadlock.
The shmem add/delete/requeue paths take info->lock first and then call
list_lru_add()/list_lru_del(), which acquire the list_lru lock. But the
current path takes the list_lru lock first, hence the use of trylock
here.
Will add a comment explaining this.
>
>
>> + if (info->shrinklist_isolated) {
>> + spin_unlock(&info->lock);
>> + return LRU_SKIP;
>> + }
>> +
>> + inode = igrab(&info->vfs_inode);
>> + if (!inode) {
>> + /*
>> + * This inode is being evicted, just drop its stale active LRU
>> + * entry.
>> + */
>> + list_lru_isolate(lru, item);
>> + memcg = shmem_get_and_clear_memcg(info);
>> + spin_unlock(&info->lock);
>> + mem_cgroup_put(memcg);
>> + return LRU_REMOVED;
>> + }
>> +
>> + info->shrinklist_isolated = true;
>> + list_lru_isolate(lru, item);
>> + memcg = shmem_get_and_clear_memcg(info);
>> + list_add_tail(&info->shrinklist_scan, &scan->list);
>> + spin_unlock(&info->lock);
>> + mem_cgroup_put(memcg);
>> +
>> + return LRU_REMOVED;
>> +}
>> +
>> +static bool is_shmem_unused_huge_readded(struct inode *inode)
>> +{
>> + struct shmem_inode_info *info = SHMEM_I(inode);
>> + bool readded;
>> +
>> + spin_lock(&info->lock);
>> + readded = info->shrinklist_isolated && !list_empty(&info->shrinklist);
>> + spin_unlock(&info->lock);
>> +
>> + return readded;
>> +}
>
> The return value is out of date as soon as the lock is dropped? A
> comment explaining why this is OK would be helpful.
Yes, the value is only a snapshot. The helper is a conservative race
detector for the case where the shrinker has an inode on the local scan
list and a concurrent fault has already queued the same inode back on
the active list_lru. If we observe that, the caller drops the scan entry
and lets the active list_lru entry represent the current tail folio.
Will add a comment explaining this.
>
>> +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;
>> +}
>>
>> ...
>>
>
> Generally speaking, the code seems a bit thin on comments explaining
> why functions exist, why the code is doing a partucular thing, etc.
Will add more comments in the next version.
Thanks,
Qi
>
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] mm: shmem: make unused huge shrinker memcg aware
2026-07-14 3:19 [PATCH] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
2026-07-15 4:32 ` Andrew Morton
@ 2026-07-15 9:37 ` Baolin Wang
2026-07-15 9:55 ` Qi Zheng
1 sibling, 1 reply; 5+ messages in thread
From: Baolin Wang @ 2026-07-15 9:37 UTC (permalink / raw)
To: Qi Zheng, hughd, akpm, usama.arif; +Cc: linux-mm, linux-kernel, Qi Zheng
Hi Qi,
On 7/14/26 11:19 AM, Qi Zheng wrote:
> 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=0, B=0
> patched A=64M, B=64M A=0, B=64M
>
> Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
> ---
Thanks for your patch. This work has been on my TODO list for a while :)
Overall it looks good. I'll take a closer look and run some tests.
> This patch is based on next-20260701 because it doesn't include this patch:
>
> https://lore.kernel.org/all/20260609123047.1948242-1-usama.arif@linux.dev/
>
> Later on, Usama will consider moving this restriction down into the fs callback.
>
> include/linux/shmem_fs.h | 14 +-
> mm/shmem.c | 379 ++++++++++++++++++++++++++++++---------
> 2 files changed, 310 insertions(+), 83 deletions(-)
>
> diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
> index e729b9b0e38d4..0b14de1e890a9 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>
>
> struct swap_iocb;
>
> @@ -56,6 +57,13 @@ struct shmem_inode_info {
> struct dquot __rcu *i_dquot[MAXQUOTAS];
> #endif
> struct inode vfs_inode;
> +
> +#ifdef CONFIG_TRANSPARENT_HUGEPAGE
> + struct list_head shrinklist_scan;
Why introduce another list_head member? Why can't we reuse 'shrinklist'
in this structure? In other words, if we isolate this inode from
list_lru, then 'inode_info->shrinklist' can be reused to link it into
the temporary list, right?
> + struct mem_cgroup *shrinklist_memcg;
> + int shrinklist_nid;
> + bool shrinklist_isolated;
Do we need an extra bool to indicate whether it's isolated? Can't we
just use 'list_empty(inode_info->shrinklist)'? I may have missed some
details.
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: [PATCH] mm: shmem: make unused huge shrinker memcg aware
2026-07-15 9:37 ` Baolin Wang
@ 2026-07-15 9:55 ` Qi Zheng
0 siblings, 0 replies; 5+ messages in thread
From: Qi Zheng @ 2026-07-15 9:55 UTC (permalink / raw)
To: Baolin Wang, hughd, akpm, usama.arif; +Cc: linux-mm, linux-kernel, Qi Zheng
Hi Baolin,
On 7/15/26 5:37 PM, Baolin Wang wrote:
> Hi Qi,
>
> On 7/14/26 11:19 AM, Qi Zheng wrote:
>> 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=0, B=0
>> patched A=64M, B=64M A=0, B=64M
>>
>> Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
>> ---
>
> Thanks for your patch. This work has been on my TODO list for a while :)
>
> Overall it looks good. I'll take a closer look and run some tests.
Thanks!
>
>> This patch is based on next-20260701 because it doesn't include this
>> patch:
>>
>> https://lore.kernel.org/all/20260609123047.1948242-1-
>> usama.arif@linux.dev/
>>
>> Later on, Usama will consider moving this restriction down into the fs
>> callback.
>>
>> include/linux/shmem_fs.h | 14 +-
>> mm/shmem.c | 379 ++++++++++++++++++++++++++++++---------
>> 2 files changed, 310 insertions(+), 83 deletions(-)
>>
>> diff --git a/include/linux/shmem_fs.h b/include/linux/shmem_fs.h
>> index e729b9b0e38d4..0b14de1e890a9 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>
>> struct swap_iocb;
>> @@ -56,6 +57,13 @@ struct shmem_inode_info {
>> struct dquot __rcu *i_dquot[MAXQUOTAS];
>> #endif
>> struct inode vfs_inode;
>> +
>> +#ifdef CONFIG_TRANSPARENT_HUGEPAGE
>> + struct list_head shrinklist_scan;
>
> Why introduce another list_head member? Why can't we reuse 'shrinklist'
> in this structure? In other words, if we isolate this inode from
> list_lru, then 'inode_info->shrinklist' can be reused to link it into
> the temporary list, right?
After list_lru isolation, the shrinker drops the list_lru lock and
processes the inode on a local scan list. During that time, shmem
faults can make the inode's current tail huge folio change, and
shmem_unused_huge_add() may need to queue the same inode again on
the active list_lru, possibly under a different memcg/node sublist.
If we reused info->shrinklist for the temporary scan list, then while
the inode is being scanned:
1. info->shrinklist would be non-empty because it is on the local scan
list, so shmem_unused_huge_add() could not tell that the inode is no
longer on the active list_lru.
2. Re-adding it to list_lru with the same list_head would corrupt the
local scan list.
3. Skipping the add would lose the new current tail huge folio if the
scan later drops the old scan entry instead of requeueing it.
So isolation removes the inode from the active list_lru and leaves
info->shrinklist empty. A concurrent fault can safely add the inode back
to the active list_lru with the current folio's memcg/node, while the
shrinker still owns the old scan entry through shrinklist_scan. The scan
loop then detects that re-add and drops the stale scan entry, leaving
the active entry for a later scan.
>
>> + struct mem_cgroup *shrinklist_memcg;
>> + int shrinklist_nid;
>> + bool shrinklist_isolated;
>
> Do we need an extra bool to indicate whether it's isolated? Can't we
> just use 'list_empty(inode_info->shrinklist)'? I may have missed some
> details.
list_empty(info->shrinklist) cannot be used for this. That list node
tracks active list_lru membership only. But you are right that the
explicit bool is not necessary, we can use
list_empty(&info->shrinklist_scan) instead.
Thanks,
Qi
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-07-15 9:55 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14 3:19 [PATCH] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
2026-07-15 4:32 ` Andrew Morton
2026-07-15 6:49 ` Qi Zheng
2026-07-15 9:37 ` Baolin Wang
2026-07-15 9:55 ` Qi Zheng
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.