* [PATCH 00/16] xswap: extendable swap device backed by zswap
@ 2026-08-27 9:44 Baoquan He
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
` (16 more replies)
0 siblings, 17 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
xswap is an extendable swap device with no backing storage. Swapped-out
pages live only in zswap, so the device wastes no disk space and its
size is independent of any physical device.
xswap decouples PTE swap entries from physical backing storage. The
cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
grown and shrunk on demand:
- Grow: when cluster allocation runs out of free clusters and the device
is below its ceiling, more physical pages are mapped into the VM_SPARSE
area and their clusters are added to the free list.
- Shrink: when contiguous free clusters accumulate at the tail of the
mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
the backing pages freed. Shrink is deferred to a workqueue to avoid
lock recursion.
A per-device ceiling (nr_clusters) bounds growth and is adjustable at
runtime via debugfs.
Interface:
/sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
create a device; percent is a
percent of RAM (0 for the default),
prio is an optional swap priority
(default DEF_SWAP_PRIO)
/sys/kernel/mm/xswap/destroy write a swap type to tear down
a device
/sys/kernel/debug/xswap/type<N>_cluster_limit
read/write the per-device
cluster ceiling
Since xswap has no backing, swapped-out pages are stored compressed in
zswap: physical writeout is skipped, and zswap writeback is disabled when
every swapfile in the system is an xswap device. xswap requires zswap, so
device creation is refused when zswap is unavailable.
Naming:
======
I'm going with "xswap" (the "x" for extendable/extension) rather than "vswap".
Chris suggested this name, and this aligns with the "VFS-like swap layers"
direction Chris Li described in the first swap abstraction LPC talk
(co-hosted with Yosry) the swap ops and the xswap extension interfaces in
this series are moving toward exactly that. I don't have a strong preference
between xswap and vswap, so if reviewers object to the name, please comment.
Note:
=====
This patchset only build the base. On top of this, the subsequent core code
implementation of xswap writeback, rmap etc can be done more easily. E.g, we
only need add one field in struct swap_cluster_info to let xs_table point to
physical swap entry, or zswap entry etc. On top of this patchset, no need to
stir core data structure too much or introduce extra data structure.
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -57,6 +57,9 @@ struct swap_cluster_info {
u8 order;
atomic_long_t __rcu *table; /* Swap table entries, see mm/swap_table.h */
unsigned int *extend_table; /* For large swap count, protected by ci->lock */
+#ifdef CONFIG_XSWAP
+ unsigned long *xs_table;
+#endif
Testing (taken on qemu kvm guest with 8G memory):
=========
1. enable zswap and create/destroy xswap device
~# echo 0 > /sys/kernel/mm/xswap/create
-bash: echo: write error: Operation not supported
~# echo 1 > /sys/module/zswap/parameters/enabled
~# echo 0 > /sys/kernel/mm/xswap/create
~# swapon
NAME TYPE SIZE USED PRIO
xswap0 xswap 2.3G 0B -1
~# echo 0 > /sys/kernel/mm/xswap/destroy
~# swapon
2. create xswap device and tune the zswap size
~# echo "50 10" > /sys/kernel/mm/xswap/create
~# echo 0 > /sys/kernel/mm/xswap/create
~# swapon
NAME TYPE SIZE USED PRIO
xswap0 xswap 3.9G 0B 10
xswap1 xswap 2.3G 0B -1
~# cat /sys/kernel/debug/xswap/type0_cluster_limit
1990
~# cat /sys/kernel/debug/xswap/type1_cluster_limit
1194
~# echo 2048 > /sys/kernel/debug/xswap/type0_cluster_limit
~# echo 2048 > /sys/kernel/debug/xswap/type1_cluster_limit
~# swapon
NAME TYPE SIZE USED PRIO
xswap0 xswap 4G 0B 10
xswap1 xswap 4G 0B -1
3. under heavy memory pressure tune swap size or destroy xswap device
~# stress-ng --vm 1 --vm-bytes 8G --vm-keep --timeout 120s &
~# echo 1024 > /sys/kernel/debug/xswap/type0_cluster_limit
~# swapon
NAME TYPE SIZE USED PRIO
xswap0 xswap 2G 2.6G 10
xswap1 xswap 4G 182M -1
~# echo 1024 > /sys/kernel/debug/xswap/type1_cluster_limit
~# swapon
NAME TYPE SIZE USED PRIO
xswap0 xswap 2G 1.4G 10
xswap1 xswap 2G 315.4M -1
~# echo 0 > /sys/kernel/mm/xswap/destroy
~# swapon
NAME TYPE SIZE USED PRIO
xswap1 xswap 2G 1.1G -1
I tried create/destroy and grow/shrink xswap device under heavy
memory pressure, all passed.
Changelog
=========
RFC v3 -> v1
- Add patch 16 to support setting xswap device priority at creation.
The create sysfs interface (/sys/kernel/mm/xswap/create) previously
hardcoded every new device's priority to DEF_SWAP_PRIO, it now
accepts an optional priority:
echo "<percent> [<prio>]" > /sys/kernel/mm/xswap/create
- Bug fix: xswap_lock init ordering. mutex_init(&si->xswap_lock) was called
after xswap_map_clusters() (which locks it), i.e. locking an uninitialized
mutex. Init now before the first xswap_map_clusters() call. Thanks to Klara.
- Bug fix: Fixes a compile error in !CONFIG_XSWAP builds. xswap_debugfs_root
is declared inside CONFIG_XSWAP ifdeffery scope, so the ungarded use
caused error when CONFIG_XSWAP is off.
RFC v2-> RFC v3:
- Replace the "header-only swap file + swapon" creation hack with a
proper file-less device created and destroyed via sysfs
(/sys/kernel/mm/xswap/{create,destroy}). This required the
__swapoff() refactor and the free_swap_cluster_info() signature
change (patches 4, 6, 14).
- Require zswap: refuse to create an xswap device when zswap is
unavailable (patch 15).
- Split the unrelated zswap -ENOENT fix out of the series into a
standalone patch (patch 1).
- Fix nr_free_tail over-counting on concurrent grow, shrink leaking
detached clusters on early bail-out, a re-init race on cluster
spinlocks in xswap_map_clusters(), the nr_clusters_mapped update
ordering, and swapoff accessing the shrinker-unmapped cluster tail.
- Minor cleanups (checkpatch, /proc/swaps alignment, commit messages).
RFC v1-> RFC v2:
- Added __GFP_HIGH | __GFP_NOMEMALLOC to alloc_page() and kmalloc_array()
in the grow path, plus memalloc_noreclaim_save/restore() wrapping,
to prevent the grow path from consuming emergency memory reserves
or recursing into swap under PF_MEMALLOC. This is folded into patch 3.
This was pointed out by Nhat.
- Folded the mutex serialization fix into the cluster grow patch (patch
3). This is suggested by Nhat.
- Fixed coding style issues: corrected indentation of declarations in
xswap_unmap_clusters(), removed unnecessary block scope around the
err variable in xswap_map_clusters().
- Rebased onto mm-unstable
Baoquan He (15):
mm: zswap: return -ENOENT when the swap device is gone
mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct
mm, swap: refactor free_swap_cluster_info to take swap_info_struct
mm, swap: add xswap cluster grow via VM_SPARSE vmalloc
mm, swap: add sysfs create interface for xswap
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: refactor swapoff + add xswap_destroy
mm, swap: require zswap for xswap devices
mm, swap: allow setting xswap device priority at creation
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 | 1027 ++++++++++++++++++++++++++++++++++++++----
mm/zswap.c | 9 +-
6 files changed, 993 insertions(+), 87 deletions(-)
base-commit: 169393fff5d1ec2690934067eeb95544ff5ebdd7
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 02/16] mm: xswap support for zswap Baoquan He
` (15 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
zswap_writeback_entry() returns -EEXIST when get_swap_device() finds no
device. -EEXIST is the shrinker's "page already in swap cache" signal,
which makes zswap_shrinker_scan() stop shrinking entirely. A NULL
get_swap_device() instead means the device is being swapped off, so the
entry is simply stale.
Return -ENOENT so the shrinker skips the stale entry and keeps scanning.
Independent of xswap; affects all swap devices.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/zswap.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mm/zswap.c b/mm/zswap.c
index 37f34e406c8e..b9948d4657d2 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -998,7 +998,7 @@ 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;
mpol = get_task_policy(current);
folio = swap_cache_alloc_folio(swpentry, GFP_KERNEL, BIT(0), NULL, mpol,
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 02/16] mm: xswap support for zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct Baoquan He
` (14 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
From: Chris Li <chrisl@kernel.org>
Introduce extendable swap device support — xswap.
An xswap device is a swap device with no backing storage and
no swap data section, so it wastes no disk 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. Creation
is via a sysfs interface added in a later patch.
Zswap writeback is disabled if all swapfiles in the system are
xswap devices (tracked via nr_real_swapfiles).
Co-developed-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Chris Li <chrisl@kernel.org>
---
include/linux/swap.h | 2 ++
mm/page_io.c | 16 ++++++++++++++++
mm/swap_state.c | 7 +++++++
mm/swapfile.c | 37 ++++++++++++++++++++++++++++++++++---
mm/zswap.c | 7 ++++++-
5 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 5658a1634b85..787fe463dcbb 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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... */
};
@@ -356,6 +357,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;
diff --git a/mm/page_io.c b/mm/page_io.c
index 88962571cb93..5483c943e3e3 100644
--- a/mm/page_io.c
+++ b/mm/page_io.c
@@ -248,6 +248,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:
@@ -480,6 +491,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);
diff --git a/mm/swap_state.c b/mm/swap_state.c
index b76eb3d876fd..2eedb7a3d7bb 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -830,6 +830,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;
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 53bf01d5f7f1..5aa1ffb97df8 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -66,6 +66,7 @@ static void move_cluster(struct swap_info_struct *si,
static DEFINE_SPINLOCK(swap_lock);
static unsigned int nr_swapfiles;
atomic_long_t nr_swap_pages;
+atomic_t nr_real_swapfiles;
/*
* Some modules use swappable objects and may try to swap them out under
* memory pressure (via the shrinker). Before doing so, they may wish to
@@ -1223,6 +1224,8 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
goto skip;
}
+ if (!(si->flags & SWP_XSWAP))
+ atomic_sub(1, &nr_real_swapfiles);
plist_del(&si->avail_list, &swap_avail_head);
skip:
@@ -1265,6 +1268,8 @@ static void add_to_avail_list(struct swap_info_struct *si, bool swapon)
}
plist_add(&si->avail_list, &swap_avail_head);
+ if (!(si->flags & SWP_XSWAP))
+ atomic_add(1, &nr_real_swapfiles);
skip:
spin_unlock(&swap_avail_lock);
@@ -2959,6 +2964,19 @@ static int setup_swap_extents(struct swap_info_struct *sis,
struct inode *inode = mapping->host;
int ret;
+ if (sis->flags & SWP_XSWAP) {
+ *span = 0;
+ /*
+ * xswap devices have no backing block device and
+ * physical writeout is skipped in swap_writeout(),
+ * but sis->ops must still be set so that callers
+ * like shrink_folio_list() can safely dereference
+ * ops->flags.
+ */
+ sis->ops = &swap_bdev_ops;
+ return 0;
+ }
+
ret = sio_pool_init();
if (ret)
return ret;
@@ -3167,7 +3185,8 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
destroy_swap_extents(p, p->swap_file);
- if (!(p->flags & SWP_SOLIDSTATE))
+ if (!(p->flags & SWP_XSWAP) &&
+ !(p->flags & SWP_SOLIDSTATE))
atomic_dec(&nr_rotate_swap);
mutex_lock(&swapon_mutex);
@@ -3277,6 +3296,19 @@ static void swap_stop(struct seq_file *swap, void *v)
mutex_unlock(&swapon_mutex);
}
+static const char *swap_type_str(struct swap_info_struct *si)
+{
+ struct file *file = si->swap_file;
+
+ if (si->flags & SWP_XSWAP)
+ return "xswap\t";
+
+ if (S_ISBLK(file_inode(file)->i_mode))
+ return "partition";
+
+ return "file\t";
+}
+
static int swap_show(struct seq_file *swap, void *v)
{
struct swap_info_struct *si = v;
@@ -3296,8 +3328,7 @@ static int swap_show(struct seq_file *swap, void *v)
len = seq_file_path(swap, file, " \t\n\\");
seq_printf(swap, "%*s%s\t%lu\t%s%lu\t%s%d\n",
len < 40 ? 40 - len : 1, " ",
- S_ISBLK(file_inode(file)->i_mode) ?
- "partition" : "file\t",
+ swap_type_str(si),
bytes, bytes < 10000000 ? "\t" : "",
inuse, inuse < 10000000 ? "\t" : "",
si->prio);
diff --git a/mm/zswap.c b/mm/zswap.c
index b9948d4657d2..064970a4393f 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -1000,6 +1000,11 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
if (!si)
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,
NO_INTERLEAVE_INDEX);
@@ -1545,7 +1550,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:
/*
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
2026-08-27 9:44 ` [PATCH 02/16] mm: xswap support for zswap Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Baoquan He
` (13 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 5 +++++
mm/Kconfig | 9 +++++++++
2 files changed, 14 insertions(+)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 787fe463dcbb..35d357a393c6 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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];
diff --git a/mm/Kconfig b/mm/Kconfig
index 604c58199acb..26cf26a2c59f 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -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
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (2 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc Baoquan He
` (12 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
Change free_swap_cluster_info() to accept struct swap_info_struct*
instead of (cluster_info, maxpages) directly. Extract cluster_info
and maxpages from si inside the function. Also clean up swapoff:
remove the snapshot locals (maxpages/cluster_info) and move the
p->max/p->cluster_info clearing after free_swap_cluster_info().
This is a preparatory refactoring, no functional change. The new
signature will allow the xswap path (added in the next patch) to
access si->flags and call xswap_unmap_clusters() from within
free_swap_cluster_info().
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 25 +++++++++++++------------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5aa1ffb97df8..cd7e889f073d 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3054,14 +3054,17 @@ static void wait_for_allocation(struct swap_info_struct *si)
}
}
-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;
+
+ 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 */
@@ -3100,11 +3103,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))
@@ -3196,10 +3197,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);
@@ -3207,7 +3204,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;
@@ -3574,6 +3573,8 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
if (!cluster_info)
goto err;
+ si->cluster_info = cluster_info;
+
for (i = 0; i < nr_clusters; i++)
spin_lock_init(&cluster_info[i].lock);
@@ -3637,7 +3638,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;
}
@@ -3856,7 +3857,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
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (3 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 06/16] mm, swap: add sysfs create interface for xswap Baoquan He
` (11 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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.
4. free_swap_cluster_info() xswap path: Unmap all clusters and
free_vm_area(). Built on the refactoring in the previous patch.
5. wait_for_allocation() xswap guard: Skip shrinker-unmapped clusters
beyond nr_clusters_mapped.
The grow path avoids emergency reserves via __GFP_HIGH|__GFP_NOMEMALLOC
and wraps allocations with memalloc_noreclaim_save(). A per-device
mutex (xswap_lock) serializes concurrent map/unmap page table
modifications.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 259 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 258 insertions(+), 2 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 35d357a393c6..c2744fa8a009 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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 */
+ 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 */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index cd7e889f073d..06f73c0a504a 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -49,6 +49,25 @@
#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);
+static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
+#endif
+
static void swap_range_alloc(struct swap_info_struct *si,
unsigned int nr_entries);
static bool folio_swapcache_freeable(struct folio *folio);
@@ -2715,15 +2734,27 @@ static unsigned int find_next_to_unuse(struct swap_info_struct *si,
unsigned int prev)
{
unsigned int i;
+ unsigned int end = si->max;
unsigned long swp_tb;
+#ifdef CONFIG_XSWAP
+ /* xswap may have shrunk and unmapped the cluster_info tail. */
+ if (si->flags & SWP_XSWAP) {
+ unsigned long mapped_end;
+
+ mapped_end = READ_ONCE(si->nr_clusters_mapped) * SWAPFILE_CLUSTER;
+ if (mapped_end < end)
+ end = mapped_end;
+ }
+#endif
+
/*
* No need for swap_lock here: we're just looking
* for whether an entry is in use, not modifying it; false
* hits are okay, and sys_swapoff() has already prevented new
* allocations from this area (while holding swap_lock).
*/
- for (i = prev + 1; i < si->max; i++) {
+ for (i = prev + 1; i < end; i++) {
swp_tb = swap_table_get(__swap_offset_to_cluster(si, i),
i % SWAPFILE_CLUSTER);
if (!swp_tb_is_null(swp_tb) && !swp_tb_is_bad(swp_tb))
@@ -2732,7 +2763,7 @@ static unsigned int find_next_to_unuse(struct swap_info_struct *si,
cond_resched();
}
- if (i == si->max)
+ if (i == end)
i = 0;
return i;
@@ -3048,6 +3079,13 @@ static void wait_for_allocation(struct swap_info_struct *si)
BUG_ON(si->flags & SWP_WRITEOK);
+#ifdef CONFIG_XSWAP
+ /* Skip shrinker-unmapped cluster tail. */
+ 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);
@@ -3064,6 +3102,19 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
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;
@@ -3560,6 +3611,150 @@ 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);
+ /* Round to page boundaries for vm_area_map_pages(). */
+ unsigned long vm_start = PAGE_ALIGN(start_addr);
+ unsigned long vm_end = PAGE_ALIGN(end_addr);
+ unsigned int noreclaim_flags;
+ unsigned long npages;
+ struct page **pages;
+ unsigned long i;
+ int err;
+
+ 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;
+ }
+
+ npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+ /* Prevent recursive reclaim during vmap page table allocation. */
+ noreclaim_flags = memalloc_noreclaim_save();
+
+ pages = kmalloc_array(npages, sizeof(*pages),
+ __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL);
+ if (!pages) {
+ memalloc_noreclaim_restore(noreclaim_flags);
+ mutex_unlock(&si->xswap_lock);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < npages; i++) {
+ /* __GFP_ZERO: cluster_info pointer fields must start NULL. */
+ pages[i] = alloc_page(__GFP_HIGH | __GFP_NOMEMALLOC |
+ GFP_KERNEL | __GFP_ZERO);
+ if (!pages[i])
+ goto fail;
+ }
+
+ /* Detect racing grower that already mapped these pages. */
+ if (apply_to_existing_page_range(&init_mm, vm_start,
+ vm_end - vm_start,
+ xswap_check_mapped, NULL)) {
+ i = npages;
+ goto fail_nounmap;
+ }
+
+ err = vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages);
+ if (err) {
+ /* -EBUSY: defensive, the page was already mapped. */
+ if (err == -EBUSY) {
+ i = npages;
+ goto fail_nounmap;
+ }
+ i = npages;
+ goto fail;
+ }
+
+ kfree(pages);
+ memalloc_noreclaim_restore(noreclaim_flags);
+
+ /* 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);
+ mutex_unlock(&si->xswap_lock);
+ return 0;
+
+fail_nounmap:
+ /*
+ * The concurrent grower already mapped the range, initialized the
+ * cluster spinlocks and advanced nr_clusters_mapped. It may still
+ * be holding those locks while adding clusters to the free list, so
+ * do not touch them here; just free our unused pages.
+ */
+ while (i > 0) {
+ i--;
+ if (pages[i])
+ __free_page(pages[i]);
+ }
+ kfree(pages);
+ memalloc_noreclaim_restore(noreclaim_flags);
+ mutex_unlock(&si->xswap_lock);
+ return 0;
+
+fail:
+ while (i > 0) {
+ i--;
+ if (pages[i])
+ __free_page(pages[i]);
+ }
+ memalloc_noreclaim_restore(noreclaim_flags);
+ kfree(pages);
+ mutex_unlock(&si->xswap_lock);
+ 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 for vm_area_unmap_pages(). */
+ unsigned long vm_start = PAGE_ALIGN(start_addr);
+ unsigned long vm_end = PAGE_ALIGN(end_addr);
+
+ mutex_lock(&si->xswap_lock);
+
+ if (vm_start >= vm_end) {
+ WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+ mutex_unlock(&si->xswap_lock);
+ return;
+ }
+
+ vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+ /* vm_area_unmap_pages() clears PTEs but does not free pages. */
+ /* TODO: free backing pages via page table walk or tracking bitmap */
+
+ /* Pairs with READ_ONCE() in shrink/grow paths. */
+ WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+ mutex_unlock(&si->xswap_lock);
+}
+
+/* Return 1 at first present PTE to signal 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,
union swap_header *swap_header,
unsigned long maxpages)
@@ -3569,6 +3764,66 @@ 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;
+
+ /* Must be initialized before xswap_map_clusters() locks it. */
+ mutex_init(&si->xswap_lock);
+
+ /* 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;
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 06/16] mm, swap: add sysfs create interface for xswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (4 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
` (10 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
xswap devices have no backing storage, so there is no file to swapon.
Add a sysfs interface to create them directly:
/sys/kernel/mm/xswap/create write a percent (0 = use default) to
create a device
The initial cluster ceiling (nr_clusters) for a new device is set by
XSWAP_DEFAULT_CLUSTER_PERCENT of totalram_pages. The device appears in
/proc/swaps as "xswap<N>". Per-device runtime sizing is tuned via the
debugfs knob added later in this series.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 165 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 161 insertions(+), 4 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 06f73c0a504a..1127e3d6596b 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -16,6 +16,8 @@
#include <linux/kernel_stat.h>
#include <linux/swap.h>
#include <linux/vmalloc.h>
+#include <linux/kobject.h>
+#include <linux/sysfs.h>
#include <linux/pagemap.h>
#include <linux/namei.h>
#include <linux/shmem_fs.h>
@@ -60,13 +62,79 @@
*/
#define XSWAP_GROW_CLUSTERS \
max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+#define XSWAP_DEFAULT_CLUSTER_PERCENT 30
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
+
+#ifdef CONFIG_SYSFS
+static int xswap_create(int percent);
+/* /sys/kernel/mm/xswap/: create.
+ * Per-device runtime size is tuned via debugfs type<N>_cluster_limit.
+ */
+
+static ssize_t xswap_create_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ unsigned long percent;
+ int err;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ err = kstrtoul(buf, 0, &percent);
+ if (err)
+ return err;
+
+ /* 0 means "use the default percent" */
+ if (percent == 0)
+ percent = XSWAP_DEFAULT_CLUSTER_PERCENT;
+
+ err = xswap_create(percent);
+ if (err < 0)
+ return err;
+
+ return count;
+}
+
+static struct kobj_attribute xswap_create_attr = __ATTR(create, 0200, NULL,
+ xswap_create_store);
+
+static struct attribute *xswap_attrs[] = {
+ &xswap_create_attr.attr,
+ NULL,
+};
+
+static const struct attribute_group xswap_attr_group = {
+ .attrs = xswap_attrs,
+};
+
+static struct kobject *xswap_kobj;
+
+static void xswap_sysfs_init(void)
+{
+ xswap_kobj = kobject_create_and_add("xswap", mm_kobj);
+ if (!xswap_kobj) {
+ pr_err("xswap: failed to create sysfs kobject\n");
+ return;
+ }
+ if (sysfs_create_group(xswap_kobj, &xswap_attr_group))
+ pr_err("xswap: failed to create sysfs group\n");
+}
+#else
+static inline void xswap_sysfs_init(void)
+{
+}
+#endif /* CONFIG_SYSFS */
+#else /* !CONFIG_XSWAP */
+static inline void xswap_sysfs_init(void)
+{
+}
+#endif /* CONFIG_XSWAP */
static void swap_range_alloc(struct swap_info_struct *si,
unsigned int nr_entries);
@@ -3312,7 +3380,7 @@ static void *swap_start(struct seq_file *swap, loff_t *pos)
return SEQ_START_TOKEN;
for (type = 0; (si = swap_type_to_info(type)); type++) {
- if (!(si->swap_file))
+ if (!(si->swap_file) && !(si->flags & SWP_XSWAP))
continue;
if (!--l)
return si;
@@ -3333,7 +3401,7 @@ static void *swap_next(struct seq_file *swap, void *v, loff_t *pos)
++(*pos);
for (; (si = swap_type_to_info(type)); type++) {
- if (!(si->swap_file))
+ if (!(si->swap_file) && !(si->flags & SWP_XSWAP))
continue;
return si;
}
@@ -3375,7 +3443,14 @@ static int swap_show(struct seq_file *swap, void *v)
inuse = K(swap_usage_in_pages(si));
file = si->swap_file;
- len = seq_file_path(swap, file, " \t\n\\");
+ if (file)
+ len = seq_file_path(swap, file, " \t\n\\");
+ else {
+ char name[16];
+
+ len = scnprintf(name, sizeof(name), "xswap%d", si->type);
+ seq_puts(swap, name);
+ }
seq_printf(swap, "%*s%s\t%lu\t%s%lu\t%s%d\n",
len < 40 ? 40 - len : 1, " ",
swap_type_str(si),
@@ -3897,6 +3972,86 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
return err;
}
+#ifdef CONFIG_XSWAP
+#ifdef CONFIG_SYSFS
+/* Create a file-less xswap device. si->max = full RAM; @percent sets the
+ * runtime nr_clusters ceiling.
+ */
+static int xswap_create(int percent)
+{
+ struct swap_info_struct *si;
+ unsigned long ram, maxpages, init_clusters;
+ int error;
+
+ if (percent < 1 || percent > 100)
+ return -EINVAL;
+
+ si = alloc_swap_info();
+ if (IS_ERR(si))
+ return PTR_ERR(si);
+
+ INIT_WORK(&si->discard_work, swap_discard_work);
+ INIT_WORK(&si->reclaim_work, swap_reclaim_work);
+
+ ram = totalram_pages();
+ maxpages = min_t(unsigned long, ram, swapfile_maximum_size);
+ if ((unsigned int)maxpages == 0)
+ maxpages = UINT_MAX;
+ if (maxpages < 2)
+ maxpages = 2;
+
+ si->bdev = NULL;
+ si->flags |= SWP_XSWAP | SWP_SOLIDSTATE;
+ si->max = maxpages;
+ si->pages = maxpages - 1;
+ /* no backing file: mirror the xswap branch of setup_swap_extents() */
+ si->ops = &swap_bdev_ops;
+
+ error = setup_swap_clusters_info(si, NULL, maxpages);
+ if (error)
+ goto bad_swap;
+
+ /* VM_SPARSE covers full RAM; runtime nr_clusters starts at percent. */
+ init_clusters = div_u64((u64)maxpages * percent, 100 * SWAPFILE_CLUSTER);
+ init_clusters = max_t(unsigned long, init_clusters, XSWAP_GROW_CLUSTERS);
+ if (init_clusters > si->nr_clusters)
+ init_clusters = si->nr_clusters;
+ si->nr_clusters = init_clusters;
+ si->pages = min_t(unsigned long, init_clusters * SWAPFILE_CLUSTER, si->max) - 1;
+
+ error = zswap_swapon(si->type, si->max);
+ if (error)
+ goto bad_swap;
+
+ mutex_lock(&swapon_mutex);
+ si->prio = DEF_SWAP_PRIO;
+ si->list.prio = -si->prio;
+ si->avail_list.prio = -si->prio;
+ /* si->swap_file stays NULL: this is a file-less device */
+ enable_swap_info(si);
+ mutex_unlock(&swapon_mutex);
+
+ pr_info("xswap: adding extendable swap type %d (%d%% of %lu pages = %u pages, max %lu)\n",
+ si->type, percent, ram, si->pages, maxpages);
+ atomic_inc(&proc_poll_event);
+ wake_up_interruptible(&proc_poll_wait);
+
+ return si->type;
+
+bad_swap:
+ kfree(si->global_cluster);
+ si->global_cluster = NULL;
+ destroy_swap_extents(si, NULL); /* safe: xswap never sets SWP_ACTIVATED */
+ free_swap_cluster_info(si);
+ si->cluster_info = NULL;
+ spin_lock(&swap_lock);
+ si->flags = 0;
+ spin_unlock(&swap_lock);
+ return error;
+}
+#endif /* CONFIG_SYSFS */
+#endif /* CONFIG_XSWAP */
+
SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
{
struct swap_info_struct *si;
@@ -4240,6 +4395,8 @@ static int __init swapfile_init(void)
swap_migration_ad_supported = true;
#endif /* CONFIG_MIGRATION */
+ xswap_sysfs_init();
+
return 0;
}
subsys_initcall(swapfile_init);
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (5 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 06/16] mm, swap: add sysfs create interface for xswap Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Baoquan He
` (9 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 1127e3d6596b..c6a01d74298a 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1273,6 +1273,51 @@ 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;
+
+ for (i = start; i < start + nr_new; i++) {
+ struct swap_cluster_info *ci = &si->cluster_info[i];
+
+ /*
+ * A concurrent grower may have already added
+ * these clusters to the free list. Only add
+ * clusters that are still off-list (NONE).
+ * Lock ci->lock first: move_cluster() takes
+ * si->lock internally.
+ */
+ spin_lock(&ci->lock);
+ if (ci->flags == CLUSTER_FLAG_NONE) {
+ move_cluster(si, ci, &si->free_clusters,
+ CLUSTER_FLAG_FREE);
+ added++;
+ }
+ spin_unlock(&ci->lock);
+ }
+ WRITE_ONCE(si->nr_free_tail,
+ READ_ONCE(si->nr_free_tail) + added);
+
+ /* Retry allocation from the free list */
+ found = alloc_swap_scan_list(si, &si->free_clusters,
+ folio, false);
+ }
+ }
+#endif
done:
if (!(si->flags & SWP_SOLIDSTATE))
spin_unlock(&si->global_cluster_lock);
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (6 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters Baoquan He
` (8 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 48 ++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 44 insertions(+), 4 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index c6a01d74298a..6b18c1b21adc 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -53,11 +53,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 \
@@ -69,6 +71,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);
#ifdef CONFIG_SYSFS
static int xswap_create(int percent);
@@ -697,6 +700,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
}
/*
@@ -3873,6 +3879,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,
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (7 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:45 ` [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection Baoquan He
` (7 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 41 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 6b18c1b21adc..0fe4834eadde 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3847,6 +3847,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)
{
@@ -3856,6 +3873,10 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
/* Round to page boundaries for vm_area_unmap_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;
mutex_lock(&si->xswap_lock);
@@ -3865,9 +3886,25 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
return;
}
+ size = vm_end - vm_start;
+ npages = size >> PAGE_SHIFT;
+
+ 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);
- /* vm_area_unmap_pages() clears PTEs but does not free pages. */
- /* TODO: free backing pages via page table walk or tracking bitmap */
+
+ /* 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. */
WRITE_ONCE(si->nr_clusters_mapped, start_idx);
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (8 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap Baoquan He
` (6 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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 += added - only the clusters actually
added to the free list extend the tail; a concurrent grower that
lost the race added none.
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).
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 128 +++++++++++++++++++++++++++++++++++++------
2 files changed, 112 insertions(+), 17 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index c2744fa8a009..fea0005b9f51 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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 */
struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 0fe4834eadde..5235c6d07ab2 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -71,6 +71,9 @@ 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_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);
#ifdef CONFIG_SYSFS
@@ -701,6 +704,7 @@ static void __free_cluster(struct swap_info_struct *si, struct swap_cluster_info
move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE);
ci->order = 0;
#ifdef CONFIG_XSWAP
+ xswap_update_free_tail(si, ci - si->cluster_info);
xswap_try_shrink(si);
#endif
}
@@ -1049,6 +1053,9 @@ static bool __swap_cluster_alloc_entries(struct swap_info_struct *si,
if (cluster_is_empty(ci))
ci->order = order;
ci->count += nr_pages;
+#ifdef CONFIG_XSWAP
+ xswap_trim_free_tail(si, cluster_index(si, ci));
+#endif
swap_range_alloc(si, nr_pages);
return true;
@@ -3918,37 +3925,124 @@ static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data)
}
/*
- * Try to shrink the cluster_info tail: unmap contiguous free clusters
- * at the end of the mapped range.
+ * Maintain si->nr_free_tail, the number of contiguous free clusters at
+ * the tail of the mapped range. Called when a cluster at @freed_idx is
+ * freed. Provides O(1) shrink detection: if nr_free_tail is non-zero,
+ * the tail can be unmapped without scanning cluster_info[].
+ *
+ * Only increments when @freed_idx is the cluster immediately before the
+ * existing tail region. Then scans backwards for already-free clusters
+ * now connected to the tail, bounded by XSWAP_GROW_CLUSTERS at a time.
*/
-static void xswap_try_shrink(struct swap_info_struct *si)
+static void xswap_update_free_tail(struct swap_info_struct *si,
+ unsigned long freed_idx)
{
+ unsigned long nr_mapped, nr_tail, tid, i;
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 */
+
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+
+ /* Protect against concurrent shrink that races past us */
+ if (nr_tail >= nr_mapped)
+ return;
+
+ tid = nr_mapped - nr_tail - 1;
+
+ /* Only the cluster immediately before the tail region counts */
+ if (freed_idx != tid)
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)
+ nr_tail++;
+ WRITE_ONCE(si->nr_free_tail, nr_tail);
+
+ /* Extend: include already-free clusters now connected to the tail */
+ for (i = 1; i < XSWAP_GROW_CLUSTERS; i++) {
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+ if (nr_tail >= nr_mapped - 1)
+ break; /* reached cluster 0 */
+ tid = nr_mapped - nr_tail - 1;
+ ci = &si->cluster_info[tid];
+
+ if (READ_ONCE(ci->count) ||
+ READ_ONCE(ci->flags) != CLUSTER_FLAG_FREE)
break;
- last = idx;
+ nr_tail++;
+ WRITE_ONCE(si->nr_free_tail, nr_tail);
}
+}
- if (last == si->nr_clusters_mapped)
- return; /* nothing to shrink */
+/*
+ * Trim si->nr_free_tail when a cluster in the tail region is allocated.
+ * @idx: index of the cluster being allocated.
+ */
+static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
+{
+ unsigned long nr_mapped, nr_tail, tail_start;
- /* Only unmap if we can free at least one full page of clusters */
- if (si->nr_clusters_mapped - last < XSWAP_GROW_CLUSTERS)
+ if (!(si->flags & SWP_XSWAP))
return;
- xswap_unmap_clusters(si, last, si->nr_clusters_mapped - last);
+ /*
+ * nr_clusters_mapped and nr_free_tail are read locklessly;
+ * concurrent updates may cause nr_free_tail to be trimmed
+ * slightly less than ideally, which is harmless.
+ */
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+ tail_start = nr_mapped - nr_tail;
+ if (idx >= tail_start)
+ WRITE_ONCE(si->nr_free_tail, nr_mapped - idx - 1);
+}
+
+/*
+ * Try to shrink the cluster_info tail. Uses si->nr_free_tail which
+ * is maintained incrementally during alloc/free — no scanning needed.
+ */
+static void xswap_try_shrink(struct swap_info_struct *si)
+{
+ unsigned long start_idx, nr_unmap, i;
+ struct swap_cluster_info *ci;
+
+ if (!(si->flags & SWP_XSWAP))
+ return;
+ if (si->nr_free_tail < XSWAP_GROW_CLUSTERS)
+ return;
+
+ nr_unmap = round_down(si->nr_free_tail, XSWAP_GROW_CLUSTERS);
+ start_idx = si->nr_clusters_mapped - nr_unmap;
+
+ /*
+ * Verify the tail clusters are still free before unmapping. Count the
+ * contiguous free run first, then detach it in a second pass once the
+ * whole run is confirmed long enough. Detaching while counting would
+ * leave a partial run off the free list if it proved too short to unmap.
+ */
+ spin_lock(&si->lock);
+ for (i = start_idx; i < si->nr_clusters_mapped; i++) {
+ ci = &si->cluster_info[i];
+ if (ci->flags != CLUSTER_FLAG_FREE)
+ break;
+ }
+ nr_unmap = i - start_idx;
+ if (nr_unmap < XSWAP_GROW_CLUSTERS) {
+ spin_unlock(&si->lock);
+ return;
+ }
+
+ for (i = start_idx; i < start_idx + nr_unmap; i++) {
+ ci = &si->cluster_info[i];
+ list_del_init(&ci->list);
+ ci->flags = CLUSTER_FLAG_NONE;
+ }
+ spin_unlock(&si->lock);
+
+ xswap_unmap_clusters(si, start_idx, nr_unmap);
+ si->nr_free_tail -= nr_unmap;
}
#endif /* CONFIG_XSWAP */
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (9 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit Baoquan He
` (5 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
Split the xswap cluster limit into two fields:
- nr_clusters_max: immutable hard limit set at device creation
- 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 creation, nr_clusters starts at nr_clusters_max (full size).
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 3 ++-
mm/swapfile.c | 29 +++++++++++++++++++++--------
2 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index fea0005b9f51..c55af4a95ef7 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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 */
struct mutex xswap_lock; /* serialize map/unmap operations */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5235c6d07ab2..b2c3bb21f082 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3235,6 +3235,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;
@@ -4005,16 +4006,27 @@ 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. Count the
@@ -4023,7 +4035,7 @@ static void xswap_try_shrink(struct swap_info_struct *si)
* leave a partial run off the free list if it proved too short to unmap.
*/
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)
break;
@@ -4042,7 +4054,7 @@ static void xswap_try_shrink(struct swap_info_struct *si)
spin_unlock(&si->lock);
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 */
@@ -4066,6 +4078,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;
@@ -4103,6 +4116,8 @@ 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:
@@ -4595,7 +4610,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
@@ -4605,7 +4619,6 @@ 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;
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (10 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Baoquan He
` (4 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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 on device creation and removed on
destruction.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 92 ++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 90 insertions(+), 3 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index c55af4a95ef7..5e50486103b9 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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 */
struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index b2c3bb21f082..6916a8b3f63b 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -50,6 +50,9 @@
#include "swap_table.h"
#include "internal.h"
#include "swap.h"
+#include <linux/debugfs.h>
+
+static DEFINE_SPINLOCK(swap_lock);
#ifdef CONFIG_XSWAP
/*
@@ -66,6 +69,8 @@
max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
#define XSWAP_DEFAULT_CLUSTER_PERCENT 30
+static struct dentry *xswap_debugfs_root;
+
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,
@@ -76,6 +81,83 @@ 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);
+/*
+ * debugfs read/write for per-device max cluster count.
+ */
+static ssize_t xswap_max_clusters_read(struct file *file, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct swap_info_struct *si = file->private_data;
+ char tmp[32];
+ int len;
+
+ len = snprintf(tmp, sizeof(tmp), "%lu\n", READ_ONCE(si->nr_clusters));
+ return simple_read_from_buffer(buf, count, ppos, tmp, len);
+}
+
+static ssize_t xswap_max_clusters_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct swap_info_struct *si = file->private_data;
+ unsigned long val, new_pages;
+ int err;
+
+ err = kstrtoul_from_user(buf, count, 0, &val);
+ if (err)
+ return err;
+
+ if (val > si->nr_clusters_max)
+ val = si->nr_clusters_max;
+
+ spin_lock(&si->lock);
+ si->nr_clusters = val;
+ spin_unlock(&si->lock);
+
+ new_pages = min_t(unsigned long, val * SWAPFILE_CLUSTER, si->max);
+ if (new_pages)
+ new_pages--;
+ if (new_pages != si->pages) {
+ long delta = (long)new_pages - (long)si->pages;
+
+ spin_lock(&swap_lock);
+ si->pages = new_pages;
+ atomic_long_add(delta, &nr_swap_pages);
+ total_swap_pages += delta;
+ spin_unlock(&swap_lock);
+ }
+
+ /* Lowering the ceiling may free tail clusters. */
+ xswap_try_shrink(si);
+
+ return count;
+}
+
+static const struct file_operations xswap_debugfs_fops = {
+ .read = xswap_max_clusters_read,
+ .write = xswap_max_clusters_write,
+ .open = simple_open,
+ .llseek = default_llseek,
+};
+
+static void xswap_debugfs_add(struct swap_info_struct *si)
+{
+ char name[32];
+
+ if (!xswap_debugfs_root)
+ return;
+
+ snprintf(name, sizeof(name), "type%d_cluster_limit", si->type);
+ si->debugfs_entry = debugfs_create_file(name, 0644, xswap_debugfs_root,
+ si, &xswap_debugfs_fops);
+}
+
+static void xswap_debugfs_del(struct swap_info_struct *si)
+{
+ debugfs_remove(si->debugfs_entry);
+ si->debugfs_entry = NULL;
+}
+
#ifdef CONFIG_SYSFS
static int xswap_create(int percent);
/* /sys/kernel/mm/xswap/: create.
@@ -156,7 +238,6 @@ static void move_cluster(struct swap_info_struct *si,
*
* Also protects swap_active_head total_swap_pages, and the SWP_WRITEOK flag.
*/
-static DEFINE_SPINLOCK(swap_lock);
static unsigned int nr_swapfiles;
atomic_long_t nr_swap_pages;
atomic_t nr_real_swapfiles;
@@ -3230,6 +3311,7 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
#ifdef CONFIG_XSWAP
if (si->flags & SWP_XSWAP) {
+ xswap_debugfs_del(si);
/* 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);
@@ -4118,6 +4200,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;
+ xswap_debugfs_add(si);
return 0;
err_unmap:
@@ -4245,8 +4328,8 @@ static int xswap_create(int percent)
/* VM_SPARSE covers full RAM; runtime nr_clusters starts at percent. */
init_clusters = div_u64((u64)maxpages * percent, 100 * SWAPFILE_CLUSTER);
init_clusters = max_t(unsigned long, init_clusters, XSWAP_GROW_CLUSTERS);
- if (init_clusters > si->nr_clusters)
- init_clusters = si->nr_clusters;
+ if (init_clusters > si->nr_clusters_max)
+ init_clusters = si->nr_clusters_max;
si->nr_clusters = init_clusters;
si->pages = min_t(unsigned long, init_clusters * SWAPFILE_CLUSTER, si->max) - 1;
@@ -4624,6 +4707,9 @@ static int __init swapfile_init(void)
swap_migration_ad_supported = true;
#endif /* CONFIG_MIGRATION */
+#ifdef CONFIG_XSWAP
+ xswap_debugfs_root = debugfs_create_dir("xswap", NULL);
+#endif
xswap_sysfs_init();
return 0;
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (11 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
` (3 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, 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.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 12 +++++++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 5e50486103b9..998f04c0b767 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -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 */
struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 6916a8b3f63b..dc112c378489 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -786,7 +786,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
}
@@ -3312,6 +3312,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);
@@ -4082,6 +4083,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.
@@ -4200,6 +4208,8 @@ 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;
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (12 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
` (2 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
1. Extract __swapoff() from sys_swapoff(): the core teardown logic
now lives in __swapoff(), shared by sys_swapoff() and the new
xswap_destroy(). swap_file operations are guarded with NULL check
so __swapoff() works for file-less devices too. sys_swapoff()
retains file-matching; a NULL guard on p->swap_file ensures xswap
devices are never matched by the file path.
2. Add xswap_destroy(int type): tears down a file-less xswap device
by its swap type. Validates SWP_XSWAP | SWP_WRITEOK, removes
from lists, delegates to __swapoff().
3. Add /sys/kernel/mm/xswap/destroy: write a swap type to tear down
that xswap device. Requires CAP_SYS_ADMIN.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 197 ++++++++++++++++++++++++++++++++++----------------
1 file changed, 136 insertions(+), 61 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index dc112c378489..451b46b747e3 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -160,6 +160,7 @@ static void xswap_debugfs_del(struct swap_info_struct *si)
#ifdef CONFIG_SYSFS
static int xswap_create(int percent);
+static int xswap_destroy(int type);
/* /sys/kernel/mm/xswap/: create.
* Per-device runtime size is tuned via debugfs type<N>_cluster_limit.
*/
@@ -192,8 +193,33 @@ static ssize_t xswap_create_store(struct kobject *kobj,
static struct kobj_attribute xswap_create_attr = __ATTR(create, 0200, NULL,
xswap_create_store);
+static ssize_t xswap_destroy_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ unsigned long type;
+ int err;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ err = kstrtoul(buf, 0, &type);
+ if (err)
+ return err;
+
+ err = xswap_destroy(type);
+ if (err)
+ return err;
+
+ return count;
+}
+
+static struct kobj_attribute xswap_destroy_attr = __ATTR(destroy, 0200, NULL,
+ xswap_destroy_store);
+
static struct attribute *xswap_attrs[] = {
&xswap_create_attr.attr,
+ &xswap_destroy_attr.attr,
NULL,
};
@@ -3361,61 +3387,13 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
}
-SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
+/* Common swap teardown after list removal; shared by sys_swapoff() and
+ * xswap_destroy().
+ */
+static int __swapoff(struct swap_info_struct *p)
{
- struct swap_info_struct *p = NULL;
- struct file *swap_file, *victim;
- struct address_space *mapping;
- struct inode *inode;
- int err, found = 0;
-
- if (!capable(CAP_SYS_ADMIN))
- return -EPERM;
-
- BUG_ON(!current->mm);
-
- CLASS(filename, pathname)(specialfile);
- victim = file_open_name(pathname, O_RDWR|O_LARGEFILE, 0);
- if (IS_ERR(victim))
- return PTR_ERR(victim);
-
- mapping = victim->f_mapping;
- spin_lock(&swap_lock);
- plist_for_each_entry(p, &swap_active_head, list) {
- if (p->flags & SWP_WRITEOK) {
- if (p->swap_file->f_mapping == mapping) {
- found = 1;
- break;
- }
- }
- }
- if (!found) {
- err = -EINVAL;
- spin_unlock(&swap_lock);
- goto out_dput;
- }
-
- /* Refuse swapoff while the device is pinned for hibernation */
- if (p->flags & SWP_HIBERNATION) {
- err = -EBUSY;
- spin_unlock(&swap_lock);
- goto out_dput;
- }
-
- if (!security_vm_enough_memory_mm(current->mm, p->pages))
- vm_unacct_memory(p->pages);
- else {
- err = -ENOMEM;
- spin_unlock(&swap_lock);
- goto out_dput;
- }
- spin_lock(&p->lock);
- del_from_avail_list(p, true);
- plist_del(&p->list, &swap_active_head);
- atomic_long_sub(p->pages, &nr_swap_pages);
- total_swap_pages -= p->pages;
- spin_unlock(&p->lock);
- spin_unlock(&swap_lock);
+ struct file *swap_file = NULL;
+ int err;
wait_for_allocation(p);
@@ -3426,7 +3404,7 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
if (err) {
/* re-insert swap space back into swap_list */
reinsert_swap_info(p);
- goto out_dput;
+ return err;
}
/*
@@ -3469,12 +3447,14 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
p->max = 0;
p->cluster_info = NULL;
- inode = mapping->host;
+ if (swap_file) {
+ struct inode *inode = swap_file->f_mapping->host;
- inode_lock(inode);
- inode->i_flags &= ~S_SWAPFILE;
- inode_unlock(inode);
- filp_close(swap_file, NULL);
+ inode_lock(inode);
+ inode->i_flags &= ~S_SWAPFILE;
+ inode_unlock(inode);
+ filp_close(swap_file, NULL);
+ }
/*
* Clear the SWP_USED flag after all resources are freed so that swapon
@@ -3485,10 +3465,69 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
p->flags = 0;
spin_unlock(&swap_lock);
- err = 0;
atomic_inc(&proc_poll_event);
wake_up_interruptible(&proc_poll_wait);
+ return 0;
+}
+
+SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
+{
+ struct swap_info_struct *p = NULL;
+ struct file *victim;
+ struct address_space *mapping;
+ int err, found = 0;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ BUG_ON(!current->mm);
+
+ CLASS(filename, pathname)(specialfile);
+ victim = file_open_name(pathname, O_RDWR|O_LARGEFILE, 0);
+ if (IS_ERR(victim))
+ return PTR_ERR(victim);
+
+ mapping = victim->f_mapping;
+ spin_lock(&swap_lock);
+ plist_for_each_entry(p, &swap_active_head, list) {
+ if (p->flags & SWP_WRITEOK) {
+ if (p->swap_file && p->swap_file->f_mapping == mapping) {
+ found = 1;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ err = -EINVAL;
+ spin_unlock(&swap_lock);
+ goto out_dput;
+ }
+
+ /* Refuse swapoff while the device is pinned for hibernation */
+ if (p->flags & SWP_HIBERNATION) {
+ err = -EBUSY;
+ spin_unlock(&swap_lock);
+ goto out_dput;
+ }
+
+ if (!security_vm_enough_memory_mm(current->mm, p->pages))
+ vm_unacct_memory(p->pages);
+ else {
+ err = -ENOMEM;
+ spin_unlock(&swap_lock);
+ goto out_dput;
+ }
+ spin_lock(&p->lock);
+ del_from_avail_list(p, true);
+ plist_del(&p->list, &swap_active_head);
+ atomic_long_sub(p->pages, &nr_swap_pages);
+ total_swap_pages -= p->pages;
+ spin_unlock(&p->lock);
+ spin_unlock(&swap_lock);
+
+ err = __swapoff(p);
+
out_dput:
filp_close(victim, NULL);
return err;
@@ -4373,6 +4412,42 @@ static int xswap_create(int percent)
spin_unlock(&swap_lock);
return error;
}
+
+/* Tear down a file-less xswap device by its swap type. */
+static int xswap_destroy(int type)
+{
+ struct swap_info_struct *p;
+
+ p = swap_type_to_info(type);
+ if (!p)
+ return -EINVAL;
+
+ spin_lock(&swap_lock);
+ if (!(p->flags & SWP_WRITEOK) || !(p->flags & SWP_XSWAP)) {
+ spin_unlock(&swap_lock);
+ return -EINVAL;
+ }
+ /* Refuse swapoff while the device is pinned for hibernation */
+ if (p->flags & SWP_HIBERNATION) {
+ spin_unlock(&swap_lock);
+ return -EBUSY;
+ }
+ if (!security_vm_enough_memory_mm(current->mm, p->pages))
+ vm_unacct_memory(p->pages);
+ else {
+ spin_unlock(&swap_lock);
+ return -ENOMEM;
+ }
+ spin_lock(&p->lock);
+ del_from_avail_list(p, true);
+ plist_del(&p->list, &swap_active_head);
+ atomic_long_sub(p->pages, &nr_swap_pages);
+ total_swap_pages -= p->pages;
+ spin_unlock(&p->lock);
+ spin_unlock(&swap_lock);
+
+ return __swapoff(p);
+}
#endif /* CONFIG_SYSFS */
#endif /* CONFIG_XSWAP */
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 15/16] mm, swap: require zswap for xswap devices
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (13 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 16/16] mm, swap: allow setting xswap device priority at creation Baoquan He
2026-08-27 13:59 ` [syzbot ci] Re: xswap: extendable swap device backed by zswap syzbot ci
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
xswap has no backing storage: swapped-out pages live only in zswap.
Without zswap, swapout always bounces back, so the device would
consume swap entry space without ever freeing memory. Refuse to
create a device when zswap is unavailable.
Runtime disabling of zswap after creation is safe: existing entries
stay loadable (zswap_load() gates on zswap_never_enabled(), not the
runtime zswap_enabled flag) and new swapouts merely bounce back to
memory without freeing it.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 451b46b747e3..ae0c549cc9aa 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -4349,6 +4349,10 @@ static int xswap_create(int percent)
if (percent < 1 || percent > 100)
return -EINVAL;
+ /* xswap has no backing store, it relies on zswap. */
+ if (!zswap_is_enabled())
+ return -EOPNOTSUPP;
+
si = alloc_swap_info();
if (IS_ERR(si))
return PTR_ERR(si);
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 16/16] mm, swap: allow setting xswap device priority at creation
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (14 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 13:59 ` [syzbot ci] Re: xswap: extendable swap device backed by zswap syzbot ci
16 siblings, 0 replies; 18+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel, Baoquan He
Let the create interface (/sys/kernel/mm/xswap/create) accept an
optional priority:
echo "<percent> [<prio>]" > /sys/kernel/mm/xswap/create
A missing prio means DEF_SWAP_PRIO; and permitted values are
DEF_SWAP_PRIO or 0..SWAP_FLAG_PRIO_MASK, being consistent with the
swapon(2) ABI. Like a regular swap device, prio is set once at creation
and is not chnageable whne the device is active; use xswap_destroy
then create to cange it.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 33 +++++++++++++++++++++------------
1 file changed, 21 insertions(+), 12 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index ae0c549cc9aa..258b52bce438 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -52,6 +52,8 @@
#include "swap.h"
#include <linux/debugfs.h>
+#define DEF_SWAP_PRIO -1
+
static DEFINE_SPINLOCK(swap_lock);
#ifdef CONFIG_XSWAP
@@ -159,9 +161,11 @@ static void xswap_debugfs_del(struct swap_info_struct *si)
}
#ifdef CONFIG_SYSFS
-static int xswap_create(int percent);
+static int xswap_create(int percent, int prio);
static int xswap_destroy(int type);
/* /sys/kernel/mm/xswap/: create.
+ * Write "<percent> [<prio>]" to add a zswap-backed swap device; a missing
+ * priority means DEF_SWAP_PRIO.
* Per-device runtime size is tuned via debugfs type<N>_cluster_limit.
*/
@@ -169,21 +173,25 @@ static ssize_t xswap_create_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
- unsigned long percent;
- int err;
+ long percent;
+ int prio = DEF_SWAP_PRIO;
+ int nr, err;
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
- err = kstrtoul(buf, 0, &percent);
- if (err)
- return err;
+ /* "<percent> [<prio>]"; a missing prio means DEF_SWAP_PRIO.
+ * %li keeps kstrtoul(buf, 0)'s base-0 behavior for percent.
+ */
+ nr = sscanf(buf, "%li %d", &percent, &prio);
+ if (nr < 1)
+ return -EINVAL;
/* 0 means "use the default percent" */
if (percent == 0)
percent = XSWAP_DEFAULT_CLUSTER_PERCENT;
- err = xswap_create(percent);
+ err = xswap_create(percent, prio);
if (err < 0)
return err;
@@ -275,7 +283,6 @@ atomic_t nr_real_swapfiles;
EXPORT_SYMBOL_GPL(nr_swap_pages);
/* protected with swap_lock. reading in vm_swap_full() doesn't need lock */
long total_swap_pages;
-#define DEF_SWAP_PRIO -1
unsigned long swapfile_maximum_size;
#ifdef CONFIG_MIGRATION
bool swap_migration_ad_supported;
@@ -4340,7 +4347,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
/* Create a file-less xswap device. si->max = full RAM; @percent sets the
* runtime nr_clusters ceiling.
*/
-static int xswap_create(int percent)
+static int xswap_create(int percent, int prio)
{
struct swap_info_struct *si;
unsigned long ram, maxpages, init_clusters;
@@ -4348,6 +4355,8 @@ static int xswap_create(int percent)
if (percent < 1 || percent > 100)
return -EINVAL;
+ if (prio != DEF_SWAP_PRIO && (prio < 0 || prio > SWAP_FLAG_PRIO_MASK))
+ return -EINVAL;
/* xswap has no backing store, it relies on zswap. */
if (!zswap_is_enabled())
@@ -4391,15 +4400,15 @@ static int xswap_create(int percent)
goto bad_swap;
mutex_lock(&swapon_mutex);
- si->prio = DEF_SWAP_PRIO;
+ si->prio = prio;
si->list.prio = -si->prio;
si->avail_list.prio = -si->prio;
/* si->swap_file stays NULL: this is a file-less device */
enable_swap_info(si);
mutex_unlock(&swapon_mutex);
- pr_info("xswap: adding extendable swap type %d (%d%% of %lu pages = %u pages, max %lu)\n",
- si->type, percent, ram, si->pages, maxpages);
+ pr_info("xswap: adding extendable swap type %d (prio %d, %d%% of %lu pages = %u pages, max %lu)\n",
+ si->type, prio, percent, ram, si->pages, maxpages);
atomic_inc(&proc_poll_event);
wake_up_interruptible(&proc_poll_wait);
--
2.54.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [syzbot ci] Re: xswap: extendable swap device backed by zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (15 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 16/16] mm, swap: allow setting xswap device priority at creation Baoquan He
@ 2026-08-27 13:59 ` syzbot ci
16 siblings, 0 replies; 18+ messages in thread
From: syzbot ci @ 2026-08-27 13:59 UTC (permalink / raw)
To: akpm, baohua, baoquan.he, chengming.zhou, chrisl, david, hannes,
hebaoquan, kasong, linux-kernel, linux-mm, nphamcs, shikemeng,
yosry, youngjun.park
Cc: syzbot, syzkaller-bugs
syzbot ci has tested the following series
[v1] xswap: extendable swap device backed by zswap
https://lore.kernel.org/all/20260827094509.1016740-1-hebaoquan@kylinos.cn
* [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
* [PATCH 02/16] mm: xswap support for zswap
* [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct
* [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct
* [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc
* [PATCH 06/16] mm, swap: add sysfs create interface for xswap
* [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
* [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free
* [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters
* [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection
* [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap
* [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit
* [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
* [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
* [PATCH 15/16] mm, swap: require zswap for xswap devices
* [PATCH 16/16] mm, swap: allow setting xswap device priority at creation
and found the following issue:
possible deadlock in console_flush_all
Full report is available here:
https://ci.syzbot.org/series/6e23a918-7d02-47ba-a61f-bcad82aebf14
***
possible deadlock in console_flush_all
tree: linux-next
URL: https://kernel.googlesource.com/pub/scm/linux/kernel/git/next/linux-next
base: 169393fff5d1ec2690934067eeb95544ff5ebdd7
arch: amd64
compiler: Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
config: https://ci.syzbot.org/builds/633d2731-0a5e-45d2-96bf-bd29305475b9/config
syz repro: https://ci.syzbot.org/findings/b9106c23-79f3-457d-bb14-c0cf63e4bed2/syz_repro
------------[ cut here ]------------
======================================================
WARNING: possible circular locking dependency detected
syzkaller #0 Not tainted
------------------------------------------------------
syz.1.20/5854 is trying to acquire lock:
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: rcu_try_lock_acquire include/linux/rcupdate.h:305 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: srcu_read_lock_nmisafe include/linux/srcu.h:428 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_srcu_read_lock kernel/printk/printk.c:291 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_flush_one_record kernel/printk/printk.c:3246 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_flush_all+0x123/0xaf0 kernel/printk/printk.c:3343
but task is already holding lock:
ffff88823c63a898 (&pool->lock){-.-.}-{2:2}, at: __queue_work+0x7fe/0x10a0 kernel/workqueue.c:2358
which lock already depends on the new lock.
the existing dependency chain (in reverse order) is:
-> #3 (&pool->lock){-.-.}-{2:2}:
__raw_spin_lock include/linux/spinlock_api_smp.h:158 [inline]
_raw_spin_lock+0x2e/0x40 kernel/locking/spinlock.c:158
__queue_work+0x72f/0x10a0 kernel/workqueue.c:2361
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
queue_work include/linux/workqueue.h:699 [inline]
rpm_suspend+0xeca/0x17b0 drivers/base/power/runtime.c:688
__pm_runtime_idle+0x12f/0x1a0 drivers/base/power/runtime.c:1129
pm_runtime_put include/linux/pm_runtime.h:551 [inline]
__device_attach+0x355/0x450 drivers/base/dd.c:1116
device_initial_probe+0xa1/0xd0 drivers/base/dd.c:1153
bus_probe_device+0x12a/0x220 drivers/base/bus.c:620
device_add+0x7d7/0xb80 drivers/base/core.c:3772
serial_base_port_add+0x18f/0x270 drivers/tty/serial/serial_base_bus.c:186
serial_core_port_device_add drivers/tty/serial/serial_core.c:3275 [inline]
serial_core_register_port+0x37f/0x2840 drivers/tty/serial/serial_core.c:3314
serial8250_register_8250_port+0x16b4/0x2090 drivers/tty/serial/8250/8250_core.c:828
serial_pnp_probe+0x56a/0x7f0 drivers/tty/serial/8250/8250_pnp.c:480
pnp_device_probe+0x30b/0x4c0 drivers/pnp/driver.c:111
call_driver_probe drivers/base/dd.c:-1 [inline]
really_probe+0x254/0xae0 drivers/base/dd.c:706
__driver_probe_device+0x1e8/0x360 drivers/base/dd.c:868
driver_probe_device+0x4f/0x240 drivers/base/dd.c:898
__driver_attach+0x339/0x600 drivers/base/dd.c:1292
bus_for_each_dev+0x23b/0x2c0 drivers/base/bus.c:383
bus_add_driver+0x345/0x670 drivers/base/bus.c:763
driver_register+0x23a/0x320 drivers/base/driver.c:174
serial8250_init+0x8f/0x160 drivers/tty/serial/8250/8250_platform.c:317
do_one_initcall+0x250/0x870 init/main.c:1347
do_initcall_level+0x10a/0x1a0 init/main.c:1409
do_initcalls+0x59/0xa0 init/main.c:1425
kernel_init_freeable+0x29d/0x3e0 init/main.c:1658
kernel_init+0x1d/0x1d0 init/main.c:1548
ret_from_fork+0x514/0xb70 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
-> #2 (&dev->power.lock){-...}-{3:3}:
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:166
__pm_runtime_resume+0x10f/0x180 drivers/base/power/runtime.c:1196
pm_runtime_get include/linux/pm_runtime.h:494 [inline]
__uart_start+0x171/0x460 drivers/tty/serial/serial_core.c:149
uart_write+0x251/0x9f0 drivers/tty/serial/serial_core.c:629
process_output_block drivers/tty/n_tty.c:557 [inline]
n_tty_write+0xd49/0x11e0 drivers/tty/n_tty.c:2366
iterate_tty_write drivers/tty/tty_io.c:1006 [inline]
file_tty_write+0x509/0x9c0 drivers/tty/tty_io.c:1081
new_sync_write fs/read_write.c:595 [inline]
vfs_write+0x612/0xba0 fs/read_write.c:687
ksys_write+0x150/0x270 fs/read_write.c:739
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x174/0x580 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
-> #1 (&port_lock_key){-.-.}-{3:3}:
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:166
uart_port_lock_irqsave include/linux/serial_core.h:719 [inline]
serial8250_console_write+0x153/0x1ba0 drivers/tty/serial/8250/8250_port.c:3344
console_emit_next_record kernel/printk/printk.c:3183 [inline]
console_flush_one_record kernel/printk/printk.c:3269 [inline]
console_flush_all+0x6ea/0xaf0 kernel/printk/printk.c:3343
__console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
_printk+0xdd/0x130 kernel/printk/printk.c:2504
register_console+0xbc3/0xfc0 kernel/printk/printk.c:4208
univ8250_console_init+0x3a/0x70 drivers/tty/serial/8250/8250_core.c:515
console_init+0x10b/0x4a0 kernel/printk/printk.c:4407
start_kernel+0x238/0x3e0 init/main.c:1112
x86_64_start_reservations+0x24/0x30 arch/x86/kernel/head64.c:310
x86_64_start_kernel+0x137/0x1b0 arch/x86/kernel/head64.c:291
common_startup_64+0x13e/0x157
-> #0 (console_owner){..-.}-{0:0}:
check_prev_add kernel/locking/lockdep.c:3165 [inline]
check_prevs_add kernel/locking/lockdep.c:3284 [inline]
validate_chain kernel/locking/lockdep.c:3908 [inline]
__lock_acquire+0x15ff/0x2e50 kernel/locking/lockdep.c:5238
lock_acquire+0x115/0x350 kernel/locking/lockdep.c:5890
console_lock_spinning_enable kernel/printk/printk.c:1902 [inline]
console_emit_next_record kernel/printk/printk.c:3177 [inline]
console_flush_one_record kernel/printk/printk.c:3269 [inline]
console_flush_all+0x693/0xaf0 kernel/printk/printk.c:3343
__console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
_printk+0xdd/0x130 kernel/printk/printk.c:2504
__report_bug+0x349/0x570 lib/bug.c:248
report_bug+0x16a/0x220 lib/bug.c:286
handle_bug+0x9c/0x200 arch/x86/kernel/traps.c:436
exc_invalid_op+0x1a/0x50 arch/x86/kernel/traps.c:490
asm_exc_invalid_op+0x1a/0x20 arch/x86/include/asm/idtentry.h:593
__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
zap_nonpresent_ptes mm/memory.c:1900 [inline]
do_zap_pte_range mm/memory.c:1967 [inline]
zap_pte_range mm/memory.c:2065 [inline]
zap_pmd_range mm/memory.c:2151 [inline]
zap_pud_range mm/memory.c:2179 [inline]
zap_p4d_range mm/memory.c:2200 [inline]
__zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
unmap_vmas+0x390/0x550 mm/memory.c:2309
exit_mmap+0x293/0x9f0 mm/mmap.c:1315
__mmput+0x118/0x420 kernel/fork.c:1187
exit_mm+0x221/0x2d0 kernel/exit.c:615
do_exit+0x6cd/0x2360 kernel/exit.c:997
do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
get_signal+0x121b/0x12c0 kernel/signal.c:3046
arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
__exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
other info that might help us debug this:
Chain exists of:
console_owner --> &dev->power.lock --> &pool->lock
Possible unsafe locking scenario:
CPU0 CPU1
---- ----
lock(&pool->lock);
lock(&dev->power.lock);
lock(&pool->lock);
lock(console_owner);
*** DEADLOCK ***
8 locks held by syz.1.20/5854:
#0: ffff8881bb3703b8 (&mm->mmap_lock){++++}-{4:4}, at: mmap_read_lock include/linux/mmap_lock.h:600 [inline]
#0: ffff8881bb3703b8 (&mm->mmap_lock){++++}-{4:4}, at: exit_mmap+0x1a4/0x9f0 mm/mmap.c:1299
#1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:300 [inline]
#1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:840 [inline]
#1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: __pte_offset_map+0x29/0x240 mm/pgtable-generic.c:290
#2: ffff88816d267f18 (ptlock_ptr(ptdesc)#2){+.+.}-{3:3}, at: spin_lock include/linux/spinlock.h:342 [inline]
#2: ffff88816d267f18 (ptlock_ptr(ptdesc)#2){+.+.}-{3:3}, at: pte_offset_map_lock+0x13d/0x210 mm/pgtable-generic.c:404
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: spin_lock include/linux/spinlock.h:342 [inline]
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: __swap_cluster_lock mm/swap.h:155 [inline]
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: swap_cluster_lock mm/swap.h:170 [inline]
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: swap_put_entries_cluster+0x109/0x4b0 mm/swapfile.c:1864
#4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:300 [inline]
#4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:840 [inline]
#4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: __queue_work+0x1ea/0x10a0 kernel/workqueue.c:2321
#5: ffff88823c63a898 (&pool->lock){-.-.}-{2:2}, at: __queue_work+0x7fe/0x10a0 kernel/workqueue.c:2358
#6: ffffffff8eb4bfa0 (console_lock){+.+.}-{0:0}, at: _printk+0xdd/0x130 kernel/printk/printk.c:2504
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: rcu_try_lock_acquire include/linux/rcupdate.h:305 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: srcu_read_lock_nmisafe include/linux/srcu.h:428 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_srcu_read_lock kernel/printk/printk.c:291 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_flush_one_record kernel/printk/printk.c:3246 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_flush_all+0x123/0xaf0 kernel/printk/printk.c:3343
stack backtrace:
CPU: 1 UID: 0 PID: 5854 Comm: syz.1.20 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
<TASK>
dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
print_circular_bug+0x2e2/0x300 kernel/locking/lockdep.c:2043
check_noncircular+0x12f/0x150 kernel/locking/lockdep.c:2175
check_prev_add kernel/locking/lockdep.c:3165 [inline]
check_prevs_add kernel/locking/lockdep.c:3284 [inline]
validate_chain kernel/locking/lockdep.c:3908 [inline]
__lock_acquire+0x15ff/0x2e50 kernel/locking/lockdep.c:5238
lock_acquire+0x115/0x350 kernel/locking/lockdep.c:5890
console_lock_spinning_enable kernel/printk/printk.c:1902 [inline]
console_emit_next_record kernel/printk/printk.c:3177 [inline]
console_flush_one_record kernel/printk/printk.c:3269 [inline]
console_flush_all+0x693/0xaf0 kernel/printk/printk.c:3343
__console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
_printk+0xdd/0x130 kernel/printk/printk.c:2504
__report_bug+0x349/0x570 lib/bug.c:248
report_bug+0x16a/0x220 lib/bug.c:286
handle_bug+0x9c/0x200 arch/x86/kernel/traps.c:436
exc_invalid_op+0x1a/0x50 arch/x86/kernel/traps.c:490
asm_exc_invalid_op+0x1a/0x20 arch/x86/include/asm/idtentry.h:593
RIP: 0010:__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
Code: 3d 7e ec b2 0e 01 0f 85 35 02 00 00 e8 a1 28 39 00 e9 82 f3 ff ff e8 97 28 39 00 90 0f 0b 90 e9 21 fd ff ff e8 89 28 39 00 90 <0f> 0b 90 e9 80 fe ff ff e8 7b 28 39 00 eb 11 e8 74 28 39 00 4c 8b
RSP: 0018:ffffc90003cdf1d8 EFLAGS: 00010093
RAX: ffffffff818dc2d7 RBX: dffffc0000000000 RCX: ffff888113955a00
RDX: 0000000000000000 RSI: ffffffff8c4bca60 RDI: ffffffff8c4bca20
RBP: 0000000000000001 R08: ffffffff90572d37 R09: 1ffffffff20ae5a6
R10: dffffc0000000000 R11: fffffbfff20ae5a7 R12: ffff888160408400
R13: ffff88810fdd7088 R14: 0000000000000001 R15: ffff88810fdd7090
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
zap_nonpresent_ptes mm/memory.c:1900 [inline]
do_zap_pte_range mm/memory.c:1967 [inline]
zap_pte_range mm/memory.c:2065 [inline]
zap_pmd_range mm/memory.c:2151 [inline]
zap_pud_range mm/memory.c:2179 [inline]
zap_p4d_range mm/memory.c:2200 [inline]
__zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
unmap_vmas+0x390/0x550 mm/memory.c:2309
exit_mmap+0x293/0x9f0 mm/mmap.c:1315
__mmput+0x118/0x420 kernel/fork.c:1187
exit_mm+0x221/0x2d0 kernel/exit.c:615
do_exit+0x6cd/0x2360 kernel/exit.c:997
do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
get_signal+0x121b/0x12c0 kernel/signal.c:3046
arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
__exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f46ce79e0d9
Code: Unable to access opcode bytes at 0x7f46ce79e0af.
RSP: 002b:00007f46cf6fc028 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
RAX: 0000000000000000 RBX: 00007f46cea26090 RCX: 00007f46ce79e0d9
RDX: 0000000000000015 RSI: 0000000000600003 RDI: 0000200000000000
RBP: 00007f46ce835024 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f46cea26128 R14: 00007f46cea26090 R15: 00007ffefd86c578
</TASK>
!list_empty(&work->entry)
WARNING: kernel/workqueue.c:2385 at __queue_work+0xd18/0x10a0 kernel/workqueue.c:2385, CPU#1: syz.1.20/5854
Modules linked in:
CPU: 1 UID: 0 PID: 5854 Comm: syz.1.20 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
RIP: 0010:__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
Code: 3d 7e ec b2 0e 01 0f 85 35 02 00 00 e8 a1 28 39 00 e9 82 f3 ff ff e8 97 28 39 00 90 0f 0b 90 e9 21 fd ff ff e8 89 28 39 00 90 <0f> 0b 90 e9 80 fe ff ff e8 7b 28 39 00 eb 11 e8 74 28 39 00 4c 8b
RSP: 0018:ffffc90003cdf1d8 EFLAGS: 00010093
RAX: ffffffff818dc2d7 RBX: dffffc0000000000 RCX: ffff888113955a00
RDX: 0000000000000000 RSI: ffffffff8c4bca60 RDI: ffffffff8c4bca20
RBP: 0000000000000001 R08: ffffffff90572d37 R09: 1ffffffff20ae5a6
R10: dffffc0000000000 R11: fffffbfff20ae5a7 R12: ffff888160408400
R13: ffff88810fdd7088 R14: 0000000000000001 R15: ffff88810fdd7090
FS: 00007f46cf6fc6c0(0000) GS:ffff8882a8f5a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000e6c000 CR3: 000000010a5a2000 CR4: 00000000000006f0
Call Trace:
<TASK>
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
zap_nonpresent_ptes mm/memory.c:1900 [inline]
do_zap_pte_range mm/memory.c:1967 [inline]
zap_pte_range mm/memory.c:2065 [inline]
zap_pmd_range mm/memory.c:2151 [inline]
zap_pud_range mm/memory.c:2179 [inline]
zap_p4d_range mm/memory.c:2200 [inline]
__zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
unmap_vmas+0x390/0x550 mm/memory.c:2309
exit_mmap+0x293/0x9f0 mm/mmap.c:1315
__mmput+0x118/0x420 kernel/fork.c:1187
exit_mm+0x221/0x2d0 kernel/exit.c:615
do_exit+0x6cd/0x2360 kernel/exit.c:997
do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
get_signal+0x121b/0x12c0 kernel/signal.c:3046
arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
__exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f46ce79e0d9
Code: Unable to access opcode bytes at 0x7f46ce79e0af.
RSP: 002b:00007f46cf6fc028 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
RAX: 0000000000000000 RBX: 00007f46cea26090 RCX: 00007f46ce79e0d9
RDX: 0000000000000015 RSI: 0000000000600003 RDI: 0000200000000000
RBP: 00007f46ce835024 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f46cea26128 R14: 00007f46cea26090 R15: 00007ffefd86c578
</TASK>
***
If these findings have caused you to resend the series or submit a
separate fix, please add the following tag to your commit message:
Tested-by: syzbot@syzkaller.appspotmail.com
---
This report is generated by a bot. It may contain errors.
syzbot ci engineers can be reached at syzkaller@googlegroups.com.
To test a fix for this bug, please reply with `#syz test`
(on a separate line) and attach the patch to the email.
Notes:
- The patch will be applied on top of the tested series (as an
incremental fix).
- To test a new version of the whole series, please send it directly
to syzbot@lists.linux.dev.
- Arguments like custom git repos and branches are not supported.
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-08-27 13:59 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
2026-08-27 9:44 ` [PATCH 02/16] mm: xswap support for zswap Baoquan He
2026-08-27 9:44 ` [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct Baoquan He
2026-08-27 9:44 ` [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Baoquan He
2026-08-27 9:44 ` [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc Baoquan He
2026-08-27 9:44 ` [PATCH 06/16] mm, swap: add sysfs create interface for xswap Baoquan He
2026-08-27 9:44 ` [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
2026-08-27 9:44 ` [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Baoquan He
2026-08-27 9:44 ` [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters Baoquan He
2026-08-27 9:45 ` [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection Baoquan He
2026-08-27 9:45 ` [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap Baoquan He
2026-08-27 9:45 ` [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit Baoquan He
2026-08-27 9:45 ` [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Baoquan He
2026-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
2026-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
2026-08-27 9:45 ` [PATCH 16/16] mm, swap: allow setting xswap device priority at creation Baoquan He
2026-08-27 13:59 ` [syzbot ci] Re: xswap: extendable swap device backed by zswap syzbot ci
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®