Skip to contentSkip to conversation

mm/vmscan: fix swappiness=max and clean up per-node proactive reclaim

PATCH In review Ridong 3 participants 16 messages Jul 23 – Aug 14

Subsystems (3)
lore
Share & tools
lore

Ridong

author
#0 lore raw

v3 cover mm/vmscan: fix swappiness=max and clean up per-node proactive reclaim

From: Ridong Chen <[email protected]>

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

---
v2 -> v3:
- Update comments and commit message.

v1 -> v2:
- Collected review tags for patches 1-3
- Added patch 4 to fix the same issue in the MGLRU path.

Ridong Chen (4):
mm/vmscan: fix anon-only reclaim evicting file pages when
swappiness=max
mm: vmscan: propagate real error code from per-node proactive reclaim
mm: vmscan: drop unused gfp_mask parameter from __node_reclaim() mm/mglru: fix anon-only reclaim evicting file pages when
swappiness=max

 mm/vmscan.c | 54 +++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 40 insertions(+), 14 deletions(-)
Signature · 1 line
2.34.1
author
#1 lore raw

PATCH v3 1/4 mm/vmscan: fix anon-only reclaim evicting file pages when swappiness=max

From: Ridong Chen <[email protected]>

As Qi mentioned [1], when swappiness=max (SWAPPINESS_ANON_ONLY) is set, the reclaim logic is expected to reclaim anonymous pages exclusively. However, due to the current ordering of checks in get_scan_count(), file pages may still be evicted if can_reclaim_anon_pages() returns false, which contradicts the semantics of SWAPPINESS_ANON_ONLY.

Reproducer in a cgroup holding 64M of file cache, with no swap configured:

Before (file cache is wrongly evicted):

    # cat memory.stat
    anon 196608
    file 67178496
    pgscan_proactive 0
    # echo "64M swappiness=max" > memory.reclaim
    # cat memory.stat
    anon 208896
    file 4096                 <- page cache evicted
    pgsteal_proactive 16400
    pgscan_proactive 16400

After (file cache is left intact):

    # cat memory.stat
    anon 200704
    file 67178496
    pgscan_proactive 0
    # echo "64M swappiness=max" > memory.reclaim
    -bash: echo: write error: Resource temporarily unavailable
    # cat memory.stat
    anon 208896
    file 67178496             <- page cache untouched
    pgsteal_proactive 0
    pgscan_proactive 0

Fix this by bailing out early when SWAPPINESS_ANON_ONLY is set and no anonymous pages are reclaimable, before falling back to file reclaim.

[1] https://lore.kernel.org/cgroups/[email protected]/

Fixes 68a1436bde00 ("mm: add swappiness=max arg to memory.reclaim for only anon reclaim")
---
 mm/vmscan.c | 24 +++++++++++++++++-------
 1 file changed, 17 insertions(+), 7 deletions(-)
