Skip to contentSkip to conversation

mm, swap: dynamic cluster management for xswap devices

RFC PATCH Superseded Baoquan He 3 participants 24 messages Jul 27 – Aug 5

Subsystems (4)

v1 of 5 Superseded Latest is v5 →

lore
Share & tools
lore

Baoquan He

author reviewer
#0 lore raw

cover mm, swap: dynamic cluster management for xswap devices

This series implements step 3 of the incremental path proposed in [1]:
a virtual swap device with dynamic cluster growth and shrink, backed only by zswap, no writeback yet.

During the discussion in [2], Chris and Nhat debated xarray-based vs array-based cluster lookup for virtual swap. Chris argued for keeping the swap table and cluster_info in the same place with O(1) array indexing, and suggested that the kvmalloc grow approach here should provide the cluster management VS needs. He further proposed using the same grow technique for the future per-slot backend pointer array (vs_table), keeping a single coherent array layout throughout.

This series therefore serves as the foundation: once the dynamic cluster management lands, Nhat's VS series can build the per-slot backend pointer, writeback, and rmap on top of it — without an extra xarray lookup in the cluster access path.

A note on naming: Nhat's series uses "virtual swap" (VS), Chris originally used "ghost swap", and now suggests "xswap" (extendable swap). This series uses "xswap" as a temporary convention; the final name is open for discussion.

xswap decouple swap slots from physical backing storage. Currently zswap requires a backing swap device sized for the full virtual capacity, even when writeback is never needed — wasting disk space on slots that zswap will never use. An xswap device is a swapfile: a 4K on-disk header advertises the capacity, but there is no swap data section. This eliminates the wasted backing storage.

This series replaces the fixed allocation with a VM_SPARSE vmalloc area that grows on demand and shrinks when clusters are freed, within the bounds set at swapon. The cluster_info pages are mapped lazily via vm_area_map_pages() and unmapped in bulk when contiguous free clusters accumulate at the tail. Lookup is O(1) via simple array indexing, which matters on the swapout hot path. The trade-off is coarser shrink granularity (full pages, not individual clusters), but for the initial landing where swap usage tends to be ratchet-like, this is a reasonable choice.

[1] https://lore.kernel.org/all/al8ohWshSSZ64AtT@MiWiFi-R3L-srv/
[2] https://lore.kernel.org/all/CACePvbVJGVDbhvPRNsZx-f4t16TU-t6H754JOUQQ4uF2Xe6Q5w@mail.gmail.com/

Design
======

The cluster_info[] array lives in a VM_SPARSE vm_struct. Physical pages are allocated and mapped into it in chunks of XSWAP_GROW_CLUSTERS (256, configurable). When the allocator runs out of free clusters and the mapped range hasn't reached the ceiling, the grow path maps more pages. Symmetrically, when clusters are freed, a shrink path unmap pages from the tail if enough contiguous free clusters accumulate.

To avoid scanning cluster_info[] on every shrink opportunity, a nr_free_tail counter provides O(1) detection — it tracks how many clusters at the tail are free. Shrink fires when the counter reaches the threshold.

A per-device runtime ceiling (nr_clusters) allows userspace to cap the mapped range below the hard limit, and a debugfs knob exposes it for live tuning without swapoff/swapon.

Testing
======
1. Create xswap swapfile
touch swap.4G
truncate -s 4G swap.4G
mkswap swap.4G
dd if=swap.4G of=ghost.4G bs=4096 count=1

2. swapon
swapon ghost.4G

3. memory pressure testing
echo 1 > /sys/module/zswap/parameters/enabled stress-ng --vm 1 --vm-bytes 4G --vm-keep --timeout 120s

4. shrink and grow memory via debugfs knob

root@fedora:~# cat /sys/kernel/debug/xswap/type0_cluster_limit 
4096
root@fedora:~# echo 2048 > /sys/kernel/debug/xswap/type0_cluster_limit
root@fedora:~# cat /sys/kernel/debug/xswap/type0_cluster_limit 
2048

I did test cases and all passed:
swapon, swapoff
swapon, during stress-ng shrink and grow xswap size swapon, stress-ng, then poweroff when stress-ng finished
=====

Baoquan He (10):
mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct mm, swap: add xswap cluster grow via VM_SPARSE vmalloc mm, swap: add xswap grow trigger on cluster allocation mm, swap: add xswap_try_shrink and shrink trigger on cluster free mm, swap: free backing pages in xswap_unmap_clusters mm, swap: add nr_free_tail for O(1) xswap shrink detection mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap mm, swap: add debugfs knob for xswap per-device cluster limit mm, swap: defer xswap shrink to workqueue to avoid lock recursion mm, swap: serialize xswap map/unmap with a mutex

Chris Li (1):
mm: xswap support for zswap

 include/linux/swap.h |  12 +
 mm/Kconfig           |   9 +
 mm/page_io.c         |  16 +
 mm/swap_state.c      |   7 +
 mm/swapfile.c        | 677 +++++++++++++++++++++++++++++++++++++++++--
 mm/zswap.c           |   9 +-
 6 files changed, 709 insertions(+), 21 deletions(-)
Signature · 1 line
2.54.0
author reviewer
#1 lore raw

RFC PATCH 1/11 mm: xswap support for zswap

From: Chris Li <[email protected]>

Introduce extendable (virtual) swap device support — xswap.

The current zswap requires a backing swapfile. The swap slot used by zswap is not able to be used by the swapfile, wasting swapfile space.

An xswap device is a swapfile that only contains the swap header, with the header indicating the size of the virtual swap space. There is no swap data section, therefore no waste of swapfile space. Any write to an xswap device will fail. To prevent accidental read or write, bdev of swap_info_struct is set to NULL. Xswap devices set the SSD flag because there is no rotational disk access when using zswap.

Zswap writeback is disabled if all swapfiles in the system are xswap devices (tracked via nr_real_swapfiles).

How to create an xswap device:
touch swap.1G
truncate -s 1G swap.1G
mkswap swap.1G
dd if=swap.1G of=xswap.1G bs=4096 count=1 # xswap.1G is 4K on disk but reports 1G capacity swapon xswap.1G

---
 include/linux/swap.h |  2 ++
 mm/page_io.c         | 16 +++++++++++++++
 mm/swap_state.c      |  7 +++++++
 mm/swapfile.c        | 49 ++++++++++++++++++++++++++++++++++++++++----
 mm/zswap.c           |  9 ++++++--
 5 files changed, 77 insertions(+), 6 deletions(-)
include/linux/swap.h +2 -0
index 0544b2ec4c56..80c83dd8804f 100644
@@ -207,6 +207,7 @@ enum {
 	SWP_STABLE_WRITES = (1 << 11),	/* no overwrite PG_writeback pages */
 	SWP_SYNCHRONOUS_IO = (1 << 12),	/* synchronous IO is efficient */
 	SWP_HIBERNATION = (1 << 13),	/* pinned for hibernation */
+	SWP_XSWAP	= (1 << 14),	/* extendable swap device */
 					/* add others here before... */
 };
 
@@ -351,6 +352,7 @@ void free_folio_and_swap_cache(struct folio *folio);
 void free_pages_and_swap_cache(struct encoded_page **, int);
 /* linux/mm/swapfile.c */
 extern atomic_long_t nr_swap_pages;
+extern atomic_t nr_real_swapfiles;
 extern long total_swap_pages;
 extern atomic_t nr_rotate_swap;
 
mm/page_io.c +16 -0
index e4fa7ffffe8b..1f3fa52131ab 100644
@@ -247,6 +247,17 @@ int swap_writeout(struct swap_io_ctx *ctx, struct folio *folio)
 	}
 	rcu_read_unlock();
 
+	/*
+	 * ctx->sis is set by swap_add_folio() which is called from
+	 * __swap_writepage() below.  Since we must avoid the writepage
+	 * path for xswap devices, use the swap_info from the folio's
+	 * swap entry directly instead of going through ctx.
+	 */
+	if (unlikely(__swap_entry_to_info(folio->swap)->flags & SWP_XSWAP)) {
+		folio_mark_dirty(folio);
+		return AOP_WRITEPAGE_ACTIVATE;
+	}
+
 	__swap_writepage(ctx, folio);
 	return 0;
 out_unlock:
@@ -479,6 +490,11 @@ void swap_read_folio(struct swap_io_ctx *ctx, struct folio *folio)
 	if (zswap_load(folio) != -ENOENT)
 		goto finish;
 
+	if (unlikely(sis->flags & SWP_XSWAP)) {
+		folio_unlock(folio);
+		goto finish;
+	}
+
 	/* We have to read from slower devices. Increase zswap protection. */
 	zswap_folio_swapin(folio);
 	swap_add_folio(ctx, folio, READ);
mm/swap_state.c +7 -0
index 5be825911e64..ebb2d2ac356f 100644
@@ -829,6 +829,13 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
 	struct blk_plug plug;
 	swp_entry_t ra_entry;
 
+	/*
+	 * The entry may have been freed by another task. Avoid swap_info_get()
+	 * which will print error message if the race happens.
+	 */
+	if (si->flags & SWP_XSWAP)
+		goto skip;
+
 	mask = swapin_nr_pages(offset) - 1;
 	if (!mask)
 		goto skip;
mm/swapfile.c +45 -4

103 lines not on this page · raw message

mm/zswap.c +7 -2
index 4e76a4a87cdc..6946793e92ef 100644
@@ -998,7 +998,12 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
 	/* try to allocate swap cache folio */
 	si = get_swap_device(swpentry);
 	if (!si)
-		return -EEXIST;
+		return -ENOENT;
+
+	if (si->flags & SWP_XSWAP) {
+		put_swap_device(si);
+		return -EINVAL;
+	}
 
 	mpol = get_task_policy(current);
 	folio = swap_cache_alloc_folio(swpentry, GFP_KERNEL, BIT(0), NULL, mpol,
@@ -1535,7 +1540,7 @@ bool zswap_store(struct folio *folio)
 	zswap_pool_put(pool);
 put_objcg:
 	obj_cgroup_put(objcg);
-	if (!ret && zswap_pool_reached_full)
+	if (!ret && zswap_pool_reached_full && atomic_read(&nr_real_swapfiles))
 		queue_work(shrink_wq, &zswap_shrink_work);
 check_old:
 	/*
Signature · 1 line
2.54.0
author reviewer
#2 lore raw

RFC PATCH 2/11 mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct

In reply to Baoquan He

Add CONFIG_XSWAP Kconfig option (depends on SWAP && 64BIT) for extendable (virtual) swap device support.

Add three fields to struct swap_info_struct under CONFIG_XSWAP:
- cluster_vm: the VM_SPARSE vm_struct backing the cluster_info array
- nr_clusters: total number of clusters in the xswap address space
- nr_clusters_mapped: number of clusters currently mapped (lazy grow)

These fields enable lazy vmalloc-based dynamic cluster management where the cluster_info array is backed by a sparse vmalloc area that grows on demand and shrinks when clusters are freed.

---
 include/linux/swap.h | 5 +++++
 mm/Kconfig           | 9 +++++++++
 2 files changed, 14 insertions(+)
include/linux/swap.h +5 -0
index 80c83dd8804f..5f6b14d30758 100644
@@ -248,6 +248,11 @@ struct swap_info_struct {
 	signed char	type;		/* strange name for an index */
 	unsigned int	max;		/* size of this swap device */
 	struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
+#ifdef CONFIG_XSWAP
+	struct vm_struct	*cluster_vm;	/* VM_SPARSE area for xswap dynamic cluster_info */
+	unsigned long		nr_clusters;	/* total cluster count for xswap */
+	unsigned long		nr_clusters_mapped; /* currently mapped cluster count */
+#endif
 	struct list_head free_clusters; /* free clusters list */
 	struct list_head full_clusters; /* full clusters list */
 	struct list_head nonfull_clusters[SWAP_NR_ORDERS];
mm/Kconfig +9 -0
index c52ab6afcb15..889d5ce20a55 100644
@@ -122,6 +122,15 @@ config ZSWAP_COMPRESSOR_DEFAULT
        default "zstd" if ZSWAP_COMPRESSOR_DEFAULT_ZSTD
        default ""
 
+config XSWAP
+	bool "Extendable (virtual) swap device"
+	depends on SWAP && 64BIT
+	help
+	  Adds support for extendable swap devices (xswap) that decouple
+	  PTE swap entries from physical backing storage. The cluster_info
+	  array is backed by a sparse vmalloc area that grows and shrinks
+	  on demand, avoiding static pre-allocation overhead.
+
 config ZSMALLOC
 	tristate
 
Signature · 1 line
2.54.0
author reviewer
#3 lore raw

RFC PATCH 3/11 mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Implement dynamic cluster_info array growth for xswap devices using a VM_SPARSE vmalloc area:

1. xswap_map_clusters(): Allocate physical pages and map them into the pre-reserved VM_SPARSE KVA region via vm_area_map_pages().

2. xswap_unmap_clusters(): Unmap pages from the VM_SPARSE area via vm_area_unmap_pages() (used by the error/teardown paths, shrink comes later).

3. setup_swap_clusters_info() xswap path: Use get_vm_area(VM_SPARSE) for the cluster_info array, lazily mapping only the initial chunk.

4. free_swap_cluster_info(): Refactor to take swap_info_struct*. For xswap, unmap all clusters and free_vm_area().

5. swapoff: Remove snapshot locals; move p->max/p->cluster_info clearing after free_swap_cluster_info().

---
 mm/swapfile.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 216 insertions(+), 12 deletions(-)
mm/swapfile.c +216 -12
index 143088dae07d..6d9c95ed09bd 100644
@@ -49,6 +49,24 @@
 #include "internal.h"
 #include "swap.h"
 
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation.  It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+	max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+			      unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+				 unsigned long start_idx, unsigned long nr);
+#endif
+
 static void swap_range_alloc(struct swap_info_struct *si,
 			     unsigned int nr_entries);
 static bool folio_swapcache_freeable(struct folio *folio);
@@ -3041,20 +3059,47 @@ static void wait_for_allocation(struct swap_info_struct *si)
 
 	BUG_ON(si->flags & SWP_WRITEOK);
 
