* [PATCH v5 1/5] mm/vmscan: introduce folio_activate_locked() helper
2026-07-20 5:07 [PATCH v5 0/5] mm: batch TLB flushing for dirty folios in vmscan Zhang Peng
@ 2026-07-20 5:07 ` Zhang Peng
2026-08-10 8:36 ` Barry Song
2026-07-20 5:07 ` [PATCH v5 2/5] mm/vmscan: extract folio_free() from shrink_folio_list() Zhang Peng
` (3 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Zhang Peng @ 2026-07-20 5:07 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, Qi Zheng
Cc: linux-mm, linux-kernel, Barry Song, Kairui Song, Zhang Peng
The activate_locked label in shrink_folio_list() reclaims swap cache
when needed, marks the folio active, and updates activation statistics.
Extract this block into folio_activate_locked() so it can be reused.
No functional change.
Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
---
mm/vmscan.c | 38 +++++++++++++++++++++++++++-----------
1 file changed, 27 insertions(+), 11 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index ca4533eba701..5ba880dce21e 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1050,6 +1050,32 @@ static bool may_enter_fs(struct folio *folio, gfp_t gfp_mask)
return !data_race(folio_swap_flags(folio) & SWP_FS_OPS);
}
+/*
+ * Prepare a locked folio to be kept active rather than reclaimed.
+ * Reclaims its swap slot if it will not be swapped, then marks it
+ * active and updates activation statistics.
+ */
+static void folio_activate_locked(struct folio *folio,
+ struct reclaim_stat *stat)
+{
+ unsigned int nr_pages = folio_nr_pages(folio);
+
+ VM_WARN_ON_ONCE_FOLIO(!folio_test_locked(folio), folio);
+ VM_WARN_ON_ONCE_FOLIO(folio_test_active(folio), folio);
+
+ /* Not a candidate for swapping, so reclaim swap space. */
+ if (folio_test_swapcache(folio) &&
+ (mem_cgroup_swap_full(folio) || folio_test_mlocked(folio)))
+ folio_free_swap(folio);
+ if (!folio_test_mlocked(folio)) {
+ int type = folio_is_file_lru(folio);
+
+ folio_set_active(folio);
+ stat->nr_activate[type] += nr_pages;
+ count_memcg_folio_events(folio, PGACTIVATE, nr_pages);
+ }
+}
+
/*
* shrink_folio_list() returns the number of reclaimed pages
*/
@@ -1525,17 +1551,7 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
nr_pages = 1;
}
activate_locked:
- /* Not a candidate for swapping, so reclaim swap space. */
- if (folio_test_swapcache(folio) &&
- (mem_cgroup_swap_full(folio) || folio_test_mlocked(folio)))
- folio_free_swap(folio);
- VM_BUG_ON_FOLIO(folio_test_active(folio), folio);
- if (!folio_test_mlocked(folio)) {
- int type = folio_is_file_lru(folio);
- folio_set_active(folio);
- stat->nr_activate[type] += nr_pages;
- count_memcg_folio_events(folio, PGACTIVATE, nr_pages);
- }
+ folio_activate_locked(folio, stat);
keep_locked:
folio_unlock(folio);
keep:
--
2.43.7
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v5 1/5] mm/vmscan: introduce folio_activate_locked() helper
2026-07-20 5:07 ` [PATCH v5 1/5] mm/vmscan: introduce folio_activate_locked() helper Zhang Peng
@ 2026-08-10 8:36 ` Barry Song
0 siblings, 0 replies; 11+ messages in thread
From: Barry Song @ 2026-08-10 8:36 UTC (permalink / raw)
To: Zhang Peng
Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, linux-mm,
linux-kernel, Kairui Song, Zhang Peng
On Mon, Jul 20, 2026 at 1:08 PM Zhang Peng <zippermonkey@icloud.com> wrote:
>
> The activate_locked label in shrink_folio_list() reclaims swap cache
> when needed, marks the folio active, and updates activation statistics.
> Extract this block into folio_activate_locked() so it can be reused.
>
> No functional change.
>
> Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
Thanks,
Reviewed-by: Barry Song <baohua@kernel.org>
[...]
> + * Prepare a locked folio to be kept active rather than reclaimed.
> + * Reclaims its swap slot if it will not be swapped, then marks it
I'm not quite sure whether this should be "if". Because, it seems
to always be true up to this point. BTW, if we really want to use
"if", shouldn't we use it to check whether swap is full?
> + * active and updates activation statistics.
> + */
> +static void folio_activate_locked(struct folio *folio,
> + struct reclaim_stat *stat)
> +{
> + unsigned int nr_pages = folio_nr_pages(folio);
> +
> + VM_WARN_ON_ONCE_FOLIO(!folio_test_locked(folio), folio);
> + VM_WARN_ON_ONCE_FOLIO(folio_test_active(folio), folio);
> +
> + /* Not a candidate for swapping, so reclaim swap space. */
> + if (folio_test_swapcache(folio) &&
> + (mem_cgroup_swap_full(folio) || folio_test_mlocked(folio)))
> + folio_free_swap(folio);
Best Regards
Barry
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v5 2/5] mm/vmscan: extract folio_free() from shrink_folio_list()
2026-07-20 5:07 [PATCH v5 0/5] mm: batch TLB flushing for dirty folios in vmscan Zhang Peng
2026-07-20 5:07 ` [PATCH v5 1/5] mm/vmscan: introduce folio_activate_locked() helper Zhang Peng
@ 2026-07-20 5:07 ` Zhang Peng
2026-08-13 21:40 ` Barry Song
2026-07-20 5:07 ` [PATCH v5 3/5] mm/vmscan: extract pageout_one() " Zhang Peng
` (2 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Zhang Peng @ 2026-07-20 5:07 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, Qi Zheng
Cc: linux-mm, linux-kernel, Barry Song, Kairui Song, Zhang Peng
shrink_folio_list() contains a self-contained folio-freeing section:
buffer release, lazyfree, __remove_mapping, and folio_batch drain.
Extract it into folio_free() to reduce the size of shrink_folio_list()
and make the freeing step independently readable.
No functional change.
Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
---
mm/vmscan.c | 164 +++++++++++++++++++++++++++++++++---------------------------
1 file changed, 89 insertions(+), 75 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 5ba880dce21e..a0807dd01c5a 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1076,6 +1076,93 @@ static void folio_activate_locked(struct folio *folio,
}
}
+static bool folio_try_reclaim_free(struct folio *folio,
+ struct folio_batch *free_folios,
+ struct scan_control *sc, struct reclaim_stat *stat,
+ unsigned int *nr_reclaimed)
+{
+ const unsigned int nr_pages = folio_nr_pages(folio);
+ struct address_space *mapping = folio_mapping(folio);
+
+ /*
+ * If the folio has buffers, try to free the buffer mappings
+ * associated with this folio. If we succeed we try to free
+ * the folio as well.
+ *
+ * We do this even if the folio is dirty.
+ * filemap_release_folio() does not perform I/O, but it is
+ * possible for a folio to have the dirty flag set, but it
+ * is actually clean (all its buffers are clean). This
+ * happens if the buffers were written out directly, with
+ * submit_bh(). ext3 will do this, as well as the blockdev
+ * mapping. filemap_release_folio() will discover that
+ * cleanness and will drop the buffers and mark the folio
+ * clean - it can be freed.
+ *
+ * Rarely, folios can have buffers and no ->mapping. These
+ * are the folios which were not successfully invalidated in
+ * truncate_cleanup_folio(). We try to drop those buffers
+ * here and if that worked, and the folio is no longer
+ * mapped into process address space (refcount == 1) it can
+ * be freed. Otherwise, leave the folio on the LRU so it is
+ * swappable.
+ */
+ if (folio_needs_release(folio)) {
+ if (!filemap_release_folio(folio, sc->gfp_mask)) {
+ folio_activate_locked(folio, stat);
+ return false;
+ }
+
+ if (!mapping && folio_ref_count(folio) == 1) {
+ folio_unlock(folio);
+ if (folio_put_testzero(folio))
+ goto free_it;
+ else {
+ /*
+ * rare race with speculative reference.
+ * the speculative reference will free
+ * this folio shortly, so we may
+ * increment nr_reclaimed here (and
+ * leave it off the LRU).
+ */
+ *nr_reclaimed += nr_pages;
+ return true;
+ }
+ }
+ }
+
+ if (folio_test_lazyfree(folio)) {
+ /* follow __remove_mapping for reference */
+ if (!folio_ref_freeze(folio, 1))
+ return false;
+ /*
+ * The folio has only one reference left, which is
+ * from the isolation. After the caller puts the
+ * folio back on the lru and drops the reference, the
+ * folio will be freed anyway. It doesn't matter
+ * which lru it goes on. So we don't bother checking
+ * the dirty flag here.
+ */
+ count_vm_events(PGLAZYFREED, nr_pages);
+ count_memcg_folio_events(folio, PGLAZYFREED, nr_pages);
+ } else if (!mapping || !__remove_mapping(mapping, folio, true,
+ sc->target_mem_cgroup))
+ return false;
+
+ folio_unlock(folio);
+free_it:
+ VM_WARN_ON_ONCE_FOLIO(folio_ref_count(folio), folio);
+ *nr_reclaimed += nr_pages;
+
+ folio_unqueue_deferred_split(folio);
+ if (folio_batch_add(free_folios, folio) == 0) {
+ mem_cgroup_uncharge_folios(free_folios);
+ try_to_unmap_flush();
+ free_unref_folios(free_folios);
+ }
+ return true;
+}
+
/*
* shrink_folio_list() returns the number of reclaimed pages
*/
@@ -1463,82 +1550,9 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
}
}
- /*
- * If the folio has buffers, try to free the buffer
- * mappings associated with this folio. If we succeed
- * we try to free the folio as well.
- *
- * We do this even if the folio is dirty.
- * filemap_release_folio() does not perform I/O, but it
- * is possible for a folio to have the dirty flag set,
- * but it is actually clean (all its buffers are clean).
- * This happens if the buffers were written out directly,
- * with submit_bh(). ext3 will do this, as well as
- * the blockdev mapping. filemap_release_folio() will
- * discover that cleanness and will drop the buffers
- * and mark the folio clean - it can be freed.
- *
- * Rarely, folios can have buffers and no ->mapping.
- * These are the folios which were not successfully
- * invalidated in truncate_cleanup_folio(). We try to
- * drop those buffers here and if that worked, and the
- * folio is no longer mapped into process address space
- * (refcount == 1) it can be freed. Otherwise, leave
- * the folio on the LRU so it is swappable.
- */
- if (folio_needs_release(folio)) {
- if (!filemap_release_folio(folio, sc->gfp_mask))
- goto activate_locked;
- if (!mapping && folio_ref_count(folio) == 1) {
- folio_unlock(folio);
- if (folio_put_testzero(folio))
- goto free_it;
- else {
- /*
- * rare race with speculative reference.
- * the speculative reference will free
- * this folio shortly, so we may
- * increment nr_reclaimed here (and
- * leave it off the LRU).
- */
- nr_reclaimed += nr_pages;
- continue;
- }
- }
- }
-
- if (folio_test_lazyfree(folio)) {
- /* follow __remove_mapping for reference */
- if (!folio_ref_freeze(folio, 1))
- goto keep_locked;
- /*
- * The folio has only one reference left, which is
- * from the isolation. After the caller puts the
- * folio back on the lru and drops the reference, the
- * folio will be freed anyway. It doesn't matter
- * which lru it goes on. So we don't bother checking
- * the dirty flag here.
- */
- count_vm_events(PGLAZYFREED, nr_pages);
- count_memcg_folio_events(folio, PGLAZYFREED, nr_pages);
- } else if (!mapping || !__remove_mapping(mapping, folio, true,
- sc->target_mem_cgroup))
+ if (!folio_try_reclaim_free(folio, &free_folios, sc, stat,
+ &nr_reclaimed))
goto keep_locked;
-
- folio_unlock(folio);
-free_it:
- /*
- * Folio may get swapped out as a whole, need to account
- * all pages in it.
- */
- nr_reclaimed += nr_pages;
-
- folio_unqueue_deferred_split(folio);
- if (folio_batch_add(&free_folios, folio) == 0) {
- mem_cgroup_uncharge_folios(&free_folios);
- try_to_unmap_flush();
- free_unref_folios(&free_folios);
- }
continue;
activate_locked_split:
--
2.43.7
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v5 2/5] mm/vmscan: extract folio_free() from shrink_folio_list()
2026-07-20 5:07 ` [PATCH v5 2/5] mm/vmscan: extract folio_free() from shrink_folio_list() Zhang Peng
@ 2026-08-13 21:40 ` Barry Song
0 siblings, 0 replies; 11+ messages in thread
From: Barry Song @ 2026-08-13 21:40 UTC (permalink / raw)
To: Zhang Peng
Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, linux-mm,
linux-kernel, Kairui Song, Zhang Peng
On Mon, Jul 20, 2026 at 1:08 PM Zhang Peng <zippermonkey@icloud.com> wrote:
>
> shrink_folio_list() contains a self-contained folio-freeing section:
> buffer release, lazyfree, __remove_mapping, and folio_batch drain.
> Extract it into folio_free() to reduce the size of shrink_folio_list()
> and make the freeing step independently readable.
>
> No functional change.
>
> Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
> ---
> mm/vmscan.c | 164 +++++++++++++++++++++++++++++++++---------------------------
> 1 file changed, 89 insertions(+), 75 deletions(-)
>
> diff --git a/mm/vmscan.c b/mm/vmscan.c
> index 5ba880dce21e..a0807dd01c5a 100644
> --- a/mm/vmscan.c
> +++ b/mm/vmscan.c
> @@ -1076,6 +1076,93 @@ static void folio_activate_locked(struct folio *folio,
> }
> }
>
> +static bool folio_try_reclaim_free(struct folio *folio,
> + struct folio_batch *free_folios,
> + struct scan_control *sc, struct reclaim_stat *stat,
> + unsigned int *nr_reclaimed)
> +{
> + const unsigned int nr_pages = folio_nr_pages(folio);
> + struct address_space *mapping = folio_mapping(folio);
> +
> + /*
> + * If the folio has buffers, try to free the buffer mappings
> + * associated with this folio. If we succeed we try to free
> + * the folio as well.
> + *
> + * We do this even if the folio is dirty.
> + * filemap_release_folio() does not perform I/O, but it is
> + * possible for a folio to have the dirty flag set, but it
> + * is actually clean (all its buffers are clean). This
> + * happens if the buffers were written out directly, with
> + * submit_bh(). ext3 will do this, as well as the blockdev
> + * mapping. filemap_release_folio() will discover that
> + * cleanness and will drop the buffers and mark the folio
> + * clean - it can be freed.
> + *
> + * Rarely, folios can have buffers and no ->mapping. These
> + * are the folios which were not successfully invalidated in
> + * truncate_cleanup_folio(). We try to drop those buffers
> + * here and if that worked, and the folio is no longer
> + * mapped into process address space (refcount == 1) it can
> + * be freed. Otherwise, leave the folio on the LRU so it is
> + * swappable.
> + */
> + if (folio_needs_release(folio)) {
> + if (!filemap_release_folio(folio, sc->gfp_mask)) {
> + folio_activate_locked(folio, stat);
Could we avoid hiding the activate semantics inside
folio_try_reclaim_free()? It makes the logic harder to read and
can be confusing.
Could we pull this out so that the three possible outcomes are
explicit?
1. activate
2. keep
3. free
[...]
> - } else if (!mapping || !__remove_mapping(mapping, folio, true,
> - sc->target_mem_cgroup))
> + if (!folio_try_reclaim_free(folio, &free_folios, sc, stat,
> + &nr_reclaimed))
> goto keep_locked;
I mean, this is confusing because an activated folio ends up in the
"keep" path. Can we make the activation semantics explicit at the
outer level?
Best Regards
Barry
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v5 3/5] mm/vmscan: extract pageout_one() from shrink_folio_list()
2026-07-20 5:07 [PATCH v5 0/5] mm: batch TLB flushing for dirty folios in vmscan Zhang Peng
2026-07-20 5:07 ` [PATCH v5 1/5] mm/vmscan: introduce folio_activate_locked() helper Zhang Peng
2026-07-20 5:07 ` [PATCH v5 2/5] mm/vmscan: extract folio_free() from shrink_folio_list() Zhang Peng
@ 2026-07-20 5:07 ` Zhang Peng
2026-08-13 21:49 ` Barry Song
2026-07-20 5:07 ` [PATCH v5 4/5] mm/vmscan: extract folio unmap logic into folio_try_unmap() Zhang Peng
2026-07-20 5:07 ` [PATCH v5 5/5] mm/vmscan: flush TLB for every 31 folios evictions Zhang Peng
4 siblings, 1 reply; 11+ messages in thread
From: Zhang Peng @ 2026-07-20 5:07 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, Qi Zheng
Cc: linux-mm, linux-kernel, Barry Song, Kairui Song, Zhang Peng
shrink_folio_list() contains a self-contained pageout() dispatch state
machine. Extract it into pageout_one() to reduce the size of
shrink_folio_list() and make the pageout step independently readable.
No functional change.
Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
---
mm/vmscan.c | 104 ++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 62 insertions(+), 42 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index a0807dd01c5a..3e18948e90d1 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1163,8 +1163,65 @@ static bool folio_try_reclaim_free(struct folio *folio,
return true;
}
+static bool folio_try_pageout(struct folio *folio,
+ struct folio_batch *free_folios,
+ struct scan_control *sc, struct reclaim_stat *stat,
+ struct swap_iocb **plug, struct list_head *folio_list,
+ unsigned int *nr_reclaimed)
+{
+ struct address_space *mapping = folio_mapping(folio);
+ unsigned int nr_pages = folio_nr_pages(folio);
+
+ switch (pageout(folio, mapping, plug, folio_list)) {
+ case PAGE_ACTIVATE:
+ /*
+ * If shmem folio is split when writeback to swap, the
+ * tail pages will make their own pass through this
+ * function and be accounted then.
+ */
+ if (nr_pages > 1 && !folio_test_large(folio))
+ sc->nr_scanned -= (nr_pages - 1);
+ folio_activate_locked(folio, stat);
+ folio_unlock(folio);
+ return false;
+ case PAGE_KEEP:
+ folio_unlock(folio);
+ return false;
+ case PAGE_SUCCESS:
+ if (nr_pages > 1 && !folio_test_large(folio)) {
+ sc->nr_scanned -= (nr_pages - 1);
+ nr_pages = 1;
+ }
+ stat->nr_pageout += nr_pages;
+
+ if (folio_test_writeback(folio))
+ return false;
+ if (folio_test_dirty(folio))
+ return false;
+
+ /*
+ * A synchronous write - probably a ramdisk. Go ahead
+ * and try to reclaim the folio.
+ */
+ if (!folio_trylock(folio))
+ return false;
+ if (folio_test_dirty(folio) ||
+ folio_test_writeback(folio)) {
+ folio_unlock(folio);
+ return false;
+ }
+ fallthrough;
+ case PAGE_CLEAN:
+ ; /* try to free the folio below */
+ }
+ if (folio_try_reclaim_free(folio, free_folios, sc, stat, nr_reclaimed))
+ return true;
+ folio_unlock(folio);
+ return false;
+}
+
/*
- * shrink_folio_list() returns the number of reclaimed pages
+ * Reclaimed folios are counted in the return value.
*/
static unsigned int shrink_folio_list(struct list_head *folio_list,
struct pglist_data *pgdat, struct scan_control *sc,
@@ -1501,53 +1558,16 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
goto keep_locked;
if (!sc->may_writepage)
goto keep_locked;
-
/*
* Folio is dirty. Flush the TLB if a writable entry
* potentially exists to avoid CPU writes after I/O
* starts and then write it out here.
*/
try_to_unmap_flush_dirty();
- switch (pageout(folio, mapping, &plug, folio_list)) {
- case PAGE_KEEP:
- goto keep_locked;
- case PAGE_ACTIVATE:
- /*
- * If shmem folio is split when writeback to swap,
- * the tail pages will make their own pass through
- * this function and be accounted then.
- */
- if (nr_pages > 1 && !folio_test_large(folio)) {
- sc->nr_scanned -= (nr_pages - 1);
- nr_pages = 1;
- }
- goto activate_locked;
- case PAGE_SUCCESS:
- if (nr_pages > 1 && !folio_test_large(folio)) {
- sc->nr_scanned -= (nr_pages - 1);
- nr_pages = 1;
- }
- stat->nr_pageout += nr_pages;
-
- if (folio_test_writeback(folio))
- goto keep;
- if (folio_test_dirty(folio))
- goto keep;
-
- /*
- * A synchronous write - probably a ramdisk. Go
- * ahead and try to reclaim the folio.
- */
- if (!folio_trylock(folio))
- goto keep;
- if (folio_test_dirty(folio) ||
- folio_test_writeback(folio))
- goto keep_locked;
- mapping = folio_mapping(folio);
- fallthrough;
- case PAGE_CLEAN:
- ; /* try to free the folio below */
- }
+ if (!folio_try_pageout(folio, &free_folios, sc, stat,
+ &plug, folio_list, &nr_reclaimed))
+ goto keep;
+ continue;
}
if (!folio_try_reclaim_free(folio, &free_folios, sc, stat,
--
2.43.7
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v5 3/5] mm/vmscan: extract pageout_one() from shrink_folio_list()
2026-07-20 5:07 ` [PATCH v5 3/5] mm/vmscan: extract pageout_one() " Zhang Peng
@ 2026-08-13 21:49 ` Barry Song
0 siblings, 0 replies; 11+ messages in thread
From: Barry Song @ 2026-08-13 21:49 UTC (permalink / raw)
To: Zhang Peng
Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, linux-mm,
linux-kernel, Kairui Song, Zhang Peng
On Mon, Jul 20, 2026 at 1:08 PM Zhang Peng <zippermonkey@icloud.com> wrote:
>
> shrink_folio_list() contains a self-contained pageout() dispatch state
> machine. Extract it into pageout_one() to reduce the size of
> shrink_folio_list() and make the pageout step independently readable.
>
> No functional change.
>
> Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
> ---
[...]
> + if (!folio_try_pageout(folio, &free_folios, sc, stat,
> + &plug, folio_list, &nr_reclaimed))
> + goto keep;
Also, this patch looks basically good, just like the previous one.
Could we also avoid hiding the activation semantics in the inner
function? It would be clearer to make the activation semantics
explicit at the outer level, so readers don't have to dig into a
deep internal function to realize that a folio may take the
activation path.
In LRU, we have two distinct possibilities: activate a folio or just
keep it. This is an important semantic distinction in the LRU logic.
Hiding the activation decision so deep in an inner function makes
that semantic much less obvious.
> + continue;
> }
>
> if (!folio_try_reclaim_free(folio, &free_folios, sc, stat,
>
Best Regards
Barry
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v5 4/5] mm/vmscan: extract folio unmap logic into folio_try_unmap()
2026-07-20 5:07 [PATCH v5 0/5] mm: batch TLB flushing for dirty folios in vmscan Zhang Peng
` (2 preceding siblings ...)
2026-07-20 5:07 ` [PATCH v5 3/5] mm/vmscan: extract pageout_one() " Zhang Peng
@ 2026-07-20 5:07 ` Zhang Peng
2026-08-13 21:52 ` Barry Song
2026-07-20 5:07 ` [PATCH v5 5/5] mm/vmscan: flush TLB for every 31 folios evictions Zhang Peng
4 siblings, 1 reply; 11+ messages in thread
From: Zhang Peng @ 2026-07-20 5:07 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, Qi Zheng
Cc: linux-mm, linux-kernel, Barry Song, Kairui Song, Zhang Peng
shrink_folio_list() contains a self-contained block that sets up
TTU flags and calls try_to_unmap(), accounting for failures via
reclaim_stat. Extract it into folio_try_unmap() to reduce the size
of shrink_folio_list() and make the unmap step independently readable.
folio_try_unmap() is only called when the folio is actually mapped;
the !folio_mapped() check stays in the caller, keeping the function's
semantics clear: it tries to unmap a mapped folio and returns whether
the unmap succeeded.
No functional change.
Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
---
mm/vmscan.c | 65 +++++++++++++++++++++++++++++++++----------------------------
1 file changed, 35 insertions(+), 30 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 3e18948e90d1..bb479ead1ee0 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1220,6 +1220,38 @@ static bool folio_try_pageout(struct folio *folio,
return false;
}
+static bool folio_try_unmap(struct folio *folio, struct reclaim_stat *stat,
+ unsigned int nr_pages)
+{
+ enum ttu_flags flags = TTU_BATCH_FLUSH;
+ const bool was_swapbacked = folio_test_swapbacked(folio);
+
+ if (folio_test_pmd_mappable(folio))
+ flags |= TTU_SPLIT_HUGE_PMD;
+ /*
+ * Without TTU_SYNC, try_to_unmap will only begin to hold PTL
+ * from the first present PTE within a large folio. Some
+ * initial PTEs might be skipped due to races with parallel
+ * PTE writes in which PTEs can be cleared temporarily before
+ * being written new present values. This will lead to a large
+ * folio is still mapped while some subpages have been
+ * partially unmapped after try_to_unmap; TTU_SYNC helps
+ * try_to_unmap acquire PTL from the first PTE, eliminating the
+ * influence of temporary PTE values.
+ */
+ if (folio_test_large(folio))
+ flags |= TTU_SYNC;
+
+ try_to_unmap(folio, flags);
+ if (folio_mapped(folio)) {
+ stat->nr_unmap_fail += nr_pages;
+ if (!was_swapbacked && folio_test_swapbacked(folio))
+ stat->nr_lazyfree_fail += nr_pages;
+ return false;
+ }
+ return true;
+}
+
/*
* Reclaimed folios are counted in the return value.
*/
@@ -1494,36 +1526,9 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
* The folio is mapped into the page tables of one or more
* processes. Try to unmap it here.
*/
- if (folio_mapped(folio)) {
- enum ttu_flags flags = TTU_BATCH_FLUSH;
- bool was_swapbacked = folio_test_swapbacked(folio);
-
- if (folio_test_pmd_mappable(folio))
- flags |= TTU_SPLIT_HUGE_PMD;
- /*
- * Without TTU_SYNC, try_to_unmap will only begin to
- * hold PTL from the first present PTE within a large
- * folio. Some initial PTEs might be skipped due to
- * races with parallel PTE writes in which PTEs can be
- * cleared temporarily before being written new present
- * values. This will lead to a large folio is still
- * mapped while some subpages have been partially
- * unmapped after try_to_unmap; TTU_SYNC helps
- * try_to_unmap acquire PTL from the first PTE,
- * eliminating the influence of temporary PTE values.
- */
- if (folio_test_large(folio))
- flags |= TTU_SYNC;
-
- try_to_unmap(folio, flags);
- if (folio_mapped(folio)) {
- stat->nr_unmap_fail += nr_pages;
- if (!was_swapbacked &&
- folio_test_swapbacked(folio))
- stat->nr_lazyfree_fail += nr_pages;
- goto activate_locked;
- }
- }
+ if (folio_mapped(folio) &&
+ !folio_try_unmap(folio, stat, nr_pages))
+ goto activate_locked;
/*
* Folio is unmapped now so it cannot be newly pinned anymore.
--
2.43.7
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v5 4/5] mm/vmscan: extract folio unmap logic into folio_try_unmap()
2026-07-20 5:07 ` [PATCH v5 4/5] mm/vmscan: extract folio unmap logic into folio_try_unmap() Zhang Peng
@ 2026-08-13 21:52 ` Barry Song
0 siblings, 0 replies; 11+ messages in thread
From: Barry Song @ 2026-08-13 21:52 UTC (permalink / raw)
To: Zhang Peng
Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, linux-mm,
linux-kernel, Kairui Song, Zhang Peng
On Mon, Jul 20, 2026 at 1:08 PM Zhang Peng <zippermonkey@icloud.com> wrote:
>
> shrink_folio_list() contains a self-contained block that sets up
> TTU flags and calls try_to_unmap(), accounting for failures via
> reclaim_stat. Extract it into folio_try_unmap() to reduce the size
> of shrink_folio_list() and make the unmap step independently readable.
>
> folio_try_unmap() is only called when the folio is actually mapped;
> the !folio_mapped() check stays in the caller, keeping the function's
> semantics clear: it tries to unmap a mapped folio and returns whether
> the unmap succeeded.
>
> No functional change.
>
> Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
Reviewed-by: Barry Song <baohua@kernel.org>
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v5 5/5] mm/vmscan: flush TLB for every 31 folios evictions
2026-07-20 5:07 [PATCH v5 0/5] mm: batch TLB flushing for dirty folios in vmscan Zhang Peng
` (3 preceding siblings ...)
2026-07-20 5:07 ` [PATCH v5 4/5] mm/vmscan: extract folio unmap logic into folio_try_unmap() Zhang Peng
@ 2026-07-20 5:07 ` Zhang Peng
2026-08-13 21:58 ` Barry Song
4 siblings, 1 reply; 11+ messages in thread
From: Zhang Peng @ 2026-07-20 5:07 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, Qi Zheng
Cc: linux-mm, linux-kernel, Barry Song, Kairui Song, Zhang Peng
Currently we flush TLB for every dirty folio, which is a bottleneck for
systems with many cores as this causes heavy IPI usage.
So instead, batch the folios, and flush once for every 31 folios (one
folio_batch). These folios will be held in a folio_batch with their lock
released, then when the folio_batch is full, do the following steps:
- For each folio: trylock - recheck still evictable (writeback, mapped,
dma_pinned). If no longer evictable, put back via ret_folios.
- Flush TLB once for the whole batch.
- Pageout each survivor via folio_try_pageout().
The recheck step is required because dropping the folio lock between
shrink_folio_list() and pageout_batch() opens a window in which a
parallel swapin (do_swap_page) can fully complete and install a new PTE;
once mapped, a parallel GUP can pin the folio without taking the folio
lock. Folios caught by any of these checks are put back via ret_folios.
Suggested-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
---
mm/vmscan.c | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 84 insertions(+), 8 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index bb479ead1ee0..251535613336 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1220,6 +1220,71 @@ static bool folio_try_pageout(struct folio *folio,
return false;
}
+static void pageout_batch(struct folio_batch *fbatch,
+ struct list_head *ret_folios,
+ struct folio_batch *free_folios,
+ struct scan_control *sc, struct reclaim_stat *stat,
+ struct swap_iocb **plug, struct list_head *folio_list,
+ unsigned int *nr_reclaimed)
+{
+ int i, nr = folio_batch_count(fbatch);
+ int count = 0;
+ struct folio *folios[FOLIO_BATCH_SIZE];
+
+ /*
+ * Collect survivors into a local array: this avoids walking and
+ * mutating @fbatch in the same loop, so its invariant (only
+ * folios[0..nr) are valid) is preserved throughout.
+ */
+ for (i = 0; i < nr; i++) {
+ struct folio *folio = fbatch->folios[i];
+
+ if (!folio_trylock(folio)) {
+ list_add(&folio->lru, ret_folios);
+ continue;
+ }
+
+ VM_WARN_ON_FOLIO(folio_test_lru(folio), folio);
+
+ /*
+ * Recheck what shrink_folio_list() verified before dropping
+ * the folio lock. Between that folio_unlock() and our
+ * folio_trylock() here, a parallel swapin (do_swap_page) can
+ * fully complete -- taking the lock, installing a new PTE,
+ * and releasing the lock -- after which a parallel GUP
+ * (O_DIRECT, vmsplice, RDMA, ...) can pin the folio through
+ * that PTE without taking the folio lock. Each flag catches
+ * one case:
+ * folio_test_writeback -- defensive guard.
+ * folio_mapped -- swapin installed a PTE.
+ * folio_maybe_dma_pinned -- GUP pinned through such a PTE.
+ */
+ if (folio_test_writeback(folio) || folio_mapped(folio) ||
+ folio_maybe_dma_pinned(folio)) {
+ folio_unlock(folio);
+ list_add(&folio->lru, ret_folios);
+ continue;
+ }
+
+ folios[count++] = folio; /* keep lock held */
+ }
+ folio_batch_reinit(fbatch);
+
+ if (!count)
+ return;
+
+ /* One TLB flush for the whole batch */
+ try_to_unmap_flush_dirty();
+
+ for (i = 0; i < count; i++) {
+ struct folio *folio = folios[i];
+
+ if (!folio_try_pageout(folio, free_folios, sc, stat, plug,
+ folio_list, nr_reclaimed))
+ list_add(&folio->lru, ret_folios);
+ }
+}
+
static bool folio_try_unmap(struct folio *folio, struct reclaim_stat *stat,
unsigned int nr_pages)
{
@@ -1261,6 +1326,7 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
struct mem_cgroup *memcg)
{
struct folio_batch free_folios;
+ struct folio_batch flush_folios;
LIST_HEAD(ret_folios);
LIST_HEAD(demote_folios);
unsigned int nr_reclaimed = 0, nr_demoted = 0;
@@ -1269,6 +1335,7 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
struct swap_iocb *plug = NULL;
folio_batch_init(&free_folios);
+ folio_batch_init(&flush_folios);
memset(stat, 0, sizeof(*stat));
cond_resched();
do_demote_pass = can_demote(pgdat->node_id, sc, memcg);
@@ -1564,15 +1631,18 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
if (!sc->may_writepage)
goto keep_locked;
/*
- * Folio is dirty. Flush the TLB if a writable entry
- * potentially exists to avoid CPU writes after I/O
- * starts and then write it out here.
+ * Drop the lock so swap faults finding this folio
+ * via swap cache lookup can make progress; the
+ * recheck that this necessitates is documented in
+ * pageout_batch().
*/
- try_to_unmap_flush_dirty();
- if (!folio_try_pageout(folio, &free_folios, sc, stat,
- &plug, folio_list, &nr_reclaimed))
- goto keep;
- continue;
+ folio_unlock(folio);
+ if (!folio_batch_add(&flush_folios, folio))
+ pageout_batch(&flush_folios,
+ &ret_folios, &free_folios,
+ sc, stat, &plug,
+ folio_list, &nr_reclaimed);
+ goto next;
}
if (!folio_try_reclaim_free(folio, &free_folios, sc, stat,
@@ -1597,6 +1667,12 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
list_add(&folio->lru, &ret_folios);
VM_BUG_ON_FOLIO(folio_test_lru(folio) ||
folio_test_unevictable(folio), folio);
+next:
+ continue;
+ }
+ if (folio_batch_count(&flush_folios)) {
+ pageout_batch(&flush_folios, &ret_folios, &free_folios, sc,
+ stat, &plug, folio_list, &nr_reclaimed);
}
/* 'folio_list' is always empty here */
--
2.43.7
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v5 5/5] mm/vmscan: flush TLB for every 31 folios evictions
2026-07-20 5:07 ` [PATCH v5 5/5] mm/vmscan: flush TLB for every 31 folios evictions Zhang Peng
@ 2026-08-13 21:58 ` Barry Song
0 siblings, 0 replies; 11+ messages in thread
From: Barry Song @ 2026-08-13 21:58 UTC (permalink / raw)
To: Zhang Peng
Cc: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Johannes Weiner, Shakeel Butt, Axel Rasmussen, Yuanchu Xie,
Wei Xu, Michal Hocko, Qi Zheng, Liam R. Howlett, linux-mm,
linux-kernel, Kairui Song, Zhang Peng
On Mon, Jul 20, 2026 at 1:08 PM Zhang Peng <zippermonkey@icloud.com> wrote:
>
> Currently we flush TLB for every dirty folio, which is a bottleneck for
> systems with many cores as this causes heavy IPI usage.
>
> So instead, batch the folios, and flush once for every 31 folios (one
> folio_batch). These folios will be held in a folio_batch with their lock
> released, then when the folio_batch is full, do the following steps:
>
> - For each folio: trylock - recheck still evictable (writeback, mapped,
> dma_pinned). If no longer evictable, put back via ret_folios.
> - Flush TLB once for the whole batch.
> - Pageout each survivor via folio_try_pageout().
>
> The recheck step is required because dropping the folio lock between
> shrink_folio_list() and pageout_batch() opens a window in which a
> parallel swapin (do_swap_page) can fully complete and install a new PTE;
> once mapped, a parallel GUP can pin the folio without taking the folio
> lock. Folios caught by any of these checks are put back via ret_folios.
>
> Suggested-by: Kairui Song <kasong@tencent.com>
> Signed-off-by: Zhang Peng <bruzzhang@tencent.com>
Yes, I think batching the dirty flush is a great idea. I can clearly
see that IPIs for dirty flushes (smp_call) on x86 take up a
significant part of the flame graph when building the kernel in a
memcg, so I think this is something we should pursue.
Could we revisit this patch after we clean up the previous ones?
Best Regards
Barry
^ permalink raw reply [flat|nested] 11+ messages in thread