Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Baolin Wang <baolin.wang@linux.alibaba.com>
To: Qi Zheng <qi.zheng@linux.dev>,
	hughd@google.com, akpm@linux-foundation.org,
	usama.arif@linux.dev
Cc: linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	Qi Zheng <zhengqi.arch@bytedance.com>
Subject: Re: [PATCH v2 2/2] mm: shmem: make unused huge shrinker memcg aware
Date: Wed, 29 Jul 2026 11:29:53 +0800	[thread overview]
Message-ID: <c169c4ec-adc8-4991-a690-ed5bfb4796a0@linux.alibaba.com> (raw)
In-Reply-To: <a0b029b7-6ab8-4886-a26e-e52cec31a8e1@linux.dev>



On 7/28/26 3:12 PM, Qi Zheng wrote:
> Hi Baolin,
> 
> On 7/27/26 1:11 PM, Baolin Wang wrote:
>> Hi Qi,
>>
>> (Sorry for the late reply due to my vacation)
> 
> No worries, and thank you for your response!
> 
>>
>> On 7/21/26 4:34 PM, 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>
>>> ---
>>>   include/linux/shmem_fs.h |  12 +-
>>>   mm/shmem.c               | 378 +++++++++++++++++++++++++++++----------
>>>   2 files changed, 294 insertions(+), 96 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
>>>   };
>>
>> Thanks. This looks better now.:) Some comments below.
>>
>>>   #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 20c24d92da430..8bf6dd850f85b 100644
>>> --- a/mm/shmem.c
>>> +++ b/mm/shmem.c
>>> @@ -724,51 +724,242 @@ static const char *shmem_format_huge(int huge)
>>>   }
>>>   #endif
>>> +static bool is_shmem_unused_huge_isolated(struct shmem_inode_info 
>>> *info)
>>> +{
>>> +
>>> +    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;
>>
>> It looks like we should return an error here instead of NULL? since 
>> callers use IS_ERR() to check the return value of this function.
> 
> Returning NULL here is intentional - it is not an error case.
> 
> The get_mem_cgroup_from_folio() returns NULL only when
> mem_cgroup_disabled() is true. In that case the list_lru is not
> memcg-aware, so use the global (root) list:
> 
> list_lru_add() /* memcg == NULL */
> --> lock_list_lru_of_memcg()
>      --> list_lru_from_memcg_idx() /* memcg_kmem_id(NULL) == -1 */
>          --> return &lru->node[nid].lru

OK. I see. Thanks.

[snip]

>>>   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) {
>>> -        pgoff_t next, end;
>>> +    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;
>>> -
>>
>> Why move this logic? If this fixes anything, then a separate fix patch 
>> would be clearer.
> 
> After converting the shmem huge shrinker to be memcg-aware, we need to
> acquire the folio first to determine which sublist it should be returned
> to.

OK. Now the requeue needs to acquire the folio.

>>>           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 +972,25 @@ 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)) {
>>> +            shmem_unused_huge_requeue(inode, folio);
>>> +            folio_put(folio);
>>
>> Maybe keeping the 'move_back' label would be clearer? since you've 
>> added several places to requeue the inode.
> 
> Agree, will do.
> 
>>
>>> +            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 +999,33 @@ 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 put;
>>> +        }
>>> +
>>> +        if (!is_shmem_unused_huge_match(folio, scan.sc)) {
>>> +            folio_unlock(folio);
>>> +            shmem_unused_huge_requeue(inode, folio);
>>>               folio_put(folio);
>>> -            goto move_back;
>>> +            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;
>>> +        freed += folio_end - 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 +1038,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 +1049,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 +1619,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,
>>> @@ -2532,28 +2727,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);
>>> -    }
>>
>> Why move this additional logic down?
> 
> Because under the following condition:
> 
>          if (sgp <= SGP_CACHE &&
>          ((loff_t)index << PAGE_SHIFT) >= i_size_read(inode)) {
>          error = -EINVAL;
>          goto unlock;
>      }
> 
> we will jump to unlock and remove the folio from page cache.
> 
> With the new memcg-aware design, shmem_unused_huge_add() takes a memcg
> reference. If the folio is then removed by the error path, the inode
> sits on the shrinker list holding a stale memcg reference and pointing
> at an i_size that no longer matches the folio.
> 
> So we should ensure the inode is only queued when the folio is fully set
> up and about to be returned successfully.

Right. But I think this deserves a separate preparation patch with above 
explanation, moving the original shrinklist addition logic to after all 
checks are completed.


  reply	other threads:[~2026-07-29  3:30 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-21  8:34 [PATCH v2 0/2] make unused huge shrinker memcg aware Qi Zheng
2026-07-21  8:34 ` [PATCH v2 1/2] fs: push nr_cached_objects memcg gating into individual filesystems Qi Zheng
2026-07-21  8:34 ` [PATCH v2 2/2] mm: shmem: make unused huge shrinker memcg aware Qi Zheng
2026-07-21  9:25   ` Qi Zheng
2026-07-27  5:11   ` Baolin Wang
2026-07-28  7:12     ` Qi Zheng
2026-07-29  3:29       ` Baolin Wang [this message]
2026-07-29  5:59         ` Qi Zheng

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=c169c4ec-adc8-4991-a690-ed5bfb4796a0@linux.alibaba.com \
    --to=baolin.wang@linux.alibaba.com \
    --cc=akpm@linux-foundation.org \
    --cc=hughd@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=qi.zheng@linux.dev \
    --cc=usama.arif@linux.dev \
    --cc=zhengqi.arch@bytedance.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox