[RFC PATCH v3 12/18] mm/swap: bound synchronous discard during allocation

From: Lian Wang

Date: Tue Oct 27 2026 - 12:28:15 EST


From: Kairui Song <kasong@xxxxxxxxxxx>

The per-device cluster cache lets the allocator select a device before
handling that device's pending discards. Issue discard on the selected
device when its free-cluster list is drained instead of waiting for a
global fallback scan.

Do not drain a discard list that freeing paths can replenish. Factor
out swap_discard_one_cluster(), isolate one cluster under si->lock,
retain CLUSTER_FLAG_DISCARD while I/O is in flight, and return it to the
free list after completion. The background worker keeps processing the
list.

Each call to the device-cluster allocator handles at most one cluster
synchronously before retrying allocation. A folio allocation can try
multiple devices, so this is a per-device-attempt budget. It bounds the
amount of discard work, not device I/O completion latency.

Link: https://lore.kernel.org/all/CAMgjq7CsYhEjvtN85XGkrONYAJxve7gG593TFeOGV-oax++kWA@xxxxxxxxxxxxxx/
Signed-off-by: Kairui Song <kasong@xxxxxxxxxxx>
Co-developed-by: Lian Wang <lianux.mm@xxxxxxxxx>
Signed-off-by: Lian Wang <lianux.mm@xxxxxxxxx>
---
mm/swapfile.c | 149 ++++++++++++++++++++++----------------------------
1 file changed, 64 insertions(+), 85 deletions(-)

diff --git a/mm/swapfile.c b/mm/swapfile.c
index 0a16343a5b03..5c26682c0bab 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -585,6 +585,26 @@ static long swap_usage_in_pages(struct swap_info_struct *si)
return atomic_long_read(&si->inuse_pages) & SWAP_USAGE_COUNTER_MASK;
}

+/*
+ * Serialize the allocation on single CPU or globally to avoid
+ * fragmentation and make the workflow easier to follow.
+ */
+static void swap_alloc_lock_device(struct swap_info_struct *si)
+{
+ if (si->flags & SWP_SOLIDSTATE)
+ local_lock(&si->percpu_cluster->lock);
+ else
+ spin_lock(&si->global_cluster->lock);
+}
+
+static void swap_alloc_unlock_device(struct swap_info_struct *si)
+{
+ if (si->flags & SWP_SOLIDSTATE)
+ local_unlock(&si->percpu_cluster->lock);
+ else
+ spin_unlock(&si->global_cluster->lock);
+}
+
/* Reclaim the swap entry anyway if possible */
#define TTRS_ANYWAY 0x1
/*
@@ -982,10 +1002,7 @@ swap_cluster_populate(struct swap_info_struct *si,
* which could give allocator hooks and failure cleanup reserve access.
*/
spin_unlock(&ci->lock);
- if (si->flags & SWP_SOLIDSTATE)
- local_unlock(&si->percpu_cluster->lock);
- else
- spin_unlock(&si->global_cluster->lock);
+ swap_alloc_unlock_device(si);

