[PATCH v6 2/3] mm: swap: drop dropbehind swap cache folios on writeback completion
From: Alexandre Ghiti
Date: Mon Sep 21 2026 - 11:28:45 EST
A PG_dropbehind folio is dropped from its cache once writeback completes
rather than left for reclaim to find later; this is implemented for file
folios in folio_end_dropbehind(). Extend it to swap cache folios.
The drop blocks on the folio lock, so it cannot run in interrupt
context. Set BIO_COMPLETE_IN_TASK on the write, as the file dropbehind
paths do, and drop the folio directly from folio_end_writeback().
It has to block rather than trylock: the folio is off the LRU, so
skipping it would leave it in the swap cache with nothing able to
reclaim it, and it cannot be put back while another thread holds its
lock.
Suggested-by: Yosry Ahmed <yosry@xxxxxxxxxx>
Suggested-by: Johannes Weiner <hannes@xxxxxxxxxxx>
Suggested-by: Nhat Pham <nphamcs@xxxxxxxxx>
Reviewed-by: Nhat Pham <nphamcs@xxxxxxxxx>
Reviewed-by: Kunwu Chan <kunwu.chan@xxxxxxxxx>
Reviewed-by: Barry Song <baohua@xxxxxxxxxx>
Signed-off-by: Alexandre Ghiti <alex@xxxxxxxx>
---
include/linux/swap.h | 6 ++++++
mm/filemap.c | 19 +++++++++++++++++
mm/page_io.c | 9 ++++++++
mm/swap_state.c | 42 +++++++++++++++++++++++++++++++++++++
mm/vmscan.c | 49 +++++++++++++++++++++++++++++++++++---------
5 files changed, 115 insertions(+), 10 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 5658a1634b85..538b723a276f 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -318,6 +318,9 @@ static inline bool lru_cache_disabled(void)
extern unsigned long shrink_all_memory(unsigned long nr_pages);
long remove_mapping(struct address_space *mapping, struct folio *folio);
+long remove_mapping_set_shadow(struct address_space *mapping,
+ struct folio *folio,
+ struct mem_cgroup *target_memcg);
#if defined(CONFIG_SYSFS) && defined(CONFIG_NUMA)
extern int reclaim_register_node(struct node *node);
@@ -402,6 +405,8 @@ void swap_put_entries_direct(swp_entry_t entry, int nr);
*/
bool folio_free_swap(struct folio *folio);
+void swap_writeback_dropbehind_folio(struct folio *folio);
+
/* Allocate / free (hibernation) exclusive entries */
swp_entry_t swap_alloc_hibernation_slot(int type);
void swap_free_hibernation_slot(swp_entry_t entry);
@@ -412,6 +417,7 @@ static inline void put_swap_device(struct swap_info_struct *si)
}
#else /* CONFIG_SWAP */
+static inline void swap_writeback_dropbehind_folio(struct folio *folio) {}
static inline struct swap_info_struct *get_swap_device(swp_entry_t entry)
{
return NULL;
diff --git a/mm/filemap.c b/mm/filemap.c
index 6afec636881f..e1f1bbe943ce 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1686,6 +1686,8 @@ EXPORT_SYMBOL_GPL(folio_end_writeback_no_dropbehind);
*/
void folio_end_writeback(struct folio *folio)
{
+ bool swap_dropbehind;
+
VM_BUG_ON_FOLIO(!folio_test_writeback(folio), folio);
/*
@@ -1695,7 +1697,24 @@ void folio_end_writeback(struct folio *folio)
* reused before the folio_wake_bit().
*/
folio_get(folio);
+
+ /*
+ * Sample this before folio_end_writeback_no_dropbehind() clears
+ * PG_writeback: until then a racing swapin cannot remove the folio from
+ * the swap cache. Afterwards it can, and the drop below then finds a
+ * non-swapcache folio and puts it back on the LRU instead. The
+ * reference taken above keeps the folio alive across that window.
+ */
+ swap_dropbehind = folio_test_swapcache(folio) &&
+ folio_test_dropbehind(folio);
+
folio_end_writeback_no_dropbehind(folio);
+
+ if (swap_dropbehind) {
+ swap_writeback_dropbehind_folio(folio);
+ return;
+ }
+
folio_end_dropbehind(folio);
folio_put(folio);
}
diff --git a/mm/page_io.c b/mm/page_io.c
index 88962571cb93..52eae99de6e3 100644
--- a/mm/page_io.c
+++ b/mm/page_io.c
@@ -602,6 +602,15 @@ static void swap_bdev_submit_write(struct swap_io_ctx *ctx)
submit_bio_wait(bio);
end_swap_bio_write(bio);
} else {
+ int p;
+
+ for (p = 0; p < sio->nr_bvecs; p++) {
+ if (folio_test_dropbehind(bvec_folio(&sio->bvecs[p]))) {
+ bio_set_flag(bio, BIO_COMPLETE_IN_TASK);
+ break;
+ }
+ }
+
bio->bi_end_io = end_swap_bio_write;
submit_bio(bio);
}
diff --git a/mm/swap_state.c b/mm/swap_state.c
index e5b7fa468ade..b1656e2d5288 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -543,6 +543,48 @@ struct folio *__swap_cache_alloc_folio(swp_entry_t targ_entry, gfp_t gfp,
return ret;
}
+/**
+ * swap_writeback_dropbehind_folio - drop a dropbehind swap cache folio
+ * @folio: the off-LRU folio whose writeback has completed
+ *
+ * Context: task context, with the reference taken by folio_end_writeback()
+ * donated to us.
+ */
+void swap_writeback_dropbehind_folio(struct folio *folio)
+{
+ struct mem_cgroup *memcg;
+
+ folio_lock(folio);
+
+ /* The folio was allocated off the LRU and nothing re-adds it here. */
+ VM_WARN_ON_ONCE_FOLIO(folio_test_lru(folio), folio);
+
+ rcu_read_lock();
+ memcg = folio_memcg(folio);
+ if (!mem_cgroup_tryget(memcg))
+ memcg = NULL;
+ rcu_read_unlock();
+
+ /*
+ * Gate remove_mapping_set_shadow() on folio_test_swapcache(): a racing
+ * swapin may have freed the swap slot (folio_free_swap()) and dropped the
+ * folio from the cache, and it must not run on a non-swapcache folio (it
+ * would trip __remove_mapping()'s mapping == folio_mapping() check).
+ */
+ if (!folio_test_swapcache(folio) || folio_test_writeback(folio) ||
+ !remove_mapping_set_shadow(swap_address_space(folio->swap), folio,
+ memcg)) {
+ /* Raced: the folio is now owned by the swapin; put it back. */
+ folio_clear_dropbehind(folio);
+ folio_add_lru(folio);
+ }
+
+ mem_cgroup_put(memcg);
+
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
/*
* If we are the only user, then try to free up the swap cache.
*
diff --git a/mm/vmscan.c b/mm/vmscan.c
index f11491ee9ed5..a02f942418d3 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -783,6 +783,22 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
return 0;
}
+static long __remove_mapping_unfreeze(struct address_space *mapping,
+ struct folio *folio, bool reclaimed,
+ struct mem_cgroup *target_memcg)
+{
+ if (__remove_mapping(mapping, folio, reclaimed, target_memcg)) {
+ /*
+ * Unfreezing the refcount with 1 effectively
+ * drops the pagecache ref for us without requiring another
+ * atomic operation.
+ */
+ folio_ref_unfreeze(folio, 1);
+ return folio_nr_pages(folio);
+ }
+ return 0;
+}
+
/**
* remove_mapping() - Attempt to remove a folio from its mapping.
* @mapping: The address space.
@@ -797,16 +813,29 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
*/
long remove_mapping(struct address_space *mapping, struct folio *folio)
{
- if (__remove_mapping(mapping, folio, false, NULL)) {
- /*
- * Unfreezing the refcount with 1 effectively
- * drops the pagecache ref for us without requiring another
- * atomic operation.
- */
- folio_ref_unfreeze(folio, 1);
- return folio_nr_pages(folio);
- }
- return 0;
+ return __remove_mapping_unfreeze(mapping, folio, false, NULL);
+}
+
+/**
+ * remove_mapping_set_shadow() - Remove a folio and record an eviction shadow.
+ * @mapping: The address space.
+ * @folio: The folio to remove.
+ * @target_memcg: The memcg to charge the eviction shadow to; the caller must
+ * keep it alive across the call.
+ *
+ * Like remove_mapping(), but stores a workingset eviction shadow the way page
+ * reclaim does, so that a later refault can be detected and the folio
+ * re-activated.
+ * Return: The number of pages removed from the mapping. 0 if the folio
+ * could not be removed.
+ * Context: The caller should have a single refcount on the folio and
+ * hold its lock.
+ */
+long remove_mapping_set_shadow(struct address_space *mapping,
+ struct folio *folio,
+ struct mem_cgroup *target_memcg)
+{
+ return __remove_mapping_unfreeze(mapping, folio, true, target_memcg);
}
/**
--
2.53.0-Meta