[PATCH v2 06/12] mm, swap: add xswap grow trigger on cluster allocation
From: Baoquan He
Date: Sun Sep 13 2026 - 03:51:23 EST
When cluster_alloc_swap_entry() fails to find a free cluster and
the xswap device still has room to grow, expand the mapped range
by XSWAP_GROW_CLUSTERS clusters.
Since xswap is always SWP_SOLIDSTATE, global_cluster_lock is never
held on this path.
This makes the xswap cluster space grow transparently as swap usage
increases, without any userspace intervention.
Growing maps pages into the VM_SPARSE area, which can sleep. The caller
holds local_lock(&percpu_swap_cluster.lock) across the whole slow path,
so drop it around xswap_map_clusters() and take it again afterwards; it
only protects the per-cpu cluster cache, which this path does not touch.
Signed-off-by: Baoquan He <hebaoquan@xxxxxxxxxx>
---
mm/swapfile.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 4861e2d49e36..f603f8c4b599 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1270,6 +1270,56 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
if (found)
goto done;
}
+
+#ifdef CONFIG_XSWAP
+ /*
+ * For xswap: if no free cluster was found and more clusters
+ * can be mapped, grow the cluster_info array and retry.
+ */
+ if (!found && (si->flags & SWP_XSWAP) &&
+ READ_ONCE(si->nr_clusters_mapped) < READ_ONCE(si->nr_clusters_max) &&
+ list_empty(&si->free_clusters)) {
+ unsigned long nr_new = min(READ_ONCE(si->nr_clusters_max) -
+ READ_ONCE(si->nr_clusters_mapped),
+ XSWAP_GROW_CLUSTERS);
+ unsigned long start = READ_ONCE(si->nr_clusters_mapped);
+ unsigned long i;
+ int ret;
+
+ /*
+ * Mapping pages into the VM_SPARSE area can sleep, which is
+ * not allowed under local_lock. The lock only protects the
+ * per-cpu cluster cache, which this path does not touch, so
+ * it can be dropped across the call.
+ */
+ local_unlock(&percpu_swap_cluster.lock);
+ ret = xswap_map_clusters(si, start, nr_new);
+ local_lock(&percpu_swap_cluster.lock);
+
+ if (!ret) {
+ for (i = start; i < start + nr_new; i++) {
+ struct swap_cluster_info *ci = &si->cluster_info[i];
+
+ /*
+ * A concurrent grower may have already added
+ * these clusters to the free list. Only add
+ * clusters that are still off-list (NONE).
+ * Lock ci->lock first: move_cluster() takes
+ * si->lock internally.
+ */
+ spin_lock(&ci->lock);
+ if (ci->flags == CLUSTER_FLAG_NONE)
+ move_cluster(si, ci, &si->free_clusters,
+ CLUSTER_FLAG_FREE);
+ spin_unlock(&ci->lock);
+ }
+
+ /* Retry allocation from the free list */
+ found = alloc_swap_scan_list(si, &si->free_clusters,
+ folio, false);
+ }
+ }
+#endif
done:
if (!(si->flags & SWP_SOLIDSTATE))
spin_unlock(&si->global_cluster_lock);
--
2.54.0