mm: mglru: fix stale batch updates after memcg reparenting
Subsystems (4)
Qi Zheng
reviewerPATCH v4 1/1 mm: mglru: fix stale batch updates after memcg reparenting
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in walk->nr_pages while walking page tables without holding the lruvec lock. The reset_batch_size() later folds those deltas into walk->lruvec under the lruvec lock.
The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
--> walk_page_range
--> update_batch_size
--> walk->nr_pages += delta
mem_cgroup_css_offline
--> memcg_reparent_objcgs
--> lock lruvec
lru_gen_reparent_memcg
--> reparent child folios to parent
unlock lruvec
lock lruvec
reset_batch_size
--> child lrugen->nr_pages += deltaThis will trigger the following warning in lru_gen_exit_memcg():
VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));
And the user-visible impact of underestimated nr_pages in MGLRU was premature OOMs because MGLRU does not try to reclaim memory when nr_pages reaches zero, but there are still more pages.
To fix it, make reset_batch_size() check CSS_DYING under RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.
---
Changes in v4:
- re-implement lock_batch_lruvec() in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630
Changes in v3:
- re-implement lock_batch_lruvec() by checking CSS_DYING under the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624
Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-by
mm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)
mm/vmscan.c +34 -7
@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); + +/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/ +while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock); + +return lruvec; +} +#else +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +lruvec_lock_irq (lruvec); + +return lruvec; +} +#endif +static void reset_batch_size (struct lru_gen_mm_walk *walk) {int gen, type, zone; -struct lruvec *lruvec = walk->lruvec; +struct lruvec *lruvec =lock_batch_lruvec (walk->lruvec);struct lru_gen_folio *lrugen = &lruvec->lrugen; walk->batched =0 ;
@@ -3285,6 +3315,8 @@ static void reset_batch_size(struct lru_gen_mm_walk *walk) lru +=LRU_ACTIVE ;__update_lru_size (lruvec, lru, zone, delta); } + +lruvec_unlock_irq (lruvec); }static int should_skip_vma (unsigned long start,unsigned long end,struct mm_walk *args)
@@ -3779,11 +3811,8 @@ static void walk_mm(struct mm_struct *mm, struct lru_gen_mm_walk *walk)mmap_read_unlock (mm); } -if (walk->batched) { -lruvec_lock_irq (lruvec); +if (walk->batched)reset_batch_size (walk); -lruvec_unlock_irq (lruvec); - }cond_resched (); }while (err == -EAGAIN );
@@ -4867,9 +4896,7 @@ static int evict_folios(unsigned long nr_to_scan, struct lruvec *lruvec, walk = current->reclaim_state->mm_walk;if (walk && walk->batched) { walk->lruvec = lruvec; -lruvec_lock_irq (lruvec);reset_batch_size (walk); -lruvec_unlock_irq (lruvec); } mod_lruvec_state(lruvec, PGDEMOTE_KSWAPD + reclaimer_offset(sc),
Signature · 1 line
2.54.0
Quoted 87 lines · On Wed, 1 Jul 2026 15:52:51 +0800 Qi Zheng <[email protected]> wrote:
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630Changes in v3:
- re-implementlock_batch_lruvec()by checkingCSS_DYINGunder the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-bymm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)mm/vmscan.c +8
-0@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); +
mm/vmscan.c static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, · line 3282
Quoted 10 lines
mm/vmscan.c +10
-0+/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/ +while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock);
Do we need an rcu_read_unlock() here?
Quoted 56 lines
mm/vmscan.c +16
-7+ + return lruvec; +} +#else +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +lruvec_lock_irq (lruvec); + +return lruvec; +} +#endif +static void reset_batch_size (struct lru_gen_mm_walk *walk) {int gen, type, zone; -struct lruvec *lruvec = walk->lruvec; +struct lruvec *lruvec =lock_batch_lruvec (walk->lruvec);struct lru_gen_folio *lrugen = &lruvec->lrugen; walk->batched =0 ;@@ -3285,6 +3315,8 @@ static void reset_batch_size(struct lru_gen_mm_walk *walk) lru +=LRU_ACTIVE ;__update_lru_size (lruvec, lru, zone, delta); } + +lruvec_unlock_irq (lruvec); }static int should_skip_vma (unsigned long start,unsigned long end,struct mm_walk *args)@@ -3779,11 +3811,8 @@ static void walk_mm(struct mm_struct *mm, struct lru_gen_mm_walk *walk)mmap_read_unlock (mm); } -if (walk->batched) { -lruvec_lock_irq (lruvec); +if (walk->batched)reset_batch_size (walk); -lruvec_unlock_irq (lruvec); - }cond_resched (); }while (err == -EAGAIN );@@ -4867,9 +4896,7 @@ static int evict_folios(unsigned long nr_to_scan, struct lruvec *lruvec, walk = current->reclaim_state->mm_walk;if (walk && walk->batched) { walk->lruvec = lruvec; -lruvec_lock_irq (lruvec);reset_batch_size (walk); -lruvec_unlock_irq (lruvec); } mod_lruvec_state(lruvec, PGDEMOTE_KSWAPD + reclaimer_offset(sc),Signature · 1 line
2.54.0
Quoted 93 lines · Usama Arif
Quoted 91 lines · On Wed, 1 Jul 2026 15:52:51 +0800 Qi Zheng <[email protected]> wrote:
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630Changes in v3:
- re-implementlock_batch_lruvec()by checkingCSS_DYINGunder the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-bymm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)mm/vmscan.c +12
-0@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); + +/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/
Quoted 8 lines
mm/vmscan.c static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, · line 3282
Quoted 6 lines
mm/vmscan.c +6
-0+while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock);Do we need an
rcu_read_unlock()here?
lruvec_unlock_irq() does that.
Signature · 2 lines
Cheers, Harry / Hyeonggon
Johannes Weiner
maintainerQuoted 105 lines · Harry Yoo
Quoted 93 lines · Usama Arif
Quoted 91 lines · On Wed, 1 Jul 2026 15:52:51 +0800 Qi Zheng <[email protected]> wrote:
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630Changes in v3:
- re-implementlock_batch_lruvec()by checkingCSS_DYINGunder the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-bymm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)mm/vmscan.c +12
-0@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); + +/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/ Quoted 8 lines
mm/vmscan.c static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, · line 3282
Quoted 6 lines
mm/vmscan.c +6
-0+while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock);Do we need an
rcu_read_unlock()here?
Quoted 1 line
lruvec_unlock_irq()does that.
Yeah, that tripped me up too. And it makes me think Shakeel was right after all: this should live next to the other lruvec_lock() primitives.
Sure, MGLRU is the only user, but it's still much easier to understand this if the code sits next to the rest of the API (and the unlock!).
lruvec_live_lock_irq()?
Qi Zheng
reviewerQuoted 108 lines · Johannes Weiner
Quoted 105 lines · Harry Yoo
Quoted 93 lines · Usama Arif
Quoted 91 lines · On Wed, 1 Jul 2026 15:52:51 +0800 Qi Zheng <[email protected]> wrote:
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630Changes in v3:
- re-implementlock_batch_lruvec()by checkingCSS_DYINGunder the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-bymm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)mm/vmscan.c +12
-0@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); + +/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/ Quoted 8 lines
mm/vmscan.c static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, · line 3282
Quoted 6 lines
mm/vmscan.c +6
-0+while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock);Do we need an
rcu_read_unlock()here?Quoted 1 line
lruvec_unlock_irq()does that.
Quoted 7 lines
Yeah, that tripped me up too. And it makes me think Shakeel was right after all: this should live next to the other
lruvec_lock()primitives.Sure, MGLRU is the only user, but it's still much easier to understand this if the code sits next to the rest of the API (and the unlock!).
lruvec_live_lock_irq()?
But lruvec_lock_irq() grabs the rcu lock too. :(
Johannes Weiner
maintainerQuoted 114 lines · Qi Zheng
Quoted 111 lines · Johannes Weiner
Quoted 105 lines · Harry Yoo
Quoted 93 lines · Usama Arif
Quoted 91 lines · On Wed, 1 Jul 2026 15:52:51 +0800 Qi Zheng <[email protected]> wrote:
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630Changes in v3:
- re-implementlock_batch_lruvec()by checkingCSS_DYINGunder the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-bymm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)mm/vmscan.c +12
-0@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); + +/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/ Quoted 8 lines
mm/vmscan.c static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, · line 3282
Quoted 6 lines
mm/vmscan.c +6
-0+while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock);Do we need an
rcu_read_unlock()here?Quoted 1 line
lruvec_unlock_irq()does that.Yeah, that tripped me up too. And it makes me think Shakeel was right after all: this should live next to the other
lruvec_lock()primitives.
Quoted 6 lines
Quoted 4 lines
Sure, MGLRU is the only user, but it's still much easier to understand this if the code sits next to the rest of the API (and the unlock!).
lruvec_live_lock_irq()?But
lruvec_lock_irq()grabs the rcu lock too. :(
Yes, but it's self-explanatory if you put it with those definitions:
static inline void lruvec_lock_irq(struct lruvec *lruvec)
{
rcu_read_lock();
spin_lock_irq(&lruvec->lru_lock);
}static struct lruvec *lruvec_live_lock_irq(struct lruvec *lruvec)
{
struct mem_cgroup *memcg = lruvec_memcg(lruvec);
rcu_read_lock();
while (unlikely(memcg && css_is_dying(&memcg->css))) {
memcg = parent_mem_cgroup(memcg);
lruvec = mem_cgroup_lruvec(memcg, lruvec_pgdat(lruvec));
}
spin_lock_irq(&lruvec->lru_lock);
}static inline void lruvec_unlock_irq(struct lruvec *lruvec)
{
spin_unlock_irq(&lruvec->lru_lock);
rcu_read_unlock();
}etc.
Qi Zheng
reviewerQuoted 131 lines · Johannes Weiner
Quoted 114 lines · Qi Zheng
Quoted 111 lines · Johannes Weiner
Quoted 105 lines · Harry Yoo
Quoted 93 lines · Usama Arif
Quoted 91 lines · On Wed, 1 Jul 2026 15:52:51 +0800 Qi Zheng <[email protected]> wrote:
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630Changes in v3:
- re-implementlock_batch_lruvec()by checkingCSS_DYINGunder the RCU lock (suggested by Harry)
- update the commit message (suggested by Harry)
- temporarily drop the previous Reviewed-by tags
(since the sync method has changed)
- rebase onto the next-20260624Changes in v2:
- update the commit message (pointed by Barry)
- collect Reviewed-bymm/vmscan.c | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-)mm/vmscan.c +12
-0@@ -3262,10 +3262,40 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, walk->nr_pages[new_gen][type][zone] += delta; } +#ifdef CONFIG_MEMCG +static struct lruvec *lock_batch_lruvec (struct lruvec *lruvec) +{ +struct pglist_data *pgdat =lruvec_pgdat (lruvec); +struct mem_cgroup *memcg =lruvec_memcg (lruvec); + +rcu_read_lock (); + +/* +* The memcg can be NULL when the memory controller is disabled. +* Otherwise, the caller keeps the memcg owning @lruvec alive. +*/ Quoted 8 lines
mm/vmscan.c static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio, · line 3282
Quoted 6 lines
mm/vmscan.c +6
-0+while (unlikely (memcg &&css_is_dying (&memcg->css))) { + memcg =parent_mem_cgroup (memcg); + lruvec =mem_cgroup_lruvec (memcg, pgdat); + } + +spin_lock_irq (&lruvec->lru_lock);Do we need an
rcu_read_unlock()here?Quoted 1 line
lruvec_unlock_irq()does that.Yeah, that tripped me up too. And it makes me think Shakeel was right after all: this should live next to the other
lruvec_lock()primitives.Quoted 6 lines
Quoted 4 lines
Sure, MGLRU is the only user, but it's still much easier to understand this if the code sits next to the rest of the API (and the unlock!).
lruvec_live_lock_irq()?But
lruvec_lock_irq()grabs the rcu lock too. :(Yes, but it's self-explanatory if you put it with those definitions:
static inline void
lruvec_lock_irq(struct lruvec *lruvec){ rcu_read_lock(); spin_lock_irq(&lruvec->lru_lock); }static struct lruvec *lruvec_live_lock_irq(struct lruvec *lruvec)
Quoted 10 lines
{
struct mem_cgroup *memcg = lruvec_memcg(lruvec);
rcu_read_lock();
while (unlikely(memcg && css_is_dying(&memcg->css))) {
memcg = parent_mem_cgroup(memcg);
lruvec = mem_cgroup_lruvec(memcg, lruvec_pgdat(lruvec));
}
spin_lock_irq(&lruvec->lru_lock);
}All right, should the implementation for !CONFIG_MEMCG be placed here too?
Will send the v5.
Thanks,
Qi
Quoted 8 lines
static inline void
lruvec_unlock_irq(struct lruvec *lruvec){ spin_unlock_irq(&lruvec->lru_lock); rcu_read_unlock(); }etc.
Johannes Weiner
maintainerQuoted 27 lines · Qi Zheng
Quoted 15 lines · Johannes Weiner
Quoted 4 lines · Qi Zheng
Quoted 1 line · Johannes Weiner
lruvec_live_lock_irq()?But
lruvec_lock_irq()grabs the rcu lock too. :(Yes, but it's self-explanatory if you put it with those definitions:
static inline void
lruvec_lock_irq(struct lruvec *lruvec){ rcu_read_lock(); spin_lock_irq(&lruvec->lru_lock); }static struct lruvec *lruvec_live_lock_irq(struct lruvec *lruvec)
Quoted 10 lines
{ struct mem_cgroup *memcg = lruvec_memcg(lruvec); rcu_read_lock(); while (unlikely(memcg && css_is_dying(&memcg->css))) { memcg = parent_mem_cgroup(memcg); lruvec = mem_cgroup_lruvec(memcg, lruvec_pgdat(lruvec)); } spin_lock_irq(&lruvec->lru_lock); }
Quoted 2 lines
All right, should the implementation for !CONFIG_MEMCG be placed here too?
Yes. You can just #ifdef, #else the function body itself.
static struct lruvec *lruvec_live_lock_irq()
{
#ifdef CONFIG_MEMCG ... #else lruvec_lock_irq(lruvec); return lruvec;
#endif
}
Quoted 1 line
Will send the v5.
Thanks.
Qi Zheng
reviewerHi Andrew and David,
Gentle ping.
Is this patch good to be applied?
Thanks,
Qi
Quoted 56 lines · Qi Zheng
From: Qi Zheng <[email protected]>
The mglru page table walker batches per-generation size deltas in
walk->nr_pageswhile walking page tables without holding the lruvec lock. Thereset_batch_size()later folds those deltas intowalk->lruvecunder the lruvec lock.The page table walker can run concurrently with the memcg reparenting path as follows:
CPU0 CPU1
==== ====
walk_mm
-->walk_page_range--> update_batch_size --> walk->nr_pages += delta mem_cgroup_css_offline --> memcg_reparent_objcgs --> lock lruvec lru_gen_reparent_memcg --> reparent child folios to parent unlock lruvec lock lruvec reset_batch_size --> child lrugen->nr_pages += deltaThis will trigger the following warning in
lru_gen_exit_memcg():VM_WARN_ON_ONCE(memchr_inv(lruvec->lrugen.nr_pages, 0, sizeof(lruvec->lrugen.nr_pages)));And the user-visible impact of underestimated
nr_pagesin MGLRU was premature OOMs because MGLRU does not try to reclaim memory whennr_pagesreaches zero, but there are still more pages.To fix it, make
reset_batch_size()checkCSS_DYINGunder RCU before flushing the pending batch. A non-dying memcg keeps the original lruvec stable against RCU-delayed offlining; a dying memcg redirects the deltas to the first non-dying ancestor.Reported-by Peiyang He <[email protected]>Closes https://lore.kernel.org/all/5A9E929D82717101+12fcf643-efb8-4b9a-a53a-1e28cc894f0b@smail.nju.edu.cn open hereFixes f304652609ea ("mm: vmscan: prepare for reparenting MGLRU folios")Cc <[email protected]>Signed-off-by Qi Zheng <[email protected]>Reviewed-by Harry Yoo (Oracle) <[email protected]>---
Changes in v4:
- re-implementlock_batch_lruvec()in a simpler way (suggested by Johannes and Harry)
- collect Reviewed-by
- rebase onto the next-20260630