Skip to contentSkip to conversation

memcg_ext: memcg policy through cgroup-attached struct_ops

RFC PATCH In review Shakeel Butt 7 participants 25 messages Sep 21 – Oct 20

Subsystems (9)
lore
Share & tools
lore

Shakeel Butt

author maintainer
#0 lore raw

cover memcg_ext: memcg policy through cgroup-attached struct_ops

This is the first series of memcg_ext, proposed at [1]. It adds bpf_memcg_ops, a struct_ops type through which memory controller policy is attached to a cgroup. A charge runs the policies of its cgroup and of every ancestor, and the kernel combines what they return. BPF picks between things the kernel already does; it never does the work itself and never touches a page counter.

The plan is to grow this one member at a time, and to add a member only when there is a concrete problem it solves and a measurement showing it does. Nothing is added because it might be useful later. So this first series adds one member, for one problem.

The problem
===========

try_charge_memcg() calls __mem_cgroup_handle_over_high() before it returns, which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on that lock is stuck behind it.

One concrete scenario which can be resolved by this new feature is the kernfs notify worker. It delivers notifications with the cgroup2 kernfs_rwsem held for read, and the charge for the delivery allocation goes to the cgroup that set the watch, usually one already under pressure. So the worker reclaims while holding the lock, a waiting writer blocks every later reader, and anything touching cgroupfs stalls for seconds. The patch proposed in [2] fixes that one path in the kernel only for kernfs_rwsem. The same path still takes kernfs_supers_rwsem.

One can make an argument that if we know the source of the issue in the kernel, why not fix it similarly to [2] instead of having a generic solution? The reason is that it will be an uphill and continuous battle as the kernel keeps evolving and new sources of lock holders doing allocations keep coming up. In addition, there will be cases where it might not be possible to move the allocations out of locks, or where doing so would make the code really ugly [3].

high_policy() lets a policy say where memory.high should be enforced instead. Its one request, BPF_MEMCG_HIGH_DEFER_INLINE, skips the inline call, leaving the debt to be paid at the return to userspace.

Results
=======

Measured on a kernel without [2], with a policy that marks cgroupfs kernfs lock holders. Reproducer at [4].

                                      baseline    policy    upstream fix
  max kernfs_rwsem hold, worker        2.049 s     199 us   not taken
  max kernfs_supers_rwsem hold         2.049 s     4.8 ms   2.056 s
  max kernfs_rwsem write wait          4.096 s     6.2 ms   8.7 ms
  walker passes over /sys/fs/cgroup        352       5711   6700
  churn mkdir+rmdir ops                 13 059    364 832   415 488
  churn max latency                     30.7 s    15.1 ms   12.7 ms

The policy matches the fix on the bystander numbers, and does better on kernfs_supers_rwsem, which the fix does not help: it drops kernfs_rwsem from the delivery loop, while the policy stops the worker stalling at all, so every lock it holds benefits.

The patches
===========

Patch 1 is a build fix. __cgroup_bpf_query() only works because CGROUP_TCP_SOCK_OPS is the one struct_ops attach type; a second one breaks the build and makes a query for type 0 return the wrong thing.

Patch 2 adds the type and its registration, with no members, so nothing is dispatched and behaviour does not change. It also moves the attach type enum out of CONFIG_CGROUP_BPF and fixes the register_bpf_struct_ops() no-op stub, which never compiled because no caller reached it.

Patch 3 adds high_policy() and wires it into try_charge_memcg().

Patch 4 is the sample. It tracks the two cgroupfs kernfs locks by hooking the rwsem primitives rather than the forty-odd functions that take them.

Known open questions
====================

The semantics of memory.high for remote chargers or kernel threads is a grey area and this series does not aim to resolve that.

Another open question is whether a bound on debt deferral is needed. At the moment, we think that rather than putting a limit on deferral for memory.high, it will be better to handle that through an async worker like memcg->high_work. We aim to introduce that later, along with the right CPU accounting for that async work.

There is no prog_tests harness, so the numbers above come from the reproducer and not from the test suite.

The sample's lock tracking costs about 8% throughput, because it instruments every rwsem operation on the system. That is fine for a sample that has to cover every path, but it argues for a cheaper way to track locks that cause isolation problems between unrelated workloads.

Future work
===========

Asking the kernel to reclaim on the policy's behalf, so a task that cannot be throttled still pays. More members, each with a use case: hard-limit policy, reclaim shaping, dynamic protection.

This builds on "bpf: A common way to attach struct_ops to a cgroup", which supplies attach, detach, ordering, update, query and the RCU rules. bpf_memcg_ops is its second user.

[1] https://lore.kernel.org/[email protected]/
[2] https://lore.kernel.org/[email protected]/
[3] https://lore.kernel.org/20260917-wehten-achtfach-getarnt-c85a4812337d@brauner/
[4] https://github.com/shakeelb/mempressure-repros/tree/main/kernfs-notify-memcg

Shakeel Butt (4):
bpf, cgroup: fix cgroup struct_ops query for a second attach type
memcg_ext: add cgroup-attached bpf_memcg_ops
memcg_ext: allow BPF to defer memory.high enforcement selftests/bpf: add a cgroupfs lock-holder bpf_memcg_ops sample

 MAINTAINERS                                   |   1 +
 include/linux/bpf-cgroup-defs.h               |  22 +-
 include/linux/bpf-cgroup.h                    |  14 +-
 include/linux/bpf.h                           |   2 +-
 include/linux/bpf_memcontrol.h                |  66 ++++++
 include/linux/cgroup.h                        |   7 +
 include/linux/sched.h                         |   4 +
 kernel/bpf/cgroup.c                           |   3 +-
 mm/bpf_memcontrol.c                           | 188 ++++++++++++++-
 mm/memcontrol.c                               |  31 ++-
 .../bpf/progs/memcg_ops_lockholder.c          | 222 ++++++++++++++++++
 11 files changed, 546 insertions(+), 14 deletions(-)
 create mode 100644 include/linux/bpf_memcontrol.h
 create mode 100644 tools/testing/selftests/bpf/progs/memcg_ops_lockholder.c

base-commit: 6e36e099b15bed1e8e5b3e3136c5e3eb56a15aa7

Signature · 1 line
2.53.0-Meta
author maintainer
#1 lore raw

RFC PATCH 1/4 bpf, cgroup: fix cgroup struct_ops query for a second attach type

Two things in __cgroup_bpf_query() work only because CGROUP_TCP_SOCK_OPS is the only one struct_ops attach type.

It calls cgroup_bpf_enabled(atype) with an atype that find_atype_by_struct_ops_id() works out at runtime. That macro is an asm goto and needs a constant. Today the compiler can see there is only one value; add a second type and the build breaks with "impossible constraint in 'asm'". Add cgroup_bpf_enabled_runtime(), which reads the key instead, and use it here. This is a syscall path, so the cost does not matter.

And find_atype_by_struct_ops_id() matches on type_id alone. An attach type whose subsystem is not built keeps type_id 0, so a query for type 0 finds it and returns success with nothing instead of -ENOENT. Skip such slots.

---
 include/linux/bpf-cgroup.h | 9 +++++++++
 kernel/bpf/cgroup.c        | 3 ++-
 2 files changed, 11 insertions(+), 1 deletion(-)
include/linux/bpf-cgroup.h +9 -0
index 8a75a6cd7309..3b2c127d401d 100644
@@ -76,6 +76,14 @@ to_cgroup_bpf_attach_type(enum bpf_attach_type attach_type)
 
 extern struct static_key_false cgroup_bpf_enabled_key[MAX_CGROUP_BPF_ATTACH_TYPE];
 #define cgroup_bpf_enabled(atype) static_branch_unlikely(&cgroup_bpf_enabled_key[atype])
+/*
+ * Same test when @atype is not a constant.  cgroup_bpf_enabled() uses
+ * static_branch_unlikely which creates jump-label site and requires statically
+ * selected key. Since key is selected dynamically, use static_key_enabled here
+ * and keep it off fast paths.
+ */
+#define cgroup_bpf_enabled_runtime(atype) \
+	static_key_enabled(&cgroup_bpf_enabled_key[atype])
 
 struct bpf_cgroup_storage_map;
 
@@ -508,6 +516,7 @@ static inline int cgroup_bpf_struct_ops_attach(struct bpf_map *map,
 }
 
 #define cgroup_bpf_enabled(atype) (0)
+#define cgroup_bpf_enabled_runtime(atype) (0)
 #define BPF_CGROUP_RUN_SA_PROG_LOCK(sk, uaddr, uaddrlen, atype, t_ctx) ({ 0; })
 #define BPF_CGROUP_RUN_SA_PROG(sk, uaddr, uaddrlen, atype) ({ 0; })
 #define BPF_CGROUP_PRE_CONNECT_ENABLED(sk) (0)
kernel/bpf/cgroup.c +2 -1
index 696b27383974..99b4e96f5db7 100644
@@ -45,6 +45,7 @@ static enum cgroup_bpf_attach_type find_atype_by_struct_ops_id(u32 type_id)
 
 	for (atype = 0; atype < MAX_CGROUP_BPF_ATTACH_TYPE; atype++) {
 		if (cgroup_bpf_is_struct_ops_atype(atype) &&
+		    cgroup_struct_ops[atype].type_id &&
 		    cgroup_struct_ops[atype].type_id == type_id)
 			return atype;
 	}