gfp = (GFP_KERNEL | __GFP_HIGH | __GFP_NOMEMALLOC) &
~__GFP_DIRECT_RECLAIM;
@@ -999,10 +1016,7 @@ swap_cluster_populate(struct swap_info_struct *si,
* could happen with ignoring the percpu cluster is fragmentation,
* which is acceptable since this fallback and race is rare.
*/
- if (si->flags & SWP_SOLIDSTATE)
- local_lock(&si->percpu_cluster->lock);
- else
- spin_lock(&si->global_cluster->lock);
+ swap_alloc_lock_device(si);
spin_lock(&ci->lock);

if (ret) {
@@ -1092,44 +1106,40 @@ static struct swap_cluster_info *isolate_lock_cluster(
}

/*
- * Doing discard actually. After a cluster discard is finished, the cluster
- * will be added to free cluster list. Discard cluster is a bit special as
- * they don't participate in allocation or reclaim, so clusters marked as
- * CLUSTER_FLAG_DISCARD must remain off-list or on discard list.
+ * Discard one cluster. After the discard is finished, the cluster will be
+ * added to the free cluster list. Discard clusters are special because they
+ * don't participate in allocation or reclaim, so CLUSTER_FLAG_DISCARD must
+ * remain set while a cluster is either queued or being discarded.
*/
-static bool swap_do_scheduled_discard(struct swap_info_struct *si)
+static bool swap_discard_one_cluster(struct swap_info_struct *si)
{
struct swap_cluster_info *ci;
- bool ret = false;
unsigned int idx;

spin_lock(&si->lock);
- while (!list_empty(&si->discard_clusters)) {
- ci = list_first_entry(&si->discard_clusters, struct swap_cluster_info, list);
- /*
- * Delete the cluster from list to prepare for discard, but keep
- * the CLUSTER_FLAG_DISCARD flag, there could be percpu_cluster
- * pointing to it, or ran into by relocate_cluster.
- */
- list_del(&ci->list);
- idx = cluster_index(si, ci);
+ if (list_empty(&si->discard_clusters)) {
spin_unlock(&si->lock);
- discard_swap_cluster(si, idx * SWAPFILE_CLUSTER,
- SWAPFILE_CLUSTER);
-
- spin_lock(&ci->lock);
- /*
- * Discard is done, clear its flags as it's off-list, then
- * return the cluster to allocation list.
- */
- ci->flags = CLUSTER_FLAG_NONE;
- __free_cluster(si, ci);
- spin_unlock(&ci->lock);
- ret = true;
- spin_lock(&si->lock);
+ return false;
}
+
+ ci = list_first_entry(&si->discard_clusters,
+ struct swap_cluster_info, list);
+ /*
+ * Delete the cluster from the list, but keep CLUSTER_FLAG_DISCARD
+ * set while the discard is in flight. A percpu cluster may still
+ * point to it, or relocate_cluster() may encounter it.
+ */
+ list_del(&ci->list);
+ idx = cluster_index(si, ci);
spin_unlock(&si->lock);
- return ret;
+
+ discard_swap_cluster(si, idx * SWAPFILE_CLUSTER, SWAPFILE_CLUSTER);
+
+ spin_lock(&ci->lock);
+ ci->flags = CLUSTER_FLAG_NONE;
+ __free_cluster(si, ci);
+ spin_unlock(&ci->lock);
+ return true;
}

static void swap_discard_work(struct work_struct *work)
@@ -1138,7 +1148,8 @@ static void swap_discard_work(struct work_struct *work)

si = container_of(work, struct swap_info_struct, discard_work);

- swap_do_scheduled_discard(si);
+ while (swap_discard_one_cluster(si))
+ cond_resched();
}

static void swap_users_ref_free(struct percpu_ref *ref)
@@ -1536,6 +1547,7 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
struct swap_cluster_info *ci;
unsigned int order = likely(folio) ? folio_order(folio) : 0;
unsigned int offset = SWAP_ENTRY_INVALID, found = SWAP_ENTRY_INVALID;
+ bool discarded = false;

/*
* Swapfile is not block device so unable
@@ -1544,15 +1556,12 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
if (order && !(si->flags & SWP_BLKDEV))
return 0;

- if (si->flags & SWP_SOLIDSTATE) {
- /* Fast path using per CPU cluster */
- local_lock(&si->percpu_cluster->lock);
+restart:
+ swap_alloc_lock_device(si);
+ if (si->flags & SWP_SOLIDSTATE)
offset = __this_cpu_read(si->percpu_cluster->next[order]);
- } else {
- /* Serialize HDD SWAP allocation for each device. */
- spin_lock(&si->global_cluster->lock);
+ else
offset = si->global_cluster->next[order];
- }

if (offset != SWAP_ENTRY_INVALID) {
ci = swap_cluster_lock(si, offset);
@@ -1576,6 +1585,15 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
found = alloc_swap_scan_list(si, &si->free_clusters, folio, false);
if (found)
goto done;
+
+ if (!discarded) {
+ swap_alloc_unlock_device(si);
+ if (swap_discard_one_cluster(si)) {
+ discarded = true;
+ goto restart;
+ }
+ swap_alloc_lock_device(si);
+ }
}

if (order < PMD_ORDER) {
@@ -1624,10 +1642,7 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
goto done;
}
done:
- if (si->flags & SWP_SOLIDSTATE)
- local_unlock(&si->percpu_cluster->lock);
- else
- spin_unlock(&si->global_cluster->lock);
+ swap_alloc_unlock_device(si);

return found;
}
@@ -1818,36 +1833,6 @@ static int swap_alloc_entry(struct folio *folio)
return ret;
}

-/*
- * Discard pending clusters in a synchronized way when under high pressure.
- * Return: true if any cluster is discarded.
- */
-static bool swap_sync_discard(void)
-{
- bool ret = false;
- struct swap_info_struct *si, *next;
-
- percpu_down_read(&swapon_rwsem);
-start_over:
- plist_for_each_entry_safe(si, next, &swap_active_head, list) {
- percpu_up_read(&swapon_rwsem);
- if (get_swap_device_info(si)) {
- if (si->flags & SWP_PAGE_DISCARD)
- ret = swap_do_scheduled_discard(si);
- put_swap_device(si);
- }
- if (ret)
- return true;
-
- percpu_down_read(&swapon_rwsem);
- if (plist_node_empty(&next->list))
- goto start_over;
- }
- percpu_up_read(&swapon_rwsem);
-
- return false;
-}
-
static int swap_extend_table_alloc(struct swap_info_struct *si,
struct swap_cluster_info *ci,
unsigned int ci_off, gfp_t gfp)
@@ -2149,14 +2134,8 @@ int folio_alloc_swap(struct folio *folio)
}
}

-again:
swap_alloc_entry(folio);

- if (!order && unlikely(!folio_test_swapcache(folio))) {
- if (swap_sync_discard())
- goto again;
- }
-
/* Need to call this even if allocation failed, for MEMCG_SWAP_FAIL. */
if (unlikely(mem_cgroup_try_charge_swap(folio))) {
swap_cache_del_folio(folio);