+#ifdef CONFIG_XSWAP
+	/*
+	 * xswap clusters beyond nr_clusters_mapped have been unmapped
+	 * by the shrinker and their vmalloc pages are no longer
+	 * accessible.  Only iterate over currently mapped clusters.
+	 */
+	if (si->flags & SWP_XSWAP)
+		end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+			  SWAPFILE_CLUSTER);
+#endif
+
 	for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
 		ci = swap_cluster_lock(si, offset);
 		swap_cluster_unlock(ci);
 	}
 }
 
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
-				   unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
 {
+	struct swap_cluster_info *cluster_info = si->cluster_info;
+	unsigned long maxpages = si->max;
 	struct swap_cluster_info *ci;
-	int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+	int i, nr_clusters;
 
 	if (!cluster_info)
 		return;
+
+#ifdef CONFIG_XSWAP
+	if (si->flags & SWP_XSWAP) {
+		/* Unmap all mapped clusters and free the VM_SPARSE area */
+		if (si->nr_clusters_mapped > 0)
+			xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+		free_vm_area(si->cluster_vm);
+		si->cluster_vm = NULL;
+		si->nr_clusters = 0;
+		si->nr_clusters_mapped = 0;
+		return;
+	}
+#endif
+
+	nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
 	for (i = 0; i < nr_clusters; i++) {
 		ci = cluster_info + i;
 		/* Cluster with bad marks count will have a remaining table */
@@ -3093,11 +3138,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
 SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 {
 	struct swap_info_struct *p = NULL;
-	struct swap_cluster_info *cluster_info;
 	struct file *swap_file, *victim;
 	struct address_space *mapping;
 	struct inode *inode;
-	unsigned int maxpages;
 	int err, found = 0;
 
 	if (!capable(CAP_SYS_ADMIN))
@@ -3189,10 +3232,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
 	swap_file = p->swap_file;
 	p->swap_file = NULL;
-	maxpages = p->max;
-	cluster_info = p->cluster_info;
-	p->max = 0;
-	p->cluster_info = NULL;
 	spin_unlock(&p->lock);
 	spin_unlock(&swap_lock);
 	arch_swap_invalidate_area(p->type);
@@ -3200,7 +3239,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 	mutex_unlock(&swapon_mutex);
 	kfree(p->global_cluster);
 	p->global_cluster = NULL;
-	free_swap_cluster_info(cluster_info, maxpages);
+	free_swap_cluster_info(p);
+	p->max = 0;
+	p->cluster_info = NULL;
 
 	inode = mapping->host;
 
@@ -3564,6 +3605,112 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
 	return maxpages;
 }
 
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+			      unsigned long start_idx, unsigned long nr)
+{
+	unsigned long start_addr = (unsigned long)si->cluster_info +
+				   (size_t)start_idx * sizeof(struct swap_cluster_info);
+	unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+	/*
+	 * vm_area_map_pages() requires that start and end be page-aligned.
+	 * If start_addr falls within a page that was already mapped by a
+	 * previous batch (grow path), round it up to skip the already-mapped
+	 * partial page.  Always round end_addr up so the vmap page table walk
+	 * terminates correctly (the walk loop exits when addr == end, and addr
+	 * advances by PAGE_SIZE each iteration).
+	 */
+	unsigned long vm_start = PAGE_ALIGN(start_addr);
+	unsigned long vm_end = PAGE_ALIGN(end_addr);
+	unsigned long npages;
+	struct page **pages;
+	unsigned long i;
+
+	if (vm_start >= vm_end) {
+		/* All requested clusters fall within already-mapped pages. */
+		for (i = start_idx; i < start_idx + nr; i++)
+			spin_lock_init(&si->cluster_info[i].lock);
+		WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+		return 0;
+	}
+
+	npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+	pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+	if (!pages)
+		return -ENOMEM;
+
+	for (i = 0; i < npages; i++) {
+		/*
+		 * __GFP_ZERO is critical: cluster_info structs contain pointer
+		 * fields (extend_table, zero_bitmap, memcg_table, table) that
+		 * must start as NULL.  Without zeroing, stale data from a
+		 * previous user of the page would look like valid pointers.
+		 */
+		pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+		if (!pages[i])
+			goto fail;
+	}
+
+	if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+		i = npages; /* free all pages on failure */
+		goto fail;
+	}
+
+	kfree(pages);
+
+	/* Initialize spinlocks for newly mapped clusters */
+	for (i = start_idx; i < start_idx + nr; i++)
+		spin_lock_init(&si->cluster_info[i].lock);
+
+	/*
+	 * Pairs with READ_ONCE() in shrink/grow paths.
+	 */
+	WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+	return 0;
+
+fail:
+	while (i > 0) {
+		i--;
+		if (pages[i])
+			__free_page(pages[i]);
+	}
+	kfree(pages);
+	return -ENOMEM;
+}
+
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+				 unsigned long start_idx, unsigned long nr)
+{
+	unsigned long start_addr = (unsigned long)si->cluster_info +
+				   (size_t)start_idx * sizeof(struct swap_cluster_info);
+	unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+	/*
+	 * Round to page boundaries: start up (skip partial page that may
+	 * contain clusters still in use before start_idx), end up so the
+	 * entire range is covered.  vm_area_unmap_pages() operates on
+	 * whole pages.
+	 */
+	unsigned long vm_start = PAGE_ALIGN(start_addr);
+	unsigned long vm_end = PAGE_ALIGN(end_addr);
+
+	if (vm_start >= vm_end)
+		goto out;
+
+	vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+	/*
+	 * vm_area_unmap_pages() only clears PTEs; it does not free the
+	 * physical pages.  Walk the page table to find and free them.
+	 */
+	/* TODO: free backing pages via page table walk or tracking bitmap */
+	/*
+	 * Pairs with READ_ONCE() in shrink/grow paths.
+	 */
+	out:
+	WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+}
+#endif /* CONFIG_XSWAP */
+
 static int setup_swap_clusters_info(struct swap_info_struct *si,
 				    union swap_header *swap_header,
 				    unsigned long maxpages)
@@ -3573,6 +3720,63 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
 	int err = -ENOMEM;
 	unsigned long i;
 
+#ifdef CONFIG_XSWAP
+	if (si->flags & SWP_XSWAP) {
+		unsigned long size = PAGE_ALIGN(nr_clusters * sizeof(*cluster_info));
+		struct vm_struct *vm;
+
+		vm = get_vm_area(size, VM_SPARSE);
+		if (!vm)
+			goto err;
+
+		cluster_info = vm->addr;
+		si->cluster_vm = vm;
+		si->nr_clusters = nr_clusters;
+		si->cluster_info = cluster_info;
+
+		/* Map the initial chunk (at least cluster 0) */
+		if (xswap_map_clusters(si, 0, min_t(unsigned long,
+					XSWAP_GROW_CLUSTERS, nr_clusters)))
+			goto err_free_vm;
+
+		/* xswap: only cluster 0 slot 0 is bad */
+		err = swap_cluster_setup_bad_slot(si, cluster_info, 0, false);
+		if (err)
+			goto err_unmap;
+
+		INIT_LIST_HEAD(&si->free_clusters);
+		INIT_LIST_HEAD(&si->full_clusters);
+		INIT_LIST_HEAD(&si->discard_clusters);
+		for (i = 0; i < SWAP_NR_ORDERS; i++) {
+			INIT_LIST_HEAD(&si->nonfull_clusters[i]);
+			INIT_LIST_HEAD(&si->frag_clusters[i]);
+		}
+
+		/* Mark mapped clusters: cluster 0 has 1 bad slot, rest free */
+		for (i = 0; i < si->nr_clusters_mapped; i++) {
+			struct swap_cluster_info *ci = &cluster_info[i];
+
+			if (i == 0) {
+				ci->flags = CLUSTER_FLAG_NONFULL;
+				list_add_tail(&ci->list, &si->nonfull_clusters[0]);
+			} else {
+				ci->flags = CLUSTER_FLAG_FREE;
+				list_add_tail(&ci->list, &si->free_clusters);
+			}
+		}
+
+		return 0;
+
+err_unmap:
+		xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+err_free_vm:
+		free_vm_area(si->cluster_vm);
+		si->cluster_vm = NULL;
+		si->cluster_info = NULL;
+		return err;
+	}
+#endif /* CONFIG_XSWAP */
+
 	cluster_info = kvzalloc_objs(*cluster_info, nr_clusters);
 	if (!cluster_info)
 		goto err;
@@ -3640,7 +3844,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
 	si->cluster_info = cluster_info;
 	return 0;
 err:
-	free_swap_cluster_info(cluster_info, maxpages);
+	free_swap_cluster_info(si);
 	return err;
 }
 
@@ -3852,7 +4056,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
 	si->global_cluster = NULL;
 	inode = NULL;
 	destroy_swap_extents(si, swap_file);
-	free_swap_cluster_info(si->cluster_info, si->max);
+	free_swap_cluster_info(si);
 	si->cluster_info = NULL;
 	/*
 	 * Clear the SWP_USED flag after all resources are freed so
Signature · 1 line
2.54.0
maintainer
#4 lore raw

Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Quoted 14 lines · Baoquan He

Implement dynamic cluster_info array growth for xswap devices using a VM_SPARSE vmalloc area:

1. xswap_map_clusters(): Allocate physical pages and map them into the pre-reserved VM_SPARSE KVA region via vm_area_map_pages().

2. xswap_unmap_clusters(): Unmap pages from the VM_SPARSE area via vm_area_unmap_pages() (used by the error/teardown paths, shrink comes later).

3. setup_swap_clusters_info() xswap path: Use get_vm_area(VM_SPARSE) for the cluster_info array, lazily mapping only the initial chunk.

Quoted 7 lines

4. free_swap_cluster_info(): Refactor to take swap_info_struct*. For xswap, unmap all clusters and free_vm_area().

5. swapoff: Remove snapshot locals; move p->max/p->cluster_info clearing after free_swap_cluster_info().

I'll give my 2 cents on the direction we're pursuing later. I've done a lot of thinking over the past weeks, and I have some new concerns now regarding how we plan to land this joint venture :)

That said, this is a new piece of extension, done per my request (kernel-driven dynamic swap address space growth - thanks for taking my concerns seriously), and I've also evaluated this vmalloc-based data structure in the past week, so I'll put in my inquiries/concerns.

Quoted 162 lines
---
 mm/swapfile.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 216 insertions(+), 12 deletions(-)
mm/swapfile.c +91 -10
index 143088dae07d..6d9c95ed09bd 100644
@@ -49,6 +49,24 @@
 #include "internal.h"
 #include "swap.h"
 
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation.  It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+       max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr);
+#endif
+
 static void swap_range_alloc(struct swap_info_struct *si,
                             unsigned int nr_entries);
 static bool folio_swapcache_freeable(struct folio *folio);
@@ -3041,20 +3059,47 @@ static void wait_for_allocation(struct swap_info_struct *si)
 
        BUG_ON(si->flags & SWP_WRITEOK);
 
+#ifdef CONFIG_XSWAP
+       /*
+        * xswap clusters beyond nr_clusters_mapped have been unmapped
+        * by the shrinker and their vmalloc pages are no longer
+        * accessible.  Only iterate over currently mapped clusters.
+        */
+       if (si->flags & SWP_XSWAP)
+               end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+                         SWAPFILE_CLUSTER);
+#endif
+
        for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
                ci = swap_cluster_lock(si, offset);
                swap_cluster_unlock(ci);
        }
 }
 
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
-                                  unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
 {
+       struct swap_cluster_info *cluster_info = si->cluster_info;
+       unsigned long maxpages = si->max;
        struct swap_cluster_info *ci;
-       int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+       int i, nr_clusters;
 
        if (!cluster_info)
                return;
+
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               /* Unmap all mapped clusters and free the VM_SPARSE area */
+               if (si->nr_clusters_mapped > 0)
+                       xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->nr_clusters = 0;
+               si->nr_clusters_mapped = 0;
+               return;
+       }
+#endif
+
+       nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        for (i = 0; i < nr_clusters; i++) {
                ci = cluster_info + i;
                /* Cluster with bad marks count will have a remaining table */
@@ -3093,11 +3138,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
 SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 {
        struct swap_info_struct *p = NULL;
-       struct swap_cluster_info *cluster_info;
        struct file *swap_file, *victim;
        struct address_space *mapping;
        struct inode *inode;
-       unsigned int maxpages;
        int err, found = 0;
 
        if (!capable(CAP_SYS_ADMIN))
@@ -3189,10 +3232,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
        swap_file = p->swap_file;
        p->swap_file = NULL;
-       maxpages = p->max;
-       cluster_info = p->cluster_info;
-       p->max = 0;
-       p->cluster_info = NULL;
        spin_unlock(&p->lock);
        spin_unlock(&swap_lock);
        arch_swap_invalidate_area(p->type);
@@ -3200,7 +3239,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
        mutex_unlock(&swapon_mutex);
        kfree(p->global_cluster);
        p->global_cluster = NULL;
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(p);
+       p->max = 0;
+       p->cluster_info = NULL;
 
        inode = mapping->host;
 
@@ -3564,6 +3605,112 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
        return maxpages;
 }
 
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * vm_area_map_pages() requires that start and end be page-aligned.
+        * If start_addr falls within a page that was already mapped by a
+        * previous batch (grow path), round it up to skip the already-mapped
+        * partial page.  Always round end_addr up so the vmap page table walk
+        * terminates correctly (the walk loop exits when addr == end, and addr
+        * advances by PAGE_SIZE each iteration).
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+       unsigned long npages;
+       struct page **pages;
+       unsigned long i;
+
+       if (vm_start >= vm_end) {
+               /* All requested clusters fall within already-mapped pages. */
+               for (i = start_idx; i < start_idx + nr; i++)
+                       spin_lock_init(&si->cluster_info[i].lock);
+               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+               return 0;
+       }
+
+       npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+       if (!pages)
+               return -ENOMEM;
+
+       for (i = 0; i < npages; i++) {
+               /*
+                * __GFP_ZERO is critical: cluster_info structs contain pointer
+                * fields (extend_table, zero_bitmap, memcg_table, table) that
+                * must start as NULL.  Without zeroing, stale data from a

mm/swapfile.c static unsigned long read_swap_header(struct swap_info_struct *si, · line 3657

Quoted 10 lines
mm/swapfile.c +10 -0
+                * previous user of the page would look like valid pointers.
+                */
+               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+               if (!pages[i])
+                       goto fail;
+       }
+
+       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+               i = npages; /* free all pages on failure */
+               goto fail;

I was evaluating whether your vmalloc array can be extended to support kernel-driven dynamic growth at least (either as a slot-in replacement for xarray, or as a follow-up optimization if it's too complicated), and I stumble this mapping action.

Seems like it does not take any GFP flag argument, and under the hood it calls GFP_KERNEL. Would this be safe in the swap allocation path (where you slotted it in in patch 4)? Kairui used a more precise set of flags in this path, for e.g for swap table allocation:

ret = swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
GFP_KERNEL);

I'm guessing this has to do with the fact that we often entered swapping out paths with PF_MEMALLOC... Is there any risk of deadlock etc.?

Kairui, any thoughts?

author reviewer
#5 lore raw

Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Quoted 215 lines · Nhat Pham
Quoted 14 lines · Baoquan He

Implement dynamic cluster_info array growth for xswap devices using a VM_SPARSE vmalloc area:

1. xswap_map_clusters(): Allocate physical pages and map them into the pre-reserved VM_SPARSE KVA region via vm_area_map_pages().

2. xswap_unmap_clusters(): Unmap pages from the VM_SPARSE area via vm_area_unmap_pages() (used by the error/teardown paths, shrink comes later).

3. setup_swap_clusters_info() xswap path: Use get_vm_area(VM_SPARSE) for the cluster_info array, lazily mapping only the initial chunk.

Quoted 7 lines

4. free_swap_cluster_info(): Refactor to take swap_info_struct*. For xswap, unmap all clusters and free_vm_area().

5. swapoff: Remove snapshot locals; move p->max/p->cluster_info clearing after free_swap_cluster_info().

I'll give my 2 cents on the direction we're pursuing later. I've done a lot of thinking over the past weeks, and I have some new concerns now regarding how we plan to land this joint venture :)

That said, this is a new piece of extension, done per my request (kernel-driven dynamic swap address space growth - thanks for taking my concerns seriously), and I've also evaluated this vmalloc-based data structure in the past week, so I'll put in my inquiries/concerns.

Quoted 162 lines
---
 mm/swapfile.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 216 insertions(+), 12 deletions(-)
mm/swapfile.c +91 -10
index 143088dae07d..6d9c95ed09bd 100644
@@ -49,6 +49,24 @@
 #include "internal.h"
 #include "swap.h"
 
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation.  It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+       max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr);
+#endif
+
 static void swap_range_alloc(struct swap_info_struct *si,
                             unsigned int nr_entries);
 static bool folio_swapcache_freeable(struct folio *folio);
