[RFC PATCH v4 16/27] mm: NP_OPS_RECLAIM - private node reclaim participation

Gregory Price
2026-02-22T08:49:59Z

Private node services that drive kswapd via watermark_boost need
control over the reclaim policy. There are three problems:

1) Boosted reclaim suppresses may_swap and may_writepage. When
demotion is not possible, swap is the only evict path, so kswapd
cannot make progress and pages are stranded.

2) __setup_per_zone_wmarks() unconditionally zeros watermark_boost,
killing the service's pressure signal.

3) Not all private nodes want reclaim to touch their pages.

Add a reclaim_policy callback to struct node_private_ops and a
struct node_reclaim_policy with:

- active: set by the helper when a callback was invoked
- may_swap: allow swap writeback during boosted reclaim
- may_writepage: allow writepage during boosted reclaim
- managed_watermarks: service owns watermark_boost lifecycle

We do not allow disabling swap/writepage, as core MM may have
explicitly enabled them on a non-boosted pass.

We only allow enablign swap/writepage, so that the supression during
a boost can be overridden. This allows a device to force evictions
even when the system otherwise would not percieve pressure.

This is important for a service like compressed RAM, as device capacity
may differ from reported capacity, and device may want to relieve real
pressure (poor compression ratio) as opposed to percieved pressure
(i.e. how many pages are in use).

Add zone_reclaim_allowed() to filter private nodes that have not
opted into reclaim.

Regular nodes fall through to cpuset_zone_allowed() unchanged.

Signed-off-by: Gregory Price <gourry@gourry.net>
---
include/linux/node_private.h | 28 ++++++++++++++++++++++++++++
mm/internal.h | 36 ++++++++++++++++++++++++++++++++++++
mm/page_alloc.c | 11 ++++++++++-
mm/vmscan.c | 25 +++++++++++++++++++++++--
4 files changed, 97 insertions(+), 3 deletions(-)

diff --git a/include/linux/node_private.h b/include/linux/node_private.h
index 27d6e5d84e61..34be52383255 100644
--- a/include/linux/node_private.h
+++ b/include/linux/node_private.h
@@ -14,6 +14,24 @@ struct page;
struct vm_area_struct;
struct vm_fault;

+/**
+ * struct node_reclaim_policy - Reclaim policy overrides for private nodes
+ * @active: set by node_private_reclaim_policy() when a callback was invoked
+ * @may_swap: allow swap writeback during boosted reclaim
+ * @may_writepage: allow writepage during boosted reclaim
+ * @managed_watermarks: service owns watermark_boost lifecycle; kswapd must
+ * not clear it after boosted reclaim
+ *
+ * Passed to the reclaim_policy callback so each private node service can
+ * inject its own reclaim policy before kswapd runs boosted reclaim.
+ */
+struct node_reclaim_policy {
+ bool active;
+ bool may_swap;
+ bool may_writepage;
+ bool managed_watermarks;
+};
+
/**
* struct node_private_ops - Callbacks for private node services
*
@@ -88,6 +106,13 @@ struct vm_fault;
*
* Returns: vm_fault_t result (0, VM_FAULT_RETRY, etc.)
*
+ * @reclaim_policy: Configure reclaim policy for boosted reclaim.
+ * [called hodling rcu_read_lock, MUST NOT sleep]
+ * Called by kswapd before boosted reclaim to let the service override
+ * may_swap / may_writepage. If provided, the service also owns the
+ * watermark_boost lifecycle (kswapd will not clear it).
+ * If NULL, normal boost policy applies.
+ *
* @flags: Operation exclusion flags (NP_OPS_* constants).
*
*/
@@ -101,6 +126,7 @@ struct node_private_ops {
void (*folio_migrate)(struct folio *src, struct folio *dst);
vm_fault_t (*handle_fault)(struct folio *folio, struct vm_fault *vmf,
enum pgtable_level level);
+ void (*reclaim_policy)(int nid, struct node_reclaim_policy *policy);
unsigned long flags;
};

