From: Baoquan He <baoquan.he@linux.dev>
To: linux-mm@kvack.org
Cc: akpm@linux-foundation.org, david@kernel.org, chrisl@kernel.org,
kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org,
youngjun.park@lge.com, corbet@lwn.net, hannes@cmpxchg.org,
chengming.zhou@linux.dev, kunwu.chan@gmail.com,
klarasmodin@gmail.com, linux-kernel@vger.kernel.org,
Baoquan He <hebaoquan@kylinos.cn>
Subject: [PATCH v5 17/19] mm, swap: add xswap shrink trigger and defer it to a workqueue
Date: Mon, 9 Nov 2026 13:54:53 +0800 [thread overview]
Message-ID: <20261109055455.536925-18-baoquan.he@linux.dev> (raw)
In-Reply-To: <20261109055455.536925-1-baoquan.he@linux.dev>
From: Baoquan He <hebaoquan@kylinos.cn>
The shrink cannot run directly from __free_cluster(), because that path
holds ci->lock. xswap_try_shrink() takes xswap_lock and can sleep in
synchronize_rcu() and vm_area_unmap_pages(). __free_cluster() schedules
xswap_shrink_work instead, and teardown disables the work.
The shrink unmaps the free tail of the cluster_info mapping. It costs
two RCU grace periods: one after draining the per-CPU cache and before
the tail is rechecked, and one inside the unmap helper. The second one
is needed because a cluster can be allocated and freed between the
recheck and the unmap, and go back into the per-CPU cache. xswap devices
are SWP_SOLIDSTATE, so the per-CPU cache is the only one that needs
draining.
The unmap helper takes a wait flag. The shrink passes true. Teardown
passes false because __swapoff() has already drained and waited. The
create failure path also passes false because it has no readers.
Count shrink operations in xswap_shrinks. Initialize the work in
xswap_create() so free_swap_cluster_info() can always call
disable_work_sync(). Using disable_work_sync() instead of only
cancel_work_sync() also rejects a schedule that read SWP_WRITEOK just
before swapoff. Then no work stays pending when the swap_info is reused
and INIT_WORK() runs again.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 233 ++++++++++++++++++++++++++++++++++++++-----
2 files changed, 211 insertions(+), 23 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 29623c5138ab..1e552a8b421b 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -247,6 +247,7 @@ struct swap_info_struct {
u64 xswap_id; /* stable id, shown to userspace */
unsigned long nr_clusters_max;/* total clusters in the xswap address space */
unsigned long nr_clusters_mapped; /* currently mapped cluster count */
+ struct work_struct xswap_shrink_work; /* deferred shrink trigger */
struct mutex xswap_lock; /* serialize map/unmap operations */
struct dentry *xswap_debugfs; /* mapped range and counters */
atomic_long_t xswap_grows; /* grow operations so far */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 097ed288b1ca..2e5383036fdc 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -66,12 +66,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
- * overhead is proportional to the number of clusters mapped.
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map/unmap in one
+ * grow/shrink operation: 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)
@@ -82,6 +83,14 @@
max_t(unsigned long, \
((XSWAP_UNMAP_BATCH_PAGES * PAGE_SIZE) / \
sizeof(struct swap_cluster_info)), 16)
+/*
+ * Shrink thresholds, as a percentage of the mapped range in use. The stop
+ * point sits inside (SHRINK_WHEN, GROW_WHEN), so shrinking cannot leave the
+ * range in a state that wakes the grow up.
+ */
+#define XSWAP_SHRINK_WHEN 50
+#define XSWAP_SHRINK_UNTIL 70
+
/*
* Grow thresholds, as a percentage of the mapped range in use. The target
* sits above the point where the shrink starts looking (50%) and below the
@@ -100,6 +109,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_mapped_end(pte_t *pte, unsigned long addr, void *data);
+static void xswap_try_shrink(struct swap_info_struct *si);
static int xswap_create(int prio);
static int xswap_destroy(u64 id);
@@ -866,6 +876,16 @@ 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
+ /*
+ * Only xswap devices, and not while the device is being torn down.
+ * A racing swapoff can still clear SWP_WRITEOK after this check;
+ * the work function checks it again, so a late schedule is benign.
+ */
+ if ((READ_ONCE(si->flags) & (SWP_XSWAP | SWP_WRITEOK)) ==
+ (SWP_XSWAP | SWP_WRITEOK))
+ schedule_work(&si->xswap_shrink_work);
+#endif
}
/*
@@ -3515,6 +3535,8 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
if (si->flags & SWP_XSWAP) {
unsigned long nr_mapped;
+ /* A late schedule must not be pending when the work is reinitialized. */
+ disable_work_sync(&si->xswap_shrink_work);
/*
* Cluster 0 keeps the bad header slot, so it never empties
* and __free_cluster() never frees its table.
@@ -3560,8 +3582,10 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
}
/*
- * Called after swap device's reference count is dead, so
- * neither scan nor allocation will use it.
+ * Invalidate the per-cpu cluster cache for @si. A caller that has not
+ * already killed @si->users and waited must follow with synchronize_rcu()
+ * before unmapping cluster_info, since a reader may already have loaded
+ * a cached offset.
*/
static void flush_percpu_swap_cluster(struct swap_info_struct *si)
{
@@ -3570,11 +3594,7 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
for_each_possible_cpu(cpu) {
pcp_si = per_cpu_ptr(percpu_swap_cluster.si, cpu);
- /*
- * Invalidate the per-cpu swap cluster cache. Any reader that
- * has already loaded an entry is waited out by the caller's
- * synchronize_rcu().
- */
+ /* Invalidate the per-cpu swap cluster cache for @si. */
for (i = 0; i < SWAP_NR_ORDERS; i++)
cmpxchg(&pcp_si[i], si, NULL);
}
@@ -3681,6 +3701,15 @@ static int __swapoff(struct swap_info_struct *p)
struct file *swap_file = NULL;
int err;
+#ifdef CONFIG_XSWAP
+ if (p->flags & SWP_XSWAP) {
+ cancel_work_sync(&p->xswap_shrink_work);
+ /* Wait out a shrink racing us from the sysfs write path. */
+ mutex_lock(&p->xswap_lock);
+ mutex_unlock(&p->xswap_lock);
+ }
+#endif
+
wait_for_allocation(p);
set_current_oom_origin();
@@ -4317,9 +4346,13 @@ static unsigned long xswap_grow(struct swap_info_struct *si)
return nr_new;
}
-/* Caller must hold si->xswap_lock. Cannot fail. */
+/*
+ * Caller must hold si->xswap_lock. Cannot fail. @wait drains the per-cpu
+ * cluster cache and waits one RCU grace period before unmapping.
+ */
static void xswap_unmap_clusters_locked(struct swap_info_struct *si,
- unsigned long start_idx, unsigned long nr)
+ unsigned long start_idx, unsigned long nr,
+ bool wait)
{
unsigned long start_addr = (unsigned long)si->cluster_info +
(size_t)start_idx * sizeof(struct swap_cluster_info);
@@ -4348,14 +4381,16 @@ static void xswap_unmap_clusters_locked(struct swap_info_struct *si,
return;
}
- /*
- * A per-cpu cluster cache can still hold an offset in this range.
- * Invalidate those references, then wait out the readers that have
- * already loaded one, so that nobody can dereference cluster_info
- * past this point. swapoff() needs the same before it releases.
- */
- flush_percpu_swap_cluster(si);
- synchronize_rcu();
+ if (wait) {
+ /*
+ * A per-cpu cluster cache can still hold an offset in this
+ * range. Invalidate those references, then wait out the
+ * readers that have already loaded one, so that nobody can
+ * dereference cluster_info past this point.
+ */
+ flush_percpu_swap_cluster(si);
+ synchronize_rcu();
+ }
npages = (vm_end - vm_start) >> PAGE_SHIFT;
@@ -4398,7 +4433,11 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr)
{
mutex_lock(&si->xswap_lock);
- xswap_unmap_clusters_locked(si, start_idx, nr);
+ /*
+ * The teardown caller has already drained and waited in __swapoff();
+ * the create failure path has no readers.
+ */
+ xswap_unmap_clusters_locked(si, start_idx, nr, false);
mutex_unlock(&si->xswap_lock);
}
@@ -4413,6 +4452,153 @@ static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data)
*mapped_end = addr + PAGE_SIZE;
return 0;
}
+
+/*
+ * Automatic reclaim: leave one chunk of the free tail mapped as slack, so
+ * that the next allocation has somewhere to land, and only unmap once
+ * several chunks can go, so the unmap is worth the two RCU grace periods
+ * it costs.
+ */
+#define XSWAP_SHRINK_SLACK XSWAP_GROW_CLUSTERS
+#define XSWAP_SHRINK_MIN (XSWAP_GROW_CLUSTERS * 8)
+
+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);
+
+ if (!(READ_ONCE(si->flags) & SWP_WRITEOK))
+ return;
+ xswap_try_shrink(si);
+}
+
+/*
+ * 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 nr_mapped, nr_tail, keep, nr_unmap, start_idx, i;
+
+ if (!(si->flags & SWP_XSWAP))
+ return;
+
+ mutex_lock(&si->xswap_lock);
+
+ /* A swapoff raced us and is about to walk this mapping. */
+ if (!(READ_ONCE(si->flags) & SWP_WRITEOK))
+ goto out_unlock;
+
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ if (nr_mapped <= 1) /* keep cluster 0 */
+ goto out_unlock;
+
+ /*
+ * Reclaim on our own, but only once the mapped range is at most
+ * SHRINK_WHEN in use: growth is demand driven, so reclaiming on a
+ * smaller dip would only map the same clusters again, and every
+ * unmap costs two RCU grace periods.
+ */
+ if (swap_usage_in_pages(si) * 100 >
+ nr_mapped * SWAPFILE_CLUSTER * XSWAP_SHRINK_WHEN)
+ goto out_unlock;
+
+ /*
+ * Count the free clusters at the tail of the mapped range. Scanned,
+ * not tracked: the count must be exact to size the unmap, and an
+ * incremental count falls behind on out-of-order frees.
+ */
+ nr_tail = 0;
+ while (nr_mapped - nr_tail > 1) {
+ ci = &si->cluster_info[nr_mapped - nr_tail - 1];
+ if (READ_ONCE(ci->count) ||
+ READ_ONCE(ci->flags) != CLUSTER_FLAG_FREE)
+ break;
+ nr_tail++;
+ }
+ if (nr_tail < XSWAP_SHRINK_SLACK + XSWAP_SHRINK_MIN)
+ goto out_unlock;
+
+ /*
+ * Stop at SHRINK_UNTIL rather than at the end of the tail, or the
+ * range comes out full enough for the grow to be woken. Below the
+ * tail everything stays mapped, and one chunk of tail with it.
+ */
+ keep = DIV_ROUND_UP(swap_usage_in_pages(si) * 100,
+ XSWAP_SHRINK_UNTIL * SWAPFILE_CLUSTER);
+ if (keep < nr_mapped - nr_tail + XSWAP_SHRINK_SLACK)
+ keep = nr_mapped - nr_tail + XSWAP_SHRINK_SLACK;
+
+ if (nr_mapped < keep + XSWAP_SHRINK_MIN)
+ goto out_unlock;
+
+ nr_unmap = rounddown(nr_mapped - keep, XSWAP_GROW_CLUSTERS);
+ if (!nr_unmap)
+ goto out_unlock;
+ start_idx = nr_mapped - nr_unmap;
+
+ /*
+ * An allocation can enter a cluster from the per-cpu cache without
+ * going through the lists, so drain the cache first. xswap devices
+ * are SWP_SOLIDSTATE, so the per-cpu cache is the only cache. Only
+ * then do the checks below hold: a cluster taken from a list takes
+ * si->lock, which is what the check holds.
+ *
+ * The unmap helper below drains the cache and waits again: a cluster
+ * can be taken and freed between this recheck and the unmap, putting
+ * it back in the per-cpu cache.
+ */
+ flush_percpu_swap_cluster(si);
+ synchronize_rcu();
+
+ /*
+ * Only shrink a run that reaches the mapped end; otherwise
+ * truncating nr_clusters_mapped would orphan the active tail.
+ * move_cluster() takes si->lock under ci->lock, so ci->lock is
+ * taken with trylock here to avoid the reverse order.
+ *
+ * With si->lock held, the writes below need no ci->lock: a FREE
+ * cluster leaves the free list only in isolate_lock_cluster(),
+ * every move_cluster() caller holds ci->lock (the trylock above
+ * catches one), a FREE cluster's count is 0 and only falls, and
+ * the drained cache holds no FREE cluster.
+ */
+ spin_lock(&si->lock);
+ for (i = start_idx; i < nr_mapped; i++) {
+ ci = &si->cluster_info[i];
+ if (READ_ONCE(ci->flags) != CLUSTER_FLAG_FREE)
+ break;
+ if (!spin_trylock(&ci->lock)) {
+ spin_unlock(&si->lock);
+ goto out_unlock;
+ }
+ spin_unlock(&ci->lock);
+ }
+ if (i != nr_mapped) {
+ spin_unlock(&si->lock);
+ goto out_unlock;
+ }
+
+ for (i = start_idx; i < nr_mapped; i++) {
+ ci = &si->cluster_info[i];
+ list_del_init(&ci->list);
+ WRITE_ONCE(ci->flags, CLUSTER_FLAG_NONE);
+ }
+ spin_unlock(&si->lock);
+
+ /*
+ * A cluster can be taken and freed between the recheck above and the
+ * unmap, putting it back in the per-cpu cache. Drain and wait again.
+ */
+ xswap_unmap_clusters_locked(si, start_idx, nr_unmap, true);
+ atomic_long_inc(&si->xswap_shrinks);
+
+out_unlock:
+ mutex_unlock(&si->xswap_lock);
+}
+
+/* Not an interface: how far the mapped range has moved, for testing. */
static ssize_t xswap_debugfs_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
@@ -4674,6 +4860,7 @@ static int xswap_create(int prio)
INIT_WORK(&si->discard_work, swap_discard_work);
INIT_WORK(&si->reclaim_work, swap_reclaim_work);
+ INIT_WORK(&si->xswap_shrink_work, xswap_shrink_work_fn);
/* Cluster-aligned, so no cluster holds a slot past si->max. */
maxpages = rounddown(maxpages, SWAPFILE_CLUSTER);
--
2.54.0
next prev parent reply other threads:[~2026-11-09 5:57 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-09 5:54 [PATCH v5 00/19] mm, swap: extendable swap devices (xswap phase I) Baoquan He
2026-11-09 5:54 ` [PATCH v5 01/19] mm: xswap support for zswap Baoquan He
2026-11-10 5:57 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 02/19] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Baoquan He
2026-11-09 5:54 ` [PATCH v5 03/19] mm, swap: add VM_SPARSE cluster-array helpers for xswap Baoquan He
2026-11-09 5:54 ` [PATCH v5 04/19] mm, swap: use a VM_SPARSE cluster array for xswap devices Baoquan He
2026-11-10 5:58 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 05/19] mm, swap: widen swap_info_struct max/pages to unsigned long Baoquan He
2026-11-09 5:54 ` [PATCH v5 06/19] mm, swap: add a sysfs interface to create xswap devices Baoquan He
2026-11-09 5:54 ` [PATCH v5 07/19] mm, swap: refactor swapoff and add xswap_destroy Baoquan He
2026-11-09 5:54 ` [PATCH v5 08/19] mm, swap: require zswap for xswap devices Baoquan He
2026-11-09 5:54 ` [PATCH v5 09/19] mm, swap: use a stable id " Baoquan He
2026-11-10 5:58 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 10/19] mm, swap: let the command line set the xswap device size Baoquan He
2026-11-09 5:54 ` [PATCH v5 11/19] mm, swap: add CONFIG_XSWAP_DEFAULT_MAX Baoquan He
2026-11-09 5:54 ` [PATCH v5 12/19] mm, swap: let swapoff destroy xswap devices by name Baoquan He
2026-11-10 5:58 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 13/19] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
2026-11-10 5:58 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 14/19] mm, swap: add debugfs counters for xswap Baoquan He
2026-11-09 5:54 ` [PATCH v5 15/19] mm, swap: make xswap cluster_info accesses RCU-safe Baoquan He
2026-11-10 5:58 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 16/19] mm, swap: prepare xswap_unmap_clusters for range shrinking Baoquan He
2026-11-09 5:54 ` Baoquan He [this message]
2026-11-10 5:58 ` [PATCH v5 17/19] mm, swap: add xswap shrink trigger and defer it to a workqueue sashiko-bot
2026-11-09 5:54 ` [PATCH v5 18/19] mm, swap: do not give an xswap slot to a cgroup that cannot zswap Baoquan He
2026-11-10 5:58 ` sashiko-bot
2026-11-09 5:54 ` [PATCH v5 19/19] mm, swap: skip the overcommit probe when removing xswap Baoquan He
2026-11-09 6:15 ` [PATCH v5 00/19] mm, swap: extendable swap devices (xswap phase I) Baoquan He
2026-11-12 7:25 ` [syzbot ci] " syzbot ci
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261109055455.536925-18-baoquan.he@linux.dev \
--to=baoquan.he@linux.dev \
--cc=akpm@linux-foundation.org \
--cc=baohua@kernel.org \
--cc=chengming.zhou@linux.dev \
--cc=chrisl@kernel.org \
--cc=corbet@lwn.net \
--cc=david@kernel.org \
--cc=hannes@cmpxchg.org \
--cc=hebaoquan@kylinos.cn \
--cc=kasong@tencent.com \
--cc=klarasmodin@gmail.com \
--cc=kunwu.chan@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=nphamcs@gmail.com \
--cc=youngjun.park@lge.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.