@@ -3041,20 +3059,47 @@ static void wait_for_allocation(struct swap_info_struct *si)
 
        BUG_ON(si->flags & SWP_WRITEOK);
 
+#ifdef CONFIG_XSWAP
+       /*
+        * xswap clusters beyond nr_clusters_mapped have been unmapped
+        * by the shrinker and their vmalloc pages are no longer
+        * accessible.  Only iterate over currently mapped clusters.
+        */
+       if (si->flags & SWP_XSWAP)
+               end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+                         SWAPFILE_CLUSTER);
+#endif
+
        for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
                ci = swap_cluster_lock(si, offset);
                swap_cluster_unlock(ci);
        }
 }
 
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
-                                  unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
 {
+       struct swap_cluster_info *cluster_info = si->cluster_info;
+       unsigned long maxpages = si->max;
        struct swap_cluster_info *ci;
-       int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+       int i, nr_clusters;
 
        if (!cluster_info)
                return;
+
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               /* Unmap all mapped clusters and free the VM_SPARSE area */
+               if (si->nr_clusters_mapped > 0)
+                       xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->nr_clusters = 0;
+               si->nr_clusters_mapped = 0;
+               return;
+       }
+#endif
+
+       nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        for (i = 0; i < nr_clusters; i++) {
                ci = cluster_info + i;
                /* Cluster with bad marks count will have a remaining table */
@@ -3093,11 +3138,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
 SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 {
        struct swap_info_struct *p = NULL;
-       struct swap_cluster_info *cluster_info;
        struct file *swap_file, *victim;
        struct address_space *mapping;
        struct inode *inode;
-       unsigned int maxpages;
        int err, found = 0;
 
        if (!capable(CAP_SYS_ADMIN))
@@ -3189,10 +3232,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
        swap_file = p->swap_file;
        p->swap_file = NULL;
-       maxpages = p->max;
-       cluster_info = p->cluster_info;
-       p->max = 0;
-       p->cluster_info = NULL;
        spin_unlock(&p->lock);
        spin_unlock(&swap_lock);
        arch_swap_invalidate_area(p->type);
@@ -3200,7 +3239,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
        mutex_unlock(&swapon_mutex);
        kfree(p->global_cluster);
        p->global_cluster = NULL;
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(p);
+       p->max = 0;
+       p->cluster_info = NULL;
 
        inode = mapping->host;
 
@@ -3564,6 +3605,112 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
        return maxpages;
 }
 
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * vm_area_map_pages() requires that start and end be page-aligned.
+        * If start_addr falls within a page that was already mapped by a
+        * previous batch (grow path), round it up to skip the already-mapped
+        * partial page.  Always round end_addr up so the vmap page table walk
+        * terminates correctly (the walk loop exits when addr == end, and addr
+        * advances by PAGE_SIZE each iteration).
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+       unsigned long npages;
+       struct page **pages;
+       unsigned long i;
+
+       if (vm_start >= vm_end) {
+               /* All requested clusters fall within already-mapped pages. */
+               for (i = start_idx; i < start_idx + nr; i++)
+                       spin_lock_init(&si->cluster_info[i].lock);
+               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+               return 0;
+       }
+
+       npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+       if (!pages)
+               return -ENOMEM;
+
+       for (i = 0; i < npages; i++) {
+               /*
+                * __GFP_ZERO is critical: cluster_info structs contain pointer
+                * fields (extend_table, zero_bitmap, memcg_table, table) that
+                * must start as NULL.  Without zeroing, stale data from a

mm/swapfile.c static unsigned long read_swap_header(struct swap_info_struct *si, · line 3657

Quoted 10 lines
mm/swapfile.c +10 -0
+                * previous user of the page would look like valid pointers.
+                */
+               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+               if (!pages[i])
+                       goto fail;
+       }
+
+       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+               i = npages; /* free all pages on failure */
+               goto fail;

I was evaluating whether your vmalloc array can be extended to support kernel-driven dynamic growth at least (either as a slot-in replacement for xarray, or as a follow-up optimization if it's too complicated), and I stumble this mapping action.

Seems like it does not take any GFP flag argument, and under the hood it calls GFP_KERNEL. Would this be safe in the swap allocation path (where you slotted it in in patch 4)? Kairui used a more precise set of flags in this path, for e.g for swap table allocation:

Quoted 7 lines

ret = swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
GFP_KERNEL);

I'm guessing this has to do with the fact that we often entered swapping out paths with PF_MEMALLOC... Is there any risk of deadlock etc.?

Thanks for catching this. You're right — there are actually three layers of hardcoded GFP_KERNEL in the VM_SPARSE path:

1. alloc_page(GFP_KERNEL | __GFP_ZERO) in xswap_map_clusters() for the backing pages
2. vmap_pages_range() hardcodes GFP_KERNEL (passed only to KMSAN, but still semantically wrong)
3. The page-table allocations inside vmap_pages_range_noflush_walk() (pte_alloc_kernel_track etc.) use GFP_PGTABLE_KERNEL, which is GFP_KERNEL | __GFP_ZERO — also lacking __GFP_NOMEMALLOC, and completely invisible to the caller

When PF_MEMALLOC is set (as it is during reclaim), __gfp_pfmemalloc_flags() returns ALLOC_NO_WATERMARKS for any allocation without __GFP_NOMEMALLOC, meaning those allocations can consume all emergency reserves — including the reserves that the reclaim machinery itself needs to make forward progress. That is a real deadlock risk.

One way is to fix them one by one. Another way is to drop the VM_SPARSE approach entirely and implemented Chris's suggestion instead: cluster_info is now a struct swap_cluster_info ** pointer array, with each cluster individually kmem_cache_zalloc()'d. The grow path (xswap_grow_clusters) now uses:

kmem_cache_zalloc(swap_cluster_cachep,
__GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL)
matching the pattern Kairui established in swap_cluster_alloc_table(). All allocations are fully GFP-controllable — no hidden vmap page-table allocations, no hardcoded GFP_PGTABLE_KERNEL.

With kmem_cache, shrink is just kmem_cache_free() — each cluster is independently allocated and freed. Seems the logic is simpler. If no objection, I will give it a shot soon.

Thanks
Baoquan

author reviewer
#6 lore raw

Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Quoted 2 lines · Baoquan He
Quoted 1 line · Nhat Pham

On Mon, Jul 27, 2026 at 7:05 AM Baoquan He <[email protected]> wrote:

...snip...

Quoted 61 lines
Quoted 61 lines
Quoted 40 lines
mm/swapfile.c +40 -0
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * vm_area_map_pages() requires that start and end be page-aligned.
+        * If start_addr falls within a page that was already mapped by a
+        * previous batch (grow path), round it up to skip the already-mapped
+        * partial page.  Always round end_addr up so the vmap page table walk
+        * terminates correctly (the walk loop exits when addr == end, and addr
+        * advances by PAGE_SIZE each iteration).
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+       unsigned long npages;
+       struct page **pages;
+       unsigned long i;
+
+       if (vm_start >= vm_end) {
+               /* All requested clusters fall within already-mapped pages. */
+               for (i = start_idx; i < start_idx + nr; i++)
+                       spin_lock_init(&si->cluster_info[i].lock);
+               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+               return 0;
+       }
+
+       npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+       if (!pages)
+               return -ENOMEM;
+
+       for (i = 0; i < npages; i++) {
+               /*
+                * __GFP_ZERO is critical: cluster_info structs contain pointer
+                * fields (extend_table, zero_bitmap, memcg_table, table) that
+                * must start as NULL.  Without zeroing, stale data from a

mm/swapfile.c static unsigned long read_swap_header(struct swap_info_struct *si, · line 3657

Quoted 10 lines
mm/swapfile.c +10 -0
+                * previous user of the page would look like valid pointers.
+                */
+               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+               if (!pages[i])
+                       goto fail;
+       }
+
+       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+               i = npages; /* free all pages on failure */
+               goto fail;

I was evaluating whether your vmalloc array can be extended to support kernel-driven dynamic growth at least (either as a slot-in replacement for xarray, or as a follow-up optimization if it's too complicated), and I stumble this mapping action.

Seems like it does not take any GFP flag argument, and under the hood it calls GFP_KERNEL. Would this be safe in the swap allocation path (where you slotted it in in patch 4)? Kairui used a more precise set of flags in this path, for e.g for swap table allocation:

Quoted 7 lines
Quoted 7 lines

ret = swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
GFP_KERNEL);

I'm guessing this has to do with the fact that we often entered swapping out paths with PF_MEMALLOC... Is there any risk of deadlock etc.?

Hi Chris,

I gave the kmem_cache approach a try, but ended up keeping VM_SPARSE. The kmem_cache approach has two structural problems:

1. With individually kmem_cache_alloc()'d clusters, ci - si->cluster_info no longer works — cluster_info is a pointer array, not a contiguous array. Each struct swap_cluster_info would need a new field to store its own index. VM_SPARSE preserves the contiguous array layout, so ci - si->cluster_info continues to work for O(1) index lookup everywhere.

2. The struct swap_cluster_info ** pointer array itself costs extra
memory: 8 bytes per cluster. That's 8 KB for a 1 GB swap device, and 8 MB for a 1 TB device — paid upfront at swapon, regardless of how many clusters are actually used. VM_SPARSE needs no such indirection array; the cluster_info is addressed directly through the vmalloc area.

By comparison, the introduced change in struct swap_cluster_info isn't that great.

How the GFP concern is addressed
--------------------------------
Hi Nhat,

You were right about the three layers of hardcoded GFP_KERNEL in v1. In the current code (the version I'll post as RFC v2), each is fixed:

1. alloc_page() and kmalloc_array() now use:

__GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL

This matches the pattern Kairui established in swap_cluster_alloc_table(). __GFP_NOMEMALLOC prevents the pfmemalloc reserve bypass during reclaim.

2. The internal page-table allocations inside vmap_pages_range() use GFP_PGTABLE_KERNEL and are not directly controllable from the caller. To prevent these from recursing into swap reclaim, the entire allocation block is wrapped with:

     noreclaim_flags = memalloc_noreclaim_save();
     ... alloc_page / kmalloc_array / vm_area_map_pages ...
     memalloc_noreclaim_restore(noreclaim_flags);

memalloc_noreclaim_save() ensures __GFP_FS and __GFP_IO are cleared for all allocations within the scope, so the page-table allocation cannot recurse into filesystem or swap reclaim.

3. The residual risk is that the page-table allocations still lack __GFP_NOMEMALLOC, meaning they could theoretically dip into emergency reserves under PF_MEMALLOC. However, each grow operation maps at most one PTE page (XSWAP_GROW_CLUSTERS clusters per page), so the total order-0 allocation is tiny — typically a single page-table page. This seems acceptable compared to the complexity of plumbing a GFP parameter through the entire vmap_pages_range() call chain.

A variant vm_area_map_pages_gfp that passes the caller's GFP context down to the page-table allocations. That cleanly solves the problem for all VM_SPARSE users, not just xswap. I'm happy to work on that if people think it's the right direction, but I'd prefer to keep it as a separate improvement rather than blocking this series on it.

The shrink path also avoids this problem entirely: it runs in a workqueue context where PF_MEMALLOC is never set, so GFP_KERNEL is correct there.

Thanks
Baoquan

Quoted 38 lines

Thanks for catching this. You're right — there are actually three layers of hardcoded GFP_KERNEL in the VM_SPARSE path:

1. alloc_page(GFP_KERNEL | __GFP_ZERO) in xswap_map_clusters() for the backing pages
2. vmap_pages_range() hardcodes GFP_KERNEL (passed only to KMSAN, but still semantically wrong)
3. The page-table allocations inside vmap_pages_range_noflush_walk() (pte_alloc_kernel_track etc.) use GFP_PGTABLE_KERNEL, which is GFP_KERNEL | __GFP_ZERO — also lacking __GFP_NOMEMALLOC, and completely invisible to the caller

When PF_MEMALLOC is set (as it is during reclaim), __gfp_pfmemalloc_flags() returns ALLOC_NO_WATERMARKS for any allocation without __GFP_NOMEMALLOC, meaning those allocations can consume all emergency reserves — including the reserves that the reclaim machinery itself needs to make forward progress. That is a real deadlock risk.

One way is to fix them one by one. Another way is to drop the VM_SPARSE approach entirely and implemented Chris's suggestion instead: cluster_info is now a struct swap_cluster_info ** pointer array, with each cluster individually kmem_cache_zalloc()'d. The grow path (xswap_grow_clusters) now uses:

kmem_cache_zalloc(swap_cluster_cachep,
__GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL)
matching the pattern Kairui established in swap_cluster_alloc_table(). All allocations are fully GFP-controllable — no hidden vmap page-table allocations, no hardcoded GFP_PGTABLE_KERNEL.

With kmem_cache, shrink is just kmem_cache_free() — each cluster is independently allocated and freed. Seems the logic is simpler. If no objection, I will give it a shot soon.

Thanks
Baoquan

maintainer
#7 lore raw

Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

In reply to Baoquan He
Quoted 11 lines · Baoquan He

Implement dynamic cluster_info array growth for xswap devices using a VM_SPARSE vmalloc area:

1. xswap_map_clusters(): Allocate physical pages and map them into the pre-reserved VM_SPARSE KVA region via vm_area_map_pages().

2. xswap_unmap_clusters(): Unmap pages from the VM_SPARSE area via vm_area_unmap_pages() (used by the error/teardown paths, shrink comes later).

Quoted 8 lines

3. setup_swap_clusters_info() xswap path: Use get_vm_area(VM_SPARSE) for the cluster_info array, lazily mapping only the initial chunk.

4. free_swap_cluster_info(): Refactor to take swap_info_struct*. For xswap, unmap all clusters and free_vm_area().

5. swapoff: Remove snapshot locals; move p->max/p->cluster_info clearing after free_swap_cluster_info().

I feel that we should provide more generic support for dynamically growing an array of element size A to size N. Cluster array is just a user of such an API. There might be more users. This patch hardcodes the map/umpa of clusters. What about other users of such a dynamic array?
Another consideration is that the map/unmap logic, while more generic, adds quite a bit of complexity to an approach intended only for growth. Any reason we want map/unmap rathern than simple grow from the current si->max?

I read a bit more about the patch. It seems you need the unmap for the error cleanup path. In that case I suspect just make cluster a kmem_cache allocation, cluster array just a pointer. The cluster pointer array should just grow, not shrink. That might be overall simpler.
I'm worried someone might abuse the cluster unmap causing unwanted side effects.

I see the other reason might be you want the cluster code to be exactly the same when XSWAP is off. For that I think it is acceptiale to turn cluster into kmem_cache allocated and cluster array as pointers array as seperate patch as the base even when XSWAP is not configed.

Chris

Quoted 317 lines
---
 mm/swapfile.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 216 insertions(+), 12 deletions(-)
mm/swapfile.c +216 -13
index 143088dae07d..6d9c95ed09bd 100644
@@ -49,6 +49,24 @@
 #include "internal.h"
 #include "swap.h"
 
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation.  It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+       max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr);
+#endif
+
 static void swap_range_alloc(struct swap_info_struct *si,
                             unsigned int nr_entries);
 static bool folio_swapcache_freeable(struct folio *folio);
@@ -3041,20 +3059,47 @@ static void wait_for_allocation(struct swap_info_struct *si)
 
        BUG_ON(si->flags & SWP_WRITEOK);
 
+#ifdef CONFIG_XSWAP
+       /*
+        * xswap clusters beyond nr_clusters_mapped have been unmapped
+        * by the shrinker and their vmalloc pages are no longer
+        * accessible.  Only iterate over currently mapped clusters.
+        */
+       if (si->flags & SWP_XSWAP)
+               end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+                         SWAPFILE_CLUSTER);
+#endif
+
        for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
                ci = swap_cluster_lock(si, offset);
                swap_cluster_unlock(ci);
        }
 }
 
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
-                                  unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
 {
+       struct swap_cluster_info *cluster_info = si->cluster_info;
+       unsigned long maxpages = si->max;
        struct swap_cluster_info *ci;
-       int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+       int i, nr_clusters;
 
        if (!cluster_info)
                return;
+
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               /* Unmap all mapped clusters and free the VM_SPARSE area */
+               if (si->nr_clusters_mapped > 0)
+                       xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->nr_clusters = 0;
+               si->nr_clusters_mapped = 0;
+               return;
+       }
+#endif
+
+       nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        for (i = 0; i < nr_clusters; i++) {
                ci = cluster_info + i;
                /* Cluster with bad marks count will have a remaining table */
@@ -3093,11 +3138,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
 SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 {
        struct swap_info_struct *p = NULL;
-       struct swap_cluster_info *cluster_info;
        struct file *swap_file, *victim;
        struct address_space *mapping;
        struct inode *inode;
-       unsigned int maxpages;
        int err, found = 0;
 
        if (!capable(CAP_SYS_ADMIN))
@@ -3189,10 +3232,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
        swap_file = p->swap_file;
        p->swap_file = NULL;
-       maxpages = p->max;
-       cluster_info = p->cluster_info;
-       p->max = 0;
-       p->cluster_info = NULL;
        spin_unlock(&p->lock);
        spin_unlock(&swap_lock);
        arch_swap_invalidate_area(p->type);
@@ -3200,7 +3239,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
        mutex_unlock(&swapon_mutex);
        kfree(p->global_cluster);
        p->global_cluster = NULL;
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(p);
+       p->max = 0;
+       p->cluster_info = NULL;
 
        inode = mapping->host;
 
@@ -3564,6 +3605,112 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
        return maxpages;
 }
 
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * vm_area_map_pages() requires that start and end be page-aligned.
+        * If start_addr falls within a page that was already mapped by a
+        * previous batch (grow path), round it up to skip the already-mapped
+        * partial page.  Always round end_addr up so the vmap page table walk
+        * terminates correctly (the walk loop exits when addr == end, and addr
+        * advances by PAGE_SIZE each iteration).
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+       unsigned long npages;
+       struct page **pages;
+       unsigned long i;
+
+       if (vm_start >= vm_end) {
+               /* All requested clusters fall within already-mapped pages. */
+               for (i = start_idx; i < start_idx + nr; i++)
+                       spin_lock_init(&si->cluster_info[i].lock);
+               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+               return 0;
+       }
+
+       npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+       if (!pages)
+               return -ENOMEM;
+
+       for (i = 0; i < npages; i++) {
+               /*
+                * __GFP_ZERO is critical: cluster_info structs contain pointer
+                * fields (extend_table, zero_bitmap, memcg_table, table) that
+                * must start as NULL.  Without zeroing, stale data from a
+                * previous user of the page would look like valid pointers.
+                */
+               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+               if (!pages[i])
+                       goto fail;
+       }
+
+       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+               i = npages; /* free all pages on failure */
+               goto fail;
+       }
+
+       kfree(pages);
+
+       /* Initialize spinlocks for newly mapped clusters */
+       for (i = start_idx; i < start_idx + nr; i++)
+               spin_lock_init(&si->cluster_info[i].lock);
+
+       /*
+        * Pairs with READ_ONCE() in shrink/grow paths.
+        */
+       WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+       return 0;
+
+fail:
+       while (i > 0) {
+               i--;
+               if (pages[i])
+                       __free_page(pages[i]);
+       }
+       kfree(pages);
+       return -ENOMEM;
+}
+
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * Round to page boundaries: start up (skip partial page that may
+        * contain clusters still in use before start_idx), end up so the
+        * entire range is covered.  vm_area_unmap_pages() operates on
+        * whole pages.
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+
+       if (vm_start >= vm_end)
+               goto out;
+
+       vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+       /*
+        * vm_area_unmap_pages() only clears PTEs; it does not free the
+        * physical pages.  Walk the page table to find and free them.
+        */
+       /* TODO: free backing pages via page table walk or tracking bitmap */
+       /*
+        * Pairs with READ_ONCE() in shrink/grow paths.
+        */
+       out:
+       WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+}
+#endif /* CONFIG_XSWAP */
+
 static int setup_swap_clusters_info(struct swap_info_struct *si,
                                    union swap_header *swap_header,
                                    unsigned long maxpages)
