memcg_ext: memcg policy through cgroup-attached struct_ops
Subsystems (9)
Shakeel Butt
maintainercover 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
Shakeel Butt
maintainerRFC 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
@@ -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
@@ -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
[email protected]
AI reviewRe: [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_opsquery for a second attach typeTwo things in
__cgroup_bpf_query()work only becauseCGROUP_TCP_SOCK_OPSis the only onestruct_opsattach type.It calls
cgroup_bpf_enabled(atype)with an atype thatfind_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'". Addcgroup_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 ontype_idalone. An attach type whose subsystem is not built keepstype_id0, so a query for type 0 finds it and returns success with nothing instead of-ENOENT. Skip such slots.Signed-off-by Shakeel Butt <[email protected]>
This looks like a bug fix for the struct_ops attach infrastructure. Should this include:
---
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
Re: [RFC PATCH 1/4] bpf, cgroup: fix cgroup struct_ops query for a second attach type
In reply to Shakeel ButtQuoted 11 lines · Shakeel Butt
Two things in
__cgroup_bpf_query()work only becauseCGROUP_TCP_SOCK_OPSis the only onestruct_opsattach type.It calls
cgroup_bpf_enabled(atype)with an atype thatfind_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'". Addcgroup_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 ontype_idalone. An attach type whose subsystem is not built keepstype_id0, so a query for type 0 finds it and returns success with nothing instead of-ENOENT. Skip such slots.Signed-off-by Shakeel Butt <[email protected]>
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@@ -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@@ -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
Shakeel Butt
maintainerRFC PATCH 2/4 memcg_ext: add cgroup-attached bpf_memcg_ops
In reply to Shakeel ButtAdd 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
@@ -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
@@ -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
@@ -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
@@ -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
[email protected]
AI reviewRe: [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
[ ... ]
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
Shakeel Butt
maintainerRFC PATCH 3/4 memcg_ext: allow BPF to defer memory.high enforcement
In reply to Shakeel ButtBefore 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
JP Kobryn
maintainerRe: [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_policyops 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_INLINEskips the inline call.A charge runs the policies of its cgroup and of every ancestor.
task_struct::in_bpf_memcgstops 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.Signed-off-by Shakeel Butt <[email protected]>--- 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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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.
Shakeel Butt
maintainerRe: [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_maskat 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 thescan_controlinstance.
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.
Re: [RFC PATCH 3/4] memcg_ext: allow BPF to defer memory.high enforcement
In reply to Shakeel Butt2026/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_policyops 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_INLINEskips the inline call.A charge runs the policies of its cgroup and of every ancestor.
task_struct::in_bpf_memcgstops 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.Signed-off-by Shakeel Butt <[email protected]>--- 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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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 ; }
Shakeel Butt
maintainerRFC PATCH 4/4 selftests/bpf: add a cgroupfs lock-holder bpf_memcg_ops sample
In reply to Shakeel ButtA 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
@@ -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
Quoted 81 lines · Shakeel Butt
This is the first series of
memcg_ext, proposed at [1]. It addsbpf_memcg_ops, astruct_opstype 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_rwsemheld 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 forkernfs_rwsem. The same path still takeskernfs_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 msThe policy matches the fix on the bystander numbers, and does better on
kernfs_supers_rwsem, which the fix does not help: it dropskernfs_rwsemfrom 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 becauseCGROUP_TCP_SOCK_OPSis the onestruct_opsattach 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_BPFand fixes theregister_bpf_struct_ops()no-op stub, which never compiled because no caller reached it.Patch 3 adds
high_policy()and wires it intotry_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_testsharness, 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_opsto a cgroup", which supplies attach, detach, ordering, update, query and the RCU rules.bpf_memcg_opsis 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-memcgShakeel Butt (4):
bpf, cgroup: fix cgroupstruct_opsquery for a second attach type
memcg_ext: add cgroup-attachedbpf_memcg_ops
memcg_ext: allow BPF to defer memory.high enforcement selftests/bpf: add a cgroupfs lock-holderbpf_memcg_opssampleMAINTAINERS | 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.cbase-commit:
6e36e099b15bed1e8e5b3e3136c5e3eb56a15aa7--
2.53.0-Meta
Signature · 2 lines
Regards Yafang
Shakeel Butt
maintainerHi 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.
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 amemcg->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
Shakeel Butt
maintainerQuoted 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_opsis 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::lockcontention caused by dentries [0]. We have not started implementing it yet, but we might introduce amemcg->dentry_limitor amemcg->vfs_cache_pressureas 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
Yafang Shao
Tested-byQuoted 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
Tejun Heo
maintainerHello, 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_rwsemheld 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
Shakeel Butt
maintainerQuoted 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_rwsemheld 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.
Tejun Heo
maintainerHello, 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 thatMEMCG_CHARGE_BATCHwas 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
Shakeel Butt
maintainerQuoted 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 thatMEMCG_CHARGE_BATCHwas 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.
Tejun Heo
maintainerHello, 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
Shakeel Butt
maintainerQuoted 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
Tejun Heo
maintainerHello, 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
Shakeel Butt
maintainerQuoted 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).
Tejun Heo
maintainerHello,
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