@@ -112,6 +138,8 @@ struct node_private_ops {
#define NP_OPS_DEMOTION BIT(2)
/* Prevent mprotect/NUMA from upgrading PTEs to writable on this node */
#define NP_OPS_PROTECT_WRITE BIT(3)
+/* Kernel reclaim (kswapd, direct reclaim, OOM) operates on this node */
+#define NP_OPS_RECLAIM BIT(4)

/**
* struct node_private - Per-node container for N_MEMORY_PRIVATE nodes
diff --git a/mm/internal.h b/mm/internal.h
index ae4ff86e8dc6..db32cb2d7a29 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1572,6 +1572,42 @@ static inline void folio_managed_migrate_notify(struct folio *src,
ops->folio_migrate(src, dst);
}

+/**
+ * node_private_reclaim_policy - invoke the service's reclaim policy callback
+ * @nid: NUMA node id
+ * @policy: reclaim policy struct to fill in
+ *
+ * Called by kswapd before boosted reclaim. Zeroes @policy, then if the
+ * private node service provides a reclaim_policy callback, invokes it
+ * and sets policy->active to true.
+ */
+#ifdef CONFIG_NUMA
+static inline void node_private_reclaim_policy(int nid,
+ struct node_reclaim_policy *policy)
+{
+ struct node_private *np;
+
+ memset(policy, 0, sizeof(*policy));
+
+ if (!node_state(nid, N_MEMORY_PRIVATE))
+ return;
+
+ rcu_read_lock();
+ np = rcu_dereference(NODE_DATA(nid)->node_private);
+ if (np && np->ops && np->ops->reclaim_policy) {
+ np->ops->reclaim_policy(nid, policy);
+ policy->active = true;
+ }
+ rcu_read_unlock();
+}
+#else
+static inline void node_private_reclaim_policy(int nid,
+ struct node_reclaim_policy *policy)
+{
+ memset(policy, 0, sizeof(*policy));
+}
+#endif
+
struct vm_struct *__get_vm_area_node(unsigned long size,
unsigned long align, unsigned long shift,
unsigned long vm_flags, unsigned long start,
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index e272dfdc6b00..9692048ab5fb 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -55,6 +55,7 @@
#include <linux/delayacct.h>
#include <linux/cacheinfo.h>
#include <linux/pgalloc_tag.h>
+#include <linux/node_private.h>
#include <asm/div64.h>
#include "internal.h"
#include "shuffle.h"
@@ -6437,6 +6438,8 @@ static void __setup_per_zone_wmarks(void)
unsigned long lowmem_pages = 0;
struct zone *zone;
unsigned long flags;
+ struct node_reclaim_policy rp;
+ int prev_nid = NUMA_NO_NODE;

/* Calculate total number of !ZONE_HIGHMEM and !ZONE_MOVABLE pages */
for_each_zone(zone) {
@@ -6446,6 +6449,7 @@ static void __setup_per_zone_wmarks(void)

for_each_zone(zone) {
u64 tmp;
+ int nid = zone_to_nid(zone);

spin_lock_irqsave(&zone->lock, flags);
tmp = (u64)pages_min * zone_managed_pages(zone);
@@ -6482,7 +6486,12 @@ static void __setup_per_zone_wmarks(void)
mult_frac(zone_managed_pages(zone),
watermark_scale_factor, 10000));

- zone->watermark_boost = 0;
+ if (nid != prev_nid) {
+ node_private_reclaim_policy(nid, &rp);
+ prev_nid = nid;
+ }
+ if (!rp.managed_watermarks)
+ zone->watermark_boost = 0;
zone->_watermark[WMARK_LOW] = min_wmark_pages(zone) + tmp;
zone->_watermark[WMARK_HIGH] = low_wmark_pages(zone) + tmp;
zone->_watermark[WMARK_PROMO] = high_wmark_pages(zone) + tmp;
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 0f534428ea88..07de666c1276 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -73,6 +73,13 @@
#define CREATE_TRACE_POINTS
#include <trace/events/vmscan.h>

+static inline bool zone_reclaim_allowed(struct zone *zone, gfp_t gfp_mask)
+{
+ if (node_state(zone_to_nid(zone), N_MEMORY_PRIVATE))
+ return zone_private_flags(zone, NP_OPS_RECLAIM);
+ return cpuset_zone_allowed(zone, gfp_mask);
+}
+
struct scan_control {
/* How many pages shrink_list() should reclaim */
unsigned long nr_to_reclaim;
@@ -6274,7 +6281,7 @@ static void shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
* to global LRU.
*/
if (!cgroup_reclaim(sc)) {
- if (!cpuset_zone_allowed(zone,
+ if (!zone_reclaim_allowed(zone,
GFP_KERNEL | __GFP_HARDWALL))
continue;

@@ -6992,6 +6999,7 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
unsigned long zone_boosts[MAX_NR_ZONES] = { 0, };
bool boosted;
struct zone *zone;
+ struct node_reclaim_policy policy;
struct scan_control sc = {
.gfp_mask = GFP_KERNEL,
.order = order,
@@ -7016,6 +7024,9 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
}
boosted = nr_boost_reclaim;

+ /* Query/cache private node reclaim policy once per balance() */
+ node_private_reclaim_policy(pgdat->node_id, &policy);
+
restart:
set_reclaim_active(pgdat, highest_zoneidx);
sc.priority = DEF_PRIORITY;
@@ -7083,6 +7094,12 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
sc.may_writepage = !laptop_mode && !nr_boost_reclaim;
sc.may_swap = !nr_boost_reclaim;

+ /* Private nodes may enable swap/writepage when using boost */
+ if (policy.active) {
+ sc.may_swap |= policy.may_swap;
+ sc.may_writepage |= policy.may_writepage;
+ }
+
/*
* Do some background aging, to give pages a chance to be
* referenced before reclaiming. All pages are rotated
@@ -7176,6 +7193,10 @@ static int balance_pgdat(pg_data_t *pgdat, int order, int highest_zoneidx)
if (!zone_boosts[i])
continue;

+ /* Some private nodes may own the\ boost lifecycle */
+ if (policy.managed_watermarks)
+ continue;
+
/* Increments are under the zone lock */
zone = pgdat->node_zones + i;
spin_lock_irqsave(&zone->lock, flags);
@@ -7406,7 +7427,7 @@ void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
if (!managed_zone(zone))
return;

- if (!cpuset_zone_allowed(zone, gfp_flags))
+ if (!zone_reclaim_allowed(zone, gfp_flags))
return;

pgdat = zone->zone_pgdat;
--
2.53.0