@@ -3573,6 +3720,63 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
        int err = -ENOMEM;
        unsigned long i;
 
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               unsigned long size = PAGE_ALIGN(nr_clusters * sizeof(*cluster_info));
+               struct vm_struct *vm;
+
+               vm = get_vm_area(size, VM_SPARSE);
+               if (!vm)
+                       goto err;
+
+               cluster_info = vm->addr;
+               si->cluster_vm = vm;
+               si->nr_clusters = nr_clusters;
+               si->cluster_info = cluster_info;
+
+               /* Map the initial chunk (at least cluster 0) */
+               if (xswap_map_clusters(si, 0, min_t(unsigned long,
+                                       XSWAP_GROW_CLUSTERS, nr_clusters)))
+                       goto err_free_vm;
+
+               /* xswap: only cluster 0 slot 0 is bad */
+               err = swap_cluster_setup_bad_slot(si, cluster_info, 0, false);
+               if (err)
+                       goto err_unmap;
+
+               INIT_LIST_HEAD(&si->free_clusters);
+               INIT_LIST_HEAD(&si->full_clusters);
+               INIT_LIST_HEAD(&si->discard_clusters);
+               for (i = 0; i < SWAP_NR_ORDERS; i++) {
+                       INIT_LIST_HEAD(&si->nonfull_clusters[i]);
+                       INIT_LIST_HEAD(&si->frag_clusters[i]);
+               }
+
+               /* Mark mapped clusters: cluster 0 has 1 bad slot, rest free */
+               for (i = 0; i < si->nr_clusters_mapped; i++) {
+                       struct swap_cluster_info *ci = &cluster_info[i];
+
+                       if (i == 0) {
+                               ci->flags = CLUSTER_FLAG_NONFULL;
+                               list_add_tail(&ci->list, &si->nonfull_clusters[0]);
+                       } else {
+                               ci->flags = CLUSTER_FLAG_FREE;
+                               list_add_tail(&ci->list, &si->free_clusters);
+                       }
+               }
+
+               return 0;
+
+err_unmap:
+               xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+err_free_vm:
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->cluster_info = NULL;
+               return err;
+       }
+#endif /* CONFIG_XSWAP */
+
        cluster_info = kvzalloc_objs(*cluster_info, nr_clusters);
        if (!cluster_info)
                goto err;
@@ -3640,7 +3844,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
        si->cluster_info = cluster_info;
        return 0;
 err:
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(si);
        return err;
 }
 
@@ -3852,7 +4056,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
        si->global_cluster = NULL;
        inode = NULL;
        destroy_swap_extents(si, swap_file);
-       free_swap_cluster_info(si->cluster_info, si->max);
+       free_swap_cluster_info(si);
        si->cluster_info = NULL;
        /*
         * Clear the SWP_USED flag after all resources are freed so
--

2.54.0

author reviewer
#8 lore raw

Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Quoted 21 lines · Chris Li
Quoted 11 lines · Baoquan He

Implement dynamic cluster_info array growth for xswap devices using a VM_SPARSE vmalloc area:

1. xswap_map_clusters(): Allocate physical pages and map them into the pre-reserved VM_SPARSE KVA region via vm_area_map_pages().

2. xswap_unmap_clusters(): Unmap pages from the VM_SPARSE area via vm_area_unmap_pages() (used by the error/teardown paths, shrink comes later).

Quoted 8 lines

3. setup_swap_clusters_info() xswap path: Use get_vm_area(VM_SPARSE) for the cluster_info array, lazily mapping only the initial chunk.

4. free_swap_cluster_info(): Refactor to take swap_info_struct*. For xswap, unmap all clusters and free_vm_area().

5. swapoff: Remove snapshot locals; move p->max/p->cluster_info clearing after free_swap_cluster_info().

Quoted 9 lines

I feel that we should provide more generic support for dynamically growing an array of element size A to size N. Cluster array is just a user of such an API. There might be more users. This patch hardcodes the map/umpa of clusters. What about other users of such a dynamic array?
Another consideration is that the map/unmap logic, while more generic, adds quite a bit of complexity to an approach intended only for growth. Any reason we want map/unmap rathern than simple grow from the current si->max?

That's because vmalloc virtual area need be allocated in advance.

Quoted 8 lines

I read a bit more about the patch. It seems you need the unmap for the error cleanup path. In that case I suspect just make cluster a kmem_cache allocation, cluster array just a pointer. The cluster pointer array should just grow, not shrink. That might be overall simpler.
I'm worried someone might abuse the cluster unmap causing unwanted side effects.

Quoted 5 lines

I see the other reason might be you want the cluster code to be exactly the same when XSWAP is off. For that I think it is acceptiale to turn cluster into kmem_cache allocated and cluster array as pointers array as seperate patch as the base even when XSWAP is not configed.

I think about your suggestion carefully. Instead of VM_SPARSE, cluster_info is now struct swap_cluster_info ** — a pointer array that only grows, never shrinks. Individual clusters are kmem_cache_zalloc()'d on demand and kmem_cache_free()'d during shrink. This completely eliminates the VM_SPARSE vmap/unmap complexity and, more importantly, gives us full control over GFP flags (__GFP_NOMEMALLOC) to avoid dipping into emergency reserves from reclaim context. The data structure would look like below:

include/linux/swap.h +5 -1
index 80c83dd8804f..dbb94686ca7c 100644
@@ -247,7 +247,7 @@ struct swap_info_struct {
        struct plist_node list;         /* entry in swap_active_head */
        signed char     type;           /* strange name for an index */
        unsigned int    max;            /* size of this swap device */
-       struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
+       struct swap_cluster_info **cluster_info; /* cluster pointer array */
        struct list_head free_clusters; /* free clusters list */
        struct list_head full_clusters; /* full clusters list */
        struct list_head nonfull_clusters[SWAP_NR_ORDERS];
@@ -277,6 +277,11 @@ struct swap_info_struct {
        struct list_head discard_clusters; /* discard clusters list */
        struct plist_node avail_list;   /* entry in swap_avail_head */
        const struct swap_ops *ops;
+#ifdef CONFIG_XSWAP
+       unsigned long nr_clusters;      /* total cluster count for xswap */
+       unsigned long nr_clusters_mapped; /* currently mapped cluster count */
+#endif
 };
 
 
mm/swap.h +2 -1
index abd26588abd2..f32dea150f76 100644
@@ -62,6 +62,7 @@ struct swap_cluster_info {
        unsigned long *zero_bitmap;
 #endif
        struct list_head list;
+       unsigned int idx;               /* index in si->cluster_info pointer array */
 };
 
@@ -402,7 +411,7 @@ static inline bool cluster_is_usable(struct swap_cluster_info *ci, int order)
 static inline unsigned int cluster_index(struct swap_info_struct *si,
                                         struct swap_cluster_info *ci)
 {
-       return ci - si->cluster_info;
+       return ci->idx;
 }
 

The pattern itself is very simple — allocate pointer array via kvmalloc_array, allocate elements via kmem_cache_zalloc, free elements via kmem_cache_free, never shrink the pointer array — so extraction should be straightforward. Seems this is doable and code change will be simpler than VM_SPARSE way. If this is agreed on, I can try to make a v2 in that way.

Thanks
Baoquan

Quoted 319 lines
Quoted 319 lines
---
 mm/swapfile.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 216 insertions(+), 12 deletions(-)
mm/swapfile.c +216 -13
index 143088dae07d..6d9c95ed09bd 100644
@@ -49,6 +49,24 @@
 #include "internal.h"
 #include "swap.h"
 
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation.  It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+       max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr);
+#endif
+
 static void swap_range_alloc(struct swap_info_struct *si,
                             unsigned int nr_entries);
 static bool folio_swapcache_freeable(struct folio *folio);
@@ -3041,20 +3059,47 @@ static void wait_for_allocation(struct swap_info_struct *si)
 
        BUG_ON(si->flags & SWP_WRITEOK);
 