mm/vmscan.c +17 -7
index 35c3bb15ae96..2c689682b952 100644
@@ -2501,6 +2501,23 @@ static void get_scan_count(struct lruvec *lruvec, struct scan_control *sc,
 	enum scan_balance scan_balance;
 	enum lru_list lru;
 
+	/*
+	 * Proactive reclaim initiated by userspace for anonymous memory only.
+	 * SWAPPINESS_ANON_ONLY is set only on the proactive reclaim path, so
+	 * warn if it shows up elsewhere. When anon cannot be reclaimed (e.g.
+	 * no swap), bail out instead of falling back to evicting file pages,
+	 * which would violate the anon-only semantics.
+	 */
+	if (swappiness == SWAPPINESS_ANON_ONLY) {
+		WARN_ON_ONCE(!sc->proactive);
+		if (!can_reclaim_anon_pages(memcg, pgdat->node_id, sc)) {
+			memset(nr, 0, sizeof(*nr) * NR_LRU_LISTS);
+			return;
+		}
+		scan_balance = SCAN_ANON;
+		goto out;
+	}
+
 	/* If we have no swap space, do not bother scanning anon folios. */
 	if (!sc->may_swap || !can_reclaim_anon_pages(memcg, pgdat->node_id, sc)) {
 		scan_balance = SCAN_FILE;
@@ -2519,13 +2536,6 @@ static void get_scan_count(struct lruvec *lruvec, struct scan_control *sc,
 		goto out;
 	}
 
-	/* Proactive reclaim initiated by userspace for anonymous memory only */
-	if (swappiness == SWAPPINESS_ANON_ONLY) {
-		WARN_ON_ONCE(!sc->proactive);
-		scan_balance = SCAN_ANON;
-		goto out;
-	}
-
 	/*
 	 * Do not apply any pressure balancing cleverness when the
 	 * system is close to OOM, scan both anon and file equally
Signature · 1 line
2.34.1
author
#2 lore raw

PATCH v3 2/4 mm: vmscan: propagate real error code from per-node proactive reclaim

In reply to Ridong

From: Ridong Chen <[email protected]>

It has been observed that per-node proactive reclaim always returns -EAGAIN when any error occurs. As discussed in the mailing list [1], the interface should distinguish between cases where no reclaimable memory is left and where another entity is concurrently using the same interface. Propagate the real error code, consistent with how memcg proactive reclaim handles errors.

[1] https://lore.kernel.org/all/20250717235604.2atyx2aobwowpge3@offworld/T/#m3514718be82a31b05726a49da9b61fbfc69a589e

Fixes b980077899ea ("mm: introduce per-node proactive reclaim interface")
---
 mm/vmscan.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
mm/vmscan.c +1 -1
index 2c689682b952..4b62d6304c49 100644
@@ -8023,7 +8023,7 @@ static ssize_t reclaim_store(struct device *dev,
 	int ret, nid = dev->id;
 
 	ret = user_proactive_reclaim((char *)buf, NULL, NODE_DATA(nid));
-	return ret ? -EAGAIN : count;
+	return ret ? ret : count;
 }
 
 static DEVICE_ATTR_WO(reclaim);
Signature · 1 line
2.34.1
author
#3 lore raw

PATCH v3 3/4 mm: vmscan: drop unused gfp_mask parameter from __node_reclaim()

In reply to Ridong

From: Ridong Chen <[email protected]>

Commit 57972c78e678 ("mm/vmscan: make __node_reclaim() more generic") moved the scan_control construction out to the callers and passed the struct in by pointer. After that change every use of the gfp mask inside __node_reclaim() goes through sc->gfp_mask, leaving the gfp_mask parameter unused. Just remove the dead parameter and update the callers accordingly. No functional change.

---
 mm/vmscan.c | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)
mm/vmscan.c +4 -5
index 4b62d6304c49..859655fcf60d 100644
@@ -7748,7 +7748,7 @@ static unsigned long node_pagecache_reclaimable(struct pglist_data *pgdat)
 /*
  * Try to free up some pages from this node through reclaim.
  */
-static unsigned long __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask,
+static unsigned long __node_reclaim(struct pglist_data *pgdat,
 				    unsigned long nr_pages,
 				    struct scan_control *sc)
 {
@@ -7840,7 +7840,7 @@ int node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
 	if (test_and_set_bit_lock(PGDAT_RECLAIM_LOCKED, &pgdat->flags))
 		return NODE_RECLAIM_NOSCAN;
 
-	ret = __node_reclaim(pgdat, gfp_mask, nr_pages, &sc) >= nr_pages;
+	ret = __node_reclaim(pgdat, nr_pages, &sc) >= nr_pages;
 	clear_bit_unlock(PGDAT_RECLAIM_LOCKED, &pgdat->flags);
 
 	if (ret)
@@ -7853,7 +7853,7 @@ int node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask, unsigned int order)
 
 #else
 
-static unsigned long __node_reclaim(struct pglist_data *pgdat, gfp_t gfp_mask,
+static unsigned long __node_reclaim(struct pglist_data *pgdat,
 				    unsigned long nr_pages,
 				    struct scan_control *sc)
 {
@@ -7955,8 +7955,7 @@ int user_proactive_reclaim(char *buf,
 						  &pgdat->flags))
 				return -EBUSY;
 
-			reclaimed = __node_reclaim(pgdat, gfp_mask,
-						   batch_size, &sc);
+			reclaimed = __node_reclaim(pgdat, batch_size, &sc);
 			clear_bit_unlock(PGDAT_RECLAIM_LOCKED, &pgdat->flags);
 		}
 
Signature · 1 line
2.34.1
author
#4 lore raw

PATCH v3 4/4 mm/mglru: fix anon-only reclaim evicting file pages when swappiness=max

In reply to Ridong

From: Ridong Chen <[email protected]>

The previous patch fixed this issue for the traditional LRU. The same problem exists in MGLRU [1]: when swappiness=max (SWAPPINESS_ANON_ONLY) is set, reclaim is expected to evict anonymous pages exclusively, but file pages can still be reclaimed when anonymous pages cannot be reclaimed (e.g. no swap and no demotion target).

With SWAPPINESS_ANON_ONLY, get_type_to_scan() always returns LRU_GEN_ANON and for_each_evictable_type() only iterates the anon type. But if get_nr_to_scan() does not bail out, evict_folios() still runs and isolate_folios() falls back to scanning file pages in the !scanned case:

shrink_one
try_to_shrink_lruvec
get_swappiness // returns SWAPPINESS_ANON_ONLY for swappiness=max
get_nr_to_scan
evict_folios
isolate_folios // falls back to file type when !scanned

This wrongly evicts file pages, violating the anon-only semantics.

Fix it the same way as the traditional LRU: keep returning SWAPPINESS_ANON_ONLY in get_swappiness(), and return 0 from get_nr_to_scan() when SWAPPINESS_ANON_ONLY is set but anon pages cannot be reclaimed. This protects file pages from being reclaimed, and as a side effect avoids the useless scan work.

The test result:
Before fix:

  # cat /sys/kernel/mm/lru_gen/enabled
  0x0007
  # cat memory.stat
  anon 204800
  file 67108864
  ...
  pgsteal_proactive 0
  pgscan_proactive 0
  # echo "64M swappiness=max" > memory.reclaim
  # cat memory.stat
  anon 208896
  file 0
  ...
  pgsteal_proactive 16384
  pgscan_proactive 16384

After fix:

  # cat memory.stat
  anon 188416
  file 67215360
  kernel 1970176
  ...
  pgsteal_proactive 0
  pgscan_proactive 0
  # echo "64M swappiness=max" > memory.reclaim
  -bash: echo: write error: Resource temporarily unavailable
  # cat memory.stat
  anon 204800
  file 67215360
  ...
  pgsteal_proactive 0
  pgscan_proactive 0

[1] https://sashiko.dev/#/patchset/[email protected]

Fixes 68a1436bde00 ("mm: add swappiness=max arg to memory.reclaim for only anon reclaim")
---
 mm/vmscan.c | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)
mm/vmscan.c +18 -1
index 859655fcf60d..20cb93fd3da8 100644
@@ -2708,6 +2708,10 @@ static int get_swappiness(struct lruvec *lruvec, struct scan_control *sc)
 {
 	struct mem_cgroup *memcg = lruvec_memcg(lruvec);
 	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
+	int swappiness = sc_swappiness(sc, memcg);
+
+	if (swappiness == SWAPPINESS_ANON_ONLY)
+		return swappiness;
 
 	if (!sc->may_swap)
 		return 0;
@@ -2716,7 +2720,7 @@ static int get_swappiness(struct lruvec *lruvec, struct scan_control *sc)
 	    mem_cgroup_get_nr_swap_pages(memcg) < MIN_LRU_BATCH)
 		return 0;
 
-	return sc_swappiness(sc, memcg);
+	return swappiness;
 }
 
 static int get_nr_gens(struct lruvec *lruvec, int type)