@@ -1448,7 +1449,7 @@ static int __cgroup_bpf_query(struct cgroup *cgrp, const union bpf_attr *attr,
 			return -ENOENT;
 		from_atype = to_atype = atype;
 		flags = 0;
-		if (!cgroup_bpf_enabled(atype))
+		if (!cgroup_bpf_enabled_runtime(atype))
 			goto skip_count;
 	} else if (type == BPF_LSM_CGROUP) {
 		if (!effective_query && attr->query.prog_cnt &&
Signature · 1 line
2.53.0-Meta
#2 lore raw

Re: [RFC PATCH 1/4] bpf, cgroup: fix cgroup struct_ops query for a second attach type

This looks like a bug fix for the struct_ops attach infrastructure. Should this include:
Quoted 12 lines

bpf, cgroup: fix cgroup struct_ops query for a second attach type

Two things in __cgroup_bpf_query() work only because CGROUP_TCP_SOCK_OPS is the only one struct_ops attach type.

It calls cgroup_bpf_enabled(atype) with an atype that find_atype_by_struct_ops_id() works out at runtime. That macro is an asm goto and needs a constant. Today the compiler can see there is only one value; add a second type and the build breaks with "impossible constraint in 'asm'". Add cgroup_bpf_enabled_runtime(), which reads the key instead, and use it here. This is a syscall path, so the cost does not matter.

Quoted 5 lines

And find_atype_by_struct_ops_id() matches on type_id alone. An attach type whose subsystem is not built keeps type_id 0, so a query for type 0 finds it and returns success with nothing instead of -ENOENT. Skip such slots.

This looks like a bug fix for the struct_ops attach infrastructure. Should this include:

Fixes 369d9dcd8fb8 ("bpf: Add infrastructure to support attaching struct_ops to cgroups")

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35646140734

Acked-by on v1 1/4
#3 lore raw

Re: [RFC PATCH 1/4] bpf, cgroup: fix cgroup struct_ops query for a second attach type

In reply to Shakeel Butt
Quoted 11 lines · Shakeel Butt

Two things in __cgroup_bpf_query() work only because CGROUP_TCP_SOCK_OPS is the only one struct_ops attach type.

It calls cgroup_bpf_enabled(atype) with an atype that find_atype_by_struct_ops_id() works out at runtime. That macro is an asm goto and needs a constant. Today the compiler can see there is only one value; add a second type and the build breaks with "impossible constraint in 'asm'". Add cgroup_bpf_enabled_runtime(), which reads the key instead, and use it here. This is a syscall path, so the cost does not matter.

Quoted 5 lines

And find_atype_by_struct_ops_id() matches on type_id alone. An attach type whose subsystem is not built keeps type_id 0, so a query for type 0 finds it and returns success with nothing instead of -ENOENT. Skip such slots.

Could you please send this fix separately from the series so it can land in bpf-next on its own?

Quoted 57 lines
---
 include/linux/bpf-cgroup.h | 9 +++++++++
 kernel/bpf/cgroup.c        | 3 ++-
 2 files changed, 11 insertions(+), 1 deletion(-)
include/linux/bpf-cgroup.h +9 -0
index 8a75a6cd7309..3b2c127d401d 100644
@@ -76,6 +76,14 @@ to_cgroup_bpf_attach_type(enum bpf_attach_type attach_type)
 
 extern struct static_key_false cgroup_bpf_enabled_key[MAX_CGROUP_BPF_ATTACH_TYPE];
 #define cgroup_bpf_enabled(atype) static_branch_unlikely(&cgroup_bpf_enabled_key[atype])
+/*
+ * Same test when @atype is not a constant.  cgroup_bpf_enabled() uses
+ * static_branch_unlikely which creates jump-label site and requires statically
+ * selected key. Since key is selected dynamically, use static_key_enabled here
+ * and keep it off fast paths.
+ */
+#define cgroup_bpf_enabled_runtime(atype) \
+       static_key_enabled(&cgroup_bpf_enabled_key[atype])
 
 struct bpf_cgroup_storage_map;
 
@@ -508,6 +516,7 @@ static inline int cgroup_bpf_struct_ops_attach(struct bpf_map *map,
 }
 
 #define cgroup_bpf_enabled(atype) (0)
+#define cgroup_bpf_enabled_runtime(atype) (0)
 #define BPF_CGROUP_RUN_SA_PROG_LOCK(sk, uaddr, uaddrlen, atype, t_ctx) ({ 0; })
 #define BPF_CGROUP_RUN_SA_PROG(sk, uaddr, uaddrlen, atype) ({ 0; })
 #define BPF_CGROUP_PRE_CONNECT_ENABLED(sk) (0)
kernel/bpf/cgroup.c +2 -2
index 696b27383974..99b4e96f5db7 100644
@@ -45,6 +45,7 @@ static enum cgroup_bpf_attach_type find_atype_by_struct_ops_id(u32 type_id)
 
        for (atype = 0; atype < MAX_CGROUP_BPF_ATTACH_TYPE; atype++) {
                if (cgroup_bpf_is_struct_ops_atype(atype) &&
+                   cgroup_struct_ops[atype].type_id &&
                    cgroup_struct_ops[atype].type_id == type_id)
                        return atype;
        }
@@ -1448,7 +1449,7 @@ static int __cgroup_bpf_query(struct cgroup *cgrp, const union bpf_attr *attr,
                        return -ENOENT;
                from_atype = to_atype = atype;
                flags = 0;
-               if (!cgroup_bpf_enabled(atype))
+               if (!cgroup_bpf_enabled_runtime(atype))
                        goto skip_count;
        } else if (type == BPF_LSM_CGROUP) {
                if (!effective_query && attr->query.prog_cnt &&
--

2.53.0-Meta

Signature · 2 lines
Regards
Yafang
author maintainer
#4 lore raw

RFC PATCH 2/4 memcg_ext: add cgroup-attached bpf_memcg_ops

In reply to Shakeel Butt

Add an empty bpf_memcg_ops type for attaching memory policies to cgroups. This is the first patch of memcg_ext, which makes memcg enforcement programmable one hook at a time. Policy hooks will be added later.

Move the cgroup BPF attach types outside CONFIG_CGROUP_BPF and fix the struct_ops registration stub so all supported configurations build.

This does not change behavior yet.

---
 include/linux/bpf-cgroup-defs.h |  22 ++++---
 include/linux/bpf-cgroup.h      |   2 +-
 include/linux/bpf.h             |   2 +-
 include/linux/bpf_memcontrol.h  |  17 +++++
 mm/bpf_memcontrol.c             | 108 +++++++++++++++++++++++++++++++-
 5 files changed, 140 insertions(+), 11 deletions(-)
 create mode 100644 include/linux/bpf_memcontrol.h
include/linux/bpf-cgroup-defs.h +14 -8
index 0147b8bec973..53d2853535c6 100644
@@ -2,14 +2,6 @@
 #ifndef _BPF_CGROUP_DEFS_H
 #define _BPF_CGROUP_DEFS_H
 
-#ifdef CONFIG_CGROUP_BPF
-
-#include <linux/list.h>
-#include <linux/percpu-refcount.h>
-#include <linux/workqueue.h>
-
-struct bpf_prog_array;
-
 #ifdef CONFIG_BPF_LSM
 /* Maximum number of concurrently attachable per-cgroup LSM hooks. */
 #define CGROUP_LSM_NUM 10
@@ -17,6 +9,10 @@ struct bpf_prog_array;
 #define CGROUP_LSM_NUM 0
 #endif
 
+/*
+ * Plain constants, so a subsystem can name its attach type without
+ * depending on CONFIG_CGROUP_BPF.
+ */
 enum cgroup_bpf_attach_type {
 	CGROUP_BPF_ATTACH_TYPE_INVALID = -1,
 	CGROUP_INET_INGRESS = 0,
@@ -48,11 +44,21 @@ enum cgroup_bpf_attach_type {
 	CGROUP_UNIX_GETSOCKNAME,
 	CGROUP_INET_SOCK_RELEASE,
 	CGROUP_TCP_SOCK_OPS,
+	CGROUP_MEMCG_OPS,
 	CGROUP_LSM_START,
 	CGROUP_LSM_END = CGROUP_LSM_START + CGROUP_LSM_NUM - 1,
 	MAX_CGROUP_BPF_ATTACH_TYPE
 };
 
+#ifdef CONFIG_CGROUP_BPF
+
+#include <linux/list.h>
+#include <linux/percpu-refcount.h>
+#include <linux/workqueue.h>
+
+struct bpf_prog_array;
+
+
 struct cgroup_bpf {
 	/* array of effective progs in this cgroup */
 	struct bpf_prog_array __rcu *effective[MAX_CGROUP_BPF_ATTACH_TYPE];
include/linux/bpf-cgroup.h +1 -1
index 3b2c127d401d..4e8150848bd2 100644
@@ -127,7 +127,7 @@ struct bpf_prog_list {
 
 static inline bool cgroup_bpf_is_struct_ops_atype(enum cgroup_bpf_attach_type atype)
 {
-	return atype == CGROUP_TCP_SOCK_OPS;
+	return atype == CGROUP_TCP_SOCK_OPS || atype == CGROUP_MEMCG_OPS;
 }
 void cgroup_bpf_struct_ops_register(int atype, u32 type_id, void *cfi_stubs, bool mult_trace);
 int cgroup_bpf_struct_ops_attach(struct bpf_map *map, const union bpf_attr *attr);
include/linux/bpf.h +1 -1
index 5033b934ffd9..f8eb102e7fc4 100644
@@ -2331,7 +2331,7 @@ int bpf_struct_ops_desc_init(struct bpf_struct_ops_desc *st_ops_desc,
 void bpf_map_struct_ops_info_fill(struct bpf_map_info *info, struct bpf_map *map);
 void bpf_struct_ops_desc_release(struct bpf_struct_ops_desc *st_ops_desc);
 #else
-#define register_bpf_struct_ops(st_ops, type) ({ (void *)(st_ops); 0; })
+#define register_bpf_struct_ops(st_ops, type) ({ (void)(st_ops); 0; })
 static inline bool bpf_try_module_get(const void *data, struct module *owner)
 {
 	return try_module_get(owner);
include/linux/bpf_memcontrol.h new file +17 -0
new file mode 100644
index 000000000000..8204d894761e
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * BPF policy hooks for the memory controller.
+ *
+ * A bpf_memcg_ops is attached to a cgroup.  A charge runs the policies of
+ * that cgroup and of every ancestor, and the kernel combines what they
+ * return.  BPF only picks between things the kernel already does.
+ *
+ * The type has no members yet; they come with the policies that use them.
+ */
+#ifndef _LINUX_BPF_MEMCONTROL_H
+#define _LINUX_BPF_MEMCONTROL_H
+
+struct bpf_memcg_ops {
+};
+
+#endif /* _LINUX_BPF_MEMCONTROL_H */
mm/bpf_memcontrol.c +107 -1

127 lines not on this page · raw message

Signature · 1 line
2.53.0-Meta
#5 lore raw

Re: [RFC PATCH 2/4] memcg_ext: add cgroup-attached bpf_memcg_ops

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] bpf: spurious boot warning when CONFIG_CGROUP_BPF is disabled - [Low] bpf: missing newline in…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] bpf: spurious boot warning when CONFIG_CGROUP_BPF is disabled
- [Low] bpf: missing newline in pr_warn for bpf_memcg_ops registration
--

commit f28f15056753b0bfc8cefa4ccf7793b6fb45150d
Author: Shakeel Butt <[email protected]>
Subject: memcg_ext: add cgroup-attached bpf_memcg_ops

This patch adds an empty bpf_memcg_ops type for attaching memory policies to cgroups. It also moves the cgroup BPF attach types outside CONFIG_CGROUP_BPF and updates the struct_ops registration stub.

Quoted 4 lines
mm/bpf_memcontrol.c +0 -0
index d8f579c285600..fd6dff150f017 100644

[ ... ]

mm/bpf_memcontrol.c static int __init bpf_memcontrol_init(void) · line 358

Quoted 13 lines
mm/bpf_memcontrol.c +7 -1
@@ -248,8 +348,14 @@ static int __init bpf_memcontrol_init(void)
 
 	err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,
 					&bpf_memcontrol_reclaim_kfunc_set);
-	if (err)
+	if (err) {
 		pr_warn("error registering bpf reclaim kfuncs: %d\n", err);
+		return err;
+	}
+
+	err = bpf_memcg_ops_register();
+	if (err)
+		pr_warn("error while registering bpf_memcg_ops: %d", err);

[Severity: Medium]
Will this print a spurious warning on every boot when CONFIG_CGROUP_BPF is disabled?

If a kernel is built with CONFIG_MEMCG=y, CONFIG_BPF_SYSCALL=y, and CONFIG_BPF_JIT=y, but CONFIG_CGROUP_BPF=n, bpf_memcontrol_init() calls bpf_memcg_ops_register().

This eventually calls register_bpf_struct_ops(), which relies on cgroup_bpf_is_struct_ops_atype() in btf_add_struct_ops(). Since the !CONFIG_CGROUP_BPF stub for cgroup_bpf_is_struct_ops_atype() unconditionally returns false, this returns -EINVAL.

Does this mean the late initcall will return an error and unconditionally pollute the kernel log with this warning on supported configurations?

[Severity: Low]
Does this pr_warn() need a terminating '\n'?

Without it, subsequent printk messages might be incorrectly appended to the same line in the kernel log.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
author maintainer
#6 lore raw

RFC PATCH 3/4 memcg_ext: allow BPF to defer memory.high enforcement

In reply to Shakeel Butt

Before it returns, try_charge_memcg() calls __mem_cgroup_handle_over_high(), which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on the lock is stuck behind it.

Let's add high_policy ops through which a program gets a read-only snapshot of the charge and returns a request. There is one so far:
BPF_MEMCG_HIGH_DEFER_INLINE skips the inline call.

A charge runs the policies of its cgroup and of every ancestor. task_struct::in_bpf_memcg stops a program that allocates from re-entering the charge path and the dispatcher with it.

A memcg outlives its cgroup while it has charges, and cgroup_bpf_release() frees the arrays when the cgroup goes. Take the reference for the walk.

---
 MAINTAINERS                    |  1 +
 include/linux/bpf-cgroup.h     |  3 ++
 include/linux/bpf_memcontrol.h | 53 +++++++++++++++++++++-
 include/linux/cgroup.h         |  7 +++
 include/linux/sched.h          |  4 ++
 mm/bpf_memcontrol.c            | 82 +++++++++++++++++++++++++++++++++-
 mm/memcontrol.c                | 31 ++++++++++++-
 7 files changed, 176 insertions(+), 5 deletions(-)
MAINTAINERS +1 -0
index 6215fcb07770..0c84beab396f 100644
@@ -5032,6 +5032,7 @@ L:	[email protected]
 L:	[email protected]
 S:	Maintained
 F:	mm/bpf_memcontrol.c
+F:	include/linux/bpf_memcontrol.h
 
 BPF [MISC]
 L:	[email protected]
include/linux/bpf-cgroup.h +3 -0
index 4e8150848bd2..e19cf83e58f3 100644
@@ -517,6 +517,9 @@ static inline int cgroup_bpf_struct_ops_attach(struct bpf_map *map,
 
 #define cgroup_bpf_enabled(atype) (0)
 #define cgroup_bpf_enabled_runtime(atype) (0)
+/* Nothing can be attached, so the walk has nothing to walk. */
+#define bpf_cgroup_struct_ops_foreach(var, item, cgrp, atype)		\
+	for ((void)(cgrp), (item) = NULL, (var) = NULL; 0; )
 #define BPF_CGROUP_RUN_SA_PROG_LOCK(sk, uaddr, uaddrlen, atype, t_ctx) ({ 0; })
 #define BPF_CGROUP_RUN_SA_PROG(sk, uaddr, uaddrlen, atype) ({ 0; })
 #define BPF_CGROUP_PRE_CONNECT_ENABLED(sk) (0)
include/linux/bpf_memcontrol.h +51 -2

64 lines not on this page · raw message

include/linux/cgroup.h +7 -0
index 5dfa915a630e..cf92b6cec819 100644
@@ -959,10 +959,17 @@ static inline void cgroup_bpf_put(struct cgroup *cgrp)
 	percpu_ref_put(&cgrp->bpf.refcnt);
 }
 
+/* Fails once the cgroup is gone and its bpf state has been freed. */
+static inline bool cgroup_bpf_tryget_live(struct cgroup *cgrp)
+{
+	return percpu_ref_tryget_live_rcu(&cgrp->bpf.refcnt);
+}
+
 #else /* CONFIG_CGROUP_BPF */
 
 static inline void cgroup_bpf_get(struct cgroup *cgrp) {}
 static inline void cgroup_bpf_put(struct cgroup *cgrp) {}
+static inline bool cgroup_bpf_tryget_live(struct cgroup *cgrp) { return false; }
 
 #endif /* CONFIG_CGROUP_BPF */
 
include/linux/sched.h +4 -0
index 8b3d47a325cc..4b20a346aaa8 100644
@@ -1031,6 +1031,10 @@ struct task_struct {
 #ifdef CONFIG_MEMCG_V1
 	unsigned			in_user_fault:1;
 #endif
+#ifdef CONFIG_MEMCG
+	/* A bpf_memcg_ops program is running; do not recurse into policy */
+	unsigned			in_bpf_memcg:1;
+#endif
 #ifdef CONFIG_LRU_GEN
 	/* whether the LRU algorithm may apply to this access */
 	unsigned			in_lru_fault:1;
mm/bpf_memcontrol.c +81 -1

95 lines not on this page · raw message

mm/memcontrol.c +29 -2
index 1271d390b617..bc283680640b 100644
@@ -28,6 +28,7 @@
 #include <linux/cgroup-defs.h>
 #include <linux/page_counter.h>
 #include <linux/memcontrol.h>
+#include <linux/bpf_memcontrol.h>
 #include <linux/cgroup.h>
 #include <linux/cpuset.h>
 #include <linux/sched/mm.h>
@@ -2642,9 +2643,26 @@ void __mem_cgroup_handle_over_high(gfp_t gfp_mask)
 	css_put(&memcg->css);
 }
 
+/*
+ * Ask the attached bpf_memcg_ops whether to skip the inline memory.high
+ * reclaim and throttle.
+ *
+ * @memcg:	the memcg being charged
+ * @over_limit:	first memcg found over memory.high or swap.high, starting at
+ *		the charged one, or NULL if the walk found none
+ */
+static bool bpf_memcg_high_defer(struct mem_cgroup *memcg,
+				 struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	u32 req = bpf_memcg_high_policy(memcg, over_limit, gfp_mask);
+
+	return req & BPF_MEMCG_HIGH_DEFER_INLINE;
+}
+
 static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
 			    unsigned int nr_pages)
 {
+	struct mem_cgroup *leaf_memcg = memcg;
 	unsigned int batch = max(MEMCG_CHARGE_BATCH, nr_pages);
 	int nr_retries = MAX_RECLAIM_RETRIES;
 	struct mem_cgroup *mem_over_limit;
@@ -2846,8 +2864,17 @@ static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
 	 */
 	if (current->memcg_nr_pages_over_high > MEMCG_CHARGE_BATCH &&
 	    !(current->flags & PF_MEMALLOC) &&
-	    gfpflags_allow_blocking(gfp_mask))
-		__mem_cgroup_handle_over_high(gfp_mask);
+	    gfpflags_allow_blocking(gfp_mask)) {
+		/*
+		 * The loop above left @memcg as the first memcg it found over
+		 * memory.high or swap.high -- possibly the charged one itself
+		 * -- or NULL if it found none.  Note the debt can be left over
+		 * from an earlier charge, so NULL does not mean no pressure.
+		 * The policy wants the memcg we charged.
+		 */
+		if (!bpf_memcg_high_defer(leaf_memcg, memcg, gfp_mask))
+			__mem_cgroup_handle_over_high(gfp_mask);
+	}
 	return 0;
 }
 
Signature · 1 line
2.53.0-Meta
maintainer
#7 lore raw

Re: [RFC PATCH 3/4] memcg_ext: allow BPF to defer memory.high enforcement

Quoted 237 lines · Shakeel Butt

Before it returns, try_charge_memcg() calls __mem_cgroup_handle_over_high(), which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on the lock is stuck behind it.

Let's add high_policy ops through which a program gets a read-only snapshot of the charge and returns a request. There is one so far:
BPF_MEMCG_HIGH_DEFER_INLINE skips the inline call.

A charge runs the policies of its cgroup and of every ancestor. task_struct::in_bpf_memcg stops a program that allocates from re-entering the charge path and the dispatcher with it.

A memcg outlives its cgroup while it has charges, and cgroup_bpf_release() frees the arrays when the cgroup goes. Take the reference for the walk.

---
  MAINTAINERS                    |  1 +
  include/linux/bpf-cgroup.h     |  3 ++
  include/linux/bpf_memcontrol.h | 53 +++++++++++++++++++++-
  include/linux/cgroup.h         |  7 +++
  include/linux/sched.h          |  4 ++
  mm/bpf_memcontrol.c            | 82 +++++++++++++++++++++++++++++++++-
  mm/memcontrol.c                | 31 ++++++++++++-
  7 files changed, 176 insertions(+), 5 deletions(-)
MAINTAINERS +1 -0
index 6215fcb07770..0c84beab396f 100644
@@ -5032,6 +5032,7 @@ L:	[email protected]
  L:	[email protected]
  S:	Maintained
  F:	mm/bpf_memcontrol.c
+F:	include/linux/bpf_memcontrol.h
 
  BPF [MISC]
  L:	[email protected]
include/linux/bpf-cgroup.h +3 -0
index 4e8150848bd2..e19cf83e58f3 100644
@@ -517,6 +517,9 @@ static inline int cgroup_bpf_struct_ops_attach(struct bpf_map *map,
 
  #define cgroup_bpf_enabled(atype) (0)
  #define cgroup_bpf_enabled_runtime(atype) (0)
+/* Nothing can be attached, so the walk has nothing to walk. */
+#define bpf_cgroup_struct_ops_foreach(var, item, cgrp, atype)		\
+	for ((void)(cgrp), (item) = NULL, (var) = NULL; 0; )
  #define BPF_CGROUP_RUN_SA_PROG_LOCK(sk, uaddr, uaddrlen, atype, t_ctx) ({ 0; })
  #define BPF_CGROUP_RUN_SA_PROG(sk, uaddr, uaddrlen, atype) ({ 0; })
  #define BPF_CGROUP_PRE_CONNECT_ENABLED(sk) (0)
include/linux/bpf_memcontrol.h +51 -2
index 8204d894761e..76ea5d1c3d32 100644
@@ -5,13 +5,62 @@
   * A bpf_memcg_ops is attached to a cgroup.  A charge runs the policies of
   * that cgroup and of every ancestor, and the kernel combines what they
   * return.  BPF only picks between things the kernel already does.
- *
- * The type has no members yet; they come with the policies that use them.
   */
  #ifndef _LINUX_BPF_MEMCONTROL_H
  #define _LINUX_BPF_MEMCONTROL_H
 
+#include <linux/types.h>
+#include <linux/gfp_types.h>
+
+struct mem_cgroup;
+struct task_struct;
+
+/*
+ * What a policy can ask for when a cgroup is over memory.high.  The kernel
+ * ORs them, so one policy cannot undo another.
+ */
+enum bpf_memcg_high_request {
+	BPF_MEMCG_HIGH_NO_OPINION	= 0,
+	/*
+	 * Skip the inline reclaim and throttle.  The debt is kept and paid on
+	 * the way back to userspace, where no kernel locks are held.
+	 */
+	BPF_MEMCG_HIGH_DEFER_INLINE	= 1U << 0,
+};
+
+#define BPF_MEMCG_HIGH_VALID_MASK	BPF_MEMCG_HIGH_DEFER_INLINE
+
+/* Read-only snapshot.  Only values the caller already has. */
+struct bpf_memcg_ctx {
+	struct mem_cgroup	*memcg;			/* charged memcg */
+	struct mem_cgroup	*memcg_over_limit;	/* NULL if none found */
+	struct task_struct	*task;			/* current */
+	u64			cgroup_id;
+	u64			over_limit_cgroup_id;	/* 0 if none */
+	u64			nr_pages_over_high;
+	u32			gfp_flags;
+};
+
  struct bpf_memcg_ops {
+	/**
+	 * high_policy - say where memory.high should be enforced
+	 * @ctx: snapshot of the charge
+	 *
+	 * Return: bits from enum bpf_memcg_high_request, or 0.  Other bits
+	 * are dropped.
+	 */
+	u32 (*high_policy)(const struct bpf_memcg_ctx *ctx);
  };
 
+/*
+ * Run every high_policy on @memcg's cgroup and its ancestors, and return the
+ * combined request for the caller to act on.
+ *
+ * @memcg:	the memcg being charged, never NULL
+ * @over_limit:	first memcg found over memory.high or swap.high, or NULL
+ * @gfp_mask:	the charge's gfp mask
+ */
+u32 bpf_memcg_high_policy(struct mem_cgroup *memcg,
+			  struct mem_cgroup *over_limit, gfp_t gfp_mask);
+
  #endif /* _LINUX_BPF_MEMCONTROL_H */
include/linux/cgroup.h +7 -0
index 5dfa915a630e..cf92b6cec819 100644
@@ -959,10 +959,17 @@ static inline void cgroup_bpf_put(struct cgroup *cgrp)
  	percpu_ref_put(&cgrp->bpf.refcnt);
  }
 
+/* Fails once the cgroup is gone and its bpf state has been freed. */
+static inline bool cgroup_bpf_tryget_live(struct cgroup *cgrp)
+{
+	return percpu_ref_tryget_live_rcu(&cgrp->bpf.refcnt);
+}
+
  #else /* CONFIG_CGROUP_BPF */
 
  static inline void cgroup_bpf_get(struct cgroup *cgrp) {}
  static inline void cgroup_bpf_put(struct cgroup *cgrp) {}
+static inline bool cgroup_bpf_tryget_live(struct cgroup *cgrp) { return false; }
 
  #endif /* CONFIG_CGROUP_BPF */
 
include/linux/sched.h +4 -0
index 8b3d47a325cc..4b20a346aaa8 100644
@@ -1031,6 +1031,10 @@ struct task_struct {
  #ifdef CONFIG_MEMCG_V1
  	unsigned			in_user_fault:1;
  #endif
+#ifdef CONFIG_MEMCG
+	/* A bpf_memcg_ops program is running; do not recurse into policy */
+	unsigned			in_bpf_memcg:1;
+#endif
  #ifdef CONFIG_LRU_GEN
  	/* whether the LRU algorithm may apply to this access */
  	unsigned			in_lru_fault:1;
mm/bpf_memcontrol.c +61 -1
index fd6dff150f01..cfd0f1d443c9 100644
@@ -246,8 +246,17 @@ static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = {
   * request and the kernel acts on it.  Nothing here reclaims or sleeps.
   */
 
-/* CFI stubs.  A slot points at these while its policy is being detached. */
+/*
+ * CFI stubs.  These really run: a slot points at them while its policy is
+ * being detached.  Return 0, the identity for the kernel's OR.
+ */
+static u32 high_policy_stub(const struct bpf_memcg_ctx *ctx)
+{
+	return BPF_MEMCG_HIGH_NO_OPINION;
+}
+
  static struct bpf_memcg_ops __bpf_memcg_ops = {
+	.high_policy = high_policy_stub,
  };
 
  static const struct bpf_func_proto *
@@ -326,6 +335,77 @@ static struct bpf_struct_ops bpf_memcg_ops_desc = {
  	 */
  };
 
+static void bpf_memcg_ctx_init(struct bpf_memcg_ctx *ctx,
+			       struct mem_cgroup *memcg,
+			       struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	ctx->memcg = memcg;
+	ctx->memcg_over_limit = over_limit;
+	ctx->task = current;
+	ctx->cgroup_id = cgroup_id(memcg->css.cgroup);
+	ctx->over_limit_cgroup_id = over_limit ?
+		cgroup_id(over_limit->css.cgroup) : 0;
+	ctx->nr_pages_over_high = current->memcg_nr_pages_over_high;
+	ctx->gfp_flags = (__force u32)gfp_mask;
+}
+
+u32 bpf_memcg_high_policy(struct mem_cgroup *memcg,
+			  struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	const struct bpf_prog_array_item *item;
+	const struct bpf_memcg_ops *ops;
+	struct bpf_memcg_ctx ctx;
+	u32 acc = BPF_MEMCG_HIGH_NO_OPINION;
+	struct cgroup *cgrp;
+
+	if (!cgroup_bpf_enabled(CGROUP_MEMCG_OPS))
+		return acc;
+
+	/*
+	 * Only the default hierarchy has a cgroup_bpf, and the static key is
+	 * global, so one policy anywhere turns this on for v1 memcgs too.  A
+	 * v1 memcg still cannot get here, because memory.high and swap.high
+	 * are both v2-only and so it never builds the debt that leads to this
+	 * call.  A hook on a path v1 can reach needs its own cgroup_on_dfl()
+	 * test: a v1 cgroup has no effective array and an uninitialised
+	 * cgrp->bpf.refcnt.
+	 */
+	cgrp = memcg->css.cgroup;
+
+	/*
+	 * A program can allocate and re-enter the charge path.  Skip the
+	 * nested call.  This guards the callbacks only.
+	 */
+	if (current->in_bpf_memcg)
+		return acc;
+	current->in_bpf_memcg = 1;
+
+	rcu_read_lock_dont_migrate();
+
+	/*
+	 * A memcg outlives its cgroup while it has charges, and
+	 * cgroup_bpf_release() frees the arrays when the cgroup goes.
+	 */

mm/bpf_memcontrol.c static struct bpf_struct_ops bpf_memcg_ops_desc = { · line 398

Quoted 10 lines
mm/bpf_memcontrol.c +10 -0
+	if (!cgroup_bpf_tryget_live(cgrp))
+		goto out;
+
+	bpf_memcg_ctx_init(&ctx, memcg, over_limit, gfp_mask);
+
+	bpf_cgroup_struct_ops_foreach(ops, item, cgrp, CGROUP_MEMCG_OPS) {
+		if (ops->high_policy)
+			acc |= ops->high_policy(&ctx) &
+			       BPF_MEMCG_HIGH_VALID_MASK;
+	}

If I'm reading correctly, the gfp_mask at this point doesn't account for task restrictions, so the BPF program may see __GFP_FS, __GFP_IO, etc which may later be cleared when setting up the scan_control instance.

author maintainer
#8 lore raw

Re: [RFC PATCH 3/4] memcg_ext: allow BPF to defer memory.high enforcement

On Thu, Sep 24, 2026 at 01:14:59PM -0700, JP Kobryn wrote:
[...]

Quoted 57 lines
Quoted 57 lines
mm/bpf_memcontrol.c +57 -0
+static void bpf_memcg_ctx_init(struct bpf_memcg_ctx *ctx,
+			       struct mem_cgroup *memcg,
+			       struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	ctx->memcg = memcg;
+	ctx->memcg_over_limit = over_limit;
+	ctx->task = current;
+	ctx->cgroup_id = cgroup_id(memcg->css.cgroup);
+	ctx->over_limit_cgroup_id = over_limit ?
+		cgroup_id(over_limit->css.cgroup) : 0;
+	ctx->nr_pages_over_high = current->memcg_nr_pages_over_high;
+	ctx->gfp_flags = (__force u32)gfp_mask;
+}
+
+u32 bpf_memcg_high_policy(struct mem_cgroup *memcg,
+			  struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	const struct bpf_prog_array_item *item;
+	const struct bpf_memcg_ops *ops;
+	struct bpf_memcg_ctx ctx;
+	u32 acc = BPF_MEMCG_HIGH_NO_OPINION;
+	struct cgroup *cgrp;
+
+	if (!cgroup_bpf_enabled(CGROUP_MEMCG_OPS))
+		return acc;
+
+	/*
+	 * Only the default hierarchy has a cgroup_bpf, and the static key is
+	 * global, so one policy anywhere turns this on for v1 memcgs too.  A
+	 * v1 memcg still cannot get here, because memory.high and swap.high
+	 * are both v2-only and so it never builds the debt that leads to this
+	 * call.  A hook on a path v1 can reach needs its own cgroup_on_dfl()
+	 * test: a v1 cgroup has no effective array and an uninitialised
+	 * cgrp->bpf.refcnt.
+	 */
+	cgrp = memcg->css.cgroup;
+
+	/*
+	 * A program can allocate and re-enter the charge path.  Skip the
+	 * nested call.  This guards the callbacks only.
+	 */
+	if (current->in_bpf_memcg)
+		return acc;
+	current->in_bpf_memcg = 1;
+
+	rcu_read_lock_dont_migrate();
+
+	/*
+	 * A memcg outlives its cgroup while it has charges, and
+	 * cgroup_bpf_release() frees the arrays when the cgroup goes.
+	 */
+	if (!cgroup_bpf_tryget_live(cgrp))
+		goto out;
+
+	bpf_memcg_ctx_init(&ctx, memcg, over_limit, gfp_mask);
+
+	bpf_cgroup_struct_ops_foreach(ops, item, cgrp, CGROUP_MEMCG_OPS) {
Quoted 8 lines

mm/bpf_memcontrol.c static struct bpf_struct_ops bpf_memcg_ops_desc = { · line 398

Quoted 4 lines
mm/bpf_memcontrol.c +4 -0
+		if (ops->high_policy)
+			acc |= ops->high_policy(&ctx) &
+			       BPF_MEMCG_HIGH_VALID_MASK;
+	}

If I'm reading correctly, the gfp_mask at this point doesn't account for task restrictions, so the BPF program may see __GFP_FS, __GFP_IO, etc which may later be cleared when setting up the scan_control instance.

I am passing the same gfp_mask which has been passed to try_charge_memcg(), so it should be same as what reclaim_high (reclaim internal) sees.

#9 lore raw

Re: [RFC PATCH 3/4] memcg_ext: allow BPF to defer memory.high enforcement

In reply to Shakeel Butt

2026/9/22 03:25, Shakeel Butt :

Hi Shakeel,

The series looks great to me. I added a prog_tests runner for the sample on top of it and ran the test:
selftests/bpf: add a prog_tests runner for memcg_ops_lockholder
https://github.com/teawater/linux/commit/d726b0d59081ace49e4e41cb0c91f813fd137d46

I have 2 suggestions for bpf_memcg_high_policy().

Quoted 223 lines

Before it returns, try_charge_memcg() calls __mem_cgroup_handle_over_high(), which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on the lock is stuck behind it.

Let's add high_policy ops through which a program gets a read-only snapshot of the charge and returns a request. There is one so far:
BPF_MEMCG_HIGH_DEFER_INLINE skips the inline call.

A charge runs the policies of its cgroup and of every ancestor. task_struct::in_bpf_memcg stops a program that allocates from re-entering the charge path and the dispatcher with it.

A memcg outlives its cgroup while it has charges, and cgroup_bpf_release() frees the arrays when the cgroup goes. Take the reference for the walk.

---
  MAINTAINERS                    |  1 +
  include/linux/bpf-cgroup.h     |  3 ++
  include/linux/bpf_memcontrol.h | 53 +++++++++++++++++++++-
  include/linux/cgroup.h         |  7 +++
  include/linux/sched.h          |  4 ++
  mm/bpf_memcontrol.c            | 82 +++++++++++++++++++++++++++++++++-
  mm/memcontrol.c                | 31 ++++++++++++-
  7 files changed, 176 insertions(+), 5 deletions(-)
MAINTAINERS +1 -0
index 6215fcb07770..0c84beab396f 100644
@@ -5032,6 +5032,7 @@ L:	[email protected]
  L:	[email protected]
  S:	Maintained
  F:	mm/bpf_memcontrol.c
+F:	include/linux/bpf_memcontrol.h
 
  BPF [MISC]
  L:	[email protected]
include/linux/bpf-cgroup.h +3 -0
index 4e8150848bd2..e19cf83e58f3 100644
@@ -517,6 +517,9 @@ static inline int cgroup_bpf_struct_ops_attach(struct bpf_map *map,
 
  #define cgroup_bpf_enabled(atype) (0)
  #define cgroup_bpf_enabled_runtime(atype) (0)
+/* Nothing can be attached, so the walk has nothing to walk. */
+#define bpf_cgroup_struct_ops_foreach(var, item, cgrp, atype)		\
+	for ((void)(cgrp), (item) = NULL, (var) = NULL; 0; )
  #define BPF_CGROUP_RUN_SA_PROG_LOCK(sk, uaddr, uaddrlen, atype, t_ctx) ({ 0; })
  #define BPF_CGROUP_RUN_SA_PROG(sk, uaddr, uaddrlen, atype) ({ 0; })
  #define BPF_CGROUP_PRE_CONNECT_ENABLED(sk) (0)
include/linux/bpf_memcontrol.h +51 -2
index 8204d894761e..76ea5d1c3d32 100644
@@ -5,13 +5,62 @@
   * A bpf_memcg_ops is attached to a cgroup.  A charge runs the policies of
   * that cgroup and of every ancestor, and the kernel combines what they
   * return.  BPF only picks between things the kernel already does.
- *
- * The type has no members yet; they come with the policies that use them.
   */
  #ifndef _LINUX_BPF_MEMCONTROL_H
  #define _LINUX_BPF_MEMCONTROL_H
 
+#include <linux/types.h>
+#include <linux/gfp_types.h>
+
+struct mem_cgroup;
+struct task_struct;
+
+/*
+ * What a policy can ask for when a cgroup is over memory.high.  The kernel
+ * ORs them, so one policy cannot undo another.
+ */
+enum bpf_memcg_high_request {
+	BPF_MEMCG_HIGH_NO_OPINION	= 0,
+	/*
+	 * Skip the inline reclaim and throttle.  The debt is kept and paid on
+	 * the way back to userspace, where no kernel locks are held.
+	 */
+	BPF_MEMCG_HIGH_DEFER_INLINE	= 1U << 0,
+};
+
+#define BPF_MEMCG_HIGH_VALID_MASK	BPF_MEMCG_HIGH_DEFER_INLINE
+
+/* Read-only snapshot.  Only values the caller already has. */
+struct bpf_memcg_ctx {
+	struct mem_cgroup	*memcg;			/* charged memcg */
+	struct mem_cgroup	*memcg_over_limit;	/* NULL if none found */
+	struct task_struct	*task;			/* current */
+	u64			cgroup_id;
+	u64			over_limit_cgroup_id;	/* 0 if none */
+	u64			nr_pages_over_high;
+	u32			gfp_flags;
+};
+
  struct bpf_memcg_ops {
+	/**
+	 * high_policy - say where memory.high should be enforced
+	 * @ctx: snapshot of the charge
+	 *
+	 * Return: bits from enum bpf_memcg_high_request, or 0.  Other bits
+	 * are dropped.
+	 */
+	u32 (*high_policy)(const struct bpf_memcg_ctx *ctx);
  };
 
+/*
+ * Run every high_policy on @memcg's cgroup and its ancestors, and return the
+ * combined request for the caller to act on.
+ *
+ * @memcg:	the memcg being charged, never NULL
+ * @over_limit:	first memcg found over memory.high or swap.high, or NULL
+ * @gfp_mask:	the charge's gfp mask
+ */
+u32 bpf_memcg_high_policy(struct mem_cgroup *memcg,
+			  struct mem_cgroup *over_limit, gfp_t gfp_mask);
+
  #endif /* _LINUX_BPF_MEMCONTROL_H */
include/linux/cgroup.h +7 -0
index 5dfa915a630e..cf92b6cec819 100644
@@ -959,10 +959,17 @@ static inline void cgroup_bpf_put(struct cgroup *cgrp)
  	percpu_ref_put(&cgrp->bpf.refcnt);
  }
 
+/* Fails once the cgroup is gone and its bpf state has been freed. */
+static inline bool cgroup_bpf_tryget_live(struct cgroup *cgrp)
+{
+	return percpu_ref_tryget_live_rcu(&cgrp->bpf.refcnt);
+}
+
  #else /* CONFIG_CGROUP_BPF */
 
  static inline void cgroup_bpf_get(struct cgroup *cgrp) {}
  static inline void cgroup_bpf_put(struct cgroup *cgrp) {}
+static inline bool cgroup_bpf_tryget_live(struct cgroup *cgrp) { return false; }
 
  #endif /* CONFIG_CGROUP_BPF */
 
include/linux/sched.h +4 -0
index 8b3d47a325cc..4b20a346aaa8 100644
@@ -1031,6 +1031,10 @@ struct task_struct {
  #ifdef CONFIG_MEMCG_V1
  	unsigned			in_user_fault:1;
  #endif
+#ifdef CONFIG_MEMCG
+	/* A bpf_memcg_ops program is running; do not recurse into policy */
+	unsigned			in_bpf_memcg:1;
+#endif
  #ifdef CONFIG_LRU_GEN
  	/* whether the LRU algorithm may apply to this access */
  	unsigned			in_lru_fault:1;
mm/bpf_memcontrol.c +47 -1
index fd6dff150f01..cfd0f1d443c9 100644
@@ -246,8 +246,17 @@ static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = {
   * request and the kernel acts on it.  Nothing here reclaims or sleeps.
   */
 
-/* CFI stubs.  A slot points at these while its policy is being detached. */
+/*
+ * CFI stubs.  These really run: a slot points at them while its policy is
+ * being detached.  Return 0, the identity for the kernel's OR.
+ */
+static u32 high_policy_stub(const struct bpf_memcg_ctx *ctx)
+{
+	return BPF_MEMCG_HIGH_NO_OPINION;
+}
+
  static struct bpf_memcg_ops __bpf_memcg_ops = {
+	.high_policy = high_policy_stub,
  };
 
  static const struct bpf_func_proto *
@@ -326,6 +335,77 @@ static struct bpf_struct_ops bpf_memcg_ops_desc = {
  	 */
  };
 
+static void bpf_memcg_ctx_init(struct bpf_memcg_ctx *ctx,
+			       struct mem_cgroup *memcg,
+			       struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	ctx->memcg = memcg;
+	ctx->memcg_over_limit = over_limit;
+	ctx->task = current;
+	ctx->cgroup_id = cgroup_id(memcg->css.cgroup);
+	ctx->over_limit_cgroup_id = over_limit ?
+		cgroup_id(over_limit->css.cgroup) : 0;
+	ctx->nr_pages_over_high = current->memcg_nr_pages_over_high;
+	ctx->gfp_flags = (__force u32)gfp_mask;
+}
+
+u32 bpf_memcg_high_policy(struct mem_cgroup *memcg,
+			  struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	const struct bpf_prog_array_item *item;
+	const struct bpf_memcg_ops *ops;
+	struct bpf_memcg_ctx ctx;
+	u32 acc = BPF_MEMCG_HIGH_NO_OPINION;
+	struct cgroup *cgrp;
+
+	if (!cgroup_bpf_enabled(CGROUP_MEMCG_OPS))
+		return acc;
+
+	/*
+	 * Only the default hierarchy has a cgroup_bpf, and the static key is
+	 * global, so one policy anywhere turns this on for v1 memcgs too.  A
+	 * v1 memcg still cannot get here, because memory.high and swap.high
+	 * are both v2-only and so it never builds the debt that leads to this
+	 * call.  A hook on a path v1 can reach needs its own cgroup_on_dfl()
+	 * test: a v1 cgroup has no effective array and an uninitialised
+	 * cgrp->bpf.refcnt.
+	 */
+	cgrp = memcg->css.cgroup;
+

mm/bpf_memcontrol.c static struct bpf_struct_ops bpf_memcg_ops_desc = { · line 384

Quoted 10 lines
mm/bpf_memcontrol.c +10 -0
+	/*
+	 * A program can allocate and re-enter the charge path.  Skip the
+	 * nested call.  This guards the callbacks only.
+	 */
+	if (current->in_bpf_memcg)
+		return acc;
+	current->in_bpf_memcg = 1;
+
+	rcu_read_lock_dont_migrate();
+

The guard stops nested dispatch, but not the nested charge itself: an allocation made by a policy program re-enters try_charge_memcg(), the task's debt is still over MEMCG_CHARGE_BATCH at that point, and if that allocation's gfp ever allowed blocking, the fallback would run __mem_cgroup_handle_over_high() — reclaim, and schedule_timeout_killable() when the penalty is large — inside rcu_read_lock_dont_migrate().

Today this is unreachable: bpf_base_func_proto() helpers only allocate atomically and check_member() rejects sleepable programs, so the gfpflags_allow_blocking() gate stays false. So this is not a problem with the current code — more a worry about how easily a future change could break it without anyone noticing. The invariant is written down nowhere, it spans two files, and the planned "kernel reclaims on the policy's behalf" extension is exactly the kind of change that would stress it.

If you agree, would you consider making it explicit: a comment on bpf_memcg_high_policy()/bpf_memcg_get_func_proto() stating that everything reachable from a policy program must be non-blocking, and a WARN_ON_ONCE(current->in_bpf_memcg) at the fallback site so a future violation trips in testing instead of surfacing as an RCU stall? For the reclaim-on-behalf work itself, keeping the current "BPF picks, the caller acts after dispatch returns" pattern (or a workqueue) would sidestep the issue entirely.

mm/bpf_memcontrol.c static struct bpf_struct_ops bpf_memcg_ops_desc = { · line 394

Quoted 10 lines
mm/bpf_memcontrol.c +10 -0
+	/*
+	 * A memcg outlives its cgroup while it has charges, and
+	 * cgroup_bpf_release() frees the arrays when the cgroup goes.
+	 */
+	if (!cgroup_bpf_tryget_live(cgrp))
+		goto out;
+
+	bpf_memcg_ctx_init(&ctx, memcg, over_limit, gfp_mask);
+
+	bpf_cgroup_struct_ops_foreach(ops, item, cgrp, CGROUP_MEMCG_OPS) {

cgroup_bpf_enabled(CGROUP_MEMCG_OPS) is a global static key, so with one policy attached anywhere, every over-high fallback in the system — including cgroups whose effective array is empty — pays the tryget/put plus bpf_memcg_ctx_init() (two cgroup_id() dereferences and the snapshot stores) before the foreach finds nothing to run.

Could we bail before building the context by peeking at the first effective item right after cgroup_bpf_tryget_live()? It has to be after the tryget, since cgroup_bpf_release() frees the effective arrays without NULLing the slots:

    if (!READ_ONCE(rcu_dereference(cgrp->bpf.effective[CGROUP_MEMCG_OPS]) ->items[0].kdata)) {
        cgroup_bpf_put(cgrp);
        goto out;
    }

Since attach forces ALLOW_MULTI and compute_effective_progs() folds ancestors into this cgroup's array, a NULL first item really does mean "nothing to run" for the whole walk. To keep mm/ config-agnostic, the test probably wants to be a small inline next to bpf_cgroup_struct_ops_foreach() in bpf-cgroup.h, with a stub for !CONFIG_CGROUP_BPF like the foreach macro has.

This is on the slow path that is about to reclaim or throttle, so the win is small — mainly keeping the unrelated-cgroup case O(1) instead of building a ctx that gets thrown away. Fine to drop if you prefer the walk uniform.

Best,
Hui

Quoted 76 lines
mm/bpf_memcontrol.c +14 -0
+		if (ops->high_policy)
+			acc |= ops->high_policy(&ctx) &
+			       BPF_MEMCG_HIGH_VALID_MASK;
+	}
+
+	cgroup_bpf_put(cgrp);
+out:
+	rcu_read_unlock_migrate();
+
+	current->in_bpf_memcg = 0;
+
+	return acc;
+}
+
  static int __init bpf_memcg_ops_register(void)
  {
  	/*
mm/memcontrol.c +29 -2
index 1271d390b617..bc283680640b 100644
@@ -28,6 +28,7 @@
  #include <linux/cgroup-defs.h>
  #include <linux/page_counter.h>
  #include <linux/memcontrol.h>
+#include <linux/bpf_memcontrol.h>
  #include <linux/cgroup.h>
  #include <linux/cpuset.h>
  #include <linux/sched/mm.h>
@@ -2642,9 +2643,26 @@ void __mem_cgroup_handle_over_high(gfp_t gfp_mask)
  	css_put(&memcg->css);
  }
 
+/*
+ * Ask the attached bpf_memcg_ops whether to skip the inline memory.high
+ * reclaim and throttle.
+ *
+ * @memcg:	the memcg being charged
+ * @over_limit:	first memcg found over memory.high or swap.high, starting at
+ *		the charged one, or NULL if the walk found none
+ */
+static bool bpf_memcg_high_defer(struct mem_cgroup *memcg,
+				 struct mem_cgroup *over_limit, gfp_t gfp_mask)
+{
+	u32 req = bpf_memcg_high_policy(memcg, over_limit, gfp_mask);
+
+	return req & BPF_MEMCG_HIGH_DEFER_INLINE;
+}
+
  static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
  			    unsigned int nr_pages)
  {
+	struct mem_cgroup *leaf_memcg = memcg;
  	unsigned int batch = max(MEMCG_CHARGE_BATCH, nr_pages);
  	int nr_retries = MAX_RECLAIM_RETRIES;
  	struct mem_cgroup *mem_over_limit;
@@ -2846,8 +2864,17 @@ static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
  	 */
  	if (current->memcg_nr_pages_over_high > MEMCG_CHARGE_BATCH &&
  	    !(current->flags & PF_MEMALLOC) &&
-	    gfpflags_allow_blocking(gfp_mask))
-		__mem_cgroup_handle_over_high(gfp_mask);
+	    gfpflags_allow_blocking(gfp_mask)) {
+		/*
+		 * The loop above left @memcg as the first memcg it found over
+		 * memory.high or swap.high -- possibly the charged one itself
+		 * -- or NULL if it found none.  Note the debt can be left over
+		 * from an earlier charge, so NULL does not mean no pressure.
+		 * The policy wants the memcg we charged.
+		 */
+		if (!bpf_memcg_high_defer(leaf_memcg, memcg, gfp_mask))
+			__mem_cgroup_handle_over_high(gfp_mask);
+	}
  	return 0;
  }
 
author maintainer
#10 lore raw

RFC PATCH 4/4 selftests/bpf: add a cgroupfs lock-holder bpf_memcg_ops sample

In reply to Shakeel Butt

A bpf_memcg_ops policy that keeps tasks holding a cgroupfs kernfs lock out of inline memory.high enforcement.

About forty functions take that lock, one of them through guard(rwsem_read), so the sample tracks the lock rather than its callers. A SEC("syscall") program finds kernfs_rwsem and kernfs_supers_rwsem inside cgrp_dfl_root.kf_root, and fentry/fexit on the rwsem primitives keeps a per-task depth for those two addresses. Every other kernfs user has its own kernfs_root, so its own locks, which keeps this to cgroupfs.

down_*() return once the lock is held, so an acquire is counted on their exit and a release on the entry of up_*(). The killable, interruptible and trylock forms are covered too, checking the return so a failed acquire does not count. downgrade_write() needs no hook. The depth is clamped at zero for tasks already holding the lock when the hooks went on.

Two counters guard against passing for the wrong reason: one counts a holder that reached inline enforcement anyway, and one counts a holder that could not be tracked. Both must be 0.

Measured with a reproducer [1] for the bug fixed by the patch proposed in [2], on a kernel without that fix:

                                      baseline    policy    upstream fix
  max kernfs_rwsem hold, worker        2.049 s     199 us   not taken
  max kernfs_supers_rwsem hold         2.049 s     4.8 ms   2.056 s
  max kernfs_rwsem write wait          4.096 s     6.2 ms   8.7 ms
  walker passes over /sys/fs/cgroup        352       5711   6700
  churn mkdir+rmdir ops                 13 059    364 832   415 488
  churn max latency                     30.7 s    15.1 ms   12.7 ms

The policy does better than the fix on kernfs_supers_rwsem, which the fix does not help: it drops kernfs_rwsem from the delivery loop, while the policy stops the worker stalling at all.

The throughput columns are about 8% under what the same policy reaches hooking one function. That is the cost of the tracking and motivates more optimized way to track the shared locks which can cause isolation issues between unrelated workloads.

There is no harness yet, so these numbers come from the reproducer and not from the test suite.

[1] https://github.com/shakeelb/mempressure-repros/tree/main/kernfs-notify-memcg
[2] https://lore.kernel.org/[email protected]/

---
 .../bpf/progs/memcg_ops_lockholder.c          | 222 ++++++++++++++++++
 1 file changed, 222 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/progs/memcg_ops_lockholder.c
tools/testing/selftests/bpf/progs/memcg_ops_lockholder.c new file +222 -0
new file mode 100644
index 000000000000..09a74f21f3da
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Keep tasks holding a cgroupfs kernfs lock out of inline memory.high
+ * enforcement.
+ */
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+
+char _license[] SEC("license") = "GPL";
+
+/* cgrp_dfl_root has no BTF variable, so take it as an untyped ksym. */
+extern const void cgrp_dfl_root __ksym;
+
+/* Set by resolve_locks() below, before any of the hooks are attached. */
+__u64 kernfs_rwsem_addr;
+__u64 kernfs_supers_rwsem_addr;
+
+struct lock_state {
+	__s32 depth;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_TASK_STORAGE);
+	__uint(map_flags, BPF_F_NO_PREALLOC);
+	__type(key, int);
+	__type(value, struct lock_state);
+} lock_state SEC(".maps");
+
+/* Charges from a task holding one of the locks: each is a stall avoided. */
+__u64 deferrals;
+/* Charges from everyone else, where we said nothing. */
+__u64 passthroughs;
+/* A holder reached inline enforcement anyway.  Must stay zero. */
+__u64 violations;
+/*
+ * A holder went untracked because task storage could not be created.  That
+ * looks just like a pass, and gets likelier under the pressure we are
+ * testing.  Must stay zero too.
+ */
+__u64 mark_failures;
+
+/*
+ * Find the two rw_semaphores in the cgroup2 kernfs_root.  The loader runs this
+ * once before attaching the hooks, so none of them sees a zero address.  Every
+ * other kernfs user has its own kernfs_root, hence its own locks, which is
+ * what keeps this to cgroupfs.
+ */
+SEC("syscall")
+int resolve_locks(void *ctx)
+{
+	struct cgroup_root *root = (struct cgroup_root *)&cgrp_dfl_root;
+	struct kernfs_root *kf;
+
+	kf = BPF_CORE_READ(root, kf_root);
+	if (!kf)
+		return 1;
+
+	kernfs_rwsem_addr = (__u64)kf +
+		bpf_core_field_offset(struct kernfs_root, kernfs_rwsem);
+	kernfs_supers_rwsem_addr = (__u64)kf +
+		bpf_core_field_offset(struct kernfs_root, kernfs_supers_rwsem);
+	return 0;
+}
+
+static __always_inline bool is_cgroup_kernfs_lock(const void *sem)
+{
+	__u64 addr = (__u64)sem;
+
+	return addr && (addr == kernfs_rwsem_addr ||
+			addr == kernfs_supers_rwsem_addr);
+}
+
+static __always_inline void note_acquire(const void *sem)
+{
+	struct lock_state *st;
+
+	if (!is_cgroup_kernfs_lock(sem))
+		return;
+
+	st = bpf_task_storage_get(&lock_state, bpf_get_current_task_btf(), NULL,
+				  BPF_LOCAL_STORAGE_GET_F_CREATE);
+	if (!st) {
+		__sync_fetch_and_add(&mark_failures, 1);
+		return;
+	}
+	st->depth++;
+}
+
+static __always_inline void note_release(const void *sem)
+{
+	struct lock_state *st;
+
+	if (!is_cgroup_kernfs_lock(sem))
+		return;
+
+	st = bpf_task_storage_get(&lock_state, bpf_get_current_task_btf(), NULL, 0);
+	if (!st)
+		return;
+	/* A task already holding it when we attached has no acquire to match. */
+	if (st->depth > 0)
+		st->depth--;
+}
+
+/*
+ * down_*() return once the lock is held, so the hold runs from their exit to
+ * the entry of up_*().  That excludes the wait.
+ *
+ * downgrade_write() needs no hook: the lock stays held, and the up_read()
+ * after it pairs with the original down_write().
+ */
+
+SEC("fexit/down_read")
+int BPF_PROG(down_read_exit, struct rw_semaphore *sem)
+{
+	note_acquire(sem);
+	return 0;
+}
+
+SEC("fexit/down_write")
+int BPF_PROG(down_write_exit, struct rw_semaphore *sem)
+{
+	note_acquire(sem);
+	return 0;
+}
+
+/* The killable and interruptible forms return 0 when they got the lock. */
+SEC("fexit/down_read_killable")
+int BPF_PROG(down_read_killable_exit, struct rw_semaphore *sem, int ret)
+{
+	if (!ret)
+		note_acquire(sem);
+	return 0;
+}
+
+SEC("fexit/down_read_interruptible")
+int BPF_PROG(down_read_interruptible_exit, struct rw_semaphore *sem, int ret)
+{
+	if (!ret)
+		note_acquire(sem);
+	return 0;
+}
+
+SEC("fexit/down_write_killable")
+int BPF_PROG(down_write_killable_exit, struct rw_semaphore *sem, int ret)
+{
+	if (!ret)
+		note_acquire(sem);
+	return 0;
+}
+
+/* The trylocks return 1 when they got the lock. */
+SEC("fexit/down_read_trylock")
+int BPF_PROG(down_read_trylock_exit, struct rw_semaphore *sem, int ret)
+{
+	if (ret == 1)
+		note_acquire(sem);
+	return 0;
+}
+
+SEC("fexit/down_write_trylock")
+int BPF_PROG(down_write_trylock_exit, struct rw_semaphore *sem, int ret)
+{
+	if (ret == 1)
+		note_acquire(sem);
+	return 0;
+}
+
+SEC("fentry/up_read")
+int BPF_PROG(up_read_enter, struct rw_semaphore *sem)
+{
+	note_release(sem);
+	return 0;
+}
+
+SEC("fentry/up_write")
+int BPF_PROG(up_write_enter, struct rw_semaphore *sem)
+{
+	note_release(sem);
+	return 0;
+}
+
+/* If a holder gets here, the policy did not work. */
+SEC("fentry/__mem_cgroup_handle_over_high")
+int BPF_PROG(over_high_enter)
+{
+	struct lock_state *st;
+
+	st = bpf_task_storage_get(&lock_state, bpf_get_current_task_btf(), NULL, 0);
+	if (st && st->depth > 0)
+		__sync_fetch_and_add(&violations, 1);
+	return 0;
+}
+
+/* BPF_PROG() uses "ctx" for the raw argument array, hence "mctx" here. */
+SEC("struct_ops")
+__u32 BPF_PROG(kernfs_high_policy, const struct bpf_memcg_ctx *mctx)
+{
+	struct lock_state *st;
+
+	/*
+	 * mctx->task is the charging task.  No F_CREATE, so this is just an
+	 * RCU read, and the counters are atomics on .bss.  Both are fine here.
+	 */
+	st = bpf_task_storage_get(&lock_state, mctx->task, NULL, 0);
+	if (!st || st->depth <= 0) {
+		__sync_fetch_and_add(&passthroughs, 1);
+		return BPF_MEMCG_HIGH_NO_OPINION;
+	}
+
+	__sync_fetch_and_add(&deferrals, 1);
+
+	/* The kernel ORs this with what the other policies return. */
+	return BPF_MEMCG_HIGH_DEFER_INLINE;
+}
+
+SEC(".struct_ops.link")
+struct bpf_memcg_ops kernfs_lockholder = {
+	.high_policy = (void *)kernfs_high_policy,
+};
Signature · 1 line
2.53.0-Meta
#11 lore raw
In reply to Shakeel Butt
Quoted 81 lines · Shakeel Butt

This is the first series of memcg_ext, proposed at [1]. It adds bpf_memcg_ops, a struct_ops type through which memory controller policy is attached to a cgroup. A charge runs the policies of its cgroup and of every ancestor, and the kernel combines what they return. BPF picks between things the kernel already does; it never does the work itself and never touches a page counter.

The plan is to grow this one member at a time, and to add a member only when there is a concrete problem it solves and a measurement showing it does. Nothing is added because it might be useful later. So this first series adds one member, for one problem.

The problem
===========

try_charge_memcg() calls __mem_cgroup_handle_over_high() before it returns, which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on that lock is stuck behind it.

One concrete scenario which can be resolved by this new feature is the kernfs notify worker. It delivers notifications with the cgroup2 kernfs_rwsem held for read, and the charge for the delivery allocation goes to the cgroup that set the watch, usually one already under pressure. So the worker reclaims while holding the lock, a waiting writer blocks every later reader, and anything touching cgroupfs stalls for seconds. The patch proposed in [2] fixes that one path in the kernel only for kernfs_rwsem. The same path still takes kernfs_supers_rwsem.

One can make an argument that if we know the source of the issue in the kernel, why not fix it similarly to [2] instead of having a generic solution? The reason is that it will be an uphill and continuous battle as the kernel keeps evolving and new sources of lock holders doing allocations keep coming up. In addition, there will be cases where it might not be possible to move the allocations out of locks, or where doing so would make the code really ugly [3].

high_policy() lets a policy say where memory.high should be enforced instead. Its one request, BPF_MEMCG_HIGH_DEFER_INLINE, skips the inline call, leaving the debt to be paid at the return to userspace.

Results
=======

Measured on a kernel without [2], with a policy that marks cgroupfs kernfs lock holders. Reproducer at [4].

                                      baseline    policy    upstream fix
  max kernfs_rwsem hold, worker        2.049 s     199 us   not taken
  max kernfs_supers_rwsem hold         2.049 s     4.8 ms   2.056 s
  max kernfs_rwsem write wait          4.096 s     6.2 ms   8.7 ms
  walker passes over /sys/fs/cgroup        352       5711   6700
  churn mkdir+rmdir ops                 13 059    364 832   415 488
  churn max latency                     30.7 s    15.1 ms   12.7 ms

The policy matches the fix on the bystander numbers, and does better on kernfs_supers_rwsem, which the fix does not help: it drops kernfs_rwsem from the delivery loop, while the policy stops the worker stalling at all, so every lock it holds benefits.

The patches
===========

Patch 1 is a build fix. __cgroup_bpf_query() only works because CGROUP_TCP_SOCK_OPS is the one struct_ops attach type; a second one breaks the build and makes a query for type 0 return the wrong thing.

Patch 2 adds the type and its registration, with no members, so nothing is dispatched and behaviour does not change. It also moves the attach type enum out of CONFIG_CGROUP_BPF and fixes the register_bpf_struct_ops() no-op stub, which never compiled because no caller reached it.

Patch 3 adds high_policy() and wires it into try_charge_memcg().

Patch 4 is the sample. It tracks the two cgroupfs kernfs locks by hooking the rwsem primitives rather than the forty-odd functions that take them.

Known open questions
====================

Quoted 8 lines

The semantics of memory.high for remote chargers or kernel threads is a grey area and this series does not aim to resolve that.

Another open question is whether a bound on debt deferral is needed. At the moment, we think that rather than putting a limit on deferral for memory.high, it will be better to handle that through an async worker like memcg->high_work. We aim to introduce that later, along with the right CPU accounting for that async work.

Hello Shakeel,

On the open question of how deferred debt eventually gets paid: would it make sense for the policy to also notify userspace (e.g. via ringbuf) when it defers, and have a userspace reclaimer do the reclaim through memory.reclaim?

I understand one of the concerns for the async worker is CPU accounting. If the concern is that the kworker's CPU usage is not charged to the target cgroup, the userspace reclaimer could instead be spawned with clone3(CLONE_INTO_CGROUP) so it runs inside the target cgroup, and both its CPU and memory usage get charged there.

One caveat: intermediate cgroups with the no-internal-process constraint cannot take processes, so this would only work for leaf cgroups.

What do you think?

Quoted 52 lines

There is no prog_tests harness, so the numbers above come from the reproducer and not from the test suite.

The sample's lock tracking costs about 8% throughput, because it instruments every rwsem operation on the system. That is fine for a sample that has to cover every path, but it argues for a cheaper way to track locks that cause isolation problems between unrelated workloads.

Future work
===========

Asking the kernel to reclaim on the policy's behalf, so a task that cannot be throttled still pays. More members, each with a use case: hard-limit policy, reclaim shaping, dynamic protection.

This builds on "bpf: A common way to attach struct_ops to a cgroup", which supplies attach, detach, ordering, update, query and the RCU rules. bpf_memcg_ops is its second user.

[1] https://lore.kernel.org/[email protected]/
[2] https://lore.kernel.org/[email protected]/
[3] https://lore.kernel.org/20260917-wehten-achtfach-getarnt-c85a4812337d@brauner/
[4] https://github.com/shakeelb/mempressure-repros/tree/main/kernfs-notify-memcg

Shakeel Butt (4):
bpf, cgroup: fix cgroup struct_ops query for a second attach type
memcg_ext: add cgroup-attached bpf_memcg_ops
memcg_ext: allow BPF to defer memory.high enforcement selftests/bpf: add a cgroupfs lock-holder bpf_memcg_ops sample

 MAINTAINERS                                   |   1 +
 include/linux/bpf-cgroup-defs.h               |  22 +-
 include/linux/bpf-cgroup.h                    |  14 +-
 include/linux/bpf.h                           |   2 +-
 include/linux/bpf_memcontrol.h                |  66 ++++++
 include/linux/cgroup.h                        |   7 +
 include/linux/sched.h                         |   4 +
 kernel/bpf/cgroup.c                           |   3 +-
 mm/bpf_memcontrol.c                           | 188 ++++++++++++++-
 mm/memcontrol.c                               |  31 ++-
 .../bpf/progs/memcg_ops_lockholder.c          | 222 ++++++++++++++++++
 11 files changed, 546 insertions(+), 14 deletions(-)
 create mode 100644 include/linux/bpf_memcontrol.h
 create mode 100644 tools/testing/selftests/bpf/progs/memcg_ops_lockholder.c

base-commit: 6e36e099b15bed1e8e5b3e3136c5e3eb56a15aa7 --
2.53.0-Meta

Signature · 2 lines
Regards
Yafang
author maintainer
#12 lore raw

Hi Yafang,

Quoted 2 lines · Yafang Shao
Quoted 1 line · Shakeel Butt

[...]

Quoted 13 lines
Quoted 4 lines

Known open questions
====================

Quoted 8 lines

The semantics of memory.high for remote chargers or kernel threads is a grey area and this series does not aim to resolve that.

Another open question is whether a bound on debt deferral is needed. At the moment, we think that rather than putting a limit on deferral for memory.high, it will be better to handle that through an async worker like memcg->high_work. We aim to introduce that later, along with the right CPU accounting for that async work.

Quoted 5 lines

Hello Shakeel,

On the open question of how deferred debt eventually gets paid: would it make sense for the policy to also notify userspace (e.g. via ringbuf) when it defers,

I think the notification through bpf programs is already possible and a bpf program deciding to bypass memory.high can already do notification via ringbuf.

Quoted 9 lines

and have a userspace reclaimer do the reclaim
through memory.reclaim?

I understand one of the concerns for the async worker is CPU accounting. If the concern is that the kworker's CPU usage is not charged to the target cgroup, the userspace reclaimer could instead be spawned with clone3(CLONE_INTO_CGROUP) so it runs inside the target cgroup, and both its CPU and memory usage get charged there.

Quoted 5 lines

One caveat: intermediate cgroups with the no-internal-process constraint cannot take processes, so this would only work for leaf cgroups.

What do you think?

I think all of this is possible without additional code and with this series. With AI, should be very easy to prototype it. Please take a stab and I will look into it as well (time permitting).

Thanks for taking a look and also please let me know what other ways you think memcg can be customized through BPF in a beneficial way.

#13 lore raw
Quoted 42 lines · Shakeel Butt

Hi Yafang,

Quoted 2 lines · Yafang Shao
Quoted 1 line · Shakeel Butt

[...]

Quoted 13 lines
Quoted 4 lines

Known open questions
====================

Quoted 8 lines

The semantics of memory.high for remote chargers or kernel threads is a grey area and this series does not aim to resolve that.

Another open question is whether a bound on debt deferral is needed. At the moment, we think that rather than putting a limit on deferral for memory.high, it will be better to handle that through an async worker like memcg->high_work. We aim to introduce that later, along with the right CPU accounting for that async work.

Quoted 5 lines

Hello Shakeel,

On the open question of how deferred debt eventually gets paid: would it make sense for the policy to also notify userspace (e.g. via ringbuf) when it defers,

I think the notification through bpf programs is already possible and a bpf program deciding to bypass memory.high can already do notification via ringbuf.

Quoted 13 lines

and have a userspace reclaimer do the reclaim
through memory.reclaim?

I understand one of the concerns for the async worker is CPU accounting. If the concern is that the kworker's CPU usage is not charged to the target cgroup, the userspace reclaimer could instead be spawned with clone3(CLONE_INTO_CGROUP) so it runs inside the target cgroup, and both its CPU and memory usage get charged there.

One caveat: intermediate cgroups with the no-internal-process constraint cannot take processes, so this would only work for leaf cgroups.

Quoted 5 lines
Quoted 1 line

What do you think?

I think all of this is possible without additional code and with this series. With AI, should be very easy to prototype it. Please take a stab and I will look into it as well (time permitting).

An LLM helped me quickly implement a userspace async memcg reclaimer based on your series, and it seems to work quite well.

Quoted 3 lines

Thanks for taking a look and also please let me know what other ways you think memcg can be customized through BPF in a beneficial way.

Sure. On our production servers we have been running a set of BPF programs to tailor kernel behavior for different workloads — all of them global programs so far — and I believe they are all good candidates for per-cgroup BPF policies now that cgroup-attached struct_ops is available. They have been really helpful in our Kubernetes production environment. I have sent some of them upstream, such as:

- BPF-THP

    https://lwn.net/Articles/1039689/
  - BPF-auto-NUMA
    https://lwn.net/Articles/1054030/

Perhaps we can revisit both of them and turn them into per-cgroup policies — what do you think?

We are also running some custom BPF programs that have not been sent upstream yet, such as:

- BPF-async-reclaimer
We don't care about the CPU accounting of the kworker, so we just
wake up a kworker to do the async reclaiming.
- BPF-fault-around

Both are really beneficial to our workloads, and both are global programs today.

We are planning a few more customizations to resolve painful production issues, such as:

- The long-standing inode::lock contention caused by dentries [0]. We have not started implementing it yet, but we might introduce a
memcg->dentry_limit or a memcg->vfs_cache_pressure as BPF policies..
- cgroup-level readahead.

So, to answer your question directly: for memcg itself, the beneficial customizations for us are the reclaim policy (the async reclaimer above), the dentry/vfs cache pressure knobs, and fault-around; the rest are per-cgroup MM policies that would need the struct_ops-to-cgroup mechanism generalized beyond memcg — which is why I hope these use cases can help make the design more generic.

[0] https://lore.kernel.org/linux-fsdevel/[email protected]/

Signature · 2 lines
Regards
Yafang
author maintainer
#14 lore raw
Quoted 29 lines · Yafang Shao
Quoted 28 lines · Shakeel Butt

Hi Yafang,

Quoted 7 lines · Yafang Shao
Quoted 1 line · Shakeel Butt

Hello Shakeel,

On the open question of how deferred debt eventually gets paid: would it make sense for the policy to also notify userspace (e.g. via ringbuf) when it defers,

I think the notification through bpf programs is already possible and a bpf program deciding to bypass memory.high can already do notification via ringbuf.

Quoted 13 lines

and have a userspace reclaimer do the reclaim
through memory.reclaim?

I understand one of the concerns for the async worker is CPU accounting. If the concern is that the kworker's CPU usage is not charged to the target cgroup, the userspace reclaimer could instead be spawned with clone3(CLONE_INTO_CGROUP) so it runs inside the target cgroup, and both its CPU and memory usage get charged there.

One caveat: intermediate cgroups with the no-internal-process constraint cannot take processes, so this would only work for leaf cgroups.

Quoted 8 lines
Quoted 5 lines
Quoted 1 line

What do you think?

I think all of this is possible without additional code and with this series. With AI, should be very easy to prototype it. Please take a stab and I will look into it as well (time permitting).

An LLM helped me quickly implement a userspace async memcg reclaimer based on your series, and it seems to work quite well.

That's awesome. Please do take a look at the code and provide feedback and if you don't mind, a tested-by tag would be awesome.

Quoted 13 lines
Quoted 4 lines

Thanks for taking a look and also please let me know what other ways you think memcg can be customized through BPF in a beneficial way.

Sure. On our production servers we have been running a set of BPF programs to tailor kernel behavior for different workloads — all of them global programs so far — and I believe they are all good candidates for per-cgroup BPF policies now that cgroup-attached struct_ops is available. They have been really helpful in our Kubernetes production environment. I have sent some of them upstream, such as:

Quoted 7 lines

- BPF-THP

    https://lwn.net/Articles/1039689/
  - BPF-auto-NUMA
    https://lwn.net/Articles/1054030/

Perhaps we can revisit both of them and turn them into per-cgroup policies — what do you think?

Yes seems interesting and I remember other folks (I think Rik) were interested in these ideas as well.

Quoted 7 lines

We are also running some custom BPF programs that have not been sent upstream yet, such as:

- BPF-async-reclaimer
We don't care about the CPU accounting of the kworker, so we just
wake up a kworker to do the async reclaiming.

I understand but I think for general solution we do need accounting for this and I have rfc out for this.

Quoted 12 lines

- BPF-fault-around

Both are really beneficial to our workloads, and both are global programs today.

We are planning a few more customizations to resolve painful production issues, such as:

- The long-standing inode::lock contention caused by dentries [0]. We have not started implementing it yet, but we might introduce a
memcg->dentry_limit or a memcg->vfs_cache_pressure as BPF policies..
- cgroup-level readahead.

Quoted 7 lines

So, to answer your question directly: for memcg itself, the beneficial customizations for us are the reclaim policy (the async reclaimer above), the dentry/vfs cache pressure knobs, and fault-around; the rest are per-cgroup MM policies that would need the struct_ops-to-cgroup mechanism generalized beyond memcg — which is why I hope these use cases can help make the design more generic.

Thanks a lot for this information, I will think more on these.

Quoted 5 lines

[0] https://lore.kernel.org/linux-fsdevel/[email protected]/

Signature · 2 lines
Regards
Yafang
Tested-by
#15 lore raw
Quoted 37 lines · Shakeel Butt
Quoted 35 lines · Yafang Shao
Quoted 33 lines · Shakeel Butt

Hi Yafang,

Quoted 7 lines · Yafang Shao
Quoted 1 line · Shakeel Butt

Hello Shakeel,

On the open question of how deferred debt eventually gets paid: would it make sense for the policy to also notify userspace (e.g. via ringbuf) when it defers,

I think the notification through bpf programs is already possible and a bpf program deciding to bypass memory.high can already do notification via ringbuf.

Quoted 9 lines

and have a userspace reclaimer do the reclaim
through memory.reclaim?

I understand one of the concerns for the async worker is CPU accounting. If the concern is that the kworker's CPU usage is not charged to the target cgroup, the userspace reclaimer could instead be spawned with clone3(CLONE_INTO_CGROUP) so it runs inside the target cgroup, and both its CPU and memory usage get charged there.

Quoted 5 lines

One caveat: intermediate cgroups with the no-internal-process constraint cannot take processes, so this would only work for leaf cgroups.

What do you think?

I think all of this is possible without additional code and with this series. With AI, should be very easy to prototype it. Please take a stab and I will look into it as well (time permitting).

Quoted 5 lines
Quoted 2 lines

An LLM helped me quickly implement a userspace async memcg reclaimer based on your series, and it seems to work quite well.

That's awesome. Please do take a look at the code and provide feedback and if you don't mind, a tested-by tag would be awesome.

For this series, feel free to add:

One nit:

Since we might extend the cgroup-ops to other subsystems, or even to the cgroup itself, could we define a more generic CGROUP_OPS instead of CGROUP_MEMCG_OPS, and also rename struct bpf_memcg_ops to struct bpf_cgroup_ops? This way, we attach to the struct cgroup itself rather than to a specific subsystem.

Signature · 2 lines
Regards
Yafang
maintainer
#16 lore raw
In reply to Shakeel Butt

Hello, Shakeel.

Quoted 4 lines · Shakeel Butt

try_charge_memcg() calls __mem_cgroup_handle_over_high() before it returns, which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on that lock is stuck behind it.

Why not just raise the lazy bound high enough that most charges never enforce inline, and maybe annotate the specific paths that can allocate a lot so that they do? Inline enforcement should be the exception, not the rule. Flipping that and then trying to reverse it with custom BPF policies doesn't make a lot of sense.

Quoted 6 lines

One concrete scenario which can be resolved by this new feature is the kernfs notify worker. It delivers notifications with the cgroup2 kernfs_rwsem held for read, and the charge for the delivery allocation goes to the cgroup that set the watch, usually one already under pressure. So the worker reclaims while holding the lock, a waiting writer blocks every later reader, and anything touching cgroupfs stalls for seconds.

Slowing down the kworker inline doesn't make sense. It's charging on behalf of the watcher through set_active_memcg(), which already tells us whose debt it is. Wouldn't it make more sense to defer the debt to that cgroup instead of slowing down the kernel thread?

Thanks.

--
tejun

author maintainer
#17 lore raw
Quoted 8 lines · Tejun Heo

Hello, Shakeel.

Quoted 4 lines · Shakeel Butt

try_charge_memcg() calls __mem_cgroup_handle_over_high() before it returns, which reclaims and can throttle the task. That happens wherever the charge happens, so a task holding a kernel lock can be stuck there, and everything waiting on that lock is stuck behind it.

Quoted 6 lines

Why not just raise the lazy bound high enough that most charges never enforce inline, and maybe annotate the specific paths that can allocate a lot so that they do? Inline enforcement should be the exception, not the rule. Flipping that and then trying to reverse it with custom BPF policies doesn't make a lot of sense.

Please correct me if I misunderstood you. Mainly, you are saying that we should have a sane default behavior for memory.high. At the moment, if the current charging process accumulates charges totaling more than MEMCG_CHARGE_BATCH pages and the target memcg is over its high limit, memory.high is enforced synchronously. You are suggesting that we should increase the threshold from MEMCG_CHARGE_BATCH to some arbitrarily large number. In that case, synchronous enforcement of memory.high will be very rare.

I am fine with changing the default behavior. Actually, I have been contemplating whether I should propose a revert of commit c9afe31ec443e ("memcg: synchronously enforce memory.high for large overcharges") because it has introduced more problems than it has solved, but that is a separate topic. The initial commit already mentioned that MEMCG_CHARGE_BATCH was used arbitrarily, so replacing it with something big might be acceptable. I want to keep that decision separate.

I am not sure about annotating specific paths. I think it would impose a greater maintenance burden as the kernel evolves, since the annotations might become stale. Also, people might object to adding memcg-internal hooks in non-memcg code paths. In any case, this can be explored separately.

Returning to the actual proposal, my plan was to start small with a narrow, specific use case. However, my long-term plan is to provide a mechanism to change the default behavior for custom use cases. For example, for memory.high, I will provide a way for users to specify what behavior they want, i.e., whether or not they want more synchronous throttling. Second, I will introduce a mechanism to trigger async reclaim workers. I also plan to extend this functionality to memory.max. I just wanted to convey that I will keep pushing this proposal, with the use cases adjusted a bit.

Quoted 7 lines
Quoted 6 lines

One concrete scenario which can be resolved by this new feature is the kernfs notify worker. It delivers notifications with the cgroup2 kernfs_rwsem held for read, and the charge for the delivery allocation goes to the cgroup that set the watch, usually one already under pressure. So the worker reclaims while holding the lock, a waiting writer blocks every later reader, and anything touching cgroupfs stalls for seconds.

Quoted 5 lines

Slowing down the kworker inline doesn't make sense. It's charging on behalf of the watcher through set_active_memcg(), which already tells us whose debt it is. Wouldn't it make more sense to defer the debt to that cgroup instead of slowing down the kernel thread?

Yes, that makes sense. When a non-task-context charge exceeds memory.high, the kernel already schedules high_work for the charged memcg. I plan to do the same for kthreads so they need not reclaim or throttle inline. This is best-effort reclaim rather than exact debt accounting; I still need to examine userspace tasks that charge another memcg through set_active_memcg(). I am addressing the reclaim worker's CPU accounting separately.

Thanks for taking a look and providing feedback.

maintainer
#18 lore raw

Hello, Shakeel.

Quoted 7 lines · Shakeel Butt

I am fine with changing the default behavior. Actually, I have been contemplating whether I should propose a revert of commit c9afe31ec443e ("memcg: synchronously enforce memory.high for large overcharges") because it has introduced more problems than it has solved, but that is a separate topic. The initial commit already mentioned that MEMCG_CHARGE_BATCH was used arbitrarily, so replacing it with something big might be acceptable. I want to keep that decision separate.

Which kernel paths allocate enough for this to matter? Can you give specific examples where the in-kernel synchronous enforcement helps?

Quoted 5 lines

Returning to the actual proposal, my plan was to start small with a narrow, specific use case. However, my long-term plan is to provide a mechanism to change the default behavior for custom use cases. For example, for memory.high, I will provide a way for users to specify what behavior they want, i.e., whether or not they want more synchronous throttling.

This doesn't seem like a policy problem. It feels like a problem that should be and can reasonably be solved for everybody, so I'm not sure whether BPF is the right call for this specific purpose.

Thanks.

--
tejun

author maintainer
#19 lore raw
Quoted 11 lines · Tejun Heo

Hello, Shakeel.

Quoted 7 lines · Shakeel Butt

I am fine with changing the default behavior. Actually, I have been contemplating whether I should propose a revert of commit c9afe31ec443e ("memcg: synchronously enforce memory.high for large overcharges") because it has introduced more problems than it has solved, but that is a separate topic. The initial commit already mentioned that MEMCG_CHARGE_BATCH was used arbitrarily, so replacing it with something big might be acceptable. I want to keep that decision separate.

Quoted 2 lines

Which kernel paths allocate enough for this to matter? Can you give specific examples where the in-kernel synchronous enforcement helps?

mlock(), madvise(POPULATE), fadvise(WILLNEED) are the obvious ones where large amount of memory can be allocated before returning to userspace. Now regarding the in-kernel sync high enforcement for these cases helps or not, I think it depends on what the user is trying to achieve with memory.high. One scenario I can think of is an overcommitted system where admin dynamically adjusts memory.high limits of colocated workloads based on their working set size. The memory spikes will be smoothen by memory.high otherwise colocated workload can be negatively impact. In this example, ineffective memory.high will not be able to avoid global pressure which can impact colocated workloads.

BTW the above scenario is just an example and not something I am proposing. Actually I think memory overcommit in the presense sync throttling can potentially cause more isolation issues.

Quoted 8 lines
Quoted 6 lines

Returning to the actual proposal, my plan was to start small with a narrow, specific use case. However, my long-term plan is to provide a mechanism to change the default behavior for custom use cases. For example, for memory.high, I will provide a way for users to specify what behavior they want, i.e., whether or not they want more synchronous throttling.

This doesn't seem like a policy problem.

What is "This" in the above statement?

Quoted 3 lines

It feels like a problem that should
be and can reasonably be solved for everybody, so I'm not sure whether BPF is the right call for this specific purpose.

Here if you meant that default behavior of memory.high should work for most (if not all) users then we are on same page. If some user want memory.high reclaim to happen in a separate thread instead of return-to-userspace or synchronously, this proposal provides mechanism through BPF to such users to achieve their goals.

maintainer
#20 lore raw

Hello, Shakeel.

Quoted 2 lines · Shakeel Butt

mlock(), madvise(POPULATE), fadvise(WILLNEED) are the obvious ones where large amount of memory can be allocated before returning to userspace.

These are explicit bulk memory operations running in the calling task's own context, so they can be special-cased. Each is a loop with points between chunks where no locks are held, and the same over-high handler the return-to-userspace path runs can run there. That keeps the throttling on the task which asked for the memory and can't cause priority inversions. The set of such paths is small and obvious, so I don't think it'd be onerous to maintain.

Quoted 3 lines
Quoted 1 line

This doesn't seem like a policy problem.

What is "This" in the above statement?

The whole thing, how memory.high should be enforced when the overrun happens inside the kernel or from a kthread.

Quoted 5 lines

Here if you meant that default behavior of memory.high should work for most (if not all) users then we are on same page. If some user want memory.high reclaim to happen in a separate thread instead of return-to-userspace or synchronously, this proposal provides mechanism through BPF to such users to achieve their goals.

I think there's a common reasonable solution here, which is deciding by who the charge is for. A task charging for itself gets return-to-userspace enforcement plus the checks in the explicit bulk operations above. A kthread or anything else charging on behalf of a cgroup shouldn't be throttled for the cgroup's overrun. The overrun is attributed to the cgroup, async reclaim is kicked, and the cgroup's own tasks absorb the throttling on their next return to userspace. The in-charge synchronous fallback goes away. Each case has one reasonable answer, so I don't see a policy choice to expose here.

Thanks.

--
tejun

author maintainer
#21 lore raw
Quoted 2 lines · Tejun Heo

Hello, Shakeel.

[...]

Quoted 7 lines
Quoted 6 lines

Here if you meant that default behavior of memory.high should work for most (if not all) users then we are on same page. If some user want memory.high reclaim to happen in a separate thread instead of return-to-userspace or synchronously, this proposal provides mechanism through BPF to such users to achieve their goals.

Quoted 9 lines

I think there's a common reasonable solution here, which is deciding by who the charge is for. A task charging for itself gets return-to-userspace enforcement plus the checks in the explicit bulk operations above. A kthread or anything else charging on behalf of a cgroup shouldn't be throttled for the cgroup's overrun. The overrun is attributed to the cgroup, async reclaim is kicked, and the cgroup's own tasks absorb the throttling on their next return to userspace. The in-charge synchronous fallback goes away. Each case has one reasonable answer, so I don't see a policy choice to expose here.

Let me list the cases explicitly to see where we agree and where we disagree:

1. For the !in_task() charge path, today we trigger async reclaim and we will continue to do the same in the future.

2. For a kthread (or remote charging), today we throttle it similarly to user threads, but we want it to be handled similarly to the !in_task() case, i.e. trigger async reclaim. Regarding your statement "the cgroup's own tasks absorb the throttling on their next return to userspace", I assume you meant that when some other user thread of that memcg goes through the charge path, it will eventually do memory.high enforcement on return to userspace.

3. For a task, today we enforce memory.high on return to userspace, and if too much charge is accumulated in a single kernel entry, we enforce the high limit synchronously. You are suggesting that we remove the sync enforcement and add a couple of throttling points at known bulk allocation sites.

Please correct me if I misunderstood.

We are in agreement on (1) and (2) completely. For (3), I am fine with removing the sync enforcement, but for throttling points for bulk operation sites, I think we should only add them when there is an actual use case for that or someone complains about overrun from those sites.

Now, setting aside the default behavior of memory.high, I want to provide additional flexibility to users for (3) specifically. One specific case is letting users opt in to async reclaim instead of the other forms of memory.high enforcement. Basically, users can specify that instead of having their application threads throttled, they would prefer async reclaim to bring their usage back below memory.high. Whether we provide this functionality through BPF or through something else, I am open to options.

thanks,
Shakeel

maintainer
#22 lore raw

Hello, Shakeel.

Quoted 4 lines · Shakeel Butt

We are in agreement on (1) and (2) completely. For (3), I am fine with removing the sync enforcement, but for throttling points for bulk operation sites, I think we should only add them when there is an actual use case for that or someone complains about overrun from those sites.

On (3), if removing synchronous enforcement wouldn't regress anything, that's fine, but why was it added in the first place?

Quoted 6 lines

Now, setting aside the default behavior of memory.high, I want to provide additional flexibility to users for (3) specifically. One specific case is letting users opt in to async reclaim instead of the other forms of memory.high enforcement. Basically, users can specify that instead of having their application threads throttled, they would prefer async reclaim to bring their usage back below memory.high.

As for flexibility, we already have a gradient of enforcement around memory.high. Is the need here to make the shape of that gradient configurable? Can you give specific examples where this is needed?

Thanks.

Signature · 1 line
tejun
author maintainer
#23 lore raw
Quoted 11 lines · Tejun Heo

Hello, Shakeel.

Quoted 4 lines · Shakeel Butt

We are in agreement on (1) and (2) completely. For (3), I am fine with removing the sync enforcement, but for throttling points for bulk operation sites, I think we should only add them when there is an actual use case for that or someone complains about overrun from those sites.

On (3), if removing synchronous enforcement wouldn't regress anything, that's fine, but why was it added in the first place?

I added the sync enforcement to replace a Google internal feature which, on memcg OOM, allows node controller couple of seconds to either increase the max limit or let the memcg die. With sync enforcement, memory.high helped in simple benchmarks. However later testing on some realistic Google workloads, I found out that several thousand threads are very normal of typical Google workload and memory.high sync enforcement is not effective on applications with large amount of threads. In addition, there were workloads which on noticing blocked threads, keep forking more threads. At the end implementing that feature using memory.high didn't pan out.

Quoted 11 lines
Quoted 6 lines

Now, setting aside the default behavior of memory.high, I want to provide additional flexibility to users for (3) specifically. One specific case is letting users opt in to async reclaim instead of the other forms of memory.high enforcement. Basically, users can specify that instead of having their application threads throttled, they would prefer async reclaim to bring their usage back below memory.high.

As for flexibility, we already have a gradient of enforcement around memory.high. Is the need here to make the shape of that gradient configurable? Can you give specific examples where this is needed?

The concrete example I have is the kswapd like async reclaimers (plural) per memcg. Kswapd is woken up on free pages falling below low watermark and then when free pages fall below min watermark, allocators get throttled (enter direct reclaim). I want to apply similar concept to memcg (but with right cpu accounting and more concurrency).

maintainer
#24 lore raw

Hello,

Quoted 4 lines · Shakeel Butt
Quoted 3 lines

On (3), if removing synchronous enforcement wouldn't regress anything, that's fine, but why was it added in the first place?

Quoted 9 lines

I added the sync enforcement to replace a Google internal feature which, on memcg OOM, allows node controller couple of seconds to either increase the max limit or let the memcg die. With sync enforcement, memory.high helped in simple benchmarks. However later testing on some realistic Google workloads, I found out that several thousand threads are very normal of typical Google workload and memory.high sync enforcement is not effective on applications with large amount of threads. In addition, there were workloads which on noticing blocked threads, keep forking more threads. At the end implementing that feature using memory.high didn't pan out.

Yeah, if there's no known active usecases, might as well start by dropping it and see whether anyone complains.

Quoted 9 lines
Quoted 3 lines

As for flexibility, we already have a gradient of enforcement around memory.high. Is the need here to make the shape of that gradient configurable? Can you give specific examples where this is needed?

The concrete example I have is the kswapd like async reclaimers (plural) per memcg. Kswapd is woken up on free pages falling below low watermark and then when free pages fall below min watermark, allocators get throttled (enter direct reclaim). I want to apply similar concept to memcg (but with right cpu accounting and more concurrency).

I see. Yeah, ISTR talking about async reclaim for memory.high. I don't know much about mm but that makes sense to me and I'm not against allowing customization of memory.high behavior via BPF; however, most use cases can likely be served with a reasonably designed dumb interface and that likely is a better place to start.

Thanks.

Signature · 1 line
tejun