+#ifdef CONFIG_XSWAP
+       /*
+        * xswap clusters beyond nr_clusters_mapped have been unmapped
+        * by the shrinker and their vmalloc pages are no longer
+        * accessible.  Only iterate over currently mapped clusters.
+        */
+       if (si->flags & SWP_XSWAP)
+               end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+                         SWAPFILE_CLUSTER);
+#endif
+
        for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
                ci = swap_cluster_lock(si, offset);
                swap_cluster_unlock(ci);
        }
 }
 
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
-                                  unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
 {
+       struct swap_cluster_info *cluster_info = si->cluster_info;
+       unsigned long maxpages = si->max;
        struct swap_cluster_info *ci;
-       int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+       int i, nr_clusters;
 
        if (!cluster_info)
                return;
+
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               /* Unmap all mapped clusters and free the VM_SPARSE area */
+               if (si->nr_clusters_mapped > 0)
+                       xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->nr_clusters = 0;
+               si->nr_clusters_mapped = 0;
+               return;
+       }
+#endif
+
+       nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        for (i = 0; i < nr_clusters; i++) {
                ci = cluster_info + i;
                /* Cluster with bad marks count will have a remaining table */
@@ -3093,11 +3138,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
 SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 {
        struct swap_info_struct *p = NULL;
-       struct swap_cluster_info *cluster_info;
        struct file *swap_file, *victim;
        struct address_space *mapping;
        struct inode *inode;
-       unsigned int maxpages;
        int err, found = 0;
 
        if (!capable(CAP_SYS_ADMIN))
@@ -3189,10 +3232,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
        swap_file = p->swap_file;
        p->swap_file = NULL;
-       maxpages = p->max;
-       cluster_info = p->cluster_info;
-       p->max = 0;
-       p->cluster_info = NULL;
        spin_unlock(&p->lock);
        spin_unlock(&swap_lock);
        arch_swap_invalidate_area(p->type);
@@ -3200,7 +3239,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
        mutex_unlock(&swapon_mutex);
        kfree(p->global_cluster);
        p->global_cluster = NULL;
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(p);
+       p->max = 0;
+       p->cluster_info = NULL;
 
        inode = mapping->host;
 
@@ -3564,6 +3605,112 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
        return maxpages;
 }
 
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * vm_area_map_pages() requires that start and end be page-aligned.
+        * If start_addr falls within a page that was already mapped by a
+        * previous batch (grow path), round it up to skip the already-mapped
+        * partial page.  Always round end_addr up so the vmap page table walk
+        * terminates correctly (the walk loop exits when addr == end, and addr
+        * advances by PAGE_SIZE each iteration).
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+       unsigned long npages;
+       struct page **pages;
+       unsigned long i;
+
+       if (vm_start >= vm_end) {
+               /* All requested clusters fall within already-mapped pages. */
+               for (i = start_idx; i < start_idx + nr; i++)
+                       spin_lock_init(&si->cluster_info[i].lock);
+               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+               return 0;
+       }
+
+       npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+       if (!pages)
+               return -ENOMEM;
+
+       for (i = 0; i < npages; i++) {
+               /*
+                * __GFP_ZERO is critical: cluster_info structs contain pointer
+                * fields (extend_table, zero_bitmap, memcg_table, table) that
+                * must start as NULL.  Without zeroing, stale data from a
+                * previous user of the page would look like valid pointers.
+                */
+               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+               if (!pages[i])
+                       goto fail;
+       }
+
+       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+               i = npages; /* free all pages on failure */
+               goto fail;
+       }
+
+       kfree(pages);
+
+       /* Initialize spinlocks for newly mapped clusters */
+       for (i = start_idx; i < start_idx + nr; i++)
+               spin_lock_init(&si->cluster_info[i].lock);
+
+       /*
+        * Pairs with READ_ONCE() in shrink/grow paths.
+        */
+       WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+       return 0;
+
+fail:
+       while (i > 0) {
+               i--;
+               if (pages[i])
+                       __free_page(pages[i]);
+       }
+       kfree(pages);
+       return -ENOMEM;
+}
+
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * Round to page boundaries: start up (skip partial page that may
+        * contain clusters still in use before start_idx), end up so the
+        * entire range is covered.  vm_area_unmap_pages() operates on
+        * whole pages.
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+
+       if (vm_start >= vm_end)
+               goto out;
+
+       vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+       /*
+        * vm_area_unmap_pages() only clears PTEs; it does not free the
+        * physical pages.  Walk the page table to find and free them.
+        */
+       /* TODO: free backing pages via page table walk or tracking bitmap */
+       /*
+        * Pairs with READ_ONCE() in shrink/grow paths.
+        */
+       out:
+       WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+}
+#endif /* CONFIG_XSWAP */
+
 static int setup_swap_clusters_info(struct swap_info_struct *si,
                                    union swap_header *swap_header,
                                    unsigned long maxpages)
@@ -3573,6 +3720,63 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
        int err = -ENOMEM;
        unsigned long i;
 
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               unsigned long size = PAGE_ALIGN(nr_clusters * sizeof(*cluster_info));
+               struct vm_struct *vm;
+
+               vm = get_vm_area(size, VM_SPARSE);
+               if (!vm)
+                       goto err;
+
+               cluster_info = vm->addr;
+               si->cluster_vm = vm;
+               si->nr_clusters = nr_clusters;
+               si->cluster_info = cluster_info;
+
+               /* Map the initial chunk (at least cluster 0) */
+               if (xswap_map_clusters(si, 0, min_t(unsigned long,
+                                       XSWAP_GROW_CLUSTERS, nr_clusters)))
+                       goto err_free_vm;
+
+               /* xswap: only cluster 0 slot 0 is bad */
+               err = swap_cluster_setup_bad_slot(si, cluster_info, 0, false);
+               if (err)
+                       goto err_unmap;
+
+               INIT_LIST_HEAD(&si->free_clusters);
+               INIT_LIST_HEAD(&si->full_clusters);
+               INIT_LIST_HEAD(&si->discard_clusters);
+               for (i = 0; i < SWAP_NR_ORDERS; i++) {
+                       INIT_LIST_HEAD(&si->nonfull_clusters[i]);
+                       INIT_LIST_HEAD(&si->frag_clusters[i]);
+               }
+
+               /* Mark mapped clusters: cluster 0 has 1 bad slot, rest free */
+               for (i = 0; i < si->nr_clusters_mapped; i++) {
+                       struct swap_cluster_info *ci = &cluster_info[i];
+
+                       if (i == 0) {
+                               ci->flags = CLUSTER_FLAG_NONFULL;
+                               list_add_tail(&ci->list, &si->nonfull_clusters[0]);
+                       } else {
+                               ci->flags = CLUSTER_FLAG_FREE;
+                               list_add_tail(&ci->list, &si->free_clusters);
+                       }
+               }
+
+               return 0;
+
+err_unmap:
+               xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+err_free_vm:
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->cluster_info = NULL;
+               return err;
+       }
+#endif /* CONFIG_XSWAP */
+
        cluster_info = kvzalloc_objs(*cluster_info, nr_clusters);
        if (!cluster_info)
                goto err;
@@ -3640,7 +3844,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
        si->cluster_info = cluster_info;
        return 0;
 err:
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(si);
        return err;
 }
 
@@ -3852,7 +4056,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
        si->global_cluster = NULL;
        inode = NULL;
        destroy_swap_extents(si, swap_file);
-       free_swap_cluster_info(si->cluster_info, si->max);
+       free_swap_cluster_info(si);
        si->cluster_info = NULL;
        /*
         * Clear the SWP_USED flag after all resources are freed so
--

2.54.0

author reviewer
#9 lore raw

Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Quoted 2 lines · Baoquan He
Quoted 1 line · Chris Li

On Mon, Jul 27, 2026 at 7:05 AM Baoquan He <[email protected]> wrote:

...snip...

Quoted 13 lines
Quoted 7 lines

I read a bit more about the patch. It seems you need the unmap for the error cleanup path. In that case I suspect just make cluster a kmem_cache allocation, cluster array just a pointer. The cluster pointer array should just grow, not shrink. That might be overall simpler.
I'm worried someone might abuse the cluster unmap causing unwanted side effects.

Quoted 5 lines

I see the other reason might be you want the cluster code to be exactly the same when XSWAP is off. For that I think it is acceptiale to turn cluster into kmem_cache allocated and cluster array as pointers array as seperate patch as the base even when XSWAP is not configed.

Quoted 8 lines

I think about your suggestion carefully. Instead of VM_SPARSE, cluster_info is now struct swap_cluster_info ** — a pointer array that only grows, never shrinks. Individual clusters are kmem_cache_zalloc()'d on demand and kmem_cache_free()'d during shrink. This completely eliminates the VM_SPARSE vmap/unmap complexity and, more importantly, gives us full control over GFP flags (__GFP_NOMEMALLOC) to avoid dipping into emergency reserves from reclaim context. The data structure would look like below:

Forgot mentioning the kmalloc way will cost extra 8 byte for each cluster because of swap_cluster_info ** pointer array. That will be 8K for 1G swap space, and 8M for 1TB swap space. With VM_SPARSE, that's not needed.

Quoted 372 lines
include/linux/swap.h +5 -1
index 80c83dd8804f..dbb94686ca7c 100644
@@ -247,7 +247,7 @@ struct swap_info_struct {
        struct plist_node list;         /* entry in swap_active_head */
        signed char     type;           /* strange name for an index */
        unsigned int    max;            /* size of this swap device */
-       struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
+       struct swap_cluster_info **cluster_info; /* cluster pointer array */
        struct list_head free_clusters; /* free clusters list */
        struct list_head full_clusters; /* full clusters list */
        struct list_head nonfull_clusters[SWAP_NR_ORDERS];
@@ -277,6 +277,11 @@ struct swap_info_struct {
        struct list_head discard_clusters; /* discard clusters list */
        struct plist_node avail_list;   /* entry in swap_avail_head */
        const struct swap_ops *ops;
+#ifdef CONFIG_XSWAP
+       unsigned long nr_clusters;      /* total cluster count for xswap */
+       unsigned long nr_clusters_mapped; /* currently mapped cluster count */
+#endif
 };
 
 
mm/swap.h +2 -1
index abd26588abd2..f32dea150f76 100644
@@ -62,6 +62,7 @@ struct swap_cluster_info {
        unsigned long *zero_bitmap;
 #endif
        struct list_head list;
+       unsigned int idx;               /* index in si->cluster_info pointer array */
 };
 
@@ -402,7 +411,7 @@ static inline bool cluster_is_usable(struct swap_cluster_info *ci, int order)
 static inline unsigned int cluster_index(struct swap_info_struct *si,
                                         struct swap_cluster_info *ci)
 {
-       return ci - si->cluster_info;
+       return ci->idx;
 }
 
 The pattern itself is very simple — allocate pointer array via kvmalloc_array,

allocate elements via kmem_cache_zalloc, free elements via kmem_cache_free, never shrink the pointer array — so extraction should be straightforward. Seems this is doable and code change will be simpler than VM_SPARSE way. If this is agreed on, I can try to make a v2 in that way.

Thanks
Baoquan

Quoted 319 lines
Quoted 319 lines
---
 mm/swapfile.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 216 insertions(+), 12 deletions(-)
mm/swapfile.c +216 -13
index 143088dae07d..6d9c95ed09bd 100644
@@ -49,6 +49,24 @@
 #include "internal.h"
 #include "swap.h"
 
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation.  It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+       max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr);
+#endif
+
 static void swap_range_alloc(struct swap_info_struct *si,
                             unsigned int nr_entries);
 static bool folio_swapcache_freeable(struct folio *folio);
@@ -3041,20 +3059,47 @@ static void wait_for_allocation(struct swap_info_struct *si)
 
        BUG_ON(si->flags & SWP_WRITEOK);
 
+#ifdef CONFIG_XSWAP
+       /*
+        * xswap clusters beyond nr_clusters_mapped have been unmapped
+        * by the shrinker and their vmalloc pages are no longer
+        * accessible.  Only iterate over currently mapped clusters.
+        */
+       if (si->flags & SWP_XSWAP)
+               end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+                         SWAPFILE_CLUSTER);
+#endif
+
        for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
                ci = swap_cluster_lock(si, offset);
                swap_cluster_unlock(ci);
        }
 }
 
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
-                                  unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
 {
+       struct swap_cluster_info *cluster_info = si->cluster_info;
+       unsigned long maxpages = si->max;
        struct swap_cluster_info *ci;
-       int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+       int i, nr_clusters;
 
        if (!cluster_info)
                return;
+
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               /* Unmap all mapped clusters and free the VM_SPARSE area */
+               if (si->nr_clusters_mapped > 0)
+                       xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->nr_clusters = 0;
+               si->nr_clusters_mapped = 0;
+               return;
+       }
+#endif
+
+       nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        for (i = 0; i < nr_clusters; i++) {
                ci = cluster_info + i;
                /* Cluster with bad marks count will have a remaining table */
@@ -3093,11 +3138,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
 SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 {
        struct swap_info_struct *p = NULL;
-       struct swap_cluster_info *cluster_info;
        struct file *swap_file, *victim;
        struct address_space *mapping;
        struct inode *inode;
-       unsigned int maxpages;
        int err, found = 0;
 
        if (!capable(CAP_SYS_ADMIN))
@@ -3189,10 +3232,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
        swap_file = p->swap_file;
        p->swap_file = NULL;
-       maxpages = p->max;
-       cluster_info = p->cluster_info;
-       p->max = 0;
-       p->cluster_info = NULL;
        spin_unlock(&p->lock);
        spin_unlock(&swap_lock);
        arch_swap_invalidate_area(p->type);
@@ -3200,7 +3239,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
        mutex_unlock(&swapon_mutex);
        kfree(p->global_cluster);
        p->global_cluster = NULL;
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(p);
+       p->max = 0;
+       p->cluster_info = NULL;
 
        inode = mapping->host;
 
@@ -3564,6 +3605,112 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
        return maxpages;
 }
 
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+                             unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * vm_area_map_pages() requires that start and end be page-aligned.
+        * If start_addr falls within a page that was already mapped by a
+        * previous batch (grow path), round it up to skip the already-mapped
+        * partial page.  Always round end_addr up so the vmap page table walk
+        * terminates correctly (the walk loop exits when addr == end, and addr
+        * advances by PAGE_SIZE each iteration).
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+       unsigned long npages;
+       struct page **pages;
+       unsigned long i;
+
+       if (vm_start >= vm_end) {
+               /* All requested clusters fall within already-mapped pages. */
+               for (i = start_idx; i < start_idx + nr; i++)
+                       spin_lock_init(&si->cluster_info[i].lock);
+               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+               return 0;
+       }
+
+       npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
+       if (!pages)
+               return -ENOMEM;
+
+       for (i = 0; i < npages; i++) {
+               /*
+                * __GFP_ZERO is critical: cluster_info structs contain pointer
+                * fields (extend_table, zero_bitmap, memcg_table, table) that
+                * must start as NULL.  Without zeroing, stale data from a
+                * previous user of the page would look like valid pointers.
+                */
+               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
+               if (!pages[i])
+                       goto fail;
+       }
+
+       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
+               i = npages; /* free all pages on failure */
+               goto fail;
+       }
+
+       kfree(pages);
+
+       /* Initialize spinlocks for newly mapped clusters */
+       for (i = start_idx; i < start_idx + nr; i++)
+               spin_lock_init(&si->cluster_info[i].lock);
+
+       /*
+        * Pairs with READ_ONCE() in shrink/grow paths.
+        */
+       WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+       return 0;
+
+fail:
+       while (i > 0) {
+               i--;
+               if (pages[i])
+                       __free_page(pages[i]);
+       }
+       kfree(pages);
+       return -ENOMEM;
+}
+
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+                                unsigned long start_idx, unsigned long nr)
+{
+       unsigned long start_addr = (unsigned long)si->cluster_info +
+                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
+       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+       /*
+        * Round to page boundaries: start up (skip partial page that may
+        * contain clusters still in use before start_idx), end up so the
+        * entire range is covered.  vm_area_unmap_pages() operates on
+        * whole pages.
+        */
+       unsigned long vm_start = PAGE_ALIGN(start_addr);
+       unsigned long vm_end = PAGE_ALIGN(end_addr);
+
+       if (vm_start >= vm_end)
+               goto out;
+
+       vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+       /*
+        * vm_area_unmap_pages() only clears PTEs; it does not free the
+        * physical pages.  Walk the page table to find and free them.
+        */
+       /* TODO: free backing pages via page table walk or tracking bitmap */
+       /*
+        * Pairs with READ_ONCE() in shrink/grow paths.
+        */
+       out:
+       WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+}
+#endif /* CONFIG_XSWAP */
+
 static int setup_swap_clusters_info(struct swap_info_struct *si,
                                    union swap_header *swap_header,
                                    unsigned long maxpages)
