* [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper
2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
@ 2026-07-14 12:15 ` Zicheng Wang
2026-07-14 12:15 ` [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file Zicheng Wang
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
To: akpm, yuanchu
Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
Zicheng Wang
Add the two pieces the rest of the series builds on: an AGING node
stat counter (the observation an aging policy pairs with
workingset_refault to spot over-aging) and a reusable helper,
lru_gen_age_memcg(), that ages a memcg by advancing max_seq without
reclaim (the control primitive memory.aging wraps).
The counter counts every pass in inc_max_seq(), regardless of caller.
The helper mirrors lru_gen_seq_write()'s reclaim_state /
memalloc_noreclaim / mm_walk setup; swappiness comes from
get_swappiness() so anon is not rotated without swap, and a racing
lruvec is skipped (-EAGAIN) without blocking other nodes.
Gated by CONFIG_LRU_GEN. No user-visible change yet.
Signed-off-by: Zicheng Wang <wangzicheng@honor.com>
---
include/linux/mmzone.h | 3 ++
mm/internal.h | 4 +++
mm/memcontrol.c | 9 +++++
mm/vmscan.c | 80 ++++++++++++++++++++++++++++++++++++++++++
mm/vmstat.c | 3 ++
5 files changed, 99 insertions(+)
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 9adb2ad21da5..ab5137c272ca 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -323,6 +323,9 @@ enum node_stat_item {
PGSCAN_ANON,
PGSCAN_FILE,
PGREFILL,
+#ifdef CONFIG_LRU_GEN
+ AGING,
+#endif
#ifdef CONFIG_HUGETLB_PAGE
NR_HUGETLB,
#endif
diff --git a/mm/internal.h b/mm/internal.h
index 5a2ddcf68e0b..96add6cd4627 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -634,6 +634,10 @@ extern void reclaim_throttle(pg_data_t *pgdat, enum vmscan_throttle_state reason
int user_proactive_reclaim(char *buf,
struct mem_cgroup *memcg, pg_data_t *pgdat);
+#ifdef CONFIG_LRU_GEN
+int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens);
+#endif
+
/*
* in mm/rmap.c:
*/
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index d978e18b9b2d..e42f97e004b6 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -420,6 +420,9 @@ static const unsigned int memcg_node_stat_items[] = {
PGSCAN_ANON,
PGSCAN_FILE,
PGREFILL,
+#ifdef CONFIG_LRU_GEN
+ AGING,
+#endif
#ifdef CONFIG_HUGETLB_PAGE
NR_HUGETLB,
#endif
@@ -1606,6 +1609,9 @@ static const struct memory_stat memory_stats[] = {
#ifdef CONFIG_NUMA_BALANCING
{ "pgpromote_success", PGPROMOTE_SUCCESS },
#endif
+#ifdef CONFIG_LRU_GEN
+ { "aging", AGING },
+#endif
};
/* The actual unit of the state item, not the same as the output unit */
@@ -1655,6 +1661,9 @@ static int memcg_page_state_output_unit(int item)
case PGSCAN_KHUGEPAGED:
case PGSCAN_PROACTIVE:
case PGREFILL:
+#ifdef CONFIG_LRU_GEN
+ case AGING:
+#endif
#ifdef CONFIG_NUMA_BALANCING
case PGPROMOTE_SUCCESS:
#endif
diff --git a/mm/vmscan.c b/mm/vmscan.c
index b3e555561417..805e29c499c8 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3987,6 +3987,9 @@ static bool inc_max_seq(struct lruvec *lruvec, unsigned long seq, int swappiness
WRITE_ONCE(lrugen->timestamps[next], jiffies);
/* make sure preceding modifications appear */
smp_store_release(&lrugen->max_seq, lrugen->max_seq + 1);
+
+ /* Count every aging pass for over-aging diagnosis. */
+ mod_lruvec_state(lruvec, AGING, 1);
unlock:
lruvec_unlock_irq(lruvec);
@@ -5855,6 +5858,83 @@ static int __init init_lru_gen(void)
};
late_initcall(init_lru_gen);
+/**
+ * lru_gen_age_memcg - Proactively age a memcg by N generations
+ * @memcg: target memcg
+ * @nr_gens: number of generations to age (1..MAX_NR_GENS)
+ *
+ * Advances max_seq without page reclaim on all nodes of @memcg, using
+ * the same lock/walk protocol as lru_gen_seq_write(). Swappiness comes
+ * from get_swappiness() so anon rotation is skipped without swap.
+ * to the next node.
+ *
+ * Return: 0 on success, -EINVAL on bad input, -EAGAIN if a lruvec did
+ * not advance, -ENOMEM on mm_walk failure, -EINTR on signal.
+ */
+int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens)
+{
+ struct scan_control sc = {
+ .may_writepage = true,
+ .may_unmap = true,
+ .may_swap = true,
+ .reclaim_idx = MAX_NR_ZONES - 1,
+ .gfp_mask = GFP_KERNEL,
+ .proactive = true,
+ };
+ int nid, swappiness;
+ unsigned long i;
+ int ret = 0;
+ unsigned int flags;
+ struct blk_plug plug;
+ struct lruvec *lruvec;
+
+ if (!memcg)
+ return -EINVAL;
+
+ if (nr_gens == 0 || nr_gens > MAX_NR_GENS)
+ return -EINVAL;
+
+ set_task_reclaim_state(current, &sc.reclaim_state);
+ flags = memalloc_noreclaim_save();
+ blk_start_plug(&plug);
+ if (!set_mm_walk(NULL, true)) {
+ ret = -ENOMEM;
+ goto done;
+ }
+
+ for_each_node_state(nid, N_MEMORY) {
+ if (signal_pending(current)) {
+ ret = -EINTR;
+ goto done;
+ }
+
+ lruvec = get_lruvec(memcg, nid);
+ if (!lruvec)
+ continue;
+
+ swappiness = get_swappiness(lruvec, &sc);
+
+ for (i = 0; i < nr_gens; i++) {
+ DEFINE_MAX_SEQ(lruvec);
+
+ if (!try_to_inc_max_seq(lruvec, max_seq,
+ swappiness, false)) {
+ ret = -EAGAIN;
+ break;
+ }
+ }
+
+ cond_resched();
+ }
+
+done:
+ clear_mm_walk();
+ blk_finish_plug(&plug);
+ memalloc_noreclaim_restore(flags);
+ set_task_reclaim_state(current, NULL);
+ return ret;
+}
+
#else /* !CONFIG_LRU_GEN */
static void lru_gen_age_node(struct pglist_data *pgdat, struct scan_control *sc)
diff --git a/mm/vmstat.c b/mm/vmstat.c
index f534972f517d..78ea093428c7 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -1290,6 +1290,9 @@ const char * const vmstat_text[] = {
[I(PGSCAN_ANON)] = "pgscan_anon",
[I(PGSCAN_FILE)] = "pgscan_file",
[I(PGREFILL)] = "pgrefill",
+#ifdef CONFIG_LRU_GEN
+ [I(AGING)] = "aging",
+#endif
#ifdef CONFIG_HUGETLB_PAGE
[I(NR_HUGETLB)] = "nr_hugetlb",
#endif
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread* [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file
2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
2026-07-14 12:15 ` [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper Zicheng Wang
@ 2026-07-14 12:15 ` Zicheng Wang
2026-07-14 12:15 ` [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat Zicheng Wang
2026-07-15 14:00 ` [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Johannes Weiner
3 siblings, 0 replies; 5+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
To: akpm, yuanchu
Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
Zicheng Wang
Add a write-only memory.aging that triggers MGLRU aging (max_seq
advancement) without reclaim - the aging counterpart of memory.reclaim.
Userspace ages to rebalance the anon/file distribution across
generations, then reclaims. This is the control half of the loop;
the observability it needs lands in the next patch.
Useful for workloads such as the Android app lifecycle, where anon
concentrates in the 2nd-youngest generation while file fills the
oldest two and gets over-reclaimed.
echo 2 > /sys/fs/cgroup/foo/memory.aging
echo "100M" > /sys/fs/cgroup/foo/memory.reclaim
Gated by CONFIG_LRU_GEN. Concurrent writers race on the lruvec lock;
a caller that did not advance the window gets -EAGAIN.
Signed-off-by: Zicheng Wang <wangzicheng@honor.com>
---
Documentation/admin-guide/cgroup-v2.rst | 35 +++++++++++++++++++
Documentation/admin-guide/mm/multigen_lru.rst | 13 +++++++
mm/memcontrol.c | 30 ++++++++++++++++
3 files changed, 78 insertions(+)
diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 6efd0095ed99..f42ac6dddbf6 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1437,6 +1437,41 @@ The following nested keys are defined.
The valid range for swappiness is [0-200, max], setting
swappiness=max exclusively reclaims anonymous memory.
+ memory.aging
+ A write-only file which exists for all cgroups when
+ CONFIG_LRU_GEN is enabled.
+
+ This interface triggers MGLRU aging (generation advancement)
+ without page reclaim. It is useful for proactively rebalancing
+ the distribution of pages across generations before invoking
+ memory.reclaim, particularly for workloads (such as the Android
+ application lifecycle) where anonymous pages concentrate in
+ the youngest generations while file pages fill up the oldest
+ generation and get over-reclaimed.
+
+ The single argument is the number of generations to age, in
+ the range [1, MAX_NR_GENS]. MAX_NR_GENS is currently 4.
+
+ Example::
+
+ echo 2 > memory.aging
+
+ Each aging pass advances both anonymous and file generations
+ simultaneously, matching MGLRU's max_seq semantics. There is
+ no way to age only one type - this is a property of MGLRU,
+ not of this interface.
+
+ Proactive aging should be followed by memory.reclaim to
+ actually free pages::
+
+ echo 2 > /sys/fs/cgroup/foo/memory.aging
+ echo "100M" > /sys/fs/cgroup/foo/memory.reclaim
+
+ Returns -EAGAIN if this call did not advance the generation
+ window (typically because a concurrent caller on the same
+ cgroup got there first). Returns -EINVAL if the argument is
+ missing, zero, or out of range.
+
memory.peak
A read-write single value file which exists on non-root cgroups.
diff --git a/Documentation/admin-guide/mm/multigen_lru.rst b/Documentation/admin-guide/mm/multigen_lru.rst
index 9cb54b4ff5d9..fd8b4286014e 100644
--- a/Documentation/admin-guide/mm/multigen_lru.rst
+++ b/Documentation/admin-guide/mm/multigen_lru.rst
@@ -161,3 +161,16 @@ cold pages because of the overestimation, it retries on the next
server according to the ranking result obtained from the working set
estimation step. This less forceful approach limits the impacts on the
existing jobs.
+
+Proactive aging
+---------------
+For workloads where it is useful to rebalance the distribution of
+anonymous and file pages across generations before reclaiming (for
+example, the Android application lifecycle, where an application
+that has just been backgrounded has most of its anonymous pages
+clustered in the youngest generations while file pages dominate
+the oldest), the ``memory.aging`` cgroup v2 file can be used to
+advance the generation counter without eviction. The file returns
+-EAGAIN if no advance happened, so callers can correlate with the
+``aging`` counter to verify how many passes actually took
+effect. See Documentation/admin-guide/cgroup-v2.rst.
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index e42f97e004b6..c6a969b56878 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -4896,6 +4896,29 @@ static ssize_t memory_reclaim(struct kernfs_open_file *of, char *buf,
return nbytes;
}
+#ifdef CONFIG_LRU_GEN
+static ssize_t memory_aging(struct kernfs_open_file *of, char *buf,
+ size_t nbytes, loff_t off)
+{
+ struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
+ unsigned long nr_gens;
+ int ret;
+
+ ret = kstrtoul(buf, 0, &nr_gens);
+ if (ret)
+ return ret;
+
+ if (nr_gens == 0 || nr_gens > MAX_NR_GENS)
+ return -EINVAL;
+
+ ret = lru_gen_age_memcg(memcg, nr_gens);
+ if (ret)
+ return ret;
+
+ return nbytes;
+}
+#endif
+
static struct cftype memory_files[] = {
{
.name = "current",
@@ -4967,6 +4990,13 @@ static struct cftype memory_files[] = {
.flags = CFTYPE_NS_DELEGATABLE,
.write = memory_reclaim,
},
+#ifdef CONFIG_LRU_GEN
+ {
+ .name = "aging",
+ .flags = CFTYPE_NS_DELEGATABLE,
+ .write = memory_aging,
+ },
+#endif
{ } /* terminate */
};
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread* [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat
2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
2026-07-14 12:15 ` [RFC v2 1/3] mm/lru_gen: add AGING counter and proactive aging helper Zicheng Wang
2026-07-14 12:15 ` [RFC v2 2/3] mm: memcontrol: add memory.aging cgroup v2 file Zicheng Wang
@ 2026-07-14 12:15 ` Zicheng Wang
2026-07-15 14:00 ` [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Johannes Weiner
3 siblings, 0 replies; 5+ messages in thread
From: Zicheng Wang @ 2026-07-14 12:15 UTC (permalink / raw)
To: akpm, yuanchu
Cc: tj, hannes, mkoutny, corbet, kasong, qi.zheng, shakeel.butt,
baohua, axelrasmussen, weixugc, david, ljs, liam, vbabka, rppt,
surenb, mhocko, roman.gushchin, muchun.song, cgroups, linux-mm,
linux-kernel, linux-doc, willy, denghaojie, baoquan.he,
kaleshsingh, tjmercier, tao.wangtao, zhangji1, wangzhen5,
Zicheng Wang
Add nr_oldest_anon and nr_oldest_file to memory.stat: pages in the
oldest generation (min_seq) of each type, aggregated over the cgroup
subtree and all N_MEMORY nodes. This is the observation half of the
proactive-aging loop - it shows a policy how much anon/file memory the
next reclaim can evict directly, so it can decide whether aging is
needed.
Computed on demand from lrugen->nr_pages; no per-page maintenance, no
hot-path cost. Paired with the AGING counter and workingset_refault_*
to confirm an aging pass took effect and was not too aggressive.
Gated by CONFIG_LRU_GEN; reported in pages, matching the lru_gen dump.
Signed-off-by: Zicheng Wang <wangzicheng@honor.com>
---
Documentation/admin-guide/cgroup-v2.rst | 18 ++++++++++++++++
mm/internal.h | 2 ++
mm/memcontrol.c | 24 +++++++++++++++++++++
mm/vmscan.c | 28 +++++++++++++++++++++++++
4 files changed, 72 insertions(+)
diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index f42ac6dddbf6..a071175d11be 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1658,6 +1658,24 @@ The following nested keys are defined.
the value for the foo counter, since the foo counter is type-based, not
list-based.
+ nr_oldest_anon, nr_oldest_file
+ Number of pages in the oldest generation of the anonymous and
+ file types. Only present when CONFIG_LRU_GEN is enabled.
+
+ With MGLRU, page reclaim starts from the oldest generation
+ (min_seq) of each type, so these counts show how much anonymous
+ and file memory the next reclaim pass (or a proactive
+ memory.reclaim) can directly evict. They are aggregated across
+ this cgroup's subtree and all NUMA nodes, and are reported in
+ pages, matching the per-generation breakdown in the ``lru_gen``
+ debugfs file.
+
+ Proactive-aging policy can use them together with the ``aging``
+ counter and ``workingset_refault_*``: a large ``nr_oldest_file``
+ with near-zero ``nr_oldest_anon`` indicates file cache piling up
+ in the oldest generation while anonymous pages stay young, which
+ is the condition ``memory.aging`` is meant to rebalance.
+
slab_reclaimable
Part of "slab" that might be reclaimed, such as
dentries and inodes.
diff --git a/mm/internal.h b/mm/internal.h
index 96add6cd4627..959d376c02a6 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -636,6 +636,8 @@ int user_proactive_reclaim(char *buf,
#ifdef CONFIG_LRU_GEN
int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens);
+void lru_gen_nr_oldest_pages(struct lruvec *lruvec,
+ unsigned long *nr_anon, unsigned long *nr_file);
#endif
/*
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index c6a969b56878..016194cabd60 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -1733,6 +1733,30 @@ static void memcg_stat_format(struct mem_cgroup *memcg, struct seq_buf *s)
}
}
+#ifdef CONFIG_LRU_GEN
+ /* Oldest-generation (min_seq) anon/file pages over this cgroup's
+ * subtree and all N_MEMORY nodes; see lru_gen_nr_oldest_pages().
+ */
+ {
+ struct mem_cgroup *mi;
+ int nid;
+ unsigned long oldest_anon = 0, oldest_file = 0;
+
+ for_each_mem_cgroup_tree(mi, memcg) {
+ for_each_node_state(nid, N_MEMORY) {
+ unsigned long nr_anon, nr_file;
+ struct lruvec *lruvec = mem_cgroup_lruvec(mi, NODE_DATA(nid));
+
+ lru_gen_nr_oldest_pages(lruvec, &nr_anon, &nr_file);
+ oldest_anon += nr_anon;
+ oldest_file += nr_file;
+ }
+ }
+ seq_buf_printf(s, "nr_oldest_anon %lu\n", oldest_anon);
+ seq_buf_printf(s, "nr_oldest_file %lu\n", oldest_file);
+ }
+#endif
+
/* Accumulated memory events */
seq_buf_printf(s, "pgscan %lu\n",
memcg_page_state(memcg, PGSCAN_KSWAPD) +
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 805e29c499c8..3800f0c4f1f5 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -5935,6 +5935,34 @@ int lru_gen_age_memcg(struct mem_cgroup *memcg, unsigned long nr_gens)
return ret;
}
+/**
+ * lru_gen_nr_oldest_pages - pages in the oldest generation of a lruvec
+ * @lruvec: target lruvec
+ * @nr_anon: filled with oldest-generation anonymous page count
+ * @nr_file: filled with oldest-generation file page count
+ *
+ * Reclaim starts from the oldest generation (min_seq) of each type, so these
+ * count what the next reclaim can evict directly. Computed from
+ * lrugen->nr_pages[min_seq type][zone]; eventually consistent, clamped to 0.
+ */
+void lru_gen_nr_oldest_pages(struct lruvec *lruvec,
+ unsigned long *nr_anon, unsigned long *nr_file)
+{
+ int type, zone;
+ struct lru_gen_folio *lrugen = &lruvec->lrugen;
+ unsigned long size[ANON_AND_FILE] = {};
+
+ for (type = 0; type < ANON_AND_FILE; type++) {
+ int gen = lru_gen_from_seq(READ_ONCE(lrugen->min_seq[type]));
+
+ for (zone = 0; zone < MAX_NR_ZONES; zone++)
+ size[type] += max(READ_ONCE(lrugen->nr_pages[gen][type][zone]), 0L);
+ }
+
+ *nr_anon = size[LRU_GEN_ANON];
+ *nr_file = size[LRU_GEN_FILE];
+}
+
#else /* !CONFIG_LRU_GEN */
static void lru_gen_age_node(struct pglist_data *pgdat, struct scan_control *sc)
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [RFC v2 0/3] mm/mglru: proactive aging via memory.aging
2026-07-14 12:15 [RFC v2 0/3] mm/mglru: proactive aging via memory.aging Zicheng Wang
` (2 preceding siblings ...)
2026-07-14 12:15 ` [RFC v2 3/3] mm/lru_gen: expose oldest-generation page counts in memory.stat Zicheng Wang
@ 2026-07-15 14:00 ` Johannes Weiner
3 siblings, 0 replies; 5+ messages in thread
From: Johannes Weiner @ 2026-07-15 14:00 UTC (permalink / raw)
To: Zicheng Wang
Cc: akpm, yuanchu, tj, mkoutny, corbet, kasong, qi.zheng,
shakeel.butt, baohua, axelrasmussen, weixugc, david, ljs, liam,
vbabka, rppt, surenb, mhocko, roman.gushchin, muchun.song,
cgroups, linux-mm, linux-kernel, linux-doc, willy, denghaojie,
baoquan.he, kaleshsingh, tjmercier, tao.wangtao, zhangji1,
wangzhen5
On Tue, Jul 14, 2026 at 08:15:26PM +0800, Zicheng Wang wrote:
> MGLRU inverts the reclaim order when anonymous memory is faulted in
> bulk: anonymous pages sit in the young generations while file pages
> sit in the oldest two, so reclaim evicts hot file pages before cold
> anonymous pages.
An aging inversion in the reclaim algorithm seems like an exceedingly
poor justification for a userspace interface to work around them.
^ permalink raw reply [flat|nested] 5+ messages in thread