[PATCH v5 2/3] mm: swap: drop dropbehind swap cache folios on writeback completion
From: Alexandre Ghiti
Date: Fri Sep 11 2026 - 08:24:59 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 takes the folio and swap cluster locks and may sleep, 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().
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>
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