@@ -3573,6 +3720,63 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
        int err = -ENOMEM;
        unsigned long i;
 
+#ifdef CONFIG_XSWAP
+       if (si->flags & SWP_XSWAP) {
+               unsigned long size = PAGE_ALIGN(nr_clusters * sizeof(*cluster_info));
+               struct vm_struct *vm;
+
+               vm = get_vm_area(size, VM_SPARSE);
+               if (!vm)
+                       goto err;
+
+               cluster_info = vm->addr;
+               si->cluster_vm = vm;
+               si->nr_clusters = nr_clusters;
+               si->cluster_info = cluster_info;
+
+               /* Map the initial chunk (at least cluster 0) */
+               if (xswap_map_clusters(si, 0, min_t(unsigned long,
+                                       XSWAP_GROW_CLUSTERS, nr_clusters)))
+                       goto err_free_vm;
+
+               /* xswap: only cluster 0 slot 0 is bad */
+               err = swap_cluster_setup_bad_slot(si, cluster_info, 0, false);
+               if (err)
+                       goto err_unmap;
+
+               INIT_LIST_HEAD(&si->free_clusters);
+               INIT_LIST_HEAD(&si->full_clusters);
+               INIT_LIST_HEAD(&si->discard_clusters);
+               for (i = 0; i < SWAP_NR_ORDERS; i++) {
+                       INIT_LIST_HEAD(&si->nonfull_clusters[i]);
+                       INIT_LIST_HEAD(&si->frag_clusters[i]);
+               }
+
+               /* Mark mapped clusters: cluster 0 has 1 bad slot, rest free */
+               for (i = 0; i < si->nr_clusters_mapped; i++) {
+                       struct swap_cluster_info *ci = &cluster_info[i];
+
+                       if (i == 0) {
+                               ci->flags = CLUSTER_FLAG_NONFULL;
+                               list_add_tail(&ci->list, &si->nonfull_clusters[0]);
+                       } else {
+                               ci->flags = CLUSTER_FLAG_FREE;
+                               list_add_tail(&ci->list, &si->free_clusters);
+                       }
+               }
+
+               return 0;
+
+err_unmap:
+               xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+err_free_vm:
+               free_vm_area(si->cluster_vm);
+               si->cluster_vm = NULL;
+               si->cluster_info = NULL;
+               return err;
+       }
+#endif /* CONFIG_XSWAP */
+
        cluster_info = kvzalloc_objs(*cluster_info, nr_clusters);
        if (!cluster_info)
                goto err;
@@ -3640,7 +3844,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
        si->cluster_info = cluster_info;
        return 0;
 err:
-       free_swap_cluster_info(cluster_info, maxpages);
+       free_swap_cluster_info(si);
        return err;
 }
 
@@ -3852,7 +4056,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
        si->global_cluster = NULL;
        inode = NULL;
        destroy_swap_extents(si, swap_file);
-       free_swap_cluster_info(si->cluster_info, si->max);
+       free_swap_cluster_info(si);
        si->cluster_info = NULL;
        /*
         * Clear the SWP_USED flag after all resources are freed so
--

2.54.0

author reviewer
#10 lore raw

RFC PATCH 4/11 mm, swap: add xswap grow trigger on cluster allocation

In reply to Baoquan He

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, no locks need to be dropped before calling xswap_map_clusters() — global_cluster_lock is never held on this path.

The grow sequence:
1. Check nr_clusters_mapped < nr_clusters and free list empty
2. Call xswap_map_clusters() to allocate and map more physical pages
3. Add newly mapped clusters to si->free_clusters under si->lock
4. Retry allocation from the fresh free clusters

This makes the xswap cluster space grow transparently as swap usage increases, without any userspace intervention.

---
 mm/swapfile.c | 106 ++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 103 insertions(+), 3 deletions(-)
mm/swapfile.c +103 -3
index 6d9c95ed09bd..8a048b2897ce 100644
@@ -65,6 +65,7 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 			      unsigned long start_idx, unsigned long nr);
 static void xswap_unmap_clusters(struct swap_info_struct *si,
 				 unsigned long start_idx, unsigned long nr);
+static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
 #endif
 
 static void swap_range_alloc(struct swap_info_struct *si,
@@ -1204,6 +1205,48 @@ 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) &&
+	    list_empty(&si->free_clusters)) {
+		unsigned long nr_new = min(READ_ONCE(si->nr_clusters) -
+					  READ_ONCE(si->nr_clusters_mapped),
+					  XSWAP_GROW_CLUSTERS);
+		unsigned long start = READ_ONCE(si->nr_clusters_mapped);
+		unsigned long i;
+
+		if (!xswap_map_clusters(si, start, nr_new)) {
+			unsigned long added = 0;
+
+			spin_lock(&si->lock);
+			for (i = start; i < start + nr_new; i++) {
+				struct swap_cluster_info *ci = &si->cluster_info[i];
+				spin_lock(&ci->lock);
+				/*
+				 * A concurrent grower may have already added
+				 * these clusters to the free list.  Only add
+				 * clusters that are still off-list (NONE).
+				 */
+				if (ci->flags == CLUSTER_FLAG_NONE) {
+					ci->flags = CLUSTER_FLAG_FREE;
+					list_add_tail(&ci->list, &si->free_clusters);
+					added++;
+				}
+				spin_unlock(&ci->lock);
+			}
+			spin_unlock(&si->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);
@@ -3652,9 +3695,37 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 			goto fail;
 	}
 
-	if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
-		i = npages; /* free all pages on failure */
-		goto fail;
+	/*
+	 * Check if the target pages are already mapped by a concurrent
+	 * grower.  We must do this after page allocation because
+	 * alloc_page(GFP_KERNEL) can sleep, opening a race window.
+	 * If someone already mapped these pages, free ours and continue.
+	 */
+	if (apply_to_existing_page_range(&init_mm, vm_start,
+					 vm_end - vm_start,
+					 xswap_check_mapped, NULL)) {
+		i = npages;
+		goto fail_nounmap;
+	}
+
+	{
+		int err = vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages);
+
+		if (err) {
+			/*
+			 * -EBUSY means the PTEs are already present:
+			 * another thread raced with us and mapped the
+			 * same pages between our check above and this
+			 * call.  Treat as success — free our unused
+			 * pages and continue.
+			 */
+			if (err == -EBUSY) {
+				i = npages;
+				goto fail_nounmap;
+			}
+			i = npages; /* free all pages on failure */
+			goto fail;
+		}
 	}
 
 	kfree(pages);
@@ -3669,6 +3740,26 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 	WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
 	return 0;
 
+fail_nounmap:
+	/*
+	 * Pages already mapped by a concurrent grower.  Free our unused
+	 * pages, then fall through to initialize spinlocks.  The vmalloc
+	 * PTEs now point to the concurrent grower's pages.
+	 */
+	while (i > 0) {
+		i--;
+		if (pages[i])
+			__free_page(pages[i]);
+	}
+	kfree(pages);
+
+	/* Initialize spinlocks for newly mapped clusters */
+	for (i = start_idx; i < start_idx + nr; i++)
+		spin_lock_init(&si->cluster_info[i].lock);
+
+	WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+	return 0;
+
 fail:
 	while (i > 0) {
 		i--;
@@ -3709,6 +3800,15 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
 	out:
 	WRITE_ONCE(si->nr_clusters_mapped, start_idx);
 }
