[RFC PATCH 6/8] mm/memcontrol: add memory.tier control file

From: liuqiqi

Date: Mon Aug 17 2026 - 22:35:09 EST


From: Qiqi Liu <liuqiqi@xxxxxxxxxx>

Introduce the memory.tier control file to expose per-tier usage and limits.
Counters are pre-allocated during css_alloc or memory hotplug:

tierN.current=<bytes> (read-only)
tierN.high=<bytes|max> (default: max)
tierN.max=<bytes|max> (default: max)

Writes are parsed via page_counter_memparse():
- high: Sets page_counter_set_high() and reclaims down to the new limit,
scoped to the tier's nodes, mirroring memory_high_write().
- max: Sets the limit via xchg() and reclaims or OOMs down to the new
limit, mirroring memory_max_write().

Since counters are pre-allocated for all online tiers, updates require
no memory allocation and return -ENOENT for unknown tiers.

Example:
# cat memory.tier
tier4.current=1048576
tier4.high=max
tier4.max=536870912
# echo "tier4.high=268435456" > memory.tier

Signed-off-by: Qiqi Liu <liuqiqi@xxxxxxxxxx>
---
mm/memcontrol.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 104 insertions(+)

diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 3aa55c287248..a6c6057f0e4f 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -4822,6 +4822,7 @@ static void mem_cgroup_css_free(struct cgroup_subsys_state *css)
static void mem_cgroup_css_reset(struct cgroup_subsys_state *css)
{
struct mem_cgroup *memcg = mem_cgroup_from_css(css);
+ struct memcg_tier_counter *tc;

page_counter_set_max(&memcg->memory, PAGE_COUNTER_MAX);
page_counter_set_max(&memcg->swap, PAGE_COUNTER_MAX);
@@ -4835,6 +4836,13 @@ static void mem_cgroup_css_reset(struct cgroup_subsys_state *css)
memcg1_soft_limit_reset(memcg);
page_counter_set_high(&memcg->swap, PAGE_COUNTER_MAX);
memcg_wb_domain_size_changed(memcg);
+
+ rcu_read_lock();
+ list_for_each_entry_rcu(tc, &memcg->tier_counters, list) {
+ page_counter_set_max(&tc->counter, PAGE_COUNTER_MAX);
+ page_counter_set_high(&tc->counter, PAGE_COUNTER_MAX);
+ }
+ rcu_read_unlock();
}

struct aggregate_control {
@@ -5331,6 +5339,96 @@ static ssize_t memory_max_write(struct kernfs_open_file *of,
return nbytes;
}

+static int memory_tier_show(struct seq_file *m, void *v)
+{
+ struct mem_cgroup *memcg = mem_cgroup_from_seq(m);
+ struct memcg_tier_counter *tc;
+
+ rcu_read_lock();
+ list_for_each_entry_rcu(tc, &memcg->tier_counters, list) {
+ seq_printf(m, "tier%d.current=%llu\n", tc->tier_id,
+ (u64)page_counter_read(&tc->counter) * PAGE_SIZE);
+ seq_printf(m, "tier%d.high=", tc->tier_id);
+ seq_puts_memcg_tunable(m, READ_ONCE(tc->counter.high));
+ seq_printf(m, "tier%d.max=", tc->tier_id);
+ seq_puts_memcg_tunable(m, READ_ONCE(tc->counter.max));
+ }
+ rcu_read_unlock();
+ return 0;
+}
+
+static ssize_t memory_tier_write(struct kernfs_open_file *of,
+ char *buf, size_t nbytes, loff_t off)
+{
+ struct mem_cgroup *memcg = mem_cgroup_from_css(of_css(of));
+ struct memcg_tier_counter *tc;
+ unsigned int nr_retries = MAX_RECLAIM_RETRIES;
+ bool drained = false;
+ nodemask_t nodes, *nmp = NULL;
+ unsigned long val;
+ char knob[8], *p;
+ int tier_id, err;
+
+ buf = strstrip(buf);
+ if (sscanf(buf, "tier%d.%7[^=]", &tier_id, knob) != 2)
+ return -EINVAL;
+ if (strcmp(knob, "high") && strcmp(knob, "max"))
+ return -EINVAL;
+ p = strchr(buf, '=');
+ if (!p)
+ return -EINVAL;
+ err = page_counter_memparse(p + 1, "max", &val);
+ if (err)
+ return err;
+
+ rcu_read_lock();
+ tc = memcg_tier_counter_find(memcg, tier_id);
+ rcu_read_unlock();
+ if (!tc)
+ return -ENOENT;
+
+ if (!tier_id_to_nodemask(tier_id, &nodes) && !nodes_empty(nodes))
+ nmp = &nodes;
+
+ if (!strcmp(knob, "high"))
+ page_counter_set_high(&tc->counter, val);
+ else
+ xchg(&tc->counter.max, val);
+
+ if (of->file->f_flags & O_NONBLOCK)
+ return nbytes;
+
+ for (;;) {
+ unsigned long nr_pages = page_counter_read(&tc->counter);
+
+ if (nr_pages <= val)
+ break;
+ if (signal_pending(current))
+ break;
+ if (!drained) {
+ drain_all_tier_stock(memcg);
+ drained = true;
+ continue;
+ }
+ if (reclaim_tier(memcg, nr_pages - val, GFP_KERNEL, nmp))
+ continue;
+ if (nr_retries) {
+ nr_retries--;
+ continue;
+ }
+ if (!strcmp(knob, "max")) {
+ memcg_memory_event(memcg, MEMCG_OOM);
+ if (!mem_cgroup_out_of_memory(memcg, GFP_KERNEL, 0))
+ break;
+ cond_resched();
+ } else {
+ break;
+ }
+ }
+
+ return nbytes;
+}
+
/*
* Note: don't forget to update the 'samples/cgroup/memcg_event_listener'
* if any new events become available.
@@ -5500,6 +5598,12 @@ static struct cftype memory_files[] = {
.seq_show = memory_max_show,
.write = memory_max_write,
},
+ {
+ .name = "tier",
+ .flags = CFTYPE_NOT_ON_ROOT,
+ .seq_show = memory_tier_show,
+ .write = memory_tier_write,
+ },
{
.name = "events",
.flags = CFTYPE_NOT_ON_ROOT,
--
2.43.0