@@ -4921,6 +4925,19 @@ static long get_nr_to_scan(struct lruvec *lruvec, struct scan_control *sc,
 			   struct mem_cgroup *memcg, int swappiness)
 {
 	unsigned long nr_to_scan, evictable;
+	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
+	/*
+	 * Proactive reclaim initiated by userspace for anonymous memory only.
+	 * SWAPPINESS_ANON_ONLY is set only on the proactive reclaim path, so
+	 * warn if it shows up elsewhere. When anon cannot be reclaimed (e.g.
+	 * no swap), return 0 instead of falling through and evicting file
+	 * pages, which would violate the anon-only semantics.
+	 */
+	if (swappiness == SWAPPINESS_ANON_ONLY &&
+	    !can_reclaim_anon_pages(memcg, pgdat->node_id, sc)) {
+		WARN_ON_ONCE(!sc->proactive);
+		return 0;
+	}
 
 	evictable = lruvec_evictable_size(lruvec, swappiness);
 
Signature · 1 line
2.34.1
reviewer
#5 lore raw

Re: [PATCH v3 4/4] mm/mglru: fix anon-only reclaim evicting file pages when swappiness=max

Quoted 14 lines · Ridong

From: Ridong Chen <[email protected]>

The previous patch fixed this issue for the traditional LRU. The same problem exists in MGLRU [1]: when swappiness=max (SWAPPINESS_ANON_ONLY) is set, reclaim is expected to evict anonymous pages exclusively, but file pages can still be reclaimed when anonymous pages cannot be reclaimed (e.g. no swap and no demotion target).

With SWAPPINESS_ANON_ONLY, get_type_to_scan() always returns LRU_GEN_ANON and for_each_evictable_type() only iterates the anon type. But if get_nr_to_scan() does not bail out, evict_folios() still runs and isolate_folios() falls back to scanning file pages in the !scanned case:

Quoted 7 lines

shrink_one
try_to_shrink_lruvec
get_swappiness // returns SWAPPINESS_ANON_ONLY for swappiness=max
get_nr_to_scan
evict_folios
isolate_folios // falls back to file type when !scanned

I am not convinced this is correct. Although we do have type = !type, when swappiness == 201, for_each_evictable_type() stops iterating after trying a single type, so there is no opportunity to fall back.

#define for_each_evictable_type(type, swappiness)                       \
        for ((type) = min_type(swappiness); (type) <=

max_type(swappiness); (type)++)

So I'm really curious what's happening here.

static int isolate_folios(unsigned long nr_to_scan, struct lruvec *lruvec,
struct scan_control *sc, int swappiness, struct list_head *list, int *isolated, int *isolate_type, int *isolate_scanned)

{
        int i;
        int total_scanned = 0;
        int type = get_type_to_scan(lruvec, swappiness);

        for_each_evictable_type(i, swappiness) {
                int scanned;
                int tier = get_tier_idx(lruvec, type);

                scanned = scan_folios(nr_to_scan, lruvec, sc,
                                      type, tier, list, isolated);

                ...
                if (!scanned)
                        type = !type;
        }

        return total_scanned;
}

since both min and max types are anon:
#define min_type(swappiness) (!(swappiness))
#define max_type(swappiness) ((swappiness) < SWAPPINESS_ANON_ONLY)

I guess the real problem only occurs when may_swap is false?

Thanks
barry

#6 lore raw

Re: [PATCH v3 4/4] mm/mglru: fix anon-only reclaim evicting file pages when swappiness=max

Quoted 23 lines · Barry Song
Quoted 14 lines · Ridong

From: Ridong Chen <[email protected]>

The previous patch fixed this issue for the traditional LRU. The same problem exists in MGLRU [1]: when swappiness=max (SWAPPINESS_ANON_ONLY) is set, reclaim is expected to evict anonymous pages exclusively, but file pages can still be reclaimed when anonymous pages cannot be reclaimed (e.g. no swap and no demotion target).

With SWAPPINESS_ANON_ONLY, get_type_to_scan() always returns LRU_GEN_ANON and for_each_evictable_type() only iterates the anon type. But if get_nr_to_scan() does not bail out, evict_folios() still runs and isolate_folios() falls back to scanning file pages in the !scanned case:

Quoted 7 lines

shrink_one
try_to_shrink_lruvec
get_swappiness // returns SWAPPINESS_ANON_ONLY for swappiness=max
get_nr_to_scan
evict_folios
isolate_folios // falls back to file type when !scanned

Quoted 8 lines

I am not convinced this is correct. Although we do have type = !type, when swappiness == 201, for_each_evictable_type() stops iterating after trying a single type, so there is no opportunity to fall back.

#define for_each_evictable_type(type, swappiness)                       \
         for ((type) = min_type(swappiness); (type) <=

max_type(swappiness); (type)++)

You're right. In my initial version, the intention was to bail out early and avoid unnecessary work.

I recall that during your review of Kairui's series, you sent a patch that used a fallback mechanism with type = !type. I was thinking that this could become an issue if we call isolate_folios() when swappiness is set to max.

Quoted 26 lines

So I'm really curious what's happening here.

static int isolate_folios(unsigned long nr_to_scan, struct lruvec *lruvec,
struct scan_control *sc, int swappiness, struct list_head *list, int *isolated, int *isolate_type, int *isolate_scanned)

{
         int i;
         int total_scanned = 0;
         int type = get_type_to_scan(lruvec, swappiness);

         for_each_evictable_type(i, swappiness) {
                 int scanned;
                 int tier = get_tier_idx(lruvec, type);

                 scanned = scan_folios(nr_to_scan, lruvec, sc,
                                       type, tier, list, isolated);

                 ...
                 if (!scanned)
                         type = !type;
         }

         return total_scanned;
}
Quoted 5 lines

since both min and max types are anon:
#define min_type(swappiness) (!(swappiness))
#define max_type(swappiness) ((swappiness) < SWAPPINESS_ANON_ONLY)

I guess the real problem only occurs when may_swap is false?

I added some trace logs to verify this behavior. If we don't make get_nr_to_scan() return 0 when swappiness=max, it won't fall back to scanning file pages, but it will still perform useless scanning that yields no reclaim.

Log:

  # echo "64M swappiness=max" > memory.reclaim
  [   78.914972] vmscan: in isolate_folios swappness 201
  [   78.916841] vmscan: call scan_folios type 0
  [   78.918311] vmscan: fall back 1  <-  can't call scan_folios, loop ends.
  [   78.919479] vmscan: out isolate_folios total_scanned 0
  [...]
  # cat memory.stat | grep proactive
  pgdemote_proactive 0
  pgsteal_proactive 0
  pgscan_proactive 1808

I will update the commit message to clarify that get_nr_to_scan() returns 0 to avoid this useless work.

Signature · 2 lines
Best regards
Ridong
maintainer
#7 lore raw
In reply to Ridong
Quoted 7 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Quoted 7 lines

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

	https://sashiko.dev/#/patchset/[email protected]
#8 lore raw
Quoted 13 lines · Andrew Morton
Quoted 11 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Quoted 8 lines
Quoted 3 lines

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

	https://sashiko.dev/#/patchset/[email protected]

Thanks. I will update patch 4/4 as suggested.

Signature · 2 lines
Best regards
Ridong
#9 lore raw
In reply to Andrew Morton
Quoted 13 lines · Andrew Morton
Quoted 11 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Quoted 8 lines
Quoted 3 lines

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

	https://sashiko.dev/#/patchset/[email protected]

Hi all, sashiko review:

This is a pre-existing issue, but I noticed that per-node proactive reclaim fails to reclaim memory if the node's unmapped file or slab pages are below minimum thresholds, completely ignoring anonymous memory.
In mm/vmscan.c:__node_reclaim():
if (node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) >
pgdat->min_slab_pages) {
Does this condition incorrectly block proactive reclaim of anonymous memory by applying legacy zone-reclaim thresholds?
It seems to bypass shrink_node() entirely if file/slab pages are low, even when sc->proactive is set, which could falsely return -EAGAIN to userspace for anon-heavy workloads.

Should we add:

if (sc->proactive ||

    node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
    node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {
Signature · 2 lines
Best regards
Ridong
reviewer
#10 lore raw
Quoted 41 lines · Ridong Chen
Quoted 13 lines · Andrew Morton
Quoted 11 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Quoted 8 lines
Quoted 3 lines

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

      https://sashiko.dev/#/patchset/[email protected]

Hi all, sashiko review:

This is a pre-existing issue, but I noticed that per-node proactive reclaim fails to reclaim memory if the node's unmapped file or slab pages are below minimum thresholds, completely ignoring anonymous memory.
In mm/vmscan.c:__node_reclaim():
if (node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) >
pgdat->min_slab_pages) {
Does this condition incorrectly block proactive reclaim of anonymous memory by applying legacy zone-reclaim thresholds?
It seems to bypass shrink_node() entirely if file/slab pages are low, even when sc->proactive is set, which could falsely return -EAGAIN to userspace for anon-heavy workloads.

Quoted 5 lines

Should we add:

if (sc->proactive ||

     node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
     node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

Nop.
I assume reclaiming file cache and slab becomes problematic when their amounts are already very limited, so we should still honor these two checks.

Maybe we could relax them only when swappiness == 201 (SWAPPINESS_ANON_ONLY)?

BTW, for global proactive reclaim, when setting swappiness to 201, does it prevent slab shrinking? If not, it seems problematic when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) < pgdat->min_slab_pages.

Your recent patchset prevents all file reclamation when swappiness is set to 201, so we only need to check whether there could be a slab issue before allowing shrink_node() to continue in this case.

Best Regards
Barry

#11 lore raw
Quoted 42 lines · Barry Song
Quoted 41 lines · Ridong Chen
Quoted 13 lines · Andrew Morton
Quoted 11 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Quoted 8 lines
Quoted 3 lines

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

       https://sashiko.dev/#/patchset/[email protected]

Hi all, sashiko review:

This is a pre-existing issue, but I noticed that per-node proactive reclaim fails to reclaim memory if the node's unmapped file or slab pages are below minimum thresholds, completely ignoring anonymous memory.
In mm/vmscan.c:__node_reclaim():
if (node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) >
pgdat->min_slab_pages) {
Does this condition incorrectly block proactive reclaim of anonymous memory by applying legacy zone-reclaim thresholds?
It seems to bypass shrink_node() entirely if file/slab pages are low, even when sc->proactive is set, which could falsely return -EAGAIN to userspace for anon-heavy workloads.

Quoted 6 lines
Quoted 5 lines

Should we add:

if (sc->proactive ||

      node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
      node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

Hi Barry, sorry for the late reply.

Quoted 5 lines

Nop.
I assume reclaiming file cache and slab becomes problematic when their amounts are already very limited, so we should still honor these two checks.

Quoted 8 lines

Maybe we could relax them only when swappiness == 201 (SWAPPINESS_ANON_ONLY)?

BTW, for global proactive reclaim, when setting swappiness to 201, does it prevent slab shrinking? If not, it seems problematic when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) < pgdat->min_slab_pages.

in __node_reclaim, we will shrink the node when node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages, even if node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages. This means slab shrinking can still occur even when below the limit (since shrink_slab is called unconditionally after shrink_lruvec). This is not an issue only for global proactive reclaim.

Quoted 4 lines

Your recent patchset prevents all file reclamation when swappiness is set to 201, so we only need to check whether there could be a slab issue before allowing shrink_node() to continue in this case.

So can we add just like?

if ((sc->proactive && node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) ||

	node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
	node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {
Signature · 2 lines
Best regards
Ridong
reviewer
#12 lore raw
Quoted 82 lines · Ridong Chen
Quoted 42 lines · Barry Song
Quoted 41 lines · Ridong Chen
Quoted 13 lines · Andrew Morton
Quoted 11 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Quoted 8 lines
Quoted 3 lines

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

       https://sashiko.dev/#/patchset/[email protected]

Hi all, sashiko review:

This is a pre-existing issue, but I noticed that per-node proactive reclaim fails to reclaim memory if the node's unmapped file or slab pages are below minimum thresholds, completely ignoring anonymous memory.
In mm/vmscan.c:__node_reclaim():
if (node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) >
pgdat->min_slab_pages) {
Does this condition incorrectly block proactive reclaim of anonymous memory by applying legacy zone-reclaim thresholds?
It seems to bypass shrink_node() entirely if file/slab pages are low, even when sc->proactive is set, which could falsely return -EAGAIN to userspace for anon-heavy workloads.

Quoted 6 lines
Quoted 5 lines

Should we add:

if (sc->proactive ||

      node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
      node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

Hi Barry, sorry for the late reply.

Quoted 5 lines

Nop.
I assume reclaiming file cache and slab becomes problematic when their amounts are already very limited, so we should still honor these two checks.

Quoted 8 lines

Maybe we could relax them only when swappiness == 201 (SWAPPINESS_ANON_ONLY)?

BTW, for global proactive reclaim, when setting swappiness to 201, does it prevent slab shrinking? If not, it seems problematic when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) < pgdat->min_slab_pages.

in __node_reclaim, we will shrink the node when node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages, even if node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages. This means slab shrinking can still occur even when below the limit (since shrink_slab is called unconditionally after shrink_lruvec). This is not an issue only for global proactive reclaim.

Quoted 4 lines

Your recent patchset prevents all file reclamation when swappiness is set to 201, so we only need to check whether there could be a slab issue before allowing shrink_node() to continue in this case.

Quoted 6 lines

So can we add just like?

if ((sc->proactive && node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) ||

        node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
        node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

I feel both pgdat->min_unmapped_pages and pgdat->min_slab_pages are quite broken in mainline.

For example, even when the page cache is below min_unmapped_pages, it may still be reclaimed. Similarly, slab may still be reclaimed even when it is below min_slab_pages.

Also, when both the page cache and slab are below their respective thresholds, node_reclaim() may reclaim nothing even if we have plenty of anon folios available.

        if (node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages &&
            node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <=
            pgdat->min_slab_pages)
                return 0;

For example, if slab > min_slab_pages but the page cache is below min_unmapped_pages, we still reclaim file pages, even though the comment says we should not.

So we are not going to introduce another broken mechanism. Maybe we should start by fixing the existing broken protection against reclaiming slab and page cache?

Best Regards
Barry

#13 lore raw
Quoted 106 lines · Barry Song
Quoted 82 lines · Ridong Chen
Quoted 42 lines · Barry Song
Quoted 41 lines · Ridong Chen
Quoted 13 lines · Andrew Morton
Quoted 11 lines · On Thu, 23 Jul 2026 12:57:14 +0800 Ridong <[email protected]> wrote:

Fixes and one cleanup.

Patch 1 fixes "swappiness=max": the anon-only test in get_scan_count() sat after the "cannot reclaim anon" check, so when no anon was reclaimable the request fell back to SCAN_FILE and evicted page cache instead.

Patch 2 fixes reclaim_store() collapsing every error into -EAGAIN, so callers can no longer tell an invalid argument from a busy interface; propagate the real error code, matching the memcg path.

Quoted 8 lines
Quoted 3 lines

Patch 3 drops the now-unused gfp_mask parameter from __node_reclaim().

Patch 4 fixes the same "swappiness=max" issue for MGLRU.

Thanks. AI review might have found several pre-existing issues which might interest the maintainers. Also a suggestion against your [4/4]:

        https://sashiko.dev/#/patchset/[email protected]

Hi all, sashiko review:

This is a pre-existing issue, but I noticed that per-node proactive reclaim fails to reclaim memory if the node's unmapped file or slab pages are below minimum thresholds, completely ignoring anonymous memory.
In mm/vmscan.c:__node_reclaim():
if (node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) >
pgdat->min_slab_pages) {
Does this condition incorrectly block proactive reclaim of anonymous memory by applying legacy zone-reclaim thresholds?
It seems to bypass shrink_node() entirely if file/slab pages are low, even when sc->proactive is set, which could falsely return -EAGAIN to userspace for anon-heavy workloads.

Quoted 6 lines
Quoted 5 lines

Should we add:

if (sc->proactive ||

       node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
       node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

Hi Barry, sorry for the late reply.

Quoted 5 lines

Nop.
I assume reclaiming file cache and slab becomes problematic when their amounts are already very limited, so we should still honor these two checks.

Quoted 8 lines

Maybe we could relax them only when swappiness == 201 (SWAPPINESS_ANON_ONLY)?

BTW, for global proactive reclaim, when setting swappiness to 201, does it prevent slab shrinking? If not, it seems problematic when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) < pgdat->min_slab_pages.

in __node_reclaim, we will shrink the node when node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages, even if node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages. This means slab shrinking can still occur even when below the limit (since shrink_slab is called unconditionally after shrink_lruvec). This is not an issue only for global proactive reclaim.

Quoted 4 lines

Your recent patchset prevents all file reclamation when swappiness is set to 201, so we only need to check whether there could be a slab issue before allowing shrink_node() to continue in this case.

Quoted 6 lines

So can we add just like?

if ((sc->proactive && node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) ||

         node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
         node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

I feel both pgdat->min_unmapped_pages and pgdat->min_slab_pages are quite broken in mainline.

For example, even when the page cache is below min_unmapped_pages, it may still be reclaimed. Similarly, slab may still be reclaimed even when it is below min_slab_pages.

Also, when both the page cache and slab are below their respective thresholds, node_reclaim() may reclaim nothing even if we have plenty of anon folios available.

         if (node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages &&
             node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <=
             pgdat->min_slab_pages)
                 return 0;
Quoted 8 lines

For example, if slab > min_slab_pages but the page cache is below min_unmapped_pages, we still reclaim file pages, even though the comment says we should not.

So we are not going to introduce another broken mechanism. Maybe we should start by fixing the existing broken protection against reclaiming slab and page cache?

For example, Maybe we can skip shrink_slab when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages?

And similarly, in get_scan_count, we could avoid reclaiming file page cache if node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages.

Signature · 2 lines
Best regards
Ridong
reviewer
#14 lore raw
Quoted 14 lines · Ridong Chen
Quoted 10 lines · Barry Song
Quoted 9 lines · Ridong Chen
Quoted 5 lines · Barry Song
Quoted 4 lines · Ridong Chen

On 7/24/2026 8:18 AM, Andrew Morton wrote:

[...]

Quoted 32 lines
Quoted 24 lines
Quoted 7 lines

So can we add just like?

if ((sc->proactive && node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) ||

         node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
         node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

I feel both pgdat->min_unmapped_pages and pgdat->min_slab_pages are quite broken in mainline.

For example, even when the page cache is below min_unmapped_pages, it may still be reclaimed. Similarly, slab may still be reclaimed even when it is below min_slab_pages.

Also, when both the page cache and slab are below their respective thresholds, node_reclaim() may reclaim nothing even if we have plenty of anon folios available.

         if (node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages &&
             node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <=
             pgdat->min_slab_pages)
                 return 0;
Quoted 8 lines

For example, if slab > min_slab_pages but the page cache is below min_unmapped_pages, we still reclaim file pages, even though the comment says we should not.

So we are not going to introduce another broken mechanism. Maybe we should start by fixing the existing broken protection against reclaiming slab and page cache?

Quoted 5 lines

For example, Maybe we can skip shrink_slab when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages?

And similarly, in get_scan_count, we could avoid reclaiming file page cache if node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages.

Yes, probably something along the lines of what you're saying. Also, get_type_to_scan() in MGLRU.

It's a bit surprising that nobody has ever reported a bug related to this. It may have caused performance problems that we simply haven't realized were related to this broken protection against the min_unmapped_pages and min_slab_pages thresholds.

I guess this fix could improve performance as well. For example, when the inode and dentry caches are already extremely low, we shouldn't keep struggling to reclaim them and waste time there. People have already complained that shrink_slab() is slow and suggested having an asynchronous shrinker for it.
Maybe it's related to something going wrong here.

Thanks
Barry

#15 lore raw
Quoted 62 lines · Barry Song
Quoted 14 lines · Ridong Chen
Quoted 10 lines · Barry Song
Quoted 9 lines · Ridong Chen
Quoted 5 lines · Barry Song
Quoted 4 lines · Ridong Chen

On 7/24/2026 8:18 AM, Andrew Morton wrote:

[...]

Quoted 32 lines
Quoted 24 lines
Quoted 7 lines

So can we add just like?

if ((sc->proactive && node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) ||

          node_pagecache_reclaimable(pgdat) > pgdat->min_unmapped_pages ||
          node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) > pgdat->min_slab_pages) {

I feel both pgdat->min_unmapped_pages and pgdat->min_slab_pages are quite broken in mainline.

For example, even when the page cache is below min_unmapped_pages, it may still be reclaimed. Similarly, slab may still be reclaimed even when it is below min_slab_pages.

Also, when both the page cache and slab are below their respective thresholds, node_reclaim() may reclaim nothing even if we have plenty of anon folios available.

          if (node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages &&
              node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <=
              pgdat->min_slab_pages)
                  return 0;
Quoted 8 lines

For example, if slab > min_slab_pages but the page cache is below min_unmapped_pages, we still reclaim file pages, even though the comment says we should not.

So we are not going to introduce another broken mechanism. Maybe we should start by fixing the existing broken protection against reclaiming slab and page cache?

Quoted 5 lines

For example, Maybe we can skip shrink_slab when node_page_state_pages(pgdat, NR_SLAB_RECLAIMABLE_B) <= pgdat->min_slab_pages?

And similarly, in get_scan_count, we could avoid reclaiming file page cache if node_pagecache_reclaimable(pgdat) <= pgdat->min_unmapped_pages.

Yes, probably something along the lines of what you're saying. Also, get_type_to_scan() in MGLRU.

It's a bit surprising that nobody has ever reported a bug related to this. It may have caused performance problems that we simply haven't realized were related to this broken protection against the min_unmapped_pages and min_slab_pages thresholds.

Quoted 7 lines

I guess this fix could improve performance as well. For example, when the inode and dentry caches are already extremely low, we shouldn't keep struggling to reclaim them and waste time there. People have already complained that shrink_slab() is slow and suggested having an asynchronous shrinker for it.
Maybe it's related to something going wrong here.

Yeah, good point. I'll give it a try and run some tests to see if it actually helps.

Signature · 2 lines
Best regards
Ridong