+
+/*
+ * Callback for apply_to_existing_page_range(): return 1 to stop at the
+ * first present PTE, signalling that the range is already mapped.
+ */
+static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data)
+{
+	return 1;
+}
 #endif /* CONFIG_XSWAP */
 
 static int setup_swap_clusters_info(struct swap_info_struct *si,
Signature · 1 line
2.54.0
author reviewer
#11 lore raw

RFC PATCH 5/11 mm, swap: add xswap_try_shrink and shrink trigger on cluster free

In reply to Baoquan He

Add xswap_try_shrink() — the shrink logic that scans backwards from the tail to find contiguous free clusters, then unmaps full pages when >= XSWAP_GROW_CLUSTERS free clusters accumulate.

Wire the trigger in __free_cluster(): after a cluster is released to the free list, call xswap_try_shrink() to attempt tail shrinking. Also update the XSWAP_GROW_CLUSTERS comment to reflect both grow and shrink semantics.

---
 mm/swapfile.c | 48 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 44 insertions(+), 4 deletions(-)
mm/swapfile.c +44 -4
index 8a048b2897ce..e0f5fe64de2a 100644
@@ -51,11 +51,13 @@
 
 #ifdef CONFIG_XSWAP
 /*
- * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ * xswap: dynamically grow and shrink the cluster_info array via a
+ * VM_SPARSE area.
  *
- * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
- * operation.  It is set to the number of cluster_info structs that
- * fit in a single page (at least 16), so that the vmalloc page table
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map/unmap in one
+ * grow/shrink operation.  It is set to the number of cluster_info
+ * structs that fit in a single page (at least 16), so that the vmalloc
+ * page table
  * overhead is proportional to the number of clusters mapped.
  */
 #define XSWAP_GROW_CLUSTERS \
@@ -66,6 +68,7 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 static void xswap_unmap_clusters(struct swap_info_struct *si,
 				 unsigned long start_idx, unsigned long nr);
 static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
+static void xswap_try_shrink(struct swap_info_struct *si);
 #endif
 
 static void swap_range_alloc(struct swap_info_struct *si,
@@ -629,6 +632,9 @@ static void __free_cluster(struct swap_info_struct *si, struct swap_cluster_info
 	swap_cluster_free_table(ci);
 	move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE);
 	ci->order = 0;
+#ifdef CONFIG_XSWAP
+	xswap_try_shrink(si);
+#endif
 }
 
 /*
@@ -3809,6 +3815,40 @@ static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data)
 {
 	return 1;
 }
+
+/*
+ * Try to shrink the cluster_info tail: unmap contiguous free clusters
+ * at the end of the mapped range.
+ */
+static void xswap_try_shrink(struct swap_info_struct *si)
+{
+	struct swap_cluster_info *ci;
+	unsigned long last, idx;
+
+	if (!(si->flags & SWP_XSWAP))
+		return;
+	if (READ_ONCE(si->nr_clusters_mapped) <= 1) /* keep cluster 0 */
+		return;
+
+	/* Find the last non-free cluster from the tail */
+	last = READ_ONCE(si->nr_clusters_mapped);
+	while (last > 1) {
+		idx = last - 1;
+		ci = &si->cluster_info[idx];
+		if (ci->count || ci->flags != CLUSTER_FLAG_FREE)
+			break;
+		last = idx;
+	}
+
+	if (last == si->nr_clusters_mapped)
+		return; /* nothing to shrink */
+
+	/* Only unmap if we can free at least one full page of clusters */
+	if (si->nr_clusters_mapped - last < XSWAP_GROW_CLUSTERS)
+		return;
+
+	xswap_unmap_clusters(si, last, si->nr_clusters_mapped - last);
+}
 #endif /* CONFIG_XSWAP */
 
 static int setup_swap_clusters_info(struct swap_info_struct *si,
Signature · 1 line
2.54.0
author reviewer
#12 lore raw

RFC PATCH 6/11 mm, swap: free backing pages in xswap_unmap_clusters

In reply to Baoquan He

vm_area_unmap_pages() only clears PTEs and frees intermediate page table pages — it does not free the backing physical pages allocated by xswap_map_clusters().

Fix this by walking the page table with apply_to_existing_page_range() before the unmap to collect all struct pages in the range. After vunmap_range() clears the PTEs, free the collected pages via __free_page().

Use a simple xswap_page_data collector callback: for each present PTE, collect pte_page() into a dynamically allocated array. The array is freed after the pages are released.

---
 mm/swapfile.c | 51 +++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 45 insertions(+), 6 deletions(-)
mm/swapfile.c +45 -6
index e0f5fe64de2a..7613ce231d70 100644
@@ -3776,6 +3776,23 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 	return -ENOMEM;
 }
 
+struct xswap_page_data {
+	struct page **pages;
+	int nr;
+	int max;
+};
+
+static int xswap_collect_page(pte_t *pte, unsigned long addr, void *data)
+{
+	struct xswap_page_data *xpd = data;
+
+	if (!pte_present(*pte))
+		return 0;
+	if (xpd->nr < xpd->max)
+		xpd->pages[xpd->nr++] = pte_page(*pte);
+	return 0;
+}
+
 static void xswap_unmap_clusters(struct swap_info_struct *si,
 				 unsigned long start_idx, unsigned long nr)
 {
@@ -3785,21 +3802,43 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
 	/*
 	 * Round to page boundaries: start up (skip partial page that may
 	 * contain clusters still in use before start_idx), end up so the
-	 * entire range is covered.  vm_area_unmap_pages() operates on
-	 * whole pages.
+	 * entire range is covered.  vm_area_unmap_pages() and
+	 * apply_to_existing_page_range() operate on whole pages.
 	 */
 	unsigned long vm_start = PAGE_ALIGN(start_addr);
 	unsigned long vm_end = PAGE_ALIGN(end_addr);
+	unsigned long size;
+	unsigned long npages;
+	struct xswap_page_data xpd;
+	int i;
 
 	if (vm_start >= vm_end)
 		goto out;
 
-	vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+	size = vm_end - vm_start;
+	npages = size >> PAGE_SHIFT;
+
 	/*
-	 * vm_area_unmap_pages() only clears PTEs; it does not free the
-	 * physical pages.  Walk the page table to find and free them.
+	 * Walk the page table to collect physical pages before unmapping.
+	 * vm_area_unmap_pages() only clears PTEs and frees intermediate
+	 * page table pages — it does not free backing pages.
 	 */
-	/* TODO: free backing pages via page table walk or tracking bitmap */
+	xpd.pages = kmalloc_array(npages, sizeof(*xpd.pages), GFP_KERNEL);
+	if (xpd.pages) {
+		xpd.nr = 0;
+		xpd.max = npages;
+		apply_to_existing_page_range(&init_mm, vm_start, size,
+					     xswap_collect_page, &xpd);
+	}
+
+	vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+
+	/* Free the collected backing pages */
+	if (xpd.pages) {
+		for (i = 0; i < xpd.nr; i++)
+			__free_page(xpd.pages[i]);
+		kfree(xpd.pages);
+	}
 	/*
 	 * Pairs with READ_ONCE() in shrink/grow paths.
 	 */
Signature · 1 line
2.54.0
author reviewer
#13 lore raw

RFC PATCH 7/11 mm, swap: add nr_free_tail for O(1) xswap shrink detection

In reply to Baoquan He

Track contiguous free clusters at the tail of the mapped range in si->nr_free_tail, maintained across alloc/free/grow paths. This eliminates the backwards scan on every shrink check.

Three paths maintain the counter:

1. xswap_update_free_tail(): called on cluster free. If the freed cluster is adjacent to the existing tail boundary, increment and extend backwards to include already-free clusters now connected.

2. xswap_trim_free_tail(): called on cluster allocation. If the allocated cluster lies within the tail free region, truncate the count to end just before it.

3. Grow path: nr_free_tail += nr_new — all newly mapped clusters are immediately free.

xswap_try_shrink() simplifies to a threshold check:
if nr_free_tail >= XSWAP_GROW_CLUSTERS → unmap

Setup initializes nr_free_tail = nr_clusters_mapped - 1 (all but cluster 0 are free at the tail).

---
 include/linux/swap.h |   1 +
 mm/swapfile.c        | 121 +++++++++++++++++++++++++++++++++++++------
 2 files changed, 105 insertions(+), 17 deletions(-)
include/linux/swap.h +1 -0
index 5f6b14d30758..c54c921378ab 100644
@@ -252,6 +252,7 @@ struct swap_info_struct {
 	struct vm_struct	*cluster_vm;	/* VM_SPARSE area for xswap dynamic cluster_info */
 	unsigned long		nr_clusters;	/* total cluster count for xswap */
 	unsigned long		nr_clusters_mapped; /* currently mapped cluster count */
+	unsigned long		nr_free_tail;	/* contiguous free clusters at tail */
 #endif
 	struct list_head free_clusters; /* free clusters list */
 	struct list_head full_clusters; /* full clusters list */
mm/swapfile.c +104 -17

165 lines not on this page · raw message

Signature · 1 line
2.54.0
author reviewer
#14 lore raw

RFC PATCH 8/11 mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap

In reply to Baoquan He

Split the xswap cluster limit into two fields:
- nr_clusters_max: immutable hard limit set at swapon from swap header
- nr_clusters: current growth ceiling, adjustable at runtime (≤ nr_clusters_max)

The grow path already uses nr_clusters as the ceiling. Shrink now also respects it: when nr_clusters drops below nr_clusters_mapped, shrinking fires on free until the mapped count reaches the ceiling. When nr_clusters == nr_clusters_max (default), shrink is effectively disabled — all growth and no shrink.

At swapon, nr_clusters starts at nr_clusters_max (full size).

---
 include/linux/swap.h |  3 ++-
 mm/swapfile.c        | 32 +++++++++++++++++++++++---------
 2 files changed, 25 insertions(+), 10 deletions(-)
include/linux/swap.h +2 -1
index c54c921378ab..e4512aeead36 100644
@@ -250,7 +250,8 @@ struct swap_info_struct {
 	struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
 #ifdef CONFIG_XSWAP
 	struct vm_struct	*cluster_vm;	/* VM_SPARSE area for xswap dynamic cluster_info */
-	unsigned long		nr_clusters;	/* total cluster count for xswap */
+	unsigned long		nr_clusters_max; /* upper limit from swap header */
+	unsigned long		nr_clusters;	/* current growth ceiling (≤ nr_clusters_max) */
 	unsigned long		nr_clusters_mapped; /* currently mapped cluster count */
 	unsigned long		nr_free_tail;	/* contiguous free clusters at tail */
 #endif
mm/swapfile.c +23 -9
index 1eb44f818b40..f57823d6449f 100644
@@ -72,6 +72,7 @@ static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
 static void xswap_update_free_tail(struct swap_info_struct *si,
 				    unsigned long freed_idx);
 static void xswap_try_shrink(struct swap_info_struct *si);
+
 #endif
 
 static void swap_range_alloc(struct swap_info_struct *si,
@@ -3151,6 +3152,7 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
 			xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
 		free_vm_area(si->cluster_vm);
 		si->cluster_vm = NULL;
+		si->nr_clusters_max = 0;
 		si->nr_clusters = 0;
 		si->nr_clusters_mapped = 0;
 		return;
@@ -3945,20 +3947,31 @@ static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
  */
 static void xswap_try_shrink(struct swap_info_struct *si)
 {
-	unsigned long start_idx, nr_unmap, i;
+	unsigned long nr_mapped, nr_ceiling, nr_tail, nr_unmap;
+	unsigned long start_idx, i;
 	struct swap_cluster_info *ci;
 
 	if (!(si->flags & SWP_XSWAP))
 		return;
-	if (si->nr_free_tail < XSWAP_GROW_CLUSTERS)
+
+	nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+	nr_ceiling = READ_ONCE(si->nr_clusters);
+	nr_tail = READ_ONCE(si->nr_free_tail);
+
+	if (nr_mapped <= nr_ceiling)
+		return;
+	if (nr_tail < XSWAP_GROW_CLUSTERS)
 		return;
 
-	nr_unmap = round_down(si->nr_free_tail, XSWAP_GROW_CLUSTERS);
-	start_idx = si->nr_clusters_mapped - nr_unmap;
+	nr_unmap = min(round_down(nr_tail, XSWAP_GROW_CLUSTERS),
+		       nr_mapped - nr_ceiling);
+	if (nr_unmap < XSWAP_GROW_CLUSTERS)
+		return;
+	start_idx = nr_mapped - nr_unmap;
 
 	/* Verify the tail clusters are still free before unmapping */
 	spin_lock(&si->lock);
-	for (i = start_idx; i < si->nr_clusters_mapped; i++) {
+	for (i = start_idx; i < nr_mapped; i++) {
 		ci = &si->cluster_info[i];
 		if (ci->flags != CLUSTER_FLAG_FREE) {
 			nr_unmap = i - start_idx;
@@ -3973,7 +3986,7 @@ static void xswap_try_shrink(struct swap_info_struct *si)
 		return;
 
 	xswap_unmap_clusters(si, start_idx, nr_unmap);
-	si->nr_free_tail -= nr_unmap;
+	WRITE_ONCE(si->nr_free_tail, nr_tail - nr_unmap);
 }
 #endif /* CONFIG_XSWAP */
 
@@ -3997,6 +4010,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
 
 		cluster_info = vm->addr;
 		si->cluster_vm = vm;
+		si->nr_clusters_max = nr_clusters;
 		si->nr_clusters = nr_clusters;
 		si->cluster_info = cluster_info;
 
@@ -4031,6 +4045,9 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
 			}
 		}
 
+		/* All mapped clusters except cluster 0 are free at the tail */
+		si->nr_free_tail = si->nr_clusters_mapped - 1;
+
 		return 0;
 
 err_unmap:
@@ -4434,7 +4451,6 @@ void __folio_throttle_swaprate(struct folio *folio, gfp_t gfp)
 static int __init swapfile_init(void)
 {
 	swapfile_maximum_size = arch_max_swapfile_size();
-
 	/*
 	 * Once a cluster is freed, it's swap table content is read
 	 * only, and all swap cache readers (swap_cache_*) verifies
@@ -4444,12 +4460,10 @@ static int __init swapfile_init(void)
 		swap_table_cachep = kmem_cache_create("swap_table",
 				    sizeof(struct swap_table),
 				    0, SLAB_PANIC | SLAB_TYPESAFE_BY_RCU, NULL);
-
 #ifdef CONFIG_MIGRATION
 	if (swapfile_maximum_size >= (1UL << SWP_MIG_TOTAL_BITS))
 		swap_migration_ad_supported = true;
 #endif	/* CONFIG_MIGRATION */
-
 	return 0;
 }
 subsys_initcall(swapfile_init);
Signature · 1 line
2.54.0
author reviewer
#15 lore raw

RFC PATCH 9/11 mm, swap: add debugfs knob for xswap per-device cluster limit

In reply to Baoquan He

Add a per-device debugfs file for runtime adjustment of xswap cluster limit:
/sys/kernel/debug/xswap/type<N>_cluster_limit

Reading shows the current ceiling (in clusters); writing sets it (clamped to [0, nr_clusters_max]). Setting below nr_clusters_mapped triggers an immediate shrink check via xswap_try_shrink().

The debugfs entry is created at swapon and removed at swapoff.

---
 include/linux/swap.h |  1 +
 mm/swapfile.c        | 97 +++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 97 insertions(+), 1 deletion(-)
include/linux/swap.h +1 -0
index e4512aeead36..ebcd5eaa7924 100644
@@ -254,6 +254,7 @@ struct swap_info_struct {
 	unsigned long		nr_clusters;	/* current growth ceiling (≤ nr_clusters_max) */
 	unsigned long		nr_clusters_mapped; /* currently mapped cluster count */
 	unsigned long		nr_free_tail;	/* contiguous free clusters at tail */
+	struct dentry		*debugfs_entry;	/* debugfs: type<N>_max_clusters */
 #endif
 	struct list_head free_clusters; /* free clusters list */
 	struct list_head full_clusters; /* full clusters list */
mm/swapfile.c +96 -1

139 lines not on this page · raw message

Signature · 1 line
2.54.0
author reviewer
#16 lore raw

RFC PATCH 10/11 mm, swap: defer xswap shrink to workqueue to avoid lock recursion

In reply to Baoquan He

xswap_try_shrink() was called directly from __free_cluster() while holding ci->lock. The shrink path calls xswap_unmap_clusters() which unmaps vmalloc pages backing cluster_info, and on return swap_cache_del_folio() tries swap_cluster_unlock(ci) on the now- unmapped address — crashing on a not-present page.

Replace direct calls with schedule_work() so shrink runs in an independent workqueue context where no cluster locks are held. Use cancel_work_sync() during swapoff to ensure no pending shrink work races with the VM area teardown.

---
 include/linux/swap.h |  1 +
 mm/swapfile.c        | 18 ++++++++++++------
 2 files changed, 13 insertions(+), 6 deletions(-)
include/linux/swap.h +1 -0
index ebcd5eaa7924..ebae01fbd100 100644
@@ -255,6 +255,7 @@ struct swap_info_struct {
 	unsigned long		nr_clusters_mapped; /* currently mapped cluster count */
 	unsigned long		nr_free_tail;	/* contiguous free clusters at tail */
 	struct dentry		*debugfs_entry;	/* debugfs: type<N>_max_clusters */
+	struct work_struct	xswap_shrink_work; /* deferred shrink trigger */
 #endif
 	struct list_head free_clusters; /* free clusters list */
 	struct list_head full_clusters; /* full clusters list */
mm/swapfile.c +12 -6
index af62981506d3..b41ff2c594fc 100644
@@ -127,11 +127,8 @@ static ssize_t xswap_max_clusters_write(struct file *file,
 		spin_unlock(&swap_lock);
 	}
 
-	/*
-	 * Lowering the ceiling may make tail clusters eligible for
-	 * shrinking.  Trigger an immediate check.
-	 */
-	xswap_try_shrink(si);
+	/* Shrink trigger: lowering the ceiling may free tail clusters. */
+	schedule_work(&si->xswap_shrink_work);
 
 	return count;
 }
@@ -726,7 +723,7 @@ static void __free_cluster(struct swap_info_struct *si, struct swap_cluster_info
 	ci->order = 0;
 #ifdef CONFIG_XSWAP
 	xswap_update_free_tail(si, ci - si->cluster_info);
-	xswap_try_shrink(si);
+	schedule_work(&si->xswap_shrink_work);
 #endif
 }
 
@@ -3236,6 +3233,7 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
 #ifdef CONFIG_XSWAP
 	if (si->flags & SWP_XSWAP) {
 		xswap_debugfs_del(si);
+		cancel_work_sync(&si->xswap_shrink_work);
 		/* Unmap all mapped clusters and free the VM_SPARSE area */
 		if (si->nr_clusters_mapped > 0)
 			xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
@@ -4030,6 +4028,13 @@ static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
 		WRITE_ONCE(si->nr_free_tail, nr_mapped - idx - 1);
 }
 
+static void xswap_shrink_work_fn(struct work_struct *work)
+{
+	struct swap_info_struct *si = container_of(work,
+			struct swap_info_struct, xswap_shrink_work);
+	xswap_try_shrink(si);
+}
+
 /*
  * Try to shrink the cluster_info tail.  Uses si->nr_free_tail which
  * is maintained incrementally during alloc/free — no scanning needed.
@@ -4137,6 +4142,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
 		/* All mapped clusters except cluster 0 are free at the tail */
 		si->nr_free_tail = si->nr_clusters_mapped - 1;
 
+		INIT_WORK(&si->xswap_shrink_work, xswap_shrink_work_fn);
 		xswap_debugfs_add(si);
 		return 0;
 
Signature · 1 line
2.54.0
author reviewer
#17 lore raw

RFC PATCH 11/11 mm, swap: serialize xswap map/unmap with a mutex

In reply to Baoquan He

Concurrent xswap grow operations race on vm_area_map_pages(), triggering WARN_ON(!pte_none) in the vmap page table walk when one thread sees a PTE already set by another. The -EBUSY safety net catches it but the warning is noisy and unavoidable without serialization.

Add a per-device mutex (xswap_lock) held across xswap_map_clusters and xswap_unmap_clusters so that only one thread can modify the VM_SPARSE page table at a time. The shrink path is already deferred to a workqueue so it does not contend with itself.

---
 include/linux/swap.h |  1 +
 mm/swapfile.c        | 17 +++++++++++++++--
 2 files changed, 16 insertions(+), 2 deletions(-)
include/linux/swap.h +1 -0
index ebae01fbd100..b0fe0475bba7 100644
@@ -256,6 +256,7 @@ struct swap_info_struct {
 	unsigned long		nr_free_tail;	/* contiguous free clusters at tail */
 	struct dentry		*debugfs_entry;	/* debugfs: type<N>_max_clusters */
 	struct work_struct	xswap_shrink_work; /* deferred shrink trigger */
+	struct mutex		xswap_lock;	/* serialize map/unmap operations */
 #endif
 	struct list_head free_clusters; /* free clusters list */
 	struct list_head full_clusters; /* full clusters list */
mm/swapfile.c +15 -2
index b41ff2c594fc..89c6a6b03a9d 100644
@@ -3773,11 +3773,14 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 	struct page **pages;
 	unsigned long i;
 
+	mutex_lock(&si->xswap_lock);
+
 	if (vm_start >= vm_end) {
 		/* All requested clusters fall within already-mapped pages. */
 		for (i = start_idx; i < start_idx + nr; i++)
 			spin_lock_init(&si->cluster_info[i].lock);
 		WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+		mutex_unlock(&si->xswap_lock);
 		return 0;
 	}
 
@@ -3842,6 +3845,7 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 	 * Pairs with READ_ONCE() in shrink/grow paths.
 	 */
 	WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+	mutex_unlock(&si->xswap_lock);
 	return 0;
 
 fail_nounmap:
@@ -3862,6 +3866,7 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 		spin_lock_init(&si->cluster_info[i].lock);
 
 	WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+	mutex_unlock(&si->xswap_lock);
 	return 0;
 
 fail:
@@ -3871,6 +3876,7 @@ static int xswap_map_clusters(struct swap_info_struct *si,
 			__free_page(pages[i]);
 	}
 	kfree(pages);
+	mutex_unlock(&si->xswap_lock);
 	return -ENOMEM;
 }
 
@@ -3910,8 +3916,12 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
 	struct xswap_page_data xpd;
 	int i;
 
-	if (vm_start >= vm_end)
+	mutex_lock(&si->xswap_lock);
+
+	if (vm_start >= vm_end) {
+		mutex_unlock(&si->xswap_lock);
 		goto out;
+	}
 
 	size = vm_end - vm_start;
 	npages = size >> PAGE_SHIFT;
@@ -3937,10 +3947,12 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
 			__free_page(xpd.pages[i]);
 		kfree(xpd.pages);
 	}
+
+	mutex_unlock(&si->xswap_lock);
+out:
 	/*
 	 * Pairs with READ_ONCE() in shrink/grow paths.
 	 */
-	out:
 	WRITE_ONCE(si->nr_clusters_mapped, start_idx);
 }
 
@@ -4142,6 +4154,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
 		/* All mapped clusters except cluster 0 are free at the tail */
 		si->nr_free_tail = si->nr_clusters_mapped - 1;
 
+		mutex_init(&si->xswap_lock);
 		INIT_WORK(&si->xswap_shrink_work, xswap_shrink_work_fn);
 		xswap_debugfs_add(si);
 		return 0;
Signature · 1 line
2.54.0
maintainer
#18 lore raw

Re: [RFC PATCH 11/11] mm, swap: serialize xswap map/unmap with a mutex

Quoted 7 lines · Baoquan He

Concurrent xswap grow operations race on vm_area_map_pages(), triggering WARN_ON(!pte_none) in the vmap page table walk when one thread sees a PTE already set by another. The -EBUSY safety net catches it but the warning is noisy and unavoidable without serialization.

Quoted 6 lines

Add a per-device mutex (xswap_lock) held across xswap_map_clusters and xswap_unmap_clusters so that only one thread can modify the VM_SPARSE page table at a time. The shrink path is already deferred to a workqueue so it does not contend with itself.

Seems like this should be squashed into one of the older patches, no?

author reviewer
#19 lore raw

Re: [RFC PATCH 11/11] mm, swap: serialize xswap map/unmap with a mutex

Quoted 8 lines · Nhat Pham
Quoted 7 lines · Baoquan He

Concurrent xswap grow operations race on vm_area_map_pages(), triggering WARN_ON(!pte_none) in the vmap page table walk when one thread sees a PTE already set by another. The -EBUSY safety net catches it but the warning is noisy and unavoidable without serialization.

Quoted 8 lines
Quoted 6 lines

Add a per-device mutex (xswap_lock) held across xswap_map_clusters and xswap_unmap_clusters so that only one thread can modify the VM_SPARSE page table at a time. The shrink path is already deferred to a workqueue so it does not contend with itself.

Seems like this should be squashed into one of the older patches, no?

Both is fine to me, because putting it in a separate patch can get a specific log to describe it with more details. Have squashed it into patch 03 as suggested since they are small number of lines of change.

Thanks
Baoquan

maintainer
#20 lore raw

Re: [RFC PATCH 02/11] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct

In reply to Baoquan He
Quoted 24 lines · Baoquan He

Add CONFIG_XSWAP Kconfig option (depends on SWAP && 64BIT) for extendable (virtual) swap device support.

Add three fields to struct swap_info_struct under CONFIG_XSWAP:
- cluster_vm: the VM_SPARSE vm_struct backing the cluster_info array
- nr_clusters: total number of clusters in the xswap address space
- nr_clusters_mapped: number of clusters currently mapped (lazy grow)

These fields enable lazy vmalloc-based dynamic cluster management where the cluster_info array is backed by a sparse vmalloc area that grows on demand and shrinks when clusters are freed.

---
 include/linux/swap.h | 5 +++++
 mm/Kconfig           | 9 +++++++++
 2 files changed, 14 insertions(+)
include/linux/swap.h +0 -0
index 80c83dd8804f..5f6b14d30758 100644

include/linux/swap.h struct swap_info_struct { · line 255

Quoted 8 lines
include/linux/swap.h +5 -0
@@ -248,6 +248,11 @@ struct swap_info_struct {
        signed char     type;           /* strange name for an index */
        unsigned int    max;            /* size of this swap device */
        struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
+#ifdef CONFIG_XSWAP
+       struct vm_struct        *cluster_vm;    /* VM_SPARSE area for xswap dynamic cluster_info */
+       unsigned long           nr_clusters;    /* total cluster count for xswap */
+       unsigned long           nr_clusters_mapped; /* currently mapped cluster count */
+#endif

We might need more than one dynamic kvmalloc grow array. Maybe abstract this into a struct so it can be reused for arrays other than clusters as well. The abstraction can also include the element size so it can have a generic function to calculate how many pages it needs for N elements.

Chris

author reviewer
#21 lore raw

Re: [RFC PATCH 02/11] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct

Quoted 31 lines · Chris Li
Quoted 30 lines · Baoquan He

Add CONFIG_XSWAP Kconfig option (depends on SWAP && 64BIT) for extendable (virtual) swap device support.

Add three fields to struct swap_info_struct under CONFIG_XSWAP:
- cluster_vm: the VM_SPARSE vm_struct backing the cluster_info array
- nr_clusters: total number of clusters in the xswap address space
- nr_clusters_mapped: number of clusters currently mapped (lazy grow)

These fields enable lazy vmalloc-based dynamic cluster management where the cluster_info array is backed by a sparse vmalloc area that grows on demand and shrinks when clusters are freed.

---
 include/linux/swap.h | 5 +++++
 mm/Kconfig           | 9 +++++++++
 2 files changed, 14 insertions(+)
include/linux/swap.h +3 -0
index 80c83dd8804f..5f6b14d30758 100644
@@ -248,6 +248,11 @@ struct swap_info_struct {
        signed char     type;           /* strange name for an index */
        unsigned int    max;            /* size of this swap device */
        struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
+#ifdef CONFIG_XSWAP
+       struct vm_struct        *cluster_vm;    /* VM_SPARSE area for xswap dynamic cluster_info */
+       unsigned long           nr_clusters;    /* total cluster count for xswap */
Quoted 8 lines

include/linux/swap.h struct swap_info_struct { · line 255

Quoted 2 lines
include/linux/swap.h +2 -0
+       unsigned long           nr_clusters_mapped; /* currently mapped cluster count */
+#endif

We might need more than one dynamic kvmalloc grow array. Maybe abstract this into a struct so it can be reused for arrays other than clusters as well. The abstraction can also include the element size so it can have a generic function to calculate how many pages it needs for N elements.

I agree in principle. I think we can pull that out as a follow-up when the second user emerges, rather than abstracting prematurely.

maintainer
#22 lore raw
In reply to Baoquan He
Quoted 13 lines · Baoquan He

This series implements step 3 of the incremental path proposed in [1]:
a virtual swap device with dynamic cluster growth and shrink, backed only by zswap, no writeback yet.

During the discussion in [2], Chris and Nhat debated xarray-based vs array-based cluster lookup for virtual swap. Chris argued for keeping the swap table and cluster_info in the same place with O(1) array indexing, and suggested that the kvmalloc grow approach here should provide the cluster management VS needs. He further proposed using the same grow technique for the future per-slot backend pointer array (vs_table), keeping a single coherent array layout throughout.

Quoted 4 lines

This series therefore serves as the foundation: once the dynamic cluster management lands, Nhat's VS series can build the per-slot backend pointer, writeback, and rmap on top of it — without an extra xarray lookup in the cluster access path.

Thanks Baoquan. I've given it some more thoughts over the past week or so, to figure out a way to so that everyone would be happy with the final results.

To be perfectly clear, I'm not married with the xarray data structure. I only care about use case aspect (as you have seen, I switch over to swap table the moment I figure out a way to realize Kairui's design proposal - it's way cleaner) - and I've seen that you have addressed a lot of the design concerns I had that would hindered the use cases I presented. If the vmalloc data structure outperforms the xarray, it should *definitely* be the data structure we use when everything settles.

That just leaves the sequencing of our efforts :)

My only fear with landing one piece at a time is that that we land something that ended up not working for the entire big series in some unforeseen cases, and we have to rip it out or rewrite a huge chunks of it. Besides, without an actual use case, we can't really test its correctness, and purported performance improvement benefits.

That's why so far I have bitten the bullet and sent out the whole series (even if it's really large) with the core use cases implemented. I understand that it is big, and is burdensome for reviewers - but at least you know that the core use cases works - and if you don't take my work for it, you can test with it and benchmark it, etc. And with Kairui's proposal, we still maintain the old machinery, which allows us to even merge with something a bit suboptimal, then optimize later. FWIW, so far in my testing, the numbers have been quite close, at least for zswap backend, and the core concern (memory overhead) has been resolved. So it might actually already be usable out of the box? :)

This patch series has reached 700 LoCs, and I still have some correctness concerns - I have commented one in another patch. It has gotten more complicated now, which is no fault of you - we just need more logic to handle the things xarray provided us out of the box, with the hope that it would be more optimal. And that's fine. I just think it would be better to land the xarray version first to unblock users, then just slot your patch series on top as a follow-up optimization. It would show the actual win of your data structure over xarray data structure with concrete numbers :) Win-win?

But thanks again for taking a stab at it. Seriously, you're awesome. I want to re-iterate again - this is NOT a Nacked-by Nhat. Not with this patch series, nor with the direction. I'm just trying to figure out the sequencing of our efforts that would make everything work. I'll spend sometimes on my own trying to synthesize this data structure with vswap design too, to see if I can fish out more gaps, and to see if I can clean things up in a way that would make the switch from xarray to this new data structure as painless as possible. As well as the other design points we have touched on in our last conversation - it makes sense to me at the time, but I need to code some prototype out and stare at it a bit more.

author reviewer
#23 lore raw
Quoted 32 lines · Nhat Pham
Quoted 13 lines · Baoquan He

This series implements step 3 of the incremental path proposed in [1]:
a virtual swap device with dynamic cluster growth and shrink, backed only by zswap, no writeback yet.

During the discussion in [2], Chris and Nhat debated xarray-based vs array-based cluster lookup for virtual swap. Chris argued for keeping the swap table and cluster_info in the same place with O(1) array indexing, and suggested that the kvmalloc grow approach here should provide the cluster management VS needs. He further proposed using the same grow technique for the future per-slot backend pointer array (vs_table), keeping a single coherent array layout throughout.

Quoted 4 lines

This series therefore serves as the foundation: once the dynamic cluster management lands, Nhat's VS series can build the per-slot backend pointer, writeback, and rmap on top of it — without an extra xarray lookup in the cluster access path.

Thanks Baoquan. I've given it some more thoughts over the past week or so, to figure out a way to so that everyone would be happy with the final results.

To be perfectly clear, I'm not married with the xarray data structure. I only care about use case aspect (as you have seen, I switch over to swap table the moment I figure out a way to realize Kairui's design proposal - it's way cleaner) - and I've seen that you have addressed a lot of the design concerns I had that would hindered the use cases I presented. If the vmalloc data structure outperforms the xarray, it should *definitely* be the data structure we use when everything settles.

Quoted 7 lines

That just leaves the sequencing of our efforts :)

My only fear with landing one piece at a time is that that we land something that ended up not working for the entire big series in some unforeseen cases, and we have to rip it out or rewrite a huge chunks of it. Besides, without an actual use case, we can't really test its correctness, and purported performance improvement benefits.

Thanks for the thoughtful and honest reply, Nhat. I really appreciate the clarity — and the fact that this is a sequencing discussion, not a technical objection.

Personally, I still lean toward the incremental approach. The cluster management layer feels like a well-defined, self-contained foundation that can be reviewed and tested independently, and landing it first would let the later change built on it with less effort.

I think the overall direction is converging nicely. We've reached rough consensus on the big-picture architecture — decoupling backends, writeback, dynamic cluster grow/shrink, and per-device xswap/vswap — that's a solid foundation. Once we settle on the cluster_info implementation, everything else should fall into place quickly. From there we can move on to actual swap-tier usage of xswap/vswap and other extensions on top.

However, if the consensus leans toward merging your xarray-based vswap series first and then switching the data structure as a follow-up optimization, I have no problem with that either. One concern: if we land the xarray-based approach first and then switch to a pointer array later, the cluster_info type change (e.g to struct swap_cluster_info * flat array or struct swap_cluster_info ** pointer array) will cause us touching those codes twice. Just something to weigh against the "land early" argument.

I'm quite optimistic about how things are shaping up. It's been a long discussion but I think we're very close to something that everyone can get behind.

Thanks
Baoquan

Quoted 34 lines

That's why so far I have bitten the bullet and sent out the whole series (even if it's really large) with the core use cases implemented. I understand that it is big, and is burdensome for reviewers - but at least you know that the core use cases works - and if you don't take my work for it, you can test with it and benchmark it, etc. And with Kairui's proposal, we still maintain the old machinery, which allows us to even merge with something a bit suboptimal, then optimize later. FWIW, so far in my testing, the numbers have been quite close, at least for zswap backend, and the core concern (memory overhead) has been resolved. So it might actually already be usable out of the box? :)

This patch series has reached 700 LoCs, and I still have some correctness concerns - I have commented one in another patch. It has gotten more complicated now, which is no fault of you - we just need more logic to handle the things xarray provided us out of the box, with the hope that it would be more optimal. And that's fine. I just think it would be better to land the xarray version first to unblock users, then just slot your patch series on top as a follow-up optimization. It would show the actual win of your data structure over xarray data structure with concrete numbers :) Win-win?

But thanks again for taking a stab at it. Seriously, you're awesome. I want to re-iterate again - this is NOT a Nacked-by Nhat. Not with this patch series, nor with the direction. I'm just trying to figure out the sequencing of our efforts that would make everything work. I'll spend sometimes on my own trying to synthesize this data structure with vswap design too, to see if I can fish out more gaps, and to see if I can clean things up in a way that would make the switch from xarray to this new data structure as painless as possible. As well as the other design points we have touched on in our last conversation - it makes sense to me at the time, but I need to code some prototype out and stare at it a bit more.