[PATCH] mm/mglru: force aging the lruvec when min_seq pinned

From: zhaoyang.huang

Date: Thu Jul 30 2026 - 03:54:07 EST


From: Zhaoyang Huang <zhaoyang.huang@xxxxxxxxxx>

This submission addresses an issue where the memcg MGLRU's min_seq is
pinned by an orphan page—a page that resides in the page cache but
lacks a corresponding reference count, while the number of GENs has not
yet met the criteria of aging. I believe such pages should be detected
and handled elsewhere rather than affecting the LRU in this way(legacy
LRU could move forward by bringing this kind of folio along with).
Therefore, I propose moving these folios to the next generation when
isolation makes no progress at all.

Signed-off-by: Zhaoyang Huang <zhaoyang.huang@xxxxxxxxxx>
---
include/linux/mmzone.h | 6 ++++++
mm/vmscan.c | 45 +++++++++++++++++++++++++++++++++++++++++-
2 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 9adb2ad21da5..b063c56ea30c 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -434,6 +434,12 @@ enum lruvec_flags {
*/
LRUVEC_CGROUP_CONGESTED,
LRUVEC_NODE_CONGESTED,
+ /*
+ * MGLRU: eviction failed to isolate from the oldest generation once
+ * while the window was at MAX_NR_GENS. A second consecutive failure
+ * forces aging so residual folios cannot pin min_seq indefinitely.
+ */
+ LRUVEC_MGLRU_ISOLATE_FAILED,
};

#endif /* !__GENERATING_BOUNDS_H */
diff --git a/mm/vmscan.c b/mm/vmscan.c
index bd1b1aa12581..c18db08c5e10 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -175,6 +175,8 @@ struct scan_control {
unsigned int immediate;
unsigned int file_taken;
unsigned int taken;
+ /* folios isolated by MGLRU eviction in the current lruvec pass */
+ unsigned int isolated;
} nr;

/* for recording the reclaimed slab by now */
@@ -4748,6 +4750,7 @@ static int scan_folios(unsigned long nr_to_scan, struct lruvec *lruvec,
trace_mm_vmscan_lru_isolate(sc->reclaim_idx, sc->order, scan_batch,
scanned, skipped, isolated,
type ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON);
+ sc->nr.isolated += isolated;
if (type == LRU_GEN_FILE)
sc->nr.file_taken += isolated;
/*
@@ -4998,6 +5001,11 @@ static bool try_to_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
long nr_to_scan;
unsigned long scanned = 0;
int swappiness = get_swappiness(lruvec, sc);
+ bool attempted = false;
+ bool rotated = false;
+
+ /* per-lruvec pass; sc is shared across lruvecs during reclaim */
+ sc->nr.isolated = 0;

while (true) {
int delta;
@@ -5006,6 +5014,7 @@ static bool try_to_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
if (nr_to_scan <= 0)
break;

+ attempted = true;
delta = evict_folios(nr_to_scan, lruvec, sc, swappiness);
if (!delta)
break;
@@ -5020,6 +5029,40 @@ static bool try_to_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
cond_resched();
}

+ /*
+ * Eviction isolated nothing while the sliding window is already at
+ * MAX_NR_GENS. In that state should_run_aging() is false, so
+ * get_nr_to_scan() never calls try_to_inc_max_seq(). Residual folios
+ * that fail isolate_folio() can pin min_seq and hide reclaimable
+ * folios in younger generations.
+ *
+ * Require two consecutive isolate failures on this lruvec before
+ * forcing aging, so a single no-progress batch does not rotate the
+ * window. Successful isolation clears the sticky bit.
+ *
+ * Key off sc->nr.isolated, not nr_reclaimed: folios isolated but
+ * rejected by shrink_folio_list() are put back with PG_active and do
+ * not pin the oldest generation.
+ */
+ if (attempted && !sc->nr.isolated) {
+ DEFINE_MAX_SEQ(lruvec);
+ DEFINE_MIN_SEQ(lruvec);
+
+ if (evictable_min_seq(min_seq, swappiness) + MIN_NR_GENS < max_seq) {
+ if (test_and_set_bit(LRUVEC_MGLRU_ISOLATE_FAILED,
+ &lruvec->flags)) {
+ rotated = try_to_inc_max_seq(lruvec, max_seq,
+ swappiness, false);
+ clear_bit(LRUVEC_MGLRU_ISOLATE_FAILED,
+ &lruvec->flags);
+ }
+ } else {
+ clear_bit(LRUVEC_MGLRU_ISOLATE_FAILED, &lruvec->flags);
+ }
+ } else if (sc->nr.isolated) {
+ clear_bit(LRUVEC_MGLRU_ISOLATE_FAILED, &lruvec->flags);
+ }
+
/*
* If too many file cache in the coldest generation can't be evicted
* due to being dirty, wake up the flusher.
@@ -5043,7 +5086,7 @@ static bool try_to_shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
}

/* whether this lruvec should be rotated */
- return nr_to_scan < 0;
+ return nr_to_scan < 0 || rotated;
}

static int shrink_one(struct lruvec *lruvec, struct scan_control *sc)
--
2.25.1