[RESEND RFC PATCH v2 11/13] mm/swap: bound synchronous discard during allocation

From: Lian Wang (ProcessMission)

Date: Sat Aug 29 2026 - 03:49:26 EST


From: Kairui Song <kasong@xxxxxxxxxxx>

The old global per-CPU cluster cache made it difficult to select a
specific device before handling its pending discards. Now that cluster
state is per-device, let an allocator issue discard for that device when
its free-cluster list is drained instead of waiting for a global fallback
scan.

Do not drain the entire discard list from one allocation. The list is
also updated by freeing paths, so an unlocked non-empty check races with
those writers, and a full synchronous drain has no fixed latency bound
when other CPUs continue to enqueue clusters.

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 the discard completes. The background worker
continues looping until the list is empty, while an allocator handles at
most one cluster before retrying allocation.

This keeps the proactive per-device policy while giving each allocation
a fixed synchronous discard budget.

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

diff --git a/mm/swapfile.c b/mm/swapfile.c
index e183dfad264e..cb27f2e246f0 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -528,6 +528,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
/*
@@ -914,10 +934,7 @@ swap_cluster_populate(struct swap_info_struct *si,
* the potential recursive allocation is limited.
*/
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);

ret = swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
GFP_KERNEL);
@@ -930,10 +947,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) {
@@ -1023,44 +1037,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)
@@ -1069,7 +1079,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)
@@ -1467,6 +1478,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
@@ -1475,15 +1487,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);
@@ -1507,6 +1516,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) {
@@ -1555,10 +1573,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;
}
@@ -1751,36 +1766,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)
@@ -2085,14 +2070,8 @@ int folio_alloc_swap(struct folio *folio)
}
}

-again:
ret = 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);
--
2.55.0