mm: mglru: fix stale batch updates after memcg reparenting
Subsystems (5)
Qi Zheng
reviewerPATCH v5 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 v5:
- rename lock_batch_lruvec() to lruvec_live_lock_irq(), and move it to include/linux/memcontrol.h
(suggested by Johannes and Shakeel)
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
include/linux/memcontrol.h | 25 +++++++++++++++++++++++++ mm/vmscan.c | 11 ++++------- 2 files changed, 29 insertions(+), 7 deletions(-)
include/linux/memcontrol.h +25 -0
@@ -1472,6 +1472,31 @@ static inline void lruvec_lock_irq(struct lruvec *lruvec)spin_lock_irq (&lruvec->lru_lock); } +static inline struct lruvec *lruvec_live_lock_irq (struct lruvec *lruvec) +{ +#ifdef CONFIG_MEMCG +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); +#else +lruvec_lock_irq (lruvec); +#endif + +return lruvec; +} +static inline void lruvec_unlock (struct lruvec *lruvec) {spin_unlock (&lruvec->lru_lock);
mm/vmscan.c +4 -7
@@ -3265,7 +3265,7 @@ static void update_batch_size(struct lru_gen_mm_walk *walk, struct folio *folio,static void reset_batch_size (struct lru_gen_mm_walk *walk) {int gen, type, zone; -struct lruvec *lruvec = walk->lruvec; +struct lruvec *lruvec =lruvec_live_lock_irq (walk->lruvec);struct lru_gen_folio *lrugen = &lruvec->lrugen; walk->batched =0 ;
@@ -3285,6 +3285,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 +3781,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 +4866,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 43 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.
Quoted 6 lines
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]>
Quoted 43 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.
Quoted 6 lines
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]>