* [PATCH bpf-next v6 0/2] bpf: BPF-driven proactive memcg reclaim @ 2026-09-01 2:21 Hui Zhu 2026-09-01 2:21 ` [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc Hui Zhu 2026-09-01 2:21 ` [PATCH bpf-next v6 2/2] selftests/bpf: Add memcg async reclaim test Hui Zhu 0 siblings, 2 replies; 7+ messages in thread From: Hui Zhu @ 2026-09-01 2:21 UTC (permalink / raw) To: Roman Gushchin, JP Kobryn, Shakeel Butt, Andrew Morton, Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai, Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Shuah Khan, Barry Song, Geliang Tang, linux-kernel, bpf, linux-mm, linux-kselftest Cc: Hui Zhu From: Hui Zhu <zhuhui@kylinos.cn> BPF programs can observe memory pressure on a cgroup (e.g. refault stats via bpf_mem_cgroup_page_state()), but cannot act on it: triggering reclaim on a chosen cgroup requires writing to memory.reclaim, which BPF cannot do. This series adds bpf_proactive_reclaim(), a sleepable kfunc performing one proactive reclaim pass on a target memcg, so when and how hard to reclaim is BPF policy rather than hard-coded thresholds. The kfunc is restricted to BPF_PROG_TYPE_SYSCALL so that reclaim always runs in a clean process context: generic sleepable programs may execute with filesystem locks held or in NOFS/NOIO contexts, where the reclaim path could deadlock in filesystem shrinkers. The bpf_wq and task_work callbacks of a SYSCALL program keep its program type and run in process context, so reclaim work can still be queued asynchronously through them, as the selftest does with bpf_wq. The use case we are looking at is protecting high-priority workloads: a BPF program monitors the state of a high-priority cgroup and, when it degrades (e.g. PSI rises or refaults increase, as in the selftest), asynchronously reclaims memory from low-priority cgroups via bpf_wq and bpf_proactive_reclaim(), giving the pressured cgroup more free pages. Another use case: several vendor-maintained kernels carry private implementations that trigger asynchronous reclaim when a memcg enters a certain state. These exist for historical and partly psychological reasons, but the underlying demand is real. We expect BPF-driven proactive reclaim, combined with the BPF hooks for the memory controller currently under discussion and development, to serve these needs in mainline, reducing kernel fragmentation and improving kernel maintainability. Hui Zhu (2): mm/bpf: Add bpf_proactive_reclaim kfunc selftests/bpf: Add memcg async reclaim test mm/bpf_memcontrol.c | 76 ++- .../bpf/prog_tests/memcg_async_reclaim.c | 480 ++++++++++++++++++ .../selftests/bpf/progs/memcg_async_reclaim.c | 181 +++++++ 3 files changed, 735 insertions(+), 2 deletions(-) create mode 100644 tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c create mode 100644 tools/testing/selftests/bpf/progs/memcg_async_reclaim.c -- 2.53.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc 2026-09-01 2:21 [PATCH bpf-next v6 0/2] bpf: BPF-driven proactive memcg reclaim Hui Zhu @ 2026-09-01 2:21 ` Hui Zhu 2026-09-01 18:19 ` JP Kobryn 2026-09-01 2:21 ` [PATCH bpf-next v6 2/2] selftests/bpf: Add memcg async reclaim test Hui Zhu 1 sibling, 1 reply; 7+ messages in thread From: Hui Zhu @ 2026-09-01 2:21 UTC (permalink / raw) To: Roman Gushchin, JP Kobryn, Shakeel Butt, Andrew Morton, Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai, Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Shuah Khan, Barry Song, Geliang Tang, linux-kernel, bpf, linux-mm, linux-kselftest Cc: Hui Zhu From: Hui Zhu <zhuhui@kylinos.cn> Add bpf_proactive_reclaim(), a sleepable kfunc which performs one proactive reclaim pass on a given memory cgroup, similar to a write to memory.reclaim but without retrying until the target is reached. The kfunc is restricted to BPF_PROG_TYPE_SYSCALL so that reclaim always runs in a clean process context. Generic sleepable programs may execute with filesystem locks held or in NOFS/NOIO contexts, where the reclaim path could deadlock in filesystem shrinkers. A SYSCALL program can still drive reclaim asynchronously through bpf_wq or task_work callbacks, which run in process context and keep the SYSCALL program type, so they can call the kfunc too. The kfunc refuses to reclaim if the calling task is already in a reclaim context, as a nested reclaim would corrupt the outer reclaim state. Signed-off-by: Hui Zhu <zhuhui@kylinos.cn> --- mm/bpf_memcontrol.c | 76 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c index 716df49d7647..fd48faa5f8b0 100644 --- a/mm/bpf_memcontrol.c +++ b/mm/bpf_memcontrol.c @@ -6,6 +6,7 @@ */ #include <linux/memcontrol.h> +#include <linux/swap.h> #include <linux/bpf.h> __bpf_kfunc_start_defs(); @@ -159,6 +160,55 @@ __bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg) mem_cgroup_flush_stats(memcg); } +/* + * Reclaim must not recurse: try_to_free_mem_cgroup_pages() overwrites + * current->reclaim_state, so a nested call would corrupt the outer + * reclaim state. Reclaim windows are marked with PF_MEMALLOC; + * reclaim_state is also checked because it is installed slightly + * before PF_MEMALLOC. + */ +static bool bpf_in_reclaim_context(void) +{ + return (current->flags & PF_MEMALLOC) || current->reclaim_state; +} + +/** + * bpf_proactive_reclaim - proactively reclaim memory from a memory + * cgroup + * @memcg: the target memory cgroup to reclaim from + * @size: the amount of memory to reclaim, in bytes + * + * Trigger one proactive reclaim pass on @memcg, similar to a write to + * memory.reclaim, but without retrying until @size is reached. + * + * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs + * in a clean process context. The SYSCALL program can schedule the + * actual reclaim work via bpf_wq or timers, which also execute in + * safe process context (workqueue, task_work). + * + * Must not be called with a filesystem lock held: the reclaim path + * may deadlock on it via filesystem shrinkers. + * + * Return: The amount of memory reclaimed, in bytes, or 0 if @size is + * smaller than a page or the task is already in a reclaim context. + */ +__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg, + unsigned long size) +{ + unsigned long nr_reclaimed; + + if (size < PAGE_SIZE || unlikely(bpf_in_reclaim_context())) + return 0; + + nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, size / PAGE_SIZE, + GFP_KERNEL, + MEMCG_RECLAIM_MAY_SWAP | + MEMCG_RECLAIM_PROACTIVE, + NULL); + + return nr_reclaimed * PAGE_SIZE; +} + __bpf_kfunc_end_defs(); BTF_KFUNCS_START(bpf_memcontrol_kfuncs) @@ -171,22 +221,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events) BTF_ID_FLAGS(func, bpf_mem_cgroup_usage) BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state) BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE) - BTF_KFUNCS_END(bpf_memcontrol_kfuncs) +/* + * Proactive reclaim needs a clean process context, so it is restricted + * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a + * SYSCALL program schedules run as the same program type, so they can + * still invoke it; generic sleepable programs (e.g. fentry on reclaim + * paths, inode_rmdir) cannot. + */ +BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs) +BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE) +BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs) + static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = { .owner = THIS_MODULE, .set = &bpf_memcontrol_kfuncs, }; +static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = { + .owner = THIS_MODULE, + .set = &bpf_memcontrol_reclaim_kfuncs, +}; + static int __init bpf_memcontrol_init(void) { int err; err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, &bpf_memcontrol_kfunc_set); - if (err) + if (err) { pr_warn("error while registering bpf memcontrol kfuncs: %d", err); + return err; + } + + err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, + &bpf_memcontrol_reclaim_kfunc_set); + if (err) + pr_warn("error registering bpf reclaim kfuncs: %d", err); return err; } -- 2.53.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc 2026-09-01 2:21 ` [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc Hui Zhu @ 2026-09-01 18:19 ` JP Kobryn 2026-09-02 6:57 ` Hui Zhu 0 siblings, 1 reply; 7+ messages in thread From: JP Kobryn @ 2026-09-01 18:19 UTC (permalink / raw) To: Hui Zhu, Roman Gushchin, Shakeel Butt, Andrew Morton, Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai, Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Shuah Khan, Barry Song, Geliang Tang, linux-kernel, bpf, linux-mm, linux-kselftest Cc: Hui Zhu Hi Hui, On 8/31/26 7:21 PM, Hui Zhu wrote: > From: Hui Zhu <zhuhui@kylinos.cn> > > Add bpf_proactive_reclaim(), a sleepable kfunc which performs one > proactive reclaim pass on a given memory cgroup, similar to a write > to memory.reclaim but without retrying until the target is reached. > > The kfunc is restricted to BPF_PROG_TYPE_SYSCALL so that reclaim > always runs in a clean process context. Generic sleepable programs > may execute with filesystem locks held or in NOFS/NOIO contexts, > where the reclaim path could deadlock in filesystem shrinkers. A > SYSCALL program can still drive reclaim asynchronously through > bpf_wq or task_work callbacks, which run in process context and > keep the SYSCALL program type, so they can call the kfunc too. > > The kfunc refuses to reclaim if the calling task is already in a > reclaim context, as a nested reclaim would corrupt the outer reclaim > state. > > Signed-off-by: Hui Zhu <zhuhui@kylinos.cn> > --- The difflog is missing in these patches. > mm/bpf_memcontrol.c | 76 +++++++++++++++++++++++++++++++++++++++++++-- > 1 file changed, 74 insertions(+), 2 deletions(-) > > diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c > index 716df49d7647..fd48faa5f8b0 100644 > --- a/mm/bpf_memcontrol.c > +++ b/mm/bpf_memcontrol.c > @@ -6,6 +6,7 @@ > */ > > #include <linux/memcontrol.h> > +#include <linux/swap.h> > #include <linux/bpf.h> > > __bpf_kfunc_start_defs(); > @@ -159,6 +160,55 @@ __bpf_kfunc void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg) > mem_cgroup_flush_stats(memcg); > } > > +/* > + * Reclaim must not recurse: try_to_free_mem_cgroup_pages() overwrites > + * current->reclaim_state, so a nested call would corrupt the outer > + * reclaim state. Reclaim windows are marked with PF_MEMALLOC; > + * reclaim_state is also checked because it is installed slightly > + * before PF_MEMALLOC. > + */ > +static bool bpf_in_reclaim_context(void) > +{ > + return (current->flags & PF_MEMALLOC) || current->reclaim_state; > +} > + > +/** > + * bpf_proactive_reclaim - proactively reclaim memory from a memory > + * cgroup > + * @memcg: the target memory cgroup to reclaim from > + * @size: the amount of memory to reclaim, in bytes > + * > + * Trigger one proactive reclaim pass on @memcg, similar to a write to > + * memory.reclaim, but without retrying until @size is reached. > + * > + * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs > + * in a clean process context. The SYSCALL program can schedule the > + * actual reclaim work via bpf_wq or timers, which also execute in > + * safe process context (workqueue, task_work). On the workqueue aspect, I could see potential issues. The target size has no upper bound so the total scan/execution time on the shared wq can easily stall other work. Contention on the lru_lock can make matters worse because interrupts are disabled while holding the lock. So the contention would not only stall other work, but can delay IPI handling leading to CSD lock stalls. It looks like the only existing path that explicitly calls try_to_free_mem_cgroup_pages() from a shared wq is the memory.high fallback used when the limit is exceeded outside of task context. But even in that case, it's more constrained. The reclaim request is bounded at MEMCG_CHARGE_BATCH (in high_work_func()) and is limited to one work item per memcg. Would it make sense to follow the existing precedent and use the same bound in your kfunc? You could then batch the wq submissions and you would also be able to stop submitting in between if needed, like in the case of the cgroup dying. > + * > + * Must not be called with a filesystem lock held: the reclaim path > + * may deadlock on it via filesystem shrinkers. > + * > + * Return: The amount of memory reclaimed, in bytes, or 0 if @size is > + * smaller than a page or the task is already in a reclaim context. > + */ > +__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup *memcg, > + unsigned long size) > +{ > + unsigned long nr_reclaimed; > + > + if (size < PAGE_SIZE || unlikely(bpf_in_reclaim_context())) > + return 0; > + > + nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, size / PAGE_SIZE, > + GFP_KERNEL, > + MEMCG_RECLAIM_MAY_SWAP | > + MEMCG_RECLAIM_PROACTIVE, > + NULL); > + > + return nr_reclaimed * PAGE_SIZE; > +} > + > __bpf_kfunc_end_defs(); > > BTF_KFUNCS_START(bpf_memcontrol_kfuncs) > @@ -171,22 +221,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events) > BTF_ID_FLAGS(func, bpf_mem_cgroup_usage) > BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state) > BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE) > - > BTF_KFUNCS_END(bpf_memcontrol_kfuncs) > > +/* > + * Proactive reclaim needs a clean process context, so it is restricted > + * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a > + * SYSCALL program schedules run as the same program type, so they can > + * still invoke it; generic sleepable programs (e.g. fentry on reclaim > + * paths, inode_rmdir) cannot. > + */ > +BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs) > +BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE) > +BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs) > + > static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = { > .owner = THIS_MODULE, > .set = &bpf_memcontrol_kfuncs, > }; > > +static const struct btf_kfunc_id_set bpf_memcontrol_reclaim_kfunc_set = { > + .owner = THIS_MODULE, > + .set = &bpf_memcontrol_reclaim_kfuncs, > +}; > + > static int __init bpf_memcontrol_init(void) > { > int err; > > err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, > &bpf_memcontrol_kfunc_set); > - if (err) > + if (err) { > pr_warn("error while registering bpf memcontrol kfuncs: %d", err); > + return err; > + } > + > + err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, > + &bpf_memcontrol_reclaim_kfunc_set); > + if (err) > + pr_warn("error registering bpf reclaim kfuncs: %d", err); > > return err; > } ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc 2026-09-01 18:19 ` JP Kobryn @ 2026-09-02 6:57 ` Hui Zhu 2026-09-02 18:14 ` JP Kobryn 0 siblings, 1 reply; 7+ messages in thread From: Hui Zhu @ 2026-09-02 6:57 UTC (permalink / raw) To: JP Kobryn, Roman Gushchin, Shakeel Butt, Andrew Morton, Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai, Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Shuah Khan, Barry Song, Geliang Tang, linux-kernel, bpf, linux-mm, linux-kselftest Cc: Hui Zhu Hi JP, Thanks for the review! > Hi Hui, > > On 8/31/26 7:21 PM, Hui Zhu wrote: >> From: Hui Zhu <zhuhui@kylinos.cn> >> >> Add bpf_proactive_reclaim(), a sleepable kfunc which performs one >> proactive reclaim pass on a given memory cgroup, similar to a write >> to memory.reclaim but without retrying until the target is reached. >> >> The kfunc is restricted to BPF_PROG_TYPE_SYSCALL so that reclaim >> always runs in a clean process context. Generic sleepable programs >> may execute with filesystem locks held or in NOFS/NOIO contexts, >> where the reclaim path could deadlock in filesystem shrinkers. A >> SYSCALL program can still drive reclaim asynchronously through >> bpf_wq or task_work callbacks, which run in process context and >> keep the SYSCALL program type, so they can call the kfunc too. >> >> The kfunc refuses to reclaim if the calling task is already in a >> reclaim context, as a nested reclaim would corrupt the outer reclaim >> state. >> >> Signed-off-by: Hui Zhu <zhuhui@kylinos.cn> >> --- > > The difflog is missing in these patches. Sorry, my mistake. Will add the changelog (changes since v4) in the next version. > >> mm/bpf_memcontrol.c | 76 +++++++++++++++++++++++++++++++++++++++++++-- >> 1 file changed, 74 insertions(+), 2 deletions(-) >> >> diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c >> index 716df49d7647..fd48faa5f8b0 100644 >> --- a/mm/bpf_memcontrol.c >> +++ b/mm/bpf_memcontrol.c >> @@ -6,6 +6,7 @@ >> */ >> #include <linux/memcontrol.h> >> +#include <linux/swap.h> >> #include <linux/bpf.h> >> __bpf_kfunc_start_defs(); >> @@ -159,6 +160,55 @@ __bpf_kfunc void >> bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg) >> mem_cgroup_flush_stats(memcg); >> } >> +/* >> + * Reclaim must not recurse: try_to_free_mem_cgroup_pages() overwrites >> + * current->reclaim_state, so a nested call would corrupt the outer >> + * reclaim state. Reclaim windows are marked with PF_MEMALLOC; >> + * reclaim_state is also checked because it is installed slightly >> + * before PF_MEMALLOC. >> + */ >> +static bool bpf_in_reclaim_context(void) >> +{ >> + return (current->flags & PF_MEMALLOC) || current->reclaim_state; >> +} >> + >> +/** >> + * bpf_proactive_reclaim - proactively reclaim memory from a memory >> + * cgroup >> + * @memcg: the target memory cgroup to reclaim from >> + * @size: the amount of memory to reclaim, in bytes >> + * >> + * Trigger one proactive reclaim pass on @memcg, similar to a write to >> + * memory.reclaim, but without retrying until @size is reached. >> + * >> + * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs >> + * in a clean process context. The SYSCALL program can schedule the >> + * actual reclaim work via bpf_wq or timers, which also execute in >> + * safe process context (workqueue, task_work). > > On the workqueue aspect, I could see potential issues. The target size > has no upper bound so the total scan/execution time on the shared > wq can easily stall other work. Contention on the lru_lock can make > matters worse because interrupts are disabled while holding the lock. So > the contention would not only stall other work, but can delay IPI > handling leading to CSD lock stalls. Agreed. I previously misread .nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX) in try_to_free_mem_cgroup_pages() as an upper bound on the reclaim target; it is actually a lower bound, so nothing inside the reclaim path limits how long a single kfunc call can run. The next version will cap the per-invocation reclaim target. > > It looks like the only existing path that explicitly calls > try_to_free_mem_cgroup_pages() from a shared wq is the memory.high > fallback used when the limit is exceeded outside of task context. But > even in that case, it's more constrained. The reclaim request is bounded > at MEMCG_CHARGE_BATCH (in high_work_func()) and is limited to one work > item per memcg. > > Would it make sense to follow the existing precedent and use the same > bound in your kfunc? You could then batch the wq submissions and you > would also be able to stop submitting in between if needed, like in the > case of the cgroup dying. > Yes, the next version will cap the reclaim target of a single bpf_proactive_reclaim() call at MEMCG_CHARGE_BATCH, following the high_work_func() precedent, so each invocation is a bounded unit of work on the shared wq. For the "one work item per memcg" side, I'd like to hear your thoughts on the following approach: allow only one in-flight bpf_proactive_reclaim() per memcg. The kfunc would take a per-memcg flag (atomic cmpxchg) on entry and return 0 if another BPF reclaim pass is already running on the same memcg, mirroring the one-work-item-per-memcg property of high_work. This would prevent wq work items from piling up reclaiming the same memcg. The reason for doing this with a per-memcg in-flight check rather than a fixed per-memcg work item is to keep bpf_proactive_reclaim() flexible: it bounds how much reclaim can run against one memcg at any moment, while leaving the reclaim policy -- when to reclaim, how many passes to batch, and when to stop (e.g. if the target cgroup is dying) -- entirely in the BPF program. Do you think this is a reasonable way to bound the total reclaim activity per memcg, or would you prefer something else? With the per-invocation cap, each kfunc call becomes a small, bounded unit of work, and the BPF program does the batching: it schedules successive wq submissions and can stop submitting between passes when needed. This keeps the "when and how hard to reclaim" policy in BPF while bounding the kernel-side cost of each invocation. Best, Hui >> + * >> + * Must not be called with a filesystem lock held: the reclaim path >> + * may deadlock on it via filesystem shrinkers. >> + * >> + * Return: The amount of memory reclaimed, in bytes, or 0 if @size is >> + * smaller than a page or the task is already in a reclaim context. >> + */ >> +__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup >> *memcg, >> + unsigned long size) >> +{ >> + unsigned long nr_reclaimed; >> + >> + if (size < PAGE_SIZE || unlikely(bpf_in_reclaim_context())) >> + return 0; >> + >> + nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, size / >> PAGE_SIZE, >> + GFP_KERNEL, >> + MEMCG_RECLAIM_MAY_SWAP | >> + MEMCG_RECLAIM_PROACTIVE, >> + NULL); >> + >> + return nr_reclaimed * PAGE_SIZE; >> +} >> + >> __bpf_kfunc_end_defs(); >> BTF_KFUNCS_START(bpf_memcontrol_kfuncs) >> @@ -171,22 +221,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events) >> BTF_ID_FLAGS(func, bpf_mem_cgroup_usage) >> BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state) >> BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE) >> - >> BTF_KFUNCS_END(bpf_memcontrol_kfuncs) >> +/* >> + * Proactive reclaim needs a clean process context, so it is restricted >> + * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a >> + * SYSCALL program schedules run as the same program type, so they can >> + * still invoke it; generic sleepable programs (e.g. fentry on reclaim >> + * paths, inode_rmdir) cannot. >> + */ >> +BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs) >> +BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE) >> +BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs) >> + >> static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = { >> .owner = THIS_MODULE, >> .set = &bpf_memcontrol_kfuncs, >> }; >> +static const struct btf_kfunc_id_set >> bpf_memcontrol_reclaim_kfunc_set = { >> + .owner = THIS_MODULE, >> + .set = &bpf_memcontrol_reclaim_kfuncs, >> +}; >> + >> static int __init bpf_memcontrol_init(void) >> { >> int err; >> err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, >> &bpf_memcontrol_kfunc_set); >> - if (err) >> + if (err) { >> pr_warn("error while registering bpf memcontrol kfuncs: >> %d", err); >> + return err; >> + } >> + >> + err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, >> + &bpf_memcontrol_reclaim_kfunc_set); >> + if (err) >> + pr_warn("error registering bpf reclaim kfuncs: %d", err); >> return err; >> } > ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc 2026-09-02 6:57 ` Hui Zhu @ 2026-09-02 18:14 ` JP Kobryn 0 siblings, 0 replies; 7+ messages in thread From: JP Kobryn @ 2026-09-02 18:14 UTC (permalink / raw) To: Hui Zhu, Roman Gushchin, Shakeel Butt, Andrew Morton, Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai, Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Shuah Khan, Barry Song, Geliang Tang, linux-kernel, bpf, linux-mm, linux-kselftest Cc: Hui Zhu On 9/1/26 11:57 PM, Hui Zhu wrote: > Hi JP, > > Thanks for the review! > >> Hi Hui, >> >> On 8/31/26 7:21 PM, Hui Zhu wrote: >>> From: Hui Zhu <zhuhui@kylinos.cn> >>> >>> Add bpf_proactive_reclaim(), a sleepable kfunc which performs one >>> proactive reclaim pass on a given memory cgroup, similar to a write >>> to memory.reclaim but without retrying until the target is reached. >>> >>> The kfunc is restricted to BPF_PROG_TYPE_SYSCALL so that reclaim >>> always runs in a clean process context. Generic sleepable programs >>> may execute with filesystem locks held or in NOFS/NOIO contexts, >>> where the reclaim path could deadlock in filesystem shrinkers. A >>> SYSCALL program can still drive reclaim asynchronously through >>> bpf_wq or task_work callbacks, which run in process context and >>> keep the SYSCALL program type, so they can call the kfunc too. >>> >>> The kfunc refuses to reclaim if the calling task is already in a >>> reclaim context, as a nested reclaim would corrupt the outer reclaim >>> state. >>> >>> Signed-off-by: Hui Zhu <zhuhui@kylinos.cn> >>> --- >> >> The difflog is missing in these patches. > Sorry, my mistake. Will add the changelog (changes since v4) in the > next version. >> >>> mm/bpf_memcontrol.c | 76 +++++++++++++++++++++++++++++++++++++++++++-- >>> 1 file changed, 74 insertions(+), 2 deletions(-) >>> >>> diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c >>> index 716df49d7647..fd48faa5f8b0 100644 >>> --- a/mm/bpf_memcontrol.c >>> +++ b/mm/bpf_memcontrol.c >>> @@ -6,6 +6,7 @@ >>> */ >>> #include <linux/memcontrol.h> >>> +#include <linux/swap.h> >>> #include <linux/bpf.h> >>> __bpf_kfunc_start_defs(); >>> @@ -159,6 +160,55 @@ __bpf_kfunc void >>> bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg) >>> mem_cgroup_flush_stats(memcg); >>> } >>> +/* >>> + * Reclaim must not recurse: try_to_free_mem_cgroup_pages() overwrites >>> + * current->reclaim_state, so a nested call would corrupt the outer >>> + * reclaim state. Reclaim windows are marked with PF_MEMALLOC; >>> + * reclaim_state is also checked because it is installed slightly >>> + * before PF_MEMALLOC. >>> + */ >>> +static bool bpf_in_reclaim_context(void) >>> +{ >>> + return (current->flags & PF_MEMALLOC) || current->reclaim_state; >>> +} >>> + >>> +/** >>> + * bpf_proactive_reclaim - proactively reclaim memory from a memory >>> + * cgroup >>> + * @memcg: the target memory cgroup to reclaim from >>> + * @size: the amount of memory to reclaim, in bytes >>> + * >>> + * Trigger one proactive reclaim pass on @memcg, similar to a write to >>> + * memory.reclaim, but without retrying until @size is reached. >>> + * >>> + * This kfunc is restricted to BPF_PROG_TYPE_SYSCALL to ensure it runs >>> + * in a clean process context. The SYSCALL program can schedule the >>> + * actual reclaim work via bpf_wq or timers, which also execute in >>> + * safe process context (workqueue, task_work). >> >> On the workqueue aspect, I could see potential issues. The target size >> has no upper bound so the total scan/execution time on the shared >> wq can easily stall other work. Contention on the lru_lock can make >> matters worse because interrupts are disabled while holding the lock. So >> the contention would not only stall other work, but can delay IPI >> handling leading to CSD lock stalls. > > > Agreed. I previously misread > > .nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX) > > in try_to_free_mem_cgroup_pages() as an upper bound on the reclaim > target; it is actually a lower bound, so nothing inside the reclaim > path limits how long a single kfunc call can run. The next version > will cap the per-invocation reclaim target. > > >> >> It looks like the only existing path that explicitly calls >> try_to_free_mem_cgroup_pages() from a shared wq is the memory.high >> fallback used when the limit is exceeded outside of task context. But >> even in that case, it's more constrained. The reclaim request is bounded >> at MEMCG_CHARGE_BATCH (in high_work_func()) and is limited to one work >> item per memcg. >> >> Would it make sense to follow the existing precedent and use the same >> bound in your kfunc? You could then batch the wq submissions and you >> would also be able to stop submitting in between if needed, like in the >> case of the cgroup dying. >> > Yes, the next version will cap the reclaim target of a single > bpf_proactive_reclaim() call at MEMCG_CHARGE_BATCH, following the > high_work_func() precedent, so each invocation is a bounded unit of > work on the shared wq. > > For the "one work item per memcg" side, I'd like to hear your > thoughts on the following approach: allow only one in-flight > bpf_proactive_reclaim() per memcg. The kfunc would take a per-memcg > flag (atomic cmpxchg) on entry and return 0 if another BPF reclaim > pass is already running on the same memcg, mirroring the > one-work-item-per-memcg property of high_work. This would prevent wq > work items from piling up reclaiming the same memcg. > > The reason for doing this with a per-memcg in-flight check rather > than a fixed per-memcg work item is to keep bpf_proactive_reclaim() > flexible: it bounds how much reclaim can run against one memcg at any > moment, while leaving the reclaim policy -- when to reclaim, how many > passes to batch, and when to stop (e.g. if the target cgroup is > dying) -- entirely in the BPF program. Do you think this is a > reasonable way to bound the total reclaim activity per memcg, or > would you prefer something else? Let's not add state to the memcg to emulate high_work in bpf. I think you should document and implement this on the bpf side. Change the selftest example so that bpf_proactive_reclaim() is invoked only once per callback (instead of a in a loop) and requeue the same work if another reclaim batch is needed. You could also check if the cgroup is dying and discontinue the work. I would also document that each memcg should use its own bpf_wq item. Correct me if I'm wrong, but it looks like that is your intention in the selftest wq_map. > > With the per-invocation cap, each kfunc call becomes a small, bounded > unit of work, and the BPF program does the batching: it schedules > successive wq submissions and can stop submitting between passes when > needed. This keeps the "when and how hard to reclaim" policy in BPF > while bounding the kernel-side cost of each invocation. > > Best, > Hui > > >>> + * >>> + * Must not be called with a filesystem lock held: the reclaim path >>> + * may deadlock on it via filesystem shrinkers. >>> + * >>> + * Return: The amount of memory reclaimed, in bytes, or 0 if @size is >>> + * smaller than a page or the task is already in a reclaim context. >>> + */ >>> +__bpf_kfunc unsigned long bpf_proactive_reclaim(struct mem_cgroup >>> *memcg, >>> + unsigned long size) >>> +{ >>> + unsigned long nr_reclaimed; >>> + >>> + if (size < PAGE_SIZE || unlikely(bpf_in_reclaim_context())) >>> + return 0; >>> + >>> + nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, size / >>> PAGE_SIZE, >>> + GFP_KERNEL, >>> + MEMCG_RECLAIM_MAY_SWAP | >>> + MEMCG_RECLAIM_PROACTIVE, >>> + NULL); >>> + >>> + return nr_reclaimed * PAGE_SIZE; >>> +} >>> + >>> __bpf_kfunc_end_defs(); >>> BTF_KFUNCS_START(bpf_memcontrol_kfuncs) >>> @@ -171,22 +221,44 @@ BTF_ID_FLAGS(func, bpf_mem_cgroup_memory_events) >>> BTF_ID_FLAGS(func, bpf_mem_cgroup_usage) >>> BTF_ID_FLAGS(func, bpf_mem_cgroup_page_state) >>> BTF_ID_FLAGS(func, bpf_mem_cgroup_flush_stats, KF_SLEEPABLE) >>> - >>> BTF_KFUNCS_END(bpf_memcontrol_kfuncs) >>> +/* >>> + * Proactive reclaim needs a clean process context, so it is restricted >>> + * to BPF_PROG_TYPE_SYSCALL. The bpf_wq and task_work callbacks that a >>> + * SYSCALL program schedules run as the same program type, so they can >>> + * still invoke it; generic sleepable programs (e.g. fentry on reclaim >>> + * paths, inode_rmdir) cannot. >>> + */ >>> +BTF_KFUNCS_START(bpf_memcontrol_reclaim_kfuncs) >>> +BTF_ID_FLAGS(func, bpf_proactive_reclaim, KF_SLEEPABLE) >>> +BTF_KFUNCS_END(bpf_memcontrol_reclaim_kfuncs) >>> + >>> static const struct btf_kfunc_id_set bpf_memcontrol_kfunc_set = { >>> .owner = THIS_MODULE, >>> .set = &bpf_memcontrol_kfuncs, >>> }; >>> +static const struct btf_kfunc_id_set >>> bpf_memcontrol_reclaim_kfunc_set = { >>> + .owner = THIS_MODULE, >>> + .set = &bpf_memcontrol_reclaim_kfuncs, >>> +}; >>> + >>> static int __init bpf_memcontrol_init(void) >>> { >>> int err; >>> err = register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC, >>> &bpf_memcontrol_kfunc_set); >>> - if (err) >>> + if (err) { >>> pr_warn("error while registering bpf memcontrol kfuncs: >>> %d", err); >>> + return err; >>> + } >>> + >>> + err = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL, >>> + &bpf_memcontrol_reclaim_kfunc_set); >>> + if (err) >>> + pr_warn("error registering bpf reclaim kfuncs: %d", err); >>> return err; >>> } >> ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH bpf-next v6 2/2] selftests/bpf: Add memcg async reclaim test 2026-09-01 2:21 [PATCH bpf-next v6 0/2] bpf: BPF-driven proactive memcg reclaim Hui Zhu 2026-09-01 2:21 ` [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc Hui Zhu @ 2026-09-01 2:21 ` Hui Zhu 2026-09-01 2:33 ` sashiko-bot 1 sibling, 1 reply; 7+ messages in thread From: Hui Zhu @ 2026-09-01 2:21 UTC (permalink / raw) To: Roman Gushchin, JP Kobryn, Shakeel Butt, Andrew Morton, Andrii Nakryiko, Eduard Zingerman, Ihor Solodrai, Alexei Starovoitov, Daniel Borkmann, Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis, Shuah Khan, Barry Song, Geliang Tang, linux-kernel, bpf, linux-mm, linux-kselftest Cc: Hui Zhu From: Hui Zhu <zhuhui@kylinos.cn> Add the memcg_async_reclaim selftest, which verifies that BPF-driven async proactive reclaim mitigates refault-induced slowdown under memory pressure: a BPF program monitors the refault stats of a memory-pressured cgroup and, once they grow, asynchronously reclaims another cgroup via bpf_wq and bpf_proactive_reclaim(), letting the pressured workload finish faster. Signed-off-by: Hui Zhu <zhuhui@kylinos.cn> --- .../bpf/prog_tests/memcg_async_reclaim.c | 480 ++++++++++++++++++ .../selftests/bpf/progs/memcg_async_reclaim.c | 181 +++++++ 2 files changed, 661 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c create mode 100644 tools/testing/selftests/bpf/progs/memcg_async_reclaim.c diff --git a/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c new file mode 100644 index 000000000000..1270d73c9116 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Memory controller eBPF async reclaim test + */ + +#include <test_progs.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/vfs.h> +#include <sys/wait.h> +#include <fcntl.h> +#include <signal.h> +#include <time.h> +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <linux/magic.h> + +#include "cgroup_helpers.h" + +struct bpf_args { + u64 high_cgroup_id; + u64 low_cgroup_id; + u64 event_delta_threshold; + u64 check_ns; +}; + +#include "memcg_async_reclaim.skel.h" + +#define FILE_SIZE (32 * 1024 * 1024ul) +#define BUFFER_SIZE (4096) +#define CG_LIMIT (32 * 1024 * 1024ul) +#define READ_TIMES 50 + +#define CG_DIR "/memcg_async_reclaim" +#define CG_HIGH_DIR CG_DIR "/high" +#define CG_LOW_DIR CG_DIR "/low" + +#define CHECK_PERIOD_NS (2 * 1000 * 1000ull) +#define EVENT_DELTA_THRESHOLD 1 + +/* + * The workload files must sit on a regular filesystem: with swap + * disabled for the cgroup, tmpfs/ramfs pages are unevictable and would + * OOM the cgroup instead of exercising reclaim; they are also charged + * as anonymous memory, so they never raise the WORKINGSET_REFAULT_FILE + * events the BPF program monitors. Fall back to the current directory + * when /tmp is backed by such a filesystem. + */ +static const char *workload_files_dir(void) +{ + struct statfs st; + + if (!statfs("/tmp", &st) && + (st.f_type == TMPFS_MAGIC || st.f_type == RAMFS_MAGIC)) + return "."; + return "/tmp"; +} + +/* + * The workload children run after test_progs hijacked stdio, so + * anything they print is lost with their private copy of the hijacked + * buffer. The exit status is the only diagnostics channel that reaches + * the parent, so each failing step gets its own code. + */ +enum child_exit_code { + CHILD_EXIT_OK = 0, + CHILD_EXIT_JOIN_CGROUP, + CHILD_EXIT_WRITE_FILE, + CHILD_EXIT_READ_FILE, + CHILD_EXIT_TIME_FILE, +}; + +static const char *child_exit_str(int code) +{ + switch (code) { + case CHILD_EXIT_OK: + return "success"; + case CHILD_EXIT_JOIN_CGROUP: + return "join cgroup"; + case CHILD_EXIT_WRITE_FILE: + return "write data file"; + case CHILD_EXIT_READ_FILE: + return "read data file"; + case CHILD_EXIT_TIME_FILE: + return "write time file"; + default: + return "unknown"; + } +} + +static int setup_high_low_cgroups(u64 *high_cgroup_id, u64 *low_cgroup_id) +{ + int ret; + char limit_buf[20]; + + ret = setup_cgroup_environment(); + if (!ASSERT_OK(ret, "setup_cgroup_environment")) + goto cleanup; + + ret = create_and_get_cgroup(CG_DIR); + if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_DIR)) + goto cleanup; + close(ret); + + ret = enable_controllers(CG_DIR, "memory"); + if (!ASSERT_OK(ret, "enable_controllers")) + goto cleanup; + + snprintf(limit_buf, sizeof(limit_buf), "%lu", CG_LIMIT); + ret = write_cgroup_file(CG_DIR, "memory.max", limit_buf); + if (!ASSERT_OK(ret, "write_cgroup_file memory.max")) + goto cleanup; + + /* + * Keep the workloads from swapping out. With CONFIG_SWAP=n the + * memory.swap.max file does not exist, and no swap can happen + * anyway, so skip the write. + */ + if (!access("/proc/swaps", F_OK)) { + ret = write_cgroup_file(CG_DIR, "memory.swap.max", "0"); + if (!ASSERT_OK(ret, "write_cgroup_file memory.swap.max")) + goto cleanup; + } + + ret = create_and_get_cgroup(CG_HIGH_DIR); + if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_HIGH_DIR)) + goto cleanup; + close(ret); + + *high_cgroup_id = get_cgroup_id(CG_HIGH_DIR); + if (!ASSERT_GT(*high_cgroup_id, 0, "get_cgroup_id")) + goto cleanup; + + ret = create_and_get_cgroup(CG_LOW_DIR); + if (!ASSERT_GE(ret, 0, "create_and_get_cgroup " CG_LOW_DIR)) + goto cleanup; + close(ret); + + *low_cgroup_id = get_cgroup_id(CG_LOW_DIR); + if (!ASSERT_GT(*low_cgroup_id, 0, "get_cgroup_id")) + goto cleanup; + + return 0; + +cleanup: + cleanup_cgroup_environment(); + return -1; +} + +static int write_file(const char *filename) +{ + int ret = -1; + size_t written = 0; + char *buffer; + FILE *fp; + + fp = fopen(filename, "wb"); + if (!fp) + goto out; + + buffer = malloc(BUFFER_SIZE); + if (!buffer) + goto cleanup_fp; + + memset(buffer, 'A', BUFFER_SIZE); + + while (written < FILE_SIZE) { + size_t to_write = FILE_SIZE - written < BUFFER_SIZE ? + FILE_SIZE - written : BUFFER_SIZE; + + if (fwrite(buffer, 1, to_write, fp) != to_write) + goto cleanup; + written += to_write; + } + + ret = 0; +cleanup: + free(buffer); +cleanup_fp: + fclose(fp); +out: + return ret; +} + +static int read_file(const char *filename, int iterations) +{ + int ret = -1; + long page_size = sysconf(_SC_PAGESIZE); + char *map; + size_t i; + int fd; + struct stat sb; + + fd = open(filename, O_RDONLY); + if (fd == -1) + goto out; + + if (fstat(fd, &sb) == -1) + goto cleanup_fd; + + if (sb.st_size != FILE_SIZE) { + fprintf(stderr, "File size mismatch: expected %lu, got %lu\n", + (unsigned long)FILE_SIZE, (unsigned long)sb.st_size); + goto cleanup_fd; + } + + map = mmap(NULL, FILE_SIZE, PROT_READ, MAP_PRIVATE, fd, 0); + if (map == MAP_FAILED) + goto cleanup_fd; + + for (int iter = 0; iter < iterations; iter++) { + for (i = 0; i < FILE_SIZE; i += page_size) { + /* access a byte to trigger page fault */ + volatile char v = map[i]; + (void)v; + } + } + + if (munmap(map, FILE_SIZE) == -1) + goto cleanup_fd; + + ret = 0; + +cleanup_fd: + close(fd); +out: + return ret; +} + +static int real_test_child_work(const char *cgroup_path, char *data_filename, + char *time_filename, int read_times) +{ + struct timespec start, end; + double elapsed; + FILE *fp; + + if (join_parent_cgroup(cgroup_path)) + return CHILD_EXIT_JOIN_CGROUP; + + clock_gettime(CLOCK_MONOTONIC, &start); + + if (write_file(data_filename)) + return CHILD_EXIT_WRITE_FILE; + + if (read_file(data_filename, read_times)) + return CHILD_EXIT_READ_FILE; + + clock_gettime(CLOCK_MONOTONIC, &end); + + if (!time_filename) + return CHILD_EXIT_OK; + + elapsed = (end.tv_sec - start.tv_sec) + + (end.tv_nsec - start.tv_nsec) / 1000000000.0; + printf("%.6f\n", elapsed); + + fp = fopen(time_filename, "w"); + if (!fp) + return CHILD_EXIT_TIME_FILE; + fprintf(fp, "%.6f", elapsed); + fclose(fp); + + return CHILD_EXIT_OK; +} + +static int get_time(char *time_filename, double *time) +{ + int ret = -1; + FILE *fp; + char buf[64]; + + fp = fopen(time_filename, "r"); + if (!ASSERT_OK_PTR(fp, "fopen")) + goto out; + + if (!ASSERT_OK_PTR(fgets(buf, sizeof(buf), fp), "fgets")) + goto cleanup; + + if (sscanf(buf, "%lf", time) != 1) { + PRINT_FAIL("sscanf %s", buf); + goto cleanup; + } + + ret = 0; +cleanup: + fclose(fp); +out: + return ret; +} + +static int +run_high_low_workload(double *high_elapsed, double *low_elapsed, int read_times) +{ + char high_data_file[PATH_MAX]; + char low_data_file[PATH_MAX]; + char high_time_file[PATH_MAX]; + char low_time_file[PATH_MAX]; + const char *dir = workload_files_dir(); + pid_t high_pid = -1, low_pid = -1; + pid_t wait_ret; + int fd, status; + int ret = -1; + + snprintf(high_data_file, sizeof(high_data_file), + "%s/memcg_async_high_data_XXXXXX", dir); + snprintf(low_data_file, sizeof(low_data_file), + "%s/memcg_async_low_data_XXXXXX", dir); + snprintf(high_time_file, sizeof(high_time_file), + "%s/memcg_async_high_time_XXXXXX", dir); + snprintf(low_time_file, sizeof(low_time_file), + "%s/memcg_async_low_time_XXXXXX", dir); + + fd = mkstemp(high_data_file); + if (!ASSERT_GE(fd, 0, "mkstemp")) + goto cleanup; + close(fd); + + fd = mkstemp(low_data_file); + if (!ASSERT_GE(fd, 0, "mkstemp")) + goto cleanup; + close(fd); + + fd = mkstemp(high_time_file); + if (!ASSERT_GE(fd, 0, "mkstemp")) + goto cleanup; + close(fd); + + fd = mkstemp(low_time_file); + if (!ASSERT_GE(fd, 0, "mkstemp")) + goto cleanup; + close(fd); + + low_pid = fork(); + if (!ASSERT_GE(low_pid, 0, "fork low")) + goto cleanup; + if (low_pid == 0) + _exit(real_test_child_work(CG_LOW_DIR, low_data_file, + low_time_file, read_times)); + + high_pid = fork(); + if (!ASSERT_GE(high_pid, 0, "fork high")) + goto cleanup; + if (high_pid == 0) + _exit(real_test_child_work(CG_HIGH_DIR, high_data_file, + high_time_file, read_times)); + + wait_ret = waitpid(low_pid, &status, 0); + if (!ASSERT_GT(wait_ret, 0, "low waitpid")) + goto cleanup; + /* + * The child has been reaped and its PID can already be reused, + * so mark it to keep cleanup from signaling an unrelated process. + */ + low_pid = -1; + if (!ASSERT_TRUE(WIFEXITED(status), "low exited")) + goto cleanup; + if (WEXITSTATUS(status) != CHILD_EXIT_OK) { + PRINT_FAIL("low child failed at: %s (exit status %d)", + child_exit_str(WEXITSTATUS(status)), + WEXITSTATUS(status)); + goto cleanup; + } + + wait_ret = waitpid(high_pid, &status, 0); + if (!ASSERT_GT(wait_ret, 0, "high waitpid")) + goto cleanup; + /* Same as above: the reaped PID must not be signaled again. */ + high_pid = -1; + if (!ASSERT_TRUE(WIFEXITED(status), "high exited")) + goto cleanup; + if (WEXITSTATUS(status) != CHILD_EXIT_OK) { + PRINT_FAIL("high child failed at: %s (exit status %d)", + child_exit_str(WEXITSTATUS(status)), + WEXITSTATUS(status)); + goto cleanup; + } + + if (get_time(high_time_file, high_elapsed)) + goto cleanup; + if (get_time(low_time_file, low_elapsed)) + goto cleanup; + + ret = 0; + +cleanup: + /* On failure, make sure no child process is left behind */ + if (ret) { + if (high_pid > 0) { + kill(high_pid, SIGKILL); + (void)waitpid(high_pid, NULL, 0); + } + if (low_pid > 0) { + kill(low_pid, SIGKILL); + (void)waitpid(low_pid, NULL, 0); + } + } + unlink(low_time_file); + unlink(high_time_file); + unlink(low_data_file); + unlink(high_data_file); + return ret; +} + +static int +setup_bpf(u64 high_cgroup_id, u64 low_cgroup_id, + struct memcg_async_reclaim **skel_ptr) +{ + struct memcg_async_reclaim *skel; + struct bpf_args args = { + .high_cgroup_id = high_cgroup_id, + .low_cgroup_id = low_cgroup_id, + .event_delta_threshold = EVENT_DELTA_THRESHOLD, + .check_ns = CHECK_PERIOD_NS, + }; + LIBBPF_OPTS(bpf_test_run_opts, run_opts, + .ctx_in = &args, + .ctx_size_in = sizeof(args)); + int prog_init_fd, err; + + skel = memcg_async_reclaim__open_and_load(); + if (!ASSERT_OK_PTR(skel, "memcg_async_reclaim__open_and_load")) + return -1; + + prog_init_fd = bpf_program__fd(skel->progs.wq_prog_init); + + err = bpf_prog_test_run_opts(prog_init_fd, &run_opts); + if (!ASSERT_OK(err, "bpf_prog_test_run_opts")) + goto error_out; + if (!ASSERT_EQ(run_opts.retval, 0, "prog_init retval")) + goto error_out; + + *skel_ptr = skel; + return 0; + +error_out: + memcg_async_reclaim__destroy(skel); + return -1; +} + +void test_memcg_async_reclaim(void) +{ + u64 high_cgroup_id, low_cgroup_id; + int err; + double high_time = 0.0, low_time = 0.0; + struct memcg_async_reclaim *skel = NULL; + + err = setup_high_low_cgroups(&high_cgroup_id, &low_cgroup_id); + if (!ASSERT_OK(err, "setup_high_low_cgroups reclaim")) + return; + + err = setup_bpf(high_cgroup_id, low_cgroup_id, &skel); + if (!ASSERT_OK(err, "setup_bpf")) + goto out; + + err = run_high_low_workload(&high_time, &low_time, READ_TIMES); + if (!ASSERT_OK(err, "run_high_low_workload reclaim")) + goto out; + + /* + * The timing comparison below alone cannot distinguish a working + * reclaim from a no-op one, so require that the BPF program + * actually reclaimed memory from the low cgroup. + */ + if (!ASSERT_GT(skel->bss->reclaim_calls, 0, "reclaim_calls")) + goto out; + if (!ASSERT_GT(skel->bss->reclaimed_bytes, 0, "reclaimed_bytes")) + goto out; + + if (high_time >= low_time) + PRINT_FAIL("high cgroup not improved: high=%f low=%f", + high_time, low_time); + +out: + if (skel) + memcg_async_reclaim__destroy(skel); + cleanup_cgroup_environment(); +} diff --git a/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c new file mode 100644 index 000000000000..b2ca5185150f --- /dev/null +++ b/tools/testing/selftests/bpf/progs/memcg_async_reclaim.c @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "vmlinux.h" +#include "bpf_experimental.h" +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> +#include <bpf/bpf_core_read.h> + +#define CLOCK_MONOTONIC_ID 1 +#define PAGE_SIZE 4096UL +#define RECLAIM_SIZE (32 * PAGE_SIZE) +#define RECLAIM_MAX_ITER 32 + +struct bpf_args { + u64 high_cgroup_id; + u64 low_cgroup_id; + u64 event_delta_threshold; + u64 check_ns; +}; + +struct cgroup_memcg { + struct cgroup *cgrp; + struct mem_cgroup *memcg; +}; + +static u64 wq_high_cgroup_id; +static u64 wq_low_cgroup_id; + +/* + * Statistics exposed to userspace through .bss, so the test can verify + * that reclaim actually happened instead of relying on timing alone. + */ +u64 reclaim_calls; +u64 reclaimed_bytes; + +static int get_cgroup_memcg_from_id(u64 cgroup_id, struct cgroup_memcg *cm) +{ + cm->cgrp = bpf_cgroup_from_id(cgroup_id); + if (!cm->cgrp) + return -1; + + cm->memcg = bpf_get_mem_cgroup(&cm->cgrp->self); + if (!cm->memcg) { + bpf_cgroup_release(cm->cgrp); + return -1; + } + + return 0; +} + +static void put_cgroup_memcg(struct cgroup_memcg *cm) +{ + bpf_put_mem_cgroup(cm->memcg); + bpf_cgroup_release(cm->cgrp); +} + +static int get_cgroup_event(u64 cgroup_id, u64 *val) +{ + struct cgroup_memcg cm; + + if (get_cgroup_memcg_from_id(cgroup_id, &cm)) + return -1; + bpf_mem_cgroup_flush_stats(cm.memcg); + *val = bpf_mem_cgroup_page_state(cm.memcg, + bpf_core_enum_value(enum node_stat_item, + WORKINGSET_REFAULT_FILE)); + put_cgroup_memcg(&cm); + + return 0; +} + +static bool +should_reclaim_cgroup(u64 cgroup_id, u64 *prev_event, u64 event_delta_threshold) +{ + u64 cur, delta; + + if (get_cgroup_event(cgroup_id, &cur)) + return false; + + delta = cur - *prev_event; + *prev_event = cur; + + return delta >= event_delta_threshold; +} + +static int reclaim_cgroup(u64 cgroup_id) +{ + struct cgroup_memcg cm; + int i; + + if (get_cgroup_memcg_from_id(cgroup_id, &cm)) + return 0; + + reclaim_calls++; + for (i = 0; i < RECLAIM_MAX_ITER; i++) { + u64 nr = bpf_proactive_reclaim(cm.memcg, RECLAIM_SIZE); + + if (!nr) + break; + reclaimed_bytes += nr; + } + + put_cgroup_memcg(&cm); + + return 0; +} + +struct wq_elem { + struct bpf_timer timer; + struct bpf_wq work; + u64 prev_event; + u64 event_delta_threshold; + u64 check_ns; +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct wq_elem); +} wq_map SEC(".maps"); + +static int reclaim_work_fn(void *map, int *key, void *value) +{ + struct wq_elem *elem = value; + + if (should_reclaim_cgroup(wq_high_cgroup_id, &elem->prev_event, + elem->event_delta_threshold)) { + reclaim_cgroup(wq_low_cgroup_id); + bpf_wq_start(&elem->work, 0); + } + + return 0; +} + +static int wq_timer_cb(void *map, int *key, struct wq_elem *elem) +{ + bpf_wq_start(&elem->work, 0); + bpf_timer_start(&elem->timer, elem->check_ns, 0); + + return 0; +} + +SEC("syscall") +int wq_prog_init(struct bpf_args *ctx) +{ + struct wq_elem *elem; + __u32 key = 0; + int ret; + + elem = bpf_map_lookup_elem(&wq_map, &key); + if (!elem) + return -1; + + ret = bpf_wq_init(&elem->work, &wq_map, 0); + if (ret) + return ret; + + ret = bpf_wq_set_callback(&elem->work, reclaim_work_fn, 0); + if (ret) + return ret; + + ret = bpf_timer_init(&elem->timer, &wq_map, CLOCK_MONOTONIC_ID); + if (ret) + return ret; + + ret = bpf_timer_set_callback(&elem->timer, wq_timer_cb); + if (ret) + return ret; + + elem->prev_event = 0; + elem->event_delta_threshold = ctx->event_delta_threshold; + elem->check_ns = ctx->check_ns; + + wq_high_cgroup_id = ctx->high_cgroup_id; + wq_low_cgroup_id = ctx->low_cgroup_id; + + return bpf_timer_start(&elem->timer, elem->check_ns, 0); +} + +char _license[] SEC("license") = "GPL"; -- 2.53.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH bpf-next v6 2/2] selftests/bpf: Add memcg async reclaim test 2026-09-01 2:21 ` [PATCH bpf-next v6 2/2] selftests/bpf: Add memcg async reclaim test Hui Zhu @ 2026-09-01 2:33 ` sashiko-bot 0 siblings, 0 replies; 7+ messages in thread From: sashiko-bot @ 2026-09-01 2:33 UTC (permalink / raw) To: Hui Zhu; +Cc: bpf Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] Insecure temporary file creation using mkstemp() followed by close() and reopening by name. -- commit 6ab0cdc46bdb6e587b82be1eda75b832722b30af Author: Hui Zhu <zhuhui@kylinos.cn> selftests/bpf: Add memcg async reclaim test This commit adds the memcg_async_reclaim selftest. It verifies that BPF-driven async proactive reclaim mitigates refault-induced slowdown under memory pressure using the bpf_wq and bpf_proactive_reclaim() APIs. > diff --git a/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c > new file mode 100644 > index 0000000000000..1270d73c9116e > --- /dev/null > +++ b/tools/testing/selftests/bpf/prog_tests/memcg_async_reclaim.c [ ... ] > @@ -0,0 +1,480 @@ [ ... ] > +static int write_file(const char *filename) > +{ > + int ret = -1; > + size_t written = 0; > + char *buffer; > + FILE *fp; > + > + fp = fopen(filename, "wb"); [Severity: Medium] Does this code reopen a securely created temporary file by name? The file descriptor from mkstemp() was closed earlier, which can create a Time-of-Check to Time-of-Use (TOCTOU) race condition. If an attacker replaces the file with a symlink during this window, it could lead to unintended file truncation or writes. [ ... ] > +static int real_test_child_work(const char *cgroup_path, char *data_filename, > + char *time_filename, int read_times) > +{ [ ... ] > + printf("%.6f\n", elapsed); > + > + fp = fopen(time_filename, "w"); [Severity: Medium] Similarly, could opening this time file by name present a symlink truncation risk? [ ... ] > +static int > +run_high_low_workload(double *high_elapsed, double *low_elapsed, int read_times) > +{ [ ... ] > + snprintf(high_time_file, sizeof(high_time_file), > + "%s/memcg_async_high_time_XXXXXX", dir); > + snprintf(low_time_file, sizeof(low_time_file), > + "%s/memcg_async_low_time_XXXXXX", dir); > + > + fd = mkstemp(high_data_file); > + if (!ASSERT_GE(fd, 0, "mkstemp")) > + goto cleanup; > + close(fd); [Severity: Medium] Closing the file descriptor returned by mkstemp() here discards the security guarantees it provides, opening the race window. Could the test be refactored to pass the open file descriptor instead of relying on the filename string? This same pattern appears to be repeated for all four temporary files created in this function. -- Sashiko AI review · https://sashiko.dev/#/patchset/cover.1788228773.git.zhuhui@kylinos.cn?part=2 ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-02 18:14 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-09-01 2:21 [PATCH bpf-next v6 0/2] bpf: BPF-driven proactive memcg reclaim Hui Zhu 2026-09-01 2:21 ` [PATCH bpf-next v6 1/2] mm/bpf: Add bpf_proactive_reclaim kfunc Hui Zhu 2026-09-01 18:19 ` JP Kobryn 2026-09-02 6:57 ` Hui Zhu 2026-09-02 18:14 ` JP Kobryn 2026-09-01 2:21 ` [PATCH bpf-next v6 2/2] selftests/bpf: Add memcg async reclaim test Hui Zhu 2026-09-01 2:33 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox