From: JP Kobryn <jp.kobryn@linux.dev>
To: Shakeel Butt <shakeel.butt@linux.dev>,
Andrew Morton <akpm@linux-foundation.org>,
Alexei Starovoitov <ast@kernel.org>
Cc: Johannes Weiner <hannes@cmpxchg.org>,
Michal Hocko <mhocko@kernel.org>,
Roman Gushchin <roman.gushchin@linux.dev>,
Muchun Song <muchun.song@linux.dev>, Tejun Heo <tj@kernel.org>,
Michal Koutny <mkoutny@suse.com>,
Amery Hung <ameryhung@gmail.com>,
Daniel Borkmann <daniel@iogearbox.net>,
Andrii Nakryiko <andrii@kernel.org>,
Eduard Zingerman <eddyz87@gmail.com>,
Kumar Kartikeya Dwivedi <memxor@gmail.com>,
Martin KaFai Lau <martin.lau@linux.dev>,
Song Liu <song@kernel.org>,
Yonghong Song <yonghong.song@linux.dev>,
Emil Tsalapatis <emil@etsalapatis.com>,
Jiri Olsa <jolsa@kernel.org>,
Ihor Solodrai <ihor.solodrai@linux.dev>,
John Fastabend <john.fastabend@gmail.com>,
Jiayuan Chen <jiayuan.chen@linux.dev>,
hui.zhu@linux.dev, Donet Tom <donettom@linux.ibm.com>,
Greg Thelen <gthelen@google.com>,
Meta kernel team <kernel-team@meta.com>,
linux-mm@kvack.org, bpf@vger.kernel.org, cgroups@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: Re: [RFC PATCH 3/4] memcg_ext: allow BPF to defer memory.high enforcement
Date: Thu, 24 Sep 2026 13:14:59 -0700 [thread overview]
Message-ID: <c8b8504c-8e54-479f-a372-3d2755edaf88@linux.dev> (raw)
In-Reply-To: <20260921192559.2619635-4-shakeel.butt@linux.dev>
On 9/21/26 12:25 PM, Shakeel Butt wrote:
> Before it returns, try_charge_memcg() calls __mem_cgroup_handle_over_high(),
> which reclaims and can throttle the task. That happens wherever the charge
> happens, so a task holding a kernel lock can be stuck there, and everything
> waiting on the lock is stuck behind it.
>
> Let's add high_policy ops through which a program gets a read-only snapshot
> of the charge and returns a request. There is one so far:
> BPF_MEMCG_HIGH_DEFER_INLINE skips the inline call.
>
> A charge runs the policies of its cgroup and of every ancestor.
> task_struct::in_bpf_memcg stops a program that allocates from re-entering
> the charge path and the dispatcher with it.
>
> A memcg outlives its cgroup while it has charges, and cgroup_bpf_release()
> frees the arrays when the cgroup goes. Take the reference for the walk.
>
> Signed-off-by: Shakeel Butt <shakeel.butt@linux.dev>
> ---
> 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(-)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6215fcb07770..0c84beab396f 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -5032,6 +5032,7 @@ L: bpf@vger.kernel.org
> L: linux-mm@kvack.org
> S: Maintained
> F: mm/bpf_memcontrol.c
> +F: include/linux/bpf_memcontrol.h
>
> BPF [MISC]
> L: bpf@vger.kernel.org
> diff --git a/include/linux/bpf-cgroup.h b/include/linux/bpf-cgroup.h
> index 4e8150848bd2..e19cf83e58f3 100644
> --- a/include/linux/bpf-cgroup.h
> +++ b/include/linux/bpf-cgroup.h
> @@ -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)
> diff --git a/include/linux/bpf_memcontrol.h b/include/linux/bpf_memcontrol.h
> index 8204d894761e..76ea5d1c3d32 100644
> --- a/include/linux/bpf_memcontrol.h
> +++ b/include/linux/bpf_memcontrol.h
> @@ -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 */
> diff --git a/include/linux/cgroup.h b/include/linux/cgroup.h
> index 5dfa915a630e..cf92b6cec819 100644
> --- a/include/linux/cgroup.h
> +++ b/include/linux/cgroup.h
> @@ -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 */
>
> diff --git a/include/linux/sched.h b/include/linux/sched.h
> index 8b3d47a325cc..4b20a346aaa8 100644
> --- a/include/linux/sched.h
> +++ b/include/linux/sched.h
> @@ -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;
> diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c
> index fd6dff150f01..cfd0f1d443c9 100644
> --- a/mm/bpf_memcontrol.c
> +++ b/mm/bpf_memcontrol.c
> @@ -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.
> + */
> + 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.
next prev parent reply other threads:[~2026-09-24 20:15 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-21 19:25 [RFC PATCH 0/4] memcg_ext: memcg policy through cgroup-attached struct_ops Shakeel Butt
2026-09-21 19:25 ` [RFC PATCH 1/4] bpf, cgroup: fix cgroup struct_ops query for a second attach type Shakeel Butt
2026-09-21 20:19 ` bot+bpf-ci
2026-09-21 19:25 ` [RFC PATCH 2/4] memcg_ext: add cgroup-attached bpf_memcg_ops Shakeel Butt
2026-09-21 19:25 ` [RFC PATCH 3/4] memcg_ext: allow BPF to defer memory.high enforcement Shakeel Butt
2026-09-24 20:14 ` JP Kobryn [this message]
2026-09-24 21:19 ` Shakeel Butt
2026-09-21 19:25 ` [RFC PATCH 4/4] selftests/bpf: add a cgroupfs lock-holder bpf_memcg_ops sample Shakeel Butt
2026-09-23 13:07 ` [RFC PATCH 0/4] memcg_ext: memcg policy through cgroup-attached struct_ops Yafang Shao
2026-09-23 15:47 ` Shakeel Butt
2026-09-24 10:01 ` Yafang Shao
2026-09-24 20:42 ` Shakeel Butt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=c8b8504c-8e54-479f-a372-3d2755edaf88@linux.dev \
--to=jp.kobryn@linux.dev \
--cc=akpm@linux-foundation.org \
--cc=ameryhung@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=cgroups@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=donettom@linux.ibm.com \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=gthelen@google.com \
--cc=hannes@cmpxchg.org \
--cc=hui.zhu@linux.dev \
--cc=ihor.solodrai@linux.dev \
--cc=jiayuan.chen@linux.dev \
--cc=john.fastabend@gmail.com \
--cc=jolsa@kernel.org \
--cc=kernel-team@meta.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=mhocko@kernel.org \
--cc=mkoutny@suse.com \
--cc=muchun.song@linux.dev \
--cc=roman.gushchin@linux.dev \
--cc=shakeel.butt@linux.dev \
--cc=song@kernel.org \
--cc=tj@kernel.org \
--cc=yonghong.song@linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®