* [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation
@ 2026-11-14 14:47 Lian Wang
2026-11-14 14:48 ` [RFC PATCH v4 01/18] mm/swap: remove unused parameter for reading swap header Lian Wang
` (17 more replies)
0 siblings, 18 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:47 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
Hi all,
This is v4 of Kairui's swap priority queue RFC [1, 2], retaining the
original authorship. It separates device selection from device-local
cluster allocation to support integration with the ongoing swap tier
work, while preserving existing swap priorities, the swapon interface,
and memcg swap accounting and limits.
The global per-CPU cluster cache couples device choice to cluster
locality. Moving that cache below device selection gives each device its
own allocation hints. The available plist's shared rotation is then
replaced by a priority-ordered array of same-priority device rings, with
per-CPU starting offsets and quotas. A task-local cursor keeps each retry
walk stable. Rotational devices retain per-device sequential hints shared
across CPUs.
This remains an RFC. The current scope is global device selection under
existing priorities; no tier ABI or per-memcg device-selection control
is introduced.
The series is organized as follows:
01-04: Parameter/error-path cleanup, device-state boundaries and
iteration helpers.
05-07: Locking, writer-serialized publication and accounting.
08: Restore per-device per-CPU cluster locality.
09: Keep cluster metadata population out of direct reclaim.
10-11: Introduce the safely published queue and remove the avail plist.
12: Bound order-0 synchronous discard outside the allocation reader.
13-14: Preserve priority-aware throttle selection while removing the
active plist and unused plist_requeue API.
15-18: Reuse reader capacity, advance cursors directly, bypass singleton
rotation and redistribute reader phases on membership changes.
Changes in v4
=============
The device-disable and discard paths have been revised following the
review of v3 [4], including Sashiko feedback [5, 6]:
- Complete writable/off-list/availability transitions before releasing
the management lock or topology writer, including intermediate patches.
- Run bounded order-0 synchronous discard outside the allocation reader,
holding a live device reference across I/O, then restart selection with
a fresh cursor.
- Handle discard queued by late reclaim scans, with at most one
synchronous cluster discard per swap type for each order-0 request.
Preserve large-folio same-priority retry and split handling without
foreground synchronous discard.
Rebase
======
The series is now based on mm-unstable 24231596edf2 (7.4-rc1).
The base includes 53a6f71c147a ("mm, swap: stop cluster list scans once
swapoff clears SWP_WRITEOK"), so the former standalone scan fix is no
longer required as a prerequisite. Its si->lock serialization of
WRITEOK changes is preserved.
The metadata allocation paths have been adapted to the upstream
allocator-lock reacquisition and publish-after-revalidation contract,
including -EAGAIN handling and the tableless-cluster relocation guard.
The upstream free-cluster preference and hibernation-slot semantics
are retained.
Test results
============
The tests compare the unpatched baseline 24231596edf2 with the same
baseline plus all 18 patches, fbb87083afcc, on the same x86-64 host.
The kernel configurations differ only in LOCALVERSION.
The kernel-build tests follow the v1 workload: eight 8-GiB ZRAM devices,
make -j96, and 12 fresh-output builds per revision in each of the 2-GiB
and 3-GiB memcgs. Both revisions built the same workload source
(4dbe24720eb3), using x86 defconfig, bzImage/modules and a fixed toolchain.
The BRD case used twelve 8-GiB devices and a memcg with 24 GiB of memory,
64 GiB of swap allowance and memory.zswap.max=0:
usemem --init-time -O -y -x -n 32 1536M
There was one complete BRD invocation per revision. The BRD capacity and
memcg limit are host adaptations; v1 did not specify those parameters.
Mean system CPU time, seconds:
n/arm Before Queue (v4) change
kbuild, 2-GiB 12 4372.4983 4417.2192 +1.02%
kbuild, 3-GiB 12 2314.6025 2322.6550 +0.35%
12-BRD 1 143.5700 137.9900 -3.89%
Mean elapsed time, seconds:
n/arm Before Queue (v4) change
kbuild, 2-GiB 12 168.8092 169.1100 +0.18%
kbuild, 3-GiB 12 136.0650 136.3500 +0.21%
12-BRD 1 6.6500 6.5100 -2.11%
For the BRD invocation, mapped throughput was 7391.28 -> 7550.23 MiB/s
(+2.15%), calculated as the requested 48 GiB divided by elapsed time,
not as a sum of worker rates.
All 50 invocations completed successfully, reported no memcg OOM events
and performed swap I/O. There were no warm-ups or profiling, and all
samples are included. Before ran first on a separate boot; the 12 builds
are within-boot repeats, and the 32 BRD workers are one invocation.
The kernel-build runs show small increases in system CPU and elapsed
time. These descriptive results do not establish equivalence, absence
of regressions or a stable performance gain.
Cooperation with swap tiers
==========================
Device-local cluster hints remain below device selection, so a future
tier policy can determine the eligible candidates before those hints
are used.
The current revision keeps the refactoring under the existing global
swap priority model. Integration with Youngjun's tier infrastructure [3]
remains follow-up work. Device membership, swapon/swapoff publication
and lifetime should have one agreed authority. Within-tier priority
semantics and reader scope need agreement with that work; this series
does not define a competing tier or memcg policy.
Youngjun and Chris, I would appreciate your feedback on the integration
boundary and ordering.
Link: https://lore.kernel.org/all/20260714-swap-pcp-priq-v1-0-de9b164ed419@tencent.com/ [1]
Link: https://lore.kernel.org/all/20260829-swap-pcp-priq-v2-resend-0-68d3d925578c@gmail.com/ [2]
Link: https://lore.kernel.org/all/20260916183437.2946306-1-youngjun.park@lge.com/ [3]
Link: https://lore.kernel.org/all/cover.1793106426.git.lianux.mm@gmail.com/ [4]
Link: https://lore.kernel.org/all/sashiko-outbox-188132@kernel.org/ [5]
Link: https://lore.kernel.org/all/sashiko-outbox-188141@kernel.org/ [6]
base-commit: 24231596edf2c9d5269d8efbc7007512026672f5
Best regards,
Lian Wang
^ permalink raw reply [flat|nested] 19+ messages in thread
* [RFC PATCH v4 01/18] mm/swap: remove unused parameter for reading swap header
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
@ 2026-11-14 14:48 ` Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 02/18] mm/swap: clean up hibernation error handling Lian Wang
` (16 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:48 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
The swap_info_struct argument to read_swap_header() is unused. Remove it
and update the swapon() call site.
No functional change.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index fcda5eba1714..c41c4d26a7e6 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3630,9 +3630,8 @@ __weak unsigned long arch_max_swapfile_size(void)
return generic_max_swapfile_size();
}
-static unsigned long read_swap_header(struct swap_info_struct *si,
- union swap_header *swap_header,
- struct inode *inode)
+static unsigned long read_swap_header(union swap_header *swap_header,
+ struct inode *inode)
{
int i;
unsigned long maxpages;
@@ -3863,7 +3862,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
}
swap_header = kmap_local_folio(folio, 0);
- maxpages = read_swap_header(si, swap_header, inode);
+ maxpages = read_swap_header(swap_header, inode);
if (unlikely(!maxpages)) {
error = -EINVAL;
goto bad_swap_unlock_inode;
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 02/18] mm/swap: clean up hibernation error handling
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
2026-11-14 14:48 ` [RFC PATCH v4 01/18] mm/swap: remove unused parameter for reading swap header Lian Wang
@ 2026-11-14 14:49 ` Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 03/18] mm/swap: cleanup and document swap device availability flag usage Lian Wang
` (15 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:49 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Restructure the error paths in pin_hibernation_swap_type() using a
goto out pattern to avoid repeated spin_unlock() calls and simplify
the return flow. Also simplify unpin_hibernation_swap_type() by
making the si NULL check inline, and clean up find_first_swap() to
avoid an early return inside the loop.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 39 +++++++++++++++++----------------------
1 file changed, 17 insertions(+), 22 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index c41c4d26a7e6..c69a924acb74 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -2331,21 +2331,18 @@ static int __find_hibernation_swap_type(dev_t device, sector_t offset)
*/
int pin_hibernation_swap_type(dev_t device, sector_t offset)
{
- int type;
+ int ret;
struct swap_info_struct *si;
spin_lock(&swap_lock);
+ ret = __find_hibernation_swap_type(device, offset);
+ if (ret < 0)
+ goto out;
- type = __find_hibernation_swap_type(device, offset);
- if (type < 0) {
- spin_unlock(&swap_lock);
- return type;
- }
-
- si = swap_type_to_info(type);
+ si = swap_type_to_info(ret);
if (WARN_ON_ONCE(!si)) {
- spin_unlock(&swap_lock);
- return -ENODEV;
+ ret = -ENODEV;
+ goto out;
}
/*
@@ -2354,14 +2351,15 @@ int pin_hibernation_swap_type(dev_t device, sector_t offset)
* the same session.
*/
if (WARN_ON_ONCE(si->flags & SWP_HIBERNATION)) {
- spin_unlock(&swap_lock);
- return -EBUSY;
+ ret = -EBUSY;
+ goto out;
}
si->flags |= SWP_HIBERNATION;
+out:
spin_unlock(&swap_lock);
- return type;
+ return ret;
}
/**
@@ -2441,11 +2439,8 @@ void unpin_hibernation_swap_type(int type)
spin_lock(&swap_lock);
si = swap_type_to_info(type);
- if (!si) {
- spin_unlock(&swap_lock);
- return;
- }
- si->flags &= ~SWP_HIBERNATION;
+ if (si)
+ si->flags &= ~SWP_HIBERNATION;
spin_unlock(&swap_lock);
}
@@ -2480,7 +2475,7 @@ int find_hibernation_swap_type(dev_t device, sector_t offset)
int find_first_swap(dev_t *device)
{
- int type;
+ int type, ret = -ENODEV;
spin_lock(&swap_lock);
for (type = 0; type < nr_swapfiles; type++) {
@@ -2489,11 +2484,11 @@ int find_first_swap(dev_t *device)
if (!(sis->flags & SWP_WRITEOK))
continue;
*device = sis->bdev->bd_dev;
- spin_unlock(&swap_lock);
- return type;
+ ret = type;
+ break;
}
spin_unlock(&swap_lock);
- return -ENODEV;
+ return ret;
}
/*
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 03/18] mm/swap: cleanup and document swap device availability flag usage
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
2026-11-14 14:48 ` [RFC PATCH v4 01/18] mm/swap: remove unused parameter for reading swap header Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 02/18] mm/swap: clean up hibernation error handling Lian Wang
@ 2026-11-14 14:49 ` Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 04/18] mm/swap: introduce swap device iteration helper Lian Wang
` (14 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:49 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Rework swap device flag and metadata handling to make the writable
publication and disable boundaries explicit.
Introduce swap_device_enable() and swap_device_disable(). Move
SWP_WRITEOK updates into these boundaries. Clear the writable flag,
set the off-list state and remove the available entry in one
swap_avail_lock critical section, before releasing swap_lock.
Preserve the existing swap_lock critical section across mapping lookup,
hibernation checks and the disable transition. Drain cluster allocators
after dropping swap_lock, retaining the device-identity protection
without holding swap_lock over all cluster locks.
Document the flag lifetime and locking rules.
Signed-off-by: Kairui Song <kasong@tencent.com>
Co-developed-by: Lian Wang <lianux.mm@gmail.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/swap.h | 29 ++++--
mm/swapfile.c | 205 ++++++++++++++++++++-----------------------
2 files changed, 118 insertions(+), 116 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 4f686709d71f..0fa77f5679a2 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -188,6 +188,24 @@ struct swap_extent {
((offsetof(union swap_header, magic.magic) - \
offsetof(union swap_header, info.badpages)) / sizeof(int))
+/*
+ * Swap device flags, except the ones documented below, all are immutable
+ * after exposed by swap_device_enable, and until the device is freed again
+ * (SWP_USED unset), except that teardown clears SWP_ACTIVATED after draining
+ * allocators and live references. The other exceptions:
+ * - SWP_USED: Protected by swap_lock. Indicates the device is inuse. Once
+ * set, won't be cleared unless all reference to this device is freed and
+ * swapoff finished.
+ * - SWP_WRITEOK: Protected by swap_lock, si->lock and swap_avail_lock.
+ * Clearing this flag is followed by waiting for all current cluster lock
+ * users to exit, so
+ * checking this flag while holding any of these locks ensures the device
+ * is safe to use at the moment. Note: clearing this flag doesn't affect
+ * pending IO or async requests, it only prevents further entry allocation
+ * or new async request (e.g. discard) from initiating.
+ * - SWP_HIBERNATION: Protected by swap_lock. Indicates if the device
+ * is pinned for hibernation.
+ */
enum {
SWP_USED = (1 << 0), /* is slot in swap_info[] used? */
SWP_WRITEOK = (1 << 1), /* ok to write to this swap? */
@@ -256,14 +274,9 @@ struct swap_info_struct {
struct file *swap_file; /* seldom referenced */
struct completion comp; /* seldom referenced */
spinlock_t lock; /*
- * protect map scan related fields like
- * inuse_pages and all cluster lists.
- * Other fields are only changed
- * at swapon/swapoff, so are protected
- * by swap_lock. changing flags need
- * hold this lock and swap_lock. If
- * both locks need hold, hold swap_lock
- * first.
+ * Protect cluster lists. Other fields
+ * are only changed at swapon/swapoff,
+ * so are protected by swap_lock.
*/
struct work_struct discard_work; /* discard worker */
struct work_struct reclaim_work; /* reclaim worker */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index c69a924acb74..6485c8020d3a 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1268,28 +1268,23 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
spin_lock(&swap_avail_lock);
- if (swapoff) {
- /*
- * Forcefully remove it. Clear the SWP_WRITEOK flags for
- * swapoff here so it's synchronized by both si->lock and
- * swap_avail_lock, to ensure the result can be seen by
- * add_to_avail_list.
- */
- lockdep_assert_held(&si->lock);
- si->flags &= ~SWP_WRITEOK;
- atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages);
- } else {
- /*
- * If not called by swapoff, take it off-list only if it's
- * full and SWAP_USAGE_OFFLIST_BIT is not set (strictly
- * si->inuse_pages == pages), any concurrent slot freeing,
- * or device already removed from plist by someone else
- * will make this return false.
- */
+ /*
+ * Force remove it only for swapoff. Else, take it off-list only if
+ * it's full and SWAP_USAGE_OFFLIST_BIT is not set (strictly
+ * si->inuse_pages == pages), so concurrent slot freeing, or
+ * concurrent list removal will make the cmpxchg fail and skip
+ * the removal.
+ */
+ if (!swapoff) {
pages = si->pages;
if (!atomic_long_try_cmpxchg(&si->inuse_pages, &pages,
pages | SWAP_USAGE_OFFLIST_BIT))
goto skip;
+ } else {
+ lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&si->lock);
+ si->flags &= ~SWP_WRITEOK;
+ atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages);
}
plist_del(&si->avail_list, &swap_avail_head);
@@ -1299,21 +1294,21 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
}
/* SWAP_USAGE_OFFLIST_BIT can only be cleared by this helper. */
-static void add_to_avail_list(struct swap_info_struct *si, bool swapon)
+static void add_to_avail_list(struct swap_info_struct *si)
{
long val;
unsigned long pages;
spin_lock(&swap_avail_lock);
- /* Corresponding to SWP_WRITEOK clearing in del_from_avail_list */
- if (swapon) {
- lockdep_assert_held(&si->lock);
- si->flags |= SWP_WRITEOK;
- } else {
- if (!(READ_ONCE(si->flags) & SWP_WRITEOK))
- goto skip;
- }
+ /*
+ * Add the device to the avail list if SWP_WRITEOK is set and
+ * SWAP_USAGE_OFFLIST_BIT is still set. Swapoff clears
+ * SWP_WRITEOK first, so the device won't be re-added after
+ * swapoff starts unless swap_device_enable resurrects it.
+ */
+ if (!(si->flags & SWP_WRITEOK))
+ goto skip;
if (!(atomic_long_read(&si->inuse_pages) & SWAP_USAGE_OFFLIST_BIT))
goto skip;
@@ -1369,7 +1364,7 @@ static void swap_usage_sub(struct swap_info_struct *si, unsigned int nr_entries)
* add it to the plist.
*/
if (unlikely(val & SWAP_USAGE_OFFLIST_BIT))
- add_to_avail_list(si, false);
+ add_to_avail_list(si);
}
static void swap_range_alloc(struct swap_info_struct *si,
@@ -1421,7 +1416,7 @@ static bool get_swap_device_info(struct swap_info_struct *si)
* up to dated.
*
* Paired with the spin_unlock() after setup_swap_info() in
- * enable_swap_info(), and smp_wmb() in swapoff.
+ * swap_device_enable(), and smp_wmb() in swapoff.
*/
smp_rmb();
return true;
@@ -3140,58 +3135,88 @@ static int setup_swap_extents(struct swap_info_struct *sis,
return generic_swapfile_activate(sis, swap_file, span);
}
-static void _enable_swap_info(struct swap_info_struct *si)
-{
- atomic_long_add(si->pages, &nr_swap_pages);
- total_swap_pages += si->pages;
-
- assert_spin_locked(&swap_lock);
-
- plist_add(&si->list, &swap_active_head);
-
- /* Add back to available list */
- add_to_avail_list(si, true);
-}
-
/*
- * Called after the swap device is ready, resurrect its percpu ref, it's now
- * safe to reference it. Add it to the list to expose it to the allocator.
+ * Mark a fully initialized swap device writable and expose it to the
+ * allocator. The caller must have resurrected its percpu ref first.
*/
-static void enable_swap_info(struct swap_info_struct *si)
+static void swap_device_enable(struct swap_info_struct *si)
{
- percpu_ref_resurrect(&si->users);
spin_lock(&swap_lock);
+
spin_lock(&si->lock);
- _enable_swap_info(si);
+ spin_lock(&swap_avail_lock);
+ si->flags |= SWP_WRITEOK;
+ spin_unlock(&swap_avail_lock);
spin_unlock(&si->lock);
+
+ atomic_long_add(si->pages, &nr_swap_pages);
+ total_swap_pages += si->pages;
+ plist_add(&si->list, &swap_active_head);
spin_unlock(&swap_lock);
+
+ add_to_avail_list(si);
}
-static void reinsert_swap_info(struct swap_info_struct *si)
+static int swap_device_disable(struct address_space *mapping,
+ struct swap_info_struct **swap_info)
{
+ struct swap_info_struct *si;
+ struct swap_cluster_info *ci;
+ unsigned long offset, end;
+ int err = -EINVAL;
+
spin_lock(&swap_lock);
+ plist_for_each_entry(si, &swap_active_head, list) {
+ if ((si->flags & SWP_WRITEOK) &&
+ si->swap_file->f_mapping == mapping) {
+ err = 0;
+ break;
+ }
+ }
+ if (err)
+ goto unlock;
+
+ /*
+ * Refuse swapoff while the device is pinned for hibernation.
+ */
+ if (si->flags & SWP_HIBERNATION) {
+ err = -EBUSY;
+ goto unlock;
+ }
+
+ if (security_vm_enough_memory_mm(current->mm, si->pages)) {
+ err = -ENOMEM;
+ goto unlock;
+ }
+ vm_unacct_memory(si->pages);
+
+ /* Serialize with the WRITEOK check in isolate_lock_cluster(). */
spin_lock(&si->lock);
- _enable_swap_info(si);
+ del_from_avail_list(si, true);
spin_unlock(&si->lock);
- spin_unlock(&swap_lock);
-}
-/*
- * Called after clearing SWP_WRITEOK, ensures cluster_alloc_range
- * see the updated flags, so there will be no more allocations.
- */
-static void wait_for_allocation(struct swap_info_struct *si)
-{
- unsigned long offset;
- unsigned long end = ALIGN(si->max, SWAPFILE_CLUSTER);
- struct swap_cluster_info *ci;
+ plist_del(&si->list, &swap_active_head);
+ total_swap_pages -= si->pages;
+ atomic_long_sub(si->pages, &nr_swap_pages);
- BUG_ON(si->flags & SWP_WRITEOK);
+ end = ALIGN(si->max, SWAPFILE_CLUSTER);
+unlock:
+ spin_unlock(&swap_lock);
+ if (err)
+ return err;
+ /*
+ * The swap allocator doesn't take swap_lock. Looping through every
+ * cluster lock after clearing SWP_WRITEOK ensures that allocators see
+ * the updated flag and that no allocation remains in flight.
+ */
for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
ci = swap_cluster_lock(si, offset);
swap_cluster_unlock(ci);
}
+
+ *swap_info = si;
+ return 0;
}
static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
@@ -3236,7 +3261,6 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
}
}
-
SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
{
struct swap_info_struct *p = NULL;
@@ -3245,7 +3269,7 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
struct address_space *mapping;
struct inode *inode;
unsigned int maxpages;
- int err, found = 0;
+ int err;
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
@@ -3258,44 +3282,11 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
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);
+ err = swap_device_disable(mapping, &p);
+ filp_close(victim, NULL);
- wait_for_allocation(p);
+ if (err)
+ return err;
set_current_oom_origin();
err = try_to_unuse(p->type);
@@ -3303,8 +3294,8 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
if (err) {
/* re-insert swap space back into swap_list */
- reinsert_swap_info(p);
- goto out_dput;
+ swap_device_enable(p);
+ return err;
}
/*
@@ -3364,13 +3355,10 @@ 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);
-out_dput:
- filp_close(victim, NULL);
- return err;
+ return 0;
}
#ifdef CONFIG_PROC_FS
@@ -3792,7 +3780,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
/*
* Allocate or reuse existing !SWP_USED swap_info. The returned
* si will stay in a dying status, so nothing will access its content
- * until enable_swap_info resurrects its percpu ref and expose it.
+ * until swap_device_enable resurrects its percpu ref and expose it.
*/
si = alloc_swap_info();
if (IS_ERR(si))
@@ -3960,7 +3948,8 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
si->swap_file = swap_file;
/* Sets SWP_WRITEOK, resurrect the percpu ref, expose the swap device */
- enable_swap_info(si);
+ percpu_ref_resurrect(&si->users);
+ swap_device_enable(si);
pr_info("Adding %uk swap on %s. Priority:%d extents:%d across:%lluk %s%s%s%s\n",
K(si->pages), name->name, si->prio, nr_extents,
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 04/18] mm/swap: introduce swap device iteration helper
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (2 preceding siblings ...)
2026-11-14 14:49 ` [RFC PATCH v4 03/18] mm/swap: cleanup and document swap device availability flag usage Lian Wang
@ 2026-11-14 14:49 ` Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 05/18] mm/swap: change the swapon lock into a percpu rwsem Lian Wang
` (13 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:49 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Several swap users scan the swap_info array to find in-use or writable
devices. Add for_each_swap() and for_each_avail_swap() with SWP_USED and
SWP_USED | SWP_WRITEOK filtering, and use them for hibernation lookups,
drain_mmlist() and si_swapinfo(). Keep the swap_lock requirement for
iteration.
Move the live-reference warning from __swap_type_to_info() into
__swap_entry_to_info(). The iterator must inspect descriptors before
applying its flag filter, including descriptors whose device reference
is dead; entry-based lookup retains the warning.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swap.h | 12 ++++----
mm/swapfile.c | 77 +++++++++++++++++++++++++++++++--------------------
2 files changed, 53 insertions(+), 36 deletions(-)
diff --git a/mm/swap.h b/mm/swap.h
index d488a69c2809..70fd3ab655ff 100644
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -108,16 +108,16 @@ static inline unsigned int swp_cluster_offset(swp_entry_t entry)
*/
static inline struct swap_info_struct *__swap_type_to_info(int type)
{
- struct swap_info_struct *si;
-
- si = READ_ONCE(swap_info[type]); /* rcu_dereference() */
- VM_WARN_ON_ONCE(percpu_ref_is_zero(&si->users)); /* race with swapoff */
- return si;
+ return READ_ONCE(swap_info[type]); /* rcu_dereference() */
}
static inline struct swap_info_struct *__swap_entry_to_info(swp_entry_t entry)
{
- return __swap_type_to_info(swp_type(entry));
+ struct swap_info_struct *si;
+
+ si = __swap_type_to_info(swp_type(entry)); /* rcu_dereference() */
+ VM_WARN_ON_ONCE(percpu_ref_is_zero(&si->users)); /* race with swapoff */
+ return si;
}
static inline struct swap_cluster_info *__swap_offset_to_cluster(
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 6485c8020d3a..1669a46515cd 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -106,6 +106,34 @@ static DEFINE_SPINLOCK(swap_avail_lock);
struct swap_info_struct *swap_info[MAX_SWAPFILES];
+static inline struct swap_info_struct *__swap_iter(int *i, unsigned long flag)
+{
+ lockdep_assert_held(&swap_lock);
+ while (*i < nr_swapfiles) {
+ struct swap_info_struct *si = __swap_type_to_info(*i);
+
+ VM_WARN_ON(!si);
+ (*i)++;
+ if (flag && !((si->flags & flag) == flag))
+ continue;
+ return si;
+ }
+ return NULL;
+}
+
+#define __for_each_swap(si, flag) \
+ for (int __i = 0; ((si) = __swap_iter(&__i, flag));)
+
+/*
+ * for_each_swap - iterate through all allocated and inuse swap devices
+ * @si: the iterator
+ *
+ * Context: The caller must hold swap_lock. The lock may be dropped during
+ * the loop body but must be re-acquired before the next iteration.
+ */
+#define for_each_swap(si) __for_each_swap(si, SWP_USED)
+#define for_each_avail_swap(si) __for_each_swap(si, SWP_USED | SWP_WRITEOK)
+
static struct kmem_cache *swap_table_cachep;
/* Protects si->swap_file for /proc/swaps usage */
@@ -2284,26 +2312,18 @@ void swap_free_hibernation_slot(swp_entry_t entry)
static int __find_hibernation_swap_type(dev_t device, sector_t offset)
{
- int type;
-
- lockdep_assert_held(&swap_lock);
+ struct swap_info_struct *si;
if (!device)
return -EINVAL;
- for (type = 0; type < nr_swapfiles; type++) {
- struct swap_info_struct *sis = swap_info[type];
-
- if (!(sis->flags & SWP_WRITEOK))
- continue;
-
- if (device == sis->bdev->bd_dev) {
- struct swap_extent *se = first_se(sis);
-
- if (se->start_block == offset)
- return type;
+ for_each_avail_swap(si) {
+ if (device == si->bdev->bd_dev) {
+ if (first_se(si)->start_block == offset)
+ return si->type;
}
}
+
return -ENODEV;
}
@@ -2470,16 +2490,13 @@ int find_hibernation_swap_type(dev_t device, sector_t offset)
int find_first_swap(dev_t *device)
{
- int type, ret = -ENODEV;
+ int ret = -ENODEV;
+ struct swap_info_struct *si;
spin_lock(&swap_lock);
- for (type = 0; type < nr_swapfiles; type++) {
- struct swap_info_struct *sis = swap_info[type];
-
- if (!(sis->flags & SWP_WRITEOK))
- continue;
- *device = sis->bdev->bd_dev;
- ret = type;
+ for_each_avail_swap(si) {
+ *device = si->bdev->bd_dev;
+ ret = si->type;
break;
}
spin_unlock(&swap_lock);
@@ -2996,12 +3013,14 @@ static int try_to_unuse(unsigned int type)
*/
static void drain_mmlist(void)
{
+ struct swap_info_struct *si;
struct list_head *p, *next;
- unsigned int type;
- for (type = 0; type < nr_swapfiles; type++)
- if (swap_usage_in_pages(swap_info[type]))
+ for_each_swap(si) {
+ if (swap_usage_in_pages(si))
return;
+ }
+
spin_lock(&mmlist_lock);
list_for_each_safe(p, next, &init_mm.mmlist)
list_del_init(p);
@@ -3997,14 +4016,12 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
void si_swapinfo(struct sysinfo *val)
{
- unsigned int type;
+ struct swap_info_struct *si;
unsigned long nr_to_be_unused = 0;
spin_lock(&swap_lock);
- for (type = 0; type < nr_swapfiles; type++) {
- struct swap_info_struct *si = swap_info[type];
-
- if ((si->flags & SWP_USED) && !(si->flags & SWP_WRITEOK))
+ for_each_swap(si) {
+ if (!(si->flags & SWP_WRITEOK))
nr_to_be_unused += swap_usage_in_pages(si);
}
val->freeswap = atomic_long_read(&nr_swap_pages) + nr_to_be_unused;
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 05/18] mm/swap: change the swapon lock into a percpu rwsem
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (3 preceding siblings ...)
2026-11-14 14:49 ` [RFC PATCH v4 04/18] mm/swap: introduce swap device iteration helper Lian Wang
@ 2026-11-14 14:49 ` Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 06/18] mm/swap: serialize swap publication before removing swapon mutex Lian Wang
` (12 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:49 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Reading swap metadata is a common hot path, while swapon and swapoff
are costly and rare. Convert swap_lock from a spinlock to the
swapon_rwsem percpu rwsem so metadata readers can run concurrently.
Writers still serialize topology changes and exclude readers.
Preserve the atomic writable/off-list/available-list transition under
swap_avail_lock. The device-selection and cluster-allocation paths do
not yet hold swapon_rwsem; the priority queue patch adds that allocation
reader protocol.
Also update the comments in swap_info_struct to reflect the lock
name change.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/swap.h | 10 ++--
mm/swapfile.c | 130 ++++++++++++++++++++++---------------------
2 files changed, 73 insertions(+), 67 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 0fa77f5679a2..717387e16a2d 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -192,18 +192,18 @@ struct swap_extent {
* Swap device flags, except the ones documented below, all are immutable
* after exposed by swap_device_enable, and until the device is freed again
* (SWP_USED unset), except that teardown clears SWP_ACTIVATED after draining
- * allocators and live references. The other exceptions:
- * - SWP_USED: Protected by swap_lock. Indicates the device is inuse. Once
+ * allocators and live references. The other exceptions use swapon_rwsem:
+ * - SWP_USED: Protected by swapon_rwsem. Indicates the device is inuse. Once
* set, won't be cleared unless all reference to this device is freed and
* swapoff finished.
- * - SWP_WRITEOK: Protected by swap_lock, si->lock and swap_avail_lock.
+ * - SWP_WRITEOK: Protected by swapon_rwsem, si->lock and swap_avail_lock.
* Clearing this flag is followed by waiting for all current cluster lock
* users to exit, so
* checking this flag while holding any of these locks ensures the device
* is safe to use at the moment. Note: clearing this flag doesn't affect
* pending IO or async requests, it only prevents further entry allocation
* or new async request (e.g. discard) from initiating.
- * - SWP_HIBERNATION: Protected by swap_lock. Indicates if the device
+ * - SWP_HIBERNATION: Protected by swapon_rwsem. Indicates if the device
* is pinned for hibernation.
*/
enum {
@@ -276,7 +276,7 @@ struct swap_info_struct {
spinlock_t lock; /*
* Protect cluster lists. Other fields
* are only changed at swapon/swapoff,
- * so are protected by swap_lock.
+ * so are protected by swapon_rwsem.
*/
struct work_struct discard_work; /* discard worker */
struct work_struct reclaim_work; /* reclaim worker */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 1669a46515cd..ae8421e18448 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -57,23 +57,28 @@ static void move_cluster(struct swap_info_struct *si,
enum swap_cluster_flags new_flags);
/*
- * Protects the swap_info array, and the SWP_USED flag. swap_info contains
- * lazily allocated & freed swap device info struts, and SWP_USED indicates
- * which device is used, ~SWP_USED devices and can be reused.
- *
- * Also protects swap_active_head total_swap_pages, and the SWP_WRITEOK flag.
+ * Serializes swapon/swapoff (writers) and protects the swap_info
+ * array, nr_swapfiles, total_swap_pages, and part of swap device
+ * info content (see comment of swap_info_struct). Read-side users
+ * take percpu_down_read() while inspecting this protected state.
+ */
+DEFINE_STATIC_PERCPU_RWSEM(swapon_rwsem);
+/*
+ * swap_info contains lazily allocated swap device info structs, and
+ * SWP_USED indicates which device is used, ~SWP_USED devices can be
+ * reused. Protected by swapon_rwsem, but reading could be lockless.
*/
-static DEFINE_SPINLOCK(swap_lock);
+struct swap_info_struct *swap_info[MAX_SWAPFILES];
static unsigned int nr_swapfiles;
-atomic_long_t nr_swap_pages;
+long total_swap_pages;
+
/*
* 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
* check to see if any swap space is available.
*/
+atomic_long_t nr_swap_pages;
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
@@ -85,7 +90,7 @@ static const char Bad_offset[] = "Bad swap offset entry ";
/*
* all active swap_info_structs
- * protected with swap_lock, and ordered by priority.
+ * protected with swapon_rwsem, and ordered by priority.
*/
static PLIST_HEAD(swap_active_head);
@@ -95,20 +100,18 @@ static PLIST_HEAD(swap_active_head);
* This is used by folio_alloc_swap() instead of swap_active_head
* because swap_active_head includes all swap_info_structs,
* but folio_alloc_swap() doesn't need to look at full ones.
- * This uses its own lock instead of swap_lock because when a
+ * This uses its own lock instead of swapon_rwsem because when a
* swap_info_struct changes between not-full/full, it needs to
* add/remove itself to/from this list, but the swap_info_struct->lock
- * is held and the locking order requires swap_lock to be taken
+ * is held and the locking order requires swapon_rwsem to be taken
* before any swap_info_struct->lock.
*/
static PLIST_HEAD(swap_avail_head);
static DEFINE_SPINLOCK(swap_avail_lock);
-struct swap_info_struct *swap_info[MAX_SWAPFILES];
-
static inline struct swap_info_struct *__swap_iter(int *i, unsigned long flag)
{
- lockdep_assert_held(&swap_lock);
+ lockdep_assert_held(&swapon_rwsem);
while (*i < nr_swapfiles) {
struct swap_info_struct *si = __swap_type_to_info(*i);
@@ -128,7 +131,7 @@ static inline struct swap_info_struct *__swap_iter(int *i, unsigned long flag)
* for_each_swap - iterate through all allocated and inuse swap devices
* @si: the iterator
*
- * Context: The caller must hold swap_lock. The lock may be dropped during
+ * Context: The caller must hold swapon_rwsem. The lock may be dropped during
* the loop body but must be re-acquired before the next iteration.
*/
#define for_each_swap(si) __for_each_swap(si, SWP_USED)
@@ -1309,7 +1312,7 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
pages | SWAP_USAGE_OFFLIST_BIT))
goto skip;
} else {
- lockdep_assert_held(&swap_lock);
+ lockdep_assert_held_write(&swapon_rwsem);
lockdep_assert_held(&si->lock);
si->flags &= ~SWP_WRITEOK;
atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages);
@@ -1441,10 +1444,10 @@ static bool get_swap_device_info(struct swap_info_struct *si)
/*
* Guarantee the si->users are checked before accessing other
* fields of swap_info_struct, and si->flags (SWP_WRITEOK) is
- * up to dated.
+ * up to date.
*
- * Paired with the spin_unlock() after setup_swap_info() in
- * swap_device_enable(), and smp_wmb() in swapoff.
+ * Paired with percpu_up_write() in swap_device_enable(), and
+ * smp_wmb() after clearing SWP_WRITEOK in swapoff.
*/
smp_rmb();
return true;
@@ -1529,10 +1532,10 @@ static bool swap_sync_discard(void)
bool ret = false;
struct swap_info_struct *si, *next;
- spin_lock(&swap_lock);
+ percpu_down_read(&swapon_rwsem);
start_over:
plist_for_each_entry_safe(si, next, &swap_active_head, list) {
- spin_unlock(&swap_lock);
+ percpu_up_read(&swapon_rwsem);
if (get_swap_device_info(si)) {
if (si->flags & SWP_PAGE_DISCARD)
ret = swap_do_scheduled_discard(si);
@@ -1541,11 +1544,11 @@ static bool swap_sync_discard(void)
if (ret)
return true;
- spin_lock(&swap_lock);
+ percpu_down_read(&swapon_rwsem);
if (plist_node_empty(&next->list))
goto start_over;
}
- spin_unlock(&swap_lock);
+ percpu_up_read(&swapon_rwsem);
return false;
}
@@ -2349,7 +2352,7 @@ int pin_hibernation_swap_type(dev_t device, sector_t offset)
int ret;
struct swap_info_struct *si;
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
ret = __find_hibernation_swap_type(device, offset);
if (ret < 0)
goto out;
@@ -2373,7 +2376,7 @@ int pin_hibernation_swap_type(dev_t device, sector_t offset)
si->flags |= SWP_HIBERNATION;
out:
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
return ret;
}
@@ -2385,7 +2388,7 @@ int pin_hibernation_swap_type(dev_t device, sector_t offset)
*
* Look up the swap device for @device/@offset and atomically transfer
* the SWP_HIBERNATION pin from @old_type (if valid) to the new device,
- * all under a single swap_lock critical section. This closes the
+ * all under a single swapon_rwsem writer section. This closes the
* swapoff() window that exists when callers unpin and re-pin in two
* separate operations.
*
@@ -2405,28 +2408,28 @@ int repin_hibernation_swap_type(int old_type, dev_t device, sector_t offset)
struct swap_info_struct *old_si, *new_si;
int new_type;
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
new_type = __find_hibernation_swap_type(device, offset);
if (new_type < 0) {
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
return new_type;
}
new_si = swap_type_to_info(new_type);
if (WARN_ON_ONCE(!new_si)) {
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
return -ENODEV;
}
old_si = swap_type_to_info(old_type);
if (new_si == old_si) {
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
return new_type;
}
if (WARN_ON_ONCE(new_si->flags & SWP_HIBERNATION)) {
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
return -EBUSY;
}
@@ -2434,7 +2437,7 @@ int repin_hibernation_swap_type(int old_type, dev_t device, sector_t offset)
old_si->flags &= ~SWP_HIBERNATION;
new_si->flags |= SWP_HIBERNATION;
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
return new_type;
}
@@ -2452,11 +2455,11 @@ void unpin_hibernation_swap_type(int type)
{
struct swap_info_struct *si;
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
si = swap_type_to_info(type);
if (si)
si->flags &= ~SWP_HIBERNATION;
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
}
/**
@@ -2481,9 +2484,9 @@ int find_hibernation_swap_type(dev_t device, sector_t offset)
{
int type;
- spin_lock(&swap_lock);
+ percpu_down_read(&swapon_rwsem);
type = __find_hibernation_swap_type(device, offset);
- spin_unlock(&swap_lock);
+ percpu_up_read(&swapon_rwsem);
return type;
}
@@ -2493,13 +2496,13 @@ int find_first_swap(dev_t *device)
int ret = -ENODEV;
struct swap_info_struct *si;
- spin_lock(&swap_lock);
+ percpu_down_read(&swapon_rwsem);
for_each_avail_swap(si) {
*device = si->bdev->bd_dev;
ret = si->type;
break;
}
- spin_unlock(&swap_lock);
+ percpu_up_read(&swapon_rwsem);
return ret;
}
@@ -2528,7 +2531,7 @@ unsigned int count_swap_pages(int type, int free)
{
unsigned int n = 0;
- spin_lock(&swap_lock);
+ percpu_down_read(&swapon_rwsem);
if ((unsigned int)type < nr_swapfiles) {
struct swap_info_struct *sis = swap_info[type];
@@ -2540,7 +2543,7 @@ unsigned int count_swap_pages(int type, int free)
}
spin_unlock(&sis->lock);
}
- spin_unlock(&swap_lock);
+ percpu_up_read(&swapon_rwsem);
return n;
}
#endif /* CONFIG_HIBERNATION */
@@ -2866,10 +2869,10 @@ static unsigned int find_next_to_unuse(struct swap_info_struct *si,
unsigned long swp_tb;
/*
- * No need for swap_lock here: we're just looking
+ * No need for swapon_rwsem 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).
+ * allocations from this area (while holding swapon_rwsem).
*/
i = prev + 1;
while (i < si->max) {
@@ -3007,8 +3010,8 @@ static int try_to_unuse(unsigned int type)
/*
* After a successful try_to_unuse, if no swap is now in use, we know
- * we can empty the mmlist. swap_lock must be held on entry and exit.
- * Note that mmlist_lock nests inside swap_lock, and an mm must be
+ * we can empty the mmlist. swapon_rwsem must be held on entry and exit.
+ * Note that mmlist_lock nests inside swapon_rwsem, and an mm must be
* added to the mmlist just after page_duplicate - before would be racy.
*/
static void drain_mmlist(void)
@@ -3160,8 +3163,7 @@ static int setup_swap_extents(struct swap_info_struct *sis,
*/
static void swap_device_enable(struct swap_info_struct *si)
{
- spin_lock(&swap_lock);
-
+ percpu_down_write(&swapon_rwsem);
spin_lock(&si->lock);
spin_lock(&swap_avail_lock);
si->flags |= SWP_WRITEOK;
@@ -3171,7 +3173,7 @@ static void swap_device_enable(struct swap_info_struct *si)
atomic_long_add(si->pages, &nr_swap_pages);
total_swap_pages += si->pages;
plist_add(&si->list, &swap_active_head);
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
add_to_avail_list(si);
}
@@ -3184,7 +3186,7 @@ static int swap_device_disable(struct address_space *mapping,
unsigned long offset, end;
int err = -EINVAL;
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
plist_for_each_entry(si, &swap_active_head, list) {
if ((si->flags & SWP_WRITEOK) &&
si->swap_file->f_mapping == mapping) {
@@ -3220,12 +3222,12 @@ static int swap_device_disable(struct address_space *mapping,
end = ALIGN(si->max, SWAPFILE_CLUSTER);
unlock:
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
if (err)
return err;
/*
- * The swap allocator doesn't take swap_lock. Looping through every
+ * The swap allocator doesn't take swapon_rwsem. Looping through every
* cluster lock after clearing SWP_WRITEOK ensures that allocators see
* the updated flag and that no allocation remains in flight.
*/
@@ -3339,7 +3341,7 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
atomic_dec(&nr_rotate_swap);
mutex_lock(&swapon_mutex);
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
spin_lock(&p->lock);
drain_mmlist();
@@ -3350,7 +3352,7 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
p->max = 0;
p->cluster_info = NULL;
spin_unlock(&p->lock);
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
arch_swap_invalidate_area(p->type);
zswap_swapoff(p->type);
mutex_unlock(&swapon_mutex);
@@ -3369,10 +3371,14 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
* Clear the SWP_USED flag after all resources are freed so that swapon
* can reuse this swap_info in alloc_swap_info() safely. It is ok to
* not hold p->lock after we cleared its SWP_WRITEOK.
+ *
+ * The write lock ensures the flag clear is visible to lockless
+ * readers of swap_type_to_info() before alloc_swap_info() reuses
+ * this slot.
*/
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
p->flags = 0;
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
atomic_inc(&proc_poll_event);
wake_up_interruptible(&proc_poll_wait);
@@ -3532,13 +3538,13 @@ static struct swap_info_struct *alloc_swap_info(void)
return ERR_PTR(-ENOMEM);
}
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
for (type = 0; type < nr_swapfiles; type++) {
if (!(swap_info[type]->flags & SWP_USED))
break;
}
if (type >= MAX_SWAPFILES) {
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
percpu_ref_exit(&p->users);
kvfree(p);
return ERR_PTR(-EPERM);
@@ -3563,7 +3569,7 @@ static struct swap_info_struct *alloc_swap_info(void)
plist_node_init(&p->list, 0);
plist_node_init(&p->avail_list, 0);
p->flags = SWP_USED;
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
if (defer) {
percpu_ref_exit(&defer->users);
kvfree(defer);
@@ -3999,9 +4005,9 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
* Clear the SWP_USED flag after all resources are freed so
* alloc_swap_info can reuse this si safely.
*/
- spin_lock(&swap_lock);
+ percpu_down_write(&swapon_rwsem);
si->flags = 0;
- spin_unlock(&swap_lock);
+ percpu_up_write(&swapon_rwsem);
if (inced_nr_rotate_swap)
atomic_dec(&nr_rotate_swap);
if (swap_file)
@@ -4019,14 +4025,14 @@ void si_swapinfo(struct sysinfo *val)
struct swap_info_struct *si;
unsigned long nr_to_be_unused = 0;
- spin_lock(&swap_lock);
+ percpu_down_read(&swapon_rwsem);
for_each_swap(si) {
if (!(si->flags & SWP_WRITEOK))
nr_to_be_unused += swap_usage_in_pages(si);
}
val->freeswap = atomic_long_read(&nr_swap_pages) + nr_to_be_unused;
val->totalswap = total_swap_pages + nr_to_be_unused;
- spin_unlock(&swap_lock);
+ percpu_up_read(&swapon_rwsem);
}
/*
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 06/18] mm/swap: serialize swap publication before removing swapon mutex
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (4 preceding siblings ...)
2026-11-14 14:49 ` [RFC PATCH v4 05/18] mm/swap: change the swapon lock into a percpu rwsem Lian Wang
@ 2026-11-14 14:49 ` Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 07/18] mm/swap: consolidate swap inuse accounting helpers Lian Wang
` (11 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:49 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Move publication of swap_file, resurrection of the device reference and
the writable transition into one swapon_rwsem writer section. Split out
a writer-held enable helper so initial publication can share this
section, while failed swapoff can use the locking wrapper.
With file publication and teardown serialized by the writer, remove
swapon_mutex and use a topology reader across /proc/swaps iteration.
Keep the SWP_USED filter when advancing the swap_info array.
The publication boundary excludes proc readers from partially published
state and gives the lockless live-reference users initialized device
metadata before the device becomes an allocation candidate.
Signed-off-by: Kairui Song <kasong@tencent.com>
Co-developed-by: Lian Wang <lianux.mm@gmail.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 39 +++++++++++++++++++--------------------
1 file changed, 19 insertions(+), 20 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index ae8421e18448..277a5c749987 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -139,9 +139,6 @@ static inline struct swap_info_struct *__swap_iter(int *i, unsigned long flag)
static struct kmem_cache *swap_table_cachep;
-/* Protects si->swap_file for /proc/swaps usage */
-static DEFINE_MUTEX(swapon_mutex);
-
static DECLARE_WAIT_QUEUE_HEAD(proc_poll_wait);
/* Activity counter to indicate that a swapon or swapoff has occurred */
static atomic_t proc_poll_event = ATOMIC_INIT(0);
@@ -3161,9 +3158,9 @@ static int setup_swap_extents(struct swap_info_struct *sis,
* Mark a fully initialized swap device writable and expose it to the
* allocator. The caller must have resurrected its percpu ref first.
*/
-static void swap_device_enable(struct swap_info_struct *si)
+static void __swap_device_enable(struct swap_info_struct *si)
{
- percpu_down_write(&swapon_rwsem);
+ lockdep_assert_held_write(&swapon_rwsem);
spin_lock(&si->lock);
spin_lock(&swap_avail_lock);
si->flags |= SWP_WRITEOK;
@@ -3173,11 +3170,16 @@ static void swap_device_enable(struct swap_info_struct *si)
atomic_long_add(si->pages, &nr_swap_pages);
total_swap_pages += si->pages;
plist_add(&si->list, &swap_active_head);
- percpu_up_write(&swapon_rwsem);
-
add_to_avail_list(si);
}
+static void swap_device_enable(struct swap_info_struct *si)
+{
+ percpu_down_write(&swapon_rwsem);
+ __swap_device_enable(si);
+ percpu_up_write(&swapon_rwsem);
+}
+
static int swap_device_disable(struct address_space *mapping,
struct swap_info_struct **swap_info)
{
@@ -3340,7 +3342,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
if (!(p->flags & SWP_SOLIDSTATE))
atomic_dec(&nr_rotate_swap);
- mutex_lock(&swapon_mutex);
percpu_down_write(&swapon_rwsem);
spin_lock(&p->lock);
drain_mmlist();
@@ -3355,7 +3356,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
percpu_up_write(&swapon_rwsem);
arch_swap_invalidate_area(p->type);
zswap_swapoff(p->type);
- mutex_unlock(&swapon_mutex);
kfree(p->global_cluster);
p->global_cluster = NULL;
free_swap_cluster_info(cluster_info, maxpages);
@@ -3401,20 +3401,18 @@ static __poll_t swaps_poll(struct file *file, poll_table *wait)
return EPOLLIN | EPOLLRDNORM;
}
-/* iterator */
static void *swap_start(struct seq_file *swap, loff_t *pos)
{
struct swap_info_struct *si;
- int type;
loff_t l = *pos;
- mutex_lock(&swapon_mutex);
+ percpu_down_read(&swapon_rwsem);
if (!l)
return SEQ_START_TOKEN;
- for (type = 0; (si = swap_type_to_info(type)); type++) {
- if (!(si->swap_file))
+ for_each_swap(si) {
+ if (!si->swap_file)
continue;
if (!--l)
return si;
@@ -3435,7 +3433,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->flags & SWP_USED) || !(si->swap_file))
continue;
return si;
}
@@ -3445,7 +3443,7 @@ static void *swap_next(struct seq_file *swap, void *v, loff_t *pos)
static void swap_stop(struct seq_file *swap, void *v)
{
- mutex_unlock(&swapon_mutex);
+ percpu_up_read(&swapon_rwsem);
}
static int swap_show(struct seq_file *swap, void *v)
@@ -3958,7 +3956,6 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
goto free_swap_zswap;
}
- mutex_lock(&swapon_mutex);
prio = DEF_SWAP_PRIO;
if (swap_flags & SWAP_FLAG_PREFER)
prio = swap_flags & SWAP_FLAG_PRIO_MASK;
@@ -3970,11 +3967,14 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
si->prio = prio;
si->list.prio = -si->prio;
si->avail_list.prio = -si->prio;
+ /* Publish the file, live reference and writable state together. */
+ percpu_down_write(&swapon_rwsem);
si->swap_file = swap_file;
- /* Sets SWP_WRITEOK, resurrect the percpu ref, expose the swap device */
+ /* Make the reference live before exposing the writable device. */
percpu_ref_resurrect(&si->users);
- swap_device_enable(si);
+ __swap_device_enable(si);
+ percpu_up_write(&swapon_rwsem);
pr_info("Adding %uk swap on %s. Priority:%d extents:%d across:%lluk %s%s%s%s\n",
K(si->pages), name->name, si->prio, nr_extents,
@@ -3984,7 +3984,6 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
(si->flags & SWP_AREA_DISCARD) ? "s" : "",
(si->flags & SWP_PAGE_DISCARD) ? "c" : "");
- mutex_unlock(&swapon_mutex);
atomic_inc(&proc_poll_event);
wake_up_interruptible(&proc_poll_wait);
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 07/18] mm/swap: consolidate swap inuse accounting helpers
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (5 preceding siblings ...)
2026-11-14 14:49 ` [RFC PATCH v4 06/18] mm/swap: serialize swap publication before removing swapon mutex Lian Wang
@ 2026-11-14 14:50 ` Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 08/18] mm/swap: change back to use each swap device's percpu cluster Lian Wang
` (10 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:50 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Inline the swap_usage_add/sub helpers into their only callers and
rename the callers to better reflect what they actually do: track
per-device inuse page counts.
Keep the existing accounting and allocation policy. In the inlined
full-device path, reclaim work is scheduled before availability removal
rather than afterwards. Slot-free notifications and zswap invalidation
retain their existing order.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 64 ++++++++++++++++++++++-----------------------------
1 file changed, 27 insertions(+), 37 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 277a5c749987..aa020f2706b4 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -49,8 +49,8 @@
#include "internal.h"
#include "swap.h"
-static void swap_range_alloc(struct swap_info_struct *si,
- unsigned int nr_entries);
+static void swap_device_inuse_add(struct swap_info_struct *si,
+ unsigned int nr_entries);
static bool folio_swapcache_freeable(struct folio *folio);
static void move_cluster(struct swap_info_struct *si,
struct swap_cluster_info *ci, struct list_head *list,
@@ -1045,7 +1045,7 @@ static bool __swap_cluster_alloc_entries(struct swap_info_struct *si,
if (cluster_is_empty(ci))
ci->order = order;
ci->count += nr_pages;
- swap_range_alloc(si, nr_pages);
+ swap_device_inuse_add(si, nr_pages);
return true;
}
@@ -1363,53 +1363,36 @@ static void add_to_avail_list(struct swap_info_struct *si)
}
/*
- * swap_usage_add / swap_usage_sub of each slot are serialized by ci->lock
- * within each cluster, so the total contribution to the global counter should
- * always be positive and cannot exceed the total number of usable slots.
+ * swap_device_inuse_add / swap_device_inuse_sub are called within cluster
+ * update critical sections and serialized by ci->lock within each cluster,
+ * so the total contribution to the global counter should always be positive
+ * and cannot exceed the total number of usable slots.
*/
-static bool swap_usage_add(struct swap_info_struct *si, unsigned int nr_entries)
+static void swap_device_inuse_add(struct swap_info_struct *si,
+ unsigned int nr_entries)
{
- long val = atomic_long_add_return_relaxed(nr_entries, &si->inuse_pages);
+ long inuse_pages;
/*
* If device is full, and SWAP_USAGE_OFFLIST_BIT is not set,
* remove it from the plist.
*/
- if (unlikely(val == si->pages)) {
- del_from_avail_list(si, false);
- return true;
- }
-
- return false;
-}
-
-static void swap_usage_sub(struct swap_info_struct *si, unsigned int nr_entries)
-{
- long val = atomic_long_sub_return_relaxed(nr_entries, &si->inuse_pages);
-
- /*
- * If device is not full, and SWAP_USAGE_OFFLIST_BIT is set,
- * add it to the plist.
- */
- if (unlikely(val & SWAP_USAGE_OFFLIST_BIT))
- add_to_avail_list(si);
-}
-
-static void swap_range_alloc(struct swap_info_struct *si,
- unsigned int nr_entries)
-{
- if (swap_usage_add(si, nr_entries)) {
+ inuse_pages = atomic_long_add_return_relaxed(nr_entries, &si->inuse_pages);
+ if (unlikely(inuse_pages == si->pages)) {
if (vm_swap_full())
schedule_work(&si->reclaim_work);
+ del_from_avail_list(si, false);
}
+
atomic_long_sub(nr_entries, &nr_swap_pages);
}
-static void swap_range_free(struct swap_info_struct *si, unsigned long offset,
- unsigned int nr_entries)
+static void swap_device_inuse_sub(struct swap_info_struct *si, unsigned long offset,
+ unsigned int nr_entries)
{
unsigned long end = offset + nr_entries - 1;
void (*swap_slot_free_notify)(struct block_device *, unsigned long);
+ long inuse_pages;
zswap_invalidate(si->type, offset, nr_entries);
@@ -1431,7 +1414,14 @@ static void swap_range_free(struct swap_info_struct *si, unsigned long offset,
*/
smp_wmb();
atomic_long_add(nr_entries, &nr_swap_pages);
- swap_usage_sub(si, nr_entries);
+
+ /*
+ * If device is not full, and SWAP_USAGE_OFFLIST_BIT is set,
+ * add it back to the plist.
+ */
+ inuse_pages = atomic_long_sub_return_relaxed(nr_entries, &si->inuse_pages);
+ if (unlikely(inuse_pages & SWAP_USAGE_OFFLIST_BIT))
+ add_to_avail_list(si);
}
static bool get_swap_device_info(struct swap_info_struct *si)
@@ -2059,7 +2049,7 @@ void __swap_cluster_free_entries(struct swap_info_struct *si,
if (batch_id)
mem_cgroup_uncharge_swap(batch_id, ci_off - batch_off);
- swap_range_free(si, ci_head + ci_start, nr_pages);
+ swap_device_inuse_sub(si, ci_head + ci_start, nr_pages);
swap_cluster_assert_empty(ci, ci_start, nr_pages, false);
if (!ci->count)
@@ -2999,7 +2989,7 @@ static int try_to_unuse(unsigned int type)
success:
/*
* Make sure that further cleanups after try_to_unuse() returns happen
- * after swap_range_free() reduces si->inuse_pages to 0.
+ * after swap_device_inuse_sub() reduces si->inuse_pages to 0.
*/
smp_mb();
return 0;
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 08/18] mm/swap: change back to use each swap device's percpu cluster
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (6 preceding siblings ...)
2026-11-14 14:50 ` [RFC PATCH v4 07/18] mm/swap: consolidate swap inuse accounting helpers Lian Wang
@ 2026-11-14 14:50 ` Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 09/18] mm/swap: avoid direct reclaim when populating swap cluster metadata Lian Wang
` (9 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:50 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Youngjun Park <youngjun.park@lge.com>
This reverts commit 1b7e90020eb7 ("mm, swap: use percpu cluster as
allocation fast path").
The global percpu cluster causes several issues:
1) It prevents efficient swap allocation for users that do not want
the default swap device policy. Because the cluster cache sits
above device selection, users that need a different policy must
bypass or replace the cache. This complicates incoming ideas like
swap tiering.
2) It can bypass a caller's preferred device policy. Consider a future
policy with per-memcg device restrictions: memcg1 can access devices
A and B, where A has higher priority; memcg2 can only access B. Memcg2
can write the global percpu cluster with device B, then memcg1 takes B
in the fast path even though higher-priority device A is not exhausted.
3) The fast-path / slow-path design bundled with the global per-CPU
cache is problematic: only the fast path uses the global cache,
and the slow path is forced to rotate the allocation list. This
complicates consumers like discard.
Revert to per-device percpu clusters first. A later patch will
introduce new infrastructure for a high-performance global
allocation path.
Suggested-by: Kairui Song <kasong@tencent.com>
Co-developed-by: Baoquan He <bhe@redhat.com>
Signed-off-by: Baoquan He <bhe@redhat.com>
Signed-off-by: Youngjun Park <youngjun.park@lge.com>
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/swap.h | 13 +++-
mm/swapfile.c | 170 ++++++++++++++-----------------------------
2 files changed, 63 insertions(+), 120 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 717387e16a2d..c8a5684b2f4f 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -241,10 +241,17 @@ enum {
#endif
/*
- * We keep using same cluster for rotational device so IO will be sequential.
- * The purpose is to optimize SWAP throughput on these device.
+ * We assign a cluster to each CPU, so each CPU can allocate swap entry from
+ * its own cluster and swapout sequentially. The purpose is to optimize swapout
+ * throughput.
*/
+struct percpu_cluster {
+ local_lock_t lock; /* Protect the percpu_cluster above */
+ unsigned int next[SWAP_NR_ORDERS]; /* Likely next allocation offset */
+};
+
struct swap_sequential_cluster {
+ spinlock_t lock; /* Serialize usage of global cluster */
unsigned int next[SWAP_NR_ORDERS]; /* Likely next allocation offset */
};
@@ -267,8 +274,8 @@ struct swap_info_struct {
/* list of cluster that are fragmented or contented */
unsigned int pages; /* total of usable pages of swap */
atomic_long_t inuse_pages; /* number of those currently in use */
+ struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
struct swap_sequential_cluster *global_cluster; /* Use one global cluster for rotating device */
- spinlock_t global_cluster_lock; /* Serialize usage of global cluster */
struct rb_root swap_extent_root;/* root of the swap extent rbtree */
struct block_device *bdev; /* swap device or bdev of swap file */
struct file *swap_file; /* seldom referenced */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index aa020f2706b4..c46b43414bf4 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -145,18 +145,6 @@ static atomic_t proc_poll_event = ATOMIC_INIT(0);
atomic_t nr_rotate_swap = ATOMIC_INIT(0);
-struct percpu_swap_cluster {
- struct swap_info_struct *si[SWAP_NR_ORDERS];
- unsigned long offset[SWAP_NR_ORDERS];
- local_lock_t lock;
-};
-
-static DEFINE_PER_CPU(struct percpu_swap_cluster, percpu_swap_cluster) = {
- .si = { NULL },
- .offset = { SWAP_ENTRY_INVALID },
- .lock = INIT_LOCAL_LOCK(),
-};
-
/* May return NULL on invalid type, caller must check for NULL return */
static struct swap_info_struct *swap_type_to_info(int type)
{
@@ -523,9 +511,10 @@ static int swap_cluster_alloc_table(struct swap_info_struct *si,
lock:
if (!locked) {
if (si) {
- local_lock(&percpu_swap_cluster.lock);
- if (!(si->flags & SWP_SOLIDSTATE))
- spin_lock(&si->global_cluster_lock);
+ if (si->flags & SWP_SOLIDSTATE)
+ local_lock(&si->percpu_cluster->lock);
+ else
+ spin_lock(&si->global_cluster->lock);
}
spin_lock(&ci->lock);
}
@@ -610,9 +599,10 @@ swap_cluster_populate(struct swap_info_struct *si,
* Only cluster isolation from the allocator does table allocation.
* Swap allocator uses percpu clusters and holds the local lock.
*/
- lockdep_assert_held(&this_cpu_ptr(&percpu_swap_cluster)->lock);
- if (!(si->flags & SWP_SOLIDSTATE))
- lockdep_assert_held(&si->global_cluster_lock);
+ if (si->flags & SWP_SOLIDSTATE)
+ lockdep_assert_held(this_cpu_ptr(&si->percpu_cluster->lock));
+ else
+ lockdep_assert_held(&si->global_cluster->lock);
lockdep_assert_held(&ci->lock);
if (!swap_cluster_alloc_table(si, ci, __GFP_HIGH | __GFP_NOMEMALLOC |
@@ -625,9 +615,10 @@ swap_cluster_populate(struct swap_info_struct *si,
* the potential recursive allocation is limited.
*/
spin_unlock(&ci->lock);
- if (!(si->flags & SWP_SOLIDSTATE))
- spin_unlock(&si->global_cluster_lock);
- local_unlock(&percpu_swap_cluster.lock);
+ if (si->flags & SWP_SOLIDSTATE)
+ local_unlock(&si->percpu_cluster->lock);
+ else
+ spin_unlock(&si->global_cluster->lock);
ret = swap_cluster_alloc_table(si, ci, __GFP_HIGH | __GFP_NOMEMALLOC |
GFP_KERNEL, false);
@@ -749,7 +740,7 @@ static bool swap_do_scheduled_discard(struct swap_info_struct *si)
ci = list_first_entry(&si->discard_clusters, struct swap_cluster_info, list);
/*
* Delete the cluster from list to prepare for discard, but keep
- * the CLUSTER_FLAG_DISCARD flag, percpu_swap_cluster could be
+ * the CLUSTER_FLAG_DISCARD flag, there could be percpu_cluster
* pointing to it, or ran into by relocate_cluster.
*/
list_del(&ci->list);
@@ -1104,12 +1095,10 @@ static unsigned int alloc_swap_scan_cluster(struct swap_info_struct *si,
out:
relocate_cluster(si, ci);
swap_cluster_unlock(ci);
- if (si->flags & SWP_SOLIDSTATE) {
- this_cpu_write(percpu_swap_cluster.offset[order], next);
- this_cpu_write(percpu_swap_cluster.si[order], si);
- } else {
+ if (si->flags & SWP_SOLIDSTATE)
+ this_cpu_write(si->percpu_cluster->next[order], next);
+ else
si->global_cluster->next[order] = next;
- }
return found;
}
@@ -1210,13 +1199,17 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
if (order && !(si->flags & SWP_BLKDEV))
return 0;
- if (!(si->flags & SWP_SOLIDSTATE)) {
+ if (si->flags & SWP_SOLIDSTATE) {
+ /* Fast path using per CPU cluster */
+ local_lock(&si->percpu_cluster->lock);
+ offset = __this_cpu_read(si->percpu_cluster->next[order]);
+ } else {
/* Serialize HDD SWAP allocation for each device. */
- spin_lock(&si->global_cluster_lock);
+ spin_lock(&si->global_cluster->lock);
offset = si->global_cluster->next[order];
- if (offset == SWAP_ENTRY_INVALID)
- goto new_cluster;
+ }
+ if (offset != SWAP_ENTRY_INVALID) {
ci = swap_cluster_lock(si, offset);
/* Cluster could have been used by another order */
if (cluster_is_usable(ci, order)) {
@@ -1230,7 +1223,6 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
goto done;
}
-new_cluster:
if (swap_prefer_free_cluster(si)) {
found = alloc_swap_scan_list(si, &si->free_clusters, folio, false);
if (found)
@@ -1283,8 +1275,10 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
goto done;
}
done:
- if (!(si->flags & SWP_SOLIDSTATE))
- spin_unlock(&si->global_cluster_lock);
+ if (si->flags & SWP_SOLIDSTATE)
+ local_unlock(&si->percpu_cluster->lock);
+ else
+ spin_unlock(&si->global_cluster->lock);
return found;
}
@@ -1440,41 +1434,8 @@ static bool get_swap_device_info(struct swap_info_struct *si)
return true;
}
-/*
- * Fast path try to get swap entries with specified order from current
- * CPU's swap entry pool (a cluster).
- */
-static bool swap_alloc_fast(struct folio *folio)
-{
- unsigned int order = folio_order(folio);
- struct swap_cluster_info *ci;
- struct swap_info_struct *si;
- unsigned int offset;
-
- /*
- * Once allocated, swap_info_struct will never be completely freed,
- * so checking it's liveness by get_swap_device_info is enough.
- */
- si = this_cpu_read(percpu_swap_cluster.si[order]);
- offset = this_cpu_read(percpu_swap_cluster.offset[order]);
- if (!si || !offset || !get_swap_device_info(si))
- return false;
-
- ci = swap_cluster_lock(si, offset);
- if (cluster_is_usable(ci, order)) {
- if (cluster_is_empty(ci))
- offset = cluster_offset(si, ci);
- alloc_swap_scan_cluster(si, ci, folio, offset);
- } else {
- swap_cluster_unlock(ci);
- }
-
- put_swap_device(si);
- return folio_test_swapcache(folio);
-}
-
/* Rotate the device and switch to a new cluster */
-static void swap_alloc_slow(struct folio *folio)
+static void swap_alloc_entry(struct folio *folio)
{
struct swap_info_struct *si, *next;
@@ -1842,10 +1803,7 @@ int folio_alloc_swap(struct folio *folio)
}
again:
- local_lock(&percpu_swap_cluster.lock);
- if (!swap_alloc_fast(folio))
- swap_alloc_slow(folio);
- local_unlock(&percpu_swap_cluster.lock);
+ swap_alloc_entry(folio);
if (!order && unlikely(!folio_test_swapcache(folio))) {
if (swap_sync_discard())
@@ -2251,31 +2209,14 @@ void swap_put_entries_direct(swp_entry_t entry, int nr)
*/
swp_entry_t swap_alloc_hibernation_slot(int type)
{
- struct swap_info_struct *pcp_si, *si = swap_type_to_info(type);
- unsigned long pcp_offset, offset = SWAP_ENTRY_INVALID;
- struct swap_cluster_info *ci;
+ struct swap_info_struct *si = swap_type_to_info(type);
+ unsigned long offset = SWAP_ENTRY_INVALID;
swp_entry_t entry = {0};
if (!si)
goto fail;
- /*
- * Try the local cluster first if it matches the device. If
- * not, try grab a new cluster and override local cluster.
- */
- local_lock(&percpu_swap_cluster.lock);
- pcp_si = this_cpu_read(percpu_swap_cluster.si[0]);
- pcp_offset = this_cpu_read(percpu_swap_cluster.offset[0]);
- if (pcp_si == si && pcp_offset) {
- ci = swap_cluster_lock(si, pcp_offset);
- if (cluster_is_usable(ci, 0))
- offset = alloc_swap_scan_cluster(si, ci, NULL, pcp_offset);
- else
- swap_cluster_unlock(ci);
- }
- if (!offset)
- offset = cluster_alloc_swap_entry(si, NULL);
- local_unlock(&percpu_swap_cluster.lock);
+ offset = cluster_alloc_swap_entry(si, NULL);
if (offset)
entry = swp_entry(si->type, offset);
@@ -3253,27 +3194,6 @@ static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
kvfree(cluster_info);
}
-/*
- * Called after swap device's reference count is dead, so
- * neither scan nor allocation will use it.
- */
-static void flush_percpu_swap_cluster(struct swap_info_struct *si)
-{
- int cpu, i;
- struct swap_info_struct **pcp_si;
-
- for_each_possible_cpu(cpu) {
- pcp_si = per_cpu_ptr(percpu_swap_cluster.si, cpu);
- /*
- * Invalidate the percpu swap cluster cache, si->users
- * is dead, so no new user will point to it, just flush
- * any existing user.
- */
- for (i = 0; i < SWAP_NR_ORDERS; i++)
- cmpxchg(&pcp_si[i], si, NULL);
- }
-}
-
SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
{
struct swap_info_struct *p = NULL;
@@ -3325,7 +3245,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
flush_work(&p->discard_work);
flush_work(&p->reclaim_work);
- flush_percpu_swap_cluster(p);
destroy_swap_extents(p, p->swap_file);
@@ -3346,6 +3265,8 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
percpu_up_write(&swapon_rwsem);
arch_swap_invalidate_area(p->type);
zswap_swapoff(p->type);
+ free_percpu(p->percpu_cluster);
+ p->percpu_cluster = NULL;
kfree(p->global_cluster);
p->global_cluster = NULL;
free_swap_cluster_info(cluster_info, maxpages);
@@ -3694,7 +3615,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
{
unsigned long nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
struct swap_cluster_info *cluster_info;
- int err = -ENOMEM;
+ int cpu, err = -ENOMEM;
unsigned long i;
cluster_info = kvzalloc_objs(*cluster_info, nr_clusters);
@@ -3704,13 +3625,26 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
for (i = 0; i < nr_clusters; i++)
spin_lock_init(&cluster_info[i].lock);
- if (!(si->flags & SWP_SOLIDSTATE)) {
+ if (si->flags & SWP_SOLIDSTATE) {
+ si->percpu_cluster = alloc_percpu(struct percpu_cluster);
+ if (!si->percpu_cluster)
+ goto err;
+
+ for_each_possible_cpu(cpu) {
+ struct percpu_cluster *cluster;
+
+ cluster = per_cpu_ptr(si->percpu_cluster, cpu);
+ for (i = 0; i < SWAP_NR_ORDERS; i++)
+ cluster->next[i] = SWAP_ENTRY_INVALID;
+ local_lock_init(&cluster->lock);
+ }
+ } else {
si->global_cluster = kmalloc_obj(*si->global_cluster);
if (!si->global_cluster)
goto err;
for (i = 0; i < SWAP_NR_ORDERS; i++)
si->global_cluster->next[i] = SWAP_ENTRY_INVALID;
- spin_lock_init(&si->global_cluster_lock);
+ spin_lock_init(&si->global_cluster->lock);
}
/*
@@ -3984,6 +3918,8 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
bad_swap_unlock_inode:
inode_unlock(inode);
bad_swap:
+ free_percpu(si->percpu_cluster);
+ si->percpu_cluster = NULL;
kfree(si->global_cluster);
si->global_cluster = NULL;
inode = NULL;
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 09/18] mm/swap: avoid direct reclaim when populating swap cluster metadata
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (7 preceding siblings ...)
2026-11-14 14:50 ` [RFC PATCH v4 08/18] mm/swap: change back to use each swap device's percpu cluster Lian Wang
@ 2026-11-14 14:50 ` Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 10/18] mm/swap: add priority queue for swap device allocation Lian Wang
` (8 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:50 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
Swap cluster population is entered from swap allocation and may itself
need memory for slot metadata. Its fallback drops the device and cluster
locks before allocating with GFP_KERNEL, which can reenter swap
allocation through direct reclaim.
Make this allocation boundary explicitly non-reclaiming so callers can
retain outer synchronization without a recursive swap allocation.
Clear __GFP_DIRECT_RECLAIM from the fallback request while retaining
__GFP_HIGH and __GFP_NOMEMALLOC. The retry can still wake kswapd, but
fails without direct reclaim when memory is unavailable.
This can make metadata allocation fail earlier under memory pressure.
Keep the existing isolated-cluster rollback and restrict this request
without adding a task-wide PF_MEMALLOC scope.
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 23 +++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index c46b43414bf4..fae862fc873c 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -586,13 +586,14 @@ static void swap_cluster_assert_empty(struct swap_cluster_info *ci,
}
/*
- * Allocate swap table for one cluster. Attempt an atomic allocation first,
- * then fallback to sleeping allocation.
+ * Allocate swap table for one cluster. Attempt allocation under the locks
+ * first, then retry with the locks dropped without entering direct reclaim.
*/
static struct swap_cluster_info *
swap_cluster_populate(struct swap_info_struct *si,
struct swap_cluster_info *ci)
{
+ gfp_t gfp;
int ret;
/*
@@ -610,9 +611,11 @@ swap_cluster_populate(struct swap_info_struct *si,
return ci;
/*
- * Try a sleep allocation. Each isolated free cluster may cause
- * a sleep allocation, but there is a limited number of them, so
- * the potential recursive allocation is limited.
+ * Drop the device and cluster locks before retrying. Metadata allocation
+ * must not recurse into swap allocation through direct reclaim: callers
+ * can retain outer synchronization across cluster population. Restrict
+ * this request instead of granting the task PF_MEMALLOC, which could
+ * give allocator hooks and failure cleanup reserve access.
*/
spin_unlock(&ci->lock);
if (si->flags & SWP_SOLIDSTATE)
@@ -620,8 +623,9 @@ swap_cluster_populate(struct swap_info_struct *si,
else
spin_unlock(&si->global_cluster->lock);
- ret = swap_cluster_alloc_table(si, ci, __GFP_HIGH | __GFP_NOMEMALLOC |
- GFP_KERNEL, false);
+ gfp = (GFP_KERNEL | __GFP_HIGH | __GFP_NOMEMALLOC) &
+ ~__GFP_DIRECT_RECLAIM;
+ ret = swap_cluster_alloc_table(si, ci, gfp, false);
/*
* Back to atomic context. We might have migrated to a new CPU with a
@@ -3160,9 +3164,8 @@ static int swap_device_disable(struct address_space *mapping,
return err;
/*
- * The swap allocator doesn't take swapon_rwsem. Looping through every
- * cluster lock after clearing SWP_WRITEOK ensures that allocators see
- * the updated flag and that no allocation remains in flight.
+ * Drain cluster-lock users after clearing SWP_WRITEOK, so swapoff
+ * does not race with operations that started before the transition.
*/
for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
ci = swap_cluster_lock(si, offset);
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 10/18] mm/swap: add priority queue for swap device allocation
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (8 preceding siblings ...)
2026-11-14 14:50 ` [RFC PATCH v4 09/18] mm/swap: avoid direct reclaim when populating swap cluster metadata Lian Wang
@ 2026-11-14 14:50 ` Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 11/18] mm/swap: remove available list Lian Wang
` (7 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:50 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Swap device selection rotates swap_avail_head with plist_requeue(),
serializing cluster transitions on swap_avail_lock. Separate selection
from device-local cluster allocation with same-priority device rings
ordered from highest priority to lowest.
Keep membership and sizes stable under swapon_rwsem during each
allocation walk. Full or disabled devices remain as masked pointers.
Serialize tag writers with swap_queue_update_lock and use READ_ONCE()
and WRITE_ONCE() for complete tagged-pointer accesses.
Each CPU has a starting offset and base-page quota per ring. A task-local
cursor snapshots its starting device so retries visit every peer once,
even if another task changes the shared reader. Disable migration across
the sleepable walk. Large folios retry same-priority peers before asking
the caller to split instead of falling through after one fragmented
device.
Prepare reader storage with GFP_NOIO outside the topology writer, then
recheck priority and required length before publication. Transfer the
prepared storage only on successful installation; otherwise free it
after unlocking. Keep per-CPU backing allocation outside the writer's
noreclaim scope because it filters caller GFP flags. Constrain small
queue/ring allocations with a scoped noreclaim context and
__GFP_NOMEMALLOC, restoring context before cleanup. Deletion keeps reader
storage until replacement or an empty queue, avoiding shrinking backing
allocations beneath the writer.
These boundaries prevent queue updates from reclaiming into a swap
allocation that would acquire a reader behind the same writer. The
cluster metadata fallback also avoids direct reclaim while the walk
retains its outer reader. These recursive paths were identified by
source inspection rather than reproduced hangs.
Publish swap_file, the live reference, queue membership and SWP_WRITEOK
under the same writer. Complete the disable flag and queue mask
transition before releasing that writer, so a new allocation reader
cannot select a device that is no longer writable.
Mask the off-list flag from the old in-use counter before its full-device
comparison so concurrent freeing/refilling keeps availability state
consistent.
Keep the old available plist in parallel while migrating its remaining
consumers.
Link: https://lore.kernel.org/20260714-swap-pcp-priq-v1-9-de9b164ed419@tencent.com
Link: https://lore.kernel.org/alZ7UBXweuuOX4qz@yjaykim-PowerEdge-T330
Link: https://lore.kernel.org/77d6da3d-10af-49a1-a356-72aa8b462e85@gmail.com
Signed-off-by: Kairui Song <kasong@tencent.com>
Co-developed-by: Lian Wang <lianux.mm@gmail.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/swap.h | 3 +-
mm/swapfile.c | 580 ++++++++++++++++++++++++++++++++++++++-----
2 files changed, 518 insertions(+), 65 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index c8a5684b2f4f..6f32de173fb9 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -196,7 +196,8 @@ struct swap_extent {
* - SWP_USED: Protected by swapon_rwsem. Indicates the device is inuse. Once
* set, won't be cleared unless all reference to this device is freed and
* swapoff finished.
- * - SWP_WRITEOK: Protected by swapon_rwsem, si->lock and swap_avail_lock.
+ * - SWP_WRITEOK: Protected by swapon_rwsem, si->lock and
+ * swap_queue_update_lock.
* Clearing this flag is followed by waiting for all current cluster lock
* users to exit, so
* checking this flag while holding any of these locks ensures the device
diff --git a/mm/swapfile.c b/mm/swapfile.c
index fae862fc873c..21d27da95898 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -55,12 +55,14 @@ static bool folio_swapcache_freeable(struct folio *folio);
static void move_cluster(struct swap_info_struct *si,
struct swap_cluster_info *ci, struct list_head *list,
enum swap_cluster_flags new_flags);
+static bool get_swap_device_info(struct swap_info_struct *si);
/*
* Serializes swapon/swapoff (writers) and protects the swap_info
* array, nr_swapfiles, total_swap_pages, and part of swap device
- * info content (see comment of swap_info_struct). Read-side users
- * take percpu_down_read() while inspecting this protected state.
+ * info content (see comment of swap_info_struct). Readers
+ * (allocation, /proc/swaps, etc.) take percpu_down_read() which
+ * is cheap as the hot path.
*/
DEFINE_STATIC_PERCPU_RWSEM(swapon_rwsem);
/*
@@ -159,18 +161,437 @@ static struct swap_info_struct *swap_entry_to_info(swp_entry_t entry)
return swap_type_to_info(swp_type(entry));
}
+/*
+ * All available swap_info_structs are grouped by priority rings, the rings
+ * are ordered in a queue by priority (higher prio value = higher priority).
+ * The allocator iterates and rotates devices within each priority ring.
+ * When all devices in a ring are iterated, it goes to the next lower
+ * priority ring.
+ */
+struct swap_prio_ring {
+ int prio;
+ unsigned int size;
+ struct swap_info_struct *dev[] __counted_by(size);
+};
+
+/*
+ * The ring is protected by swapon_rwsem so updating it is costly. To make
+ * the allocator and other users skip full devices faster, the lowest bit of
+ * a device pointer is used to mark it disabled (temporarily unavailable).
+ * This relies on the natural alignment of struct swap_info_struct.
+ */
+#define SWAP_DEVICE_MASKED_SHIFT 0
+#define SWAP_DEVICE_MASKED_BIT BIT(SWAP_DEVICE_MASKED_SHIFT)
+static_assert(__alignof__(struct swap_info_struct) >= 2);
+
+/*
+ * Serializes queue content mutations and keeps SWAP_USAGE_OFFLIST_BIT
+ * consistent with the masked state of each device pointer.
+ */
+static DEFINE_SPINLOCK(swap_queue_update_lock);
+
+/*
+ * Swap queue is protected by both swap_queue_update_lock and swapon_rwsem.
+ * Only swapon/swapoff will take the write lock, and modify the queue length
+ * or any ring's length. swap_queue_update_lock protects the content so
+ * devices can be masked easily without taking the writelock, which is heavy.
+ */
+static struct swap_prio_ring **swap_queue;
+static unsigned int swap_queue_len;
+
+/*
+ * Each CPU has its read iterator, so the queue itself will remain read
+ * only and the CPU side reader rotates by iterating the devices
+ * periodically using the counter.
+ */
+#define SWAP_ROUND_ROBIN_QUOTA SWAPFILE_CLUSTER
+struct swap_ring_iterator {
+ int offset;
+ long rr_counter;
+};
+
+struct swap_queue_reader {
+ local_lock_t lock;
+ struct swap_ring_iterator ri[];
+};
+
+struct swap_queue_cursor {
+ bool valid;
+ bool ring_only;
+ unsigned int ring_idx;
+ unsigned int offset;
+};
+
+static __percpu struct swap_queue_reader *swap_queue_readers;
+
+static inline bool swap_device_masked(struct swap_info_struct *si)
+{
+ return (unsigned long)si & SWAP_DEVICE_MASKED_BIT;
+}
+
+static inline struct swap_info_struct *swap_device_mask_ptr(struct swap_info_struct *si)
+{
+ return (struct swap_info_struct *)((unsigned long)si |
+ SWAP_DEVICE_MASKED_BIT);
+}
+
+static inline struct swap_info_struct *swap_device_unmask_ptr(struct swap_info_struct *si)
+{
+ return (struct swap_info_struct *)((unsigned long)si & ~SWAP_DEVICE_MASKED_BIT);
+}
+
+static struct swap_queue_reader __percpu *swap_queue_prealloc_readers(int nr_rings, gfp_t gfp)
+{
+ struct swap_queue_reader __percpu *readers;
+
+ if (!nr_rings)
+ return NULL;
+
+ readers = __alloc_percpu_gfp(struct_size(readers, ri, nr_rings),
+ __alignof__(*readers), gfp);
+ return readers;
+}
+
+static bool swap_queue_needs_readers(int prio)
+{
+ unsigned int i;
+
+ lockdep_assert_held_write(&swapon_rwsem);
+ for (i = 0; i < swap_queue_len; i++) {
+ if (swap_queue[i]->prio == prio)
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Return with swapon_rwsem held for writing, or unlocked on allocation failure.
+ * Per-CPU backing allocations may reclaim, so prepare them before publishing
+ * the device and outside the topology lock and our noreclaim scope. Keep NOIO
+ * because the swapon caller still holds the swapfile's inode lock.
+ */
+static int swap_queue_lock_for_add(int prio,
+ struct swap_queue_reader __percpu **readers,
+ unsigned int *nr_readers)
+{
+ *readers = NULL;
+ *nr_readers = 0;
+ for (;;) {
+ percpu_down_write(&swapon_rwsem);
+ if (!swap_queue_needs_readers(prio) ||
+ *nr_readers > swap_queue_len)
+ return 0;
+
+ *nr_readers = swap_queue_len + 1;
+ percpu_up_write(&swapon_rwsem);
+ free_percpu(*readers);
+ *readers = swap_queue_prealloc_readers(*nr_readers, GFP_NOIO);
+ if (!*readers)
+ return -ENOMEM;
+ }
+}
+
+static void swap_queue_reset_readers(void)
+{
+ int ring_idx, cpu;
+ struct swap_prio_ring *ring;
+
+ lockdep_assert_held_write(&swapon_rwsem);
+ /* A new topology starts a fresh, distributed rotation on every CPU. */
+ for_each_possible_cpu(cpu) {
+ for (ring_idx = 0; ring_idx < swap_queue_len; ring_idx++) {
+ ring = swap_queue[ring_idx];
+ per_cpu_ptr(swap_queue_readers, cpu)->ri[ring_idx].offset =
+ cpu % ring->size;
+ per_cpu_ptr(swap_queue_readers, cpu)->ri[ring_idx].rr_counter =
+ SWAP_ROUND_ROBIN_QUOTA;
+ }
+ }
+}
+
+static void swap_queue_install_readers(struct swap_queue_reader __percpu *readers)
+{
+ int cpu;
+
+ lockdep_assert_held_write(&swapon_rwsem);
+ free_percpu(swap_queue_readers);
+ swap_queue_readers = readers;
+ if (!readers)
+ return;
+
+ for_each_possible_cpu(cpu)
+ local_lock_init(&per_cpu_ptr(readers, cpu)->lock);
+ swap_queue_reset_readers();
+}
+
+static struct swap_info_struct *swap_queue_get_device(long nr_alloc, int nr_iter,
+ struct swap_queue_cursor *cursor)
+{
+ bool rotate = false;
+ struct swap_info_struct *si;
+ struct swap_ring_iterator *ri;
+ struct swap_prio_ring *ring;
+ unsigned int dev_idx, queue_idx;
+
+ if (!swap_queue_len)
+ return ERR_PTR(-ENOENT);
+
+ queue_idx = 0;
+ while (nr_iter >= swap_queue[queue_idx]->size) {
+ nr_iter -= swap_queue[queue_idx]->size;
+ if (++queue_idx >= swap_queue_len)
+ return ERR_PTR(cursor->ring_only ? -E2BIG : -ENOENT);
+ }
+ if (cursor->ring_only && cursor->ring_idx != queue_idx)
+ return ERR_PTR(-E2BIG);
+
+ ring = swap_queue[queue_idx];
+ local_lock(&swap_queue_readers->lock);
+ ri = this_cpu_ptr(&swap_queue_readers->ri[queue_idx]);
+ /*
+ * Snapshot the starting offset for this allocation's walk. The shared
+ * iterator can move between retries, but the cursor must visit every
+ * device in the ring exactly once before falling through.
+ */
+ if (!cursor->valid || cursor->ring_idx != queue_idx) {
+ cursor->valid = true;
+ cursor->ring_idx = queue_idx;
+ if (ri->rr_counter < nr_alloc)
+ rotate = true;
+ else if (ri->offset >= ring->size)
+ rotate = true;
+ if (rotate) {
+ ri->offset++;
+ ri->offset %= ring->size;
+ ri->rr_counter = SWAP_ROUND_ROBIN_QUOTA;
+ }
+ cursor->offset = ri->offset;
+ }
+
+ dev_idx = (cursor->offset + nr_iter) % ring->size;
+ if (nr_iter) {
+ ri->offset = dev_idx;
+ ri->rr_counter = SWAP_ROUND_ROBIN_QUOTA;
+ }
+ ri->rr_counter -= nr_alloc;
+ si = READ_ONCE(ring->dev[dev_idx]);
+ local_unlock(&swap_queue_readers->lock);
+
+ if (swap_device_masked(si))
+ return ERR_PTR(-EBUSY);
+
+ si = swap_device_unmask_ptr(si);
+ return si;
+}
+
+static bool swap_queue_find(struct swap_info_struct *si,
+ unsigned int *ring_idx, unsigned int *dev_idx)
+{
+ unsigned int i, j;
+ struct swap_prio_ring *ring;
+
+ lockdep_assert(lockdep_is_held(&swapon_rwsem) ||
+ lockdep_is_held(&swap_queue_update_lock));
+
+ for (i = 0; i < swap_queue_len; i++) {
+ ring = swap_queue[i];
+ if (ring->prio != si->prio)
+ continue;
+ for (j = 0; j < ring->size; j++) {
+ if (swap_device_unmask_ptr(READ_ONCE(ring->dev[j])) != si)
+ continue;
+ *ring_idx = i;
+ *dev_idx = j;
+ return true;
+ }
+ }
+ return false;
+}
+
+static void swap_queue_mask(struct swap_info_struct *si)
+{
+ unsigned int ring_idx, dev_idx;
+
+ lockdep_assert_held(&swap_queue_update_lock);
+ if (swap_queue_find(si, &ring_idx, &dev_idx))
+ WRITE_ONCE(swap_queue[ring_idx]->dev[dev_idx],
+ swap_device_mask_ptr(si));
+}
+
+static void swap_queue_unmask(struct swap_info_struct *si)
+{
+ unsigned int ring_idx, dev_idx;
+
+ lockdep_assert_held(&swap_queue_update_lock);
+ if (swap_queue_find(si, &ring_idx, &dev_idx))
+ WRITE_ONCE(swap_queue[ring_idx]->dev[dev_idx], si);
+}
+
+/* Prepared readers remain caller-owned unless installed successfully. */
+static int swap_queue_add(struct swap_info_struct *si,
+ struct swap_queue_reader __percpu **readers,
+ unsigned int nr_readers)
+{
+ struct swap_prio_ring **new_queue = NULL, **old_queue = NULL;
+ struct swap_prio_ring *ring, *new_ring = NULL, *old_ring = NULL;
+ int prio = si->prio;
+ int i, pos, err = -ENOMEM;
+ unsigned int noreclaim_flag;
+ gfp_t gfp;
+
+ /* A writer must not reclaim into another swap allocation. */
+ gfp = GFP_NOIO | __GFP_HIGH | __GFP_NOMEMALLOC;
+ lockdep_assert_held_write(&swapon_rwsem);
+ noreclaim_flag = memalloc_noreclaim_save();
+
+ for (pos = 0; pos < swap_queue_len; pos++) {
+ if (swap_queue[pos]->prio == prio)
+ goto add_to_ring;
+ if (swap_queue[pos]->prio < prio)
+ break;
+ }
+
+ /* No ring at this priority: insert a new one at pos. */
+ if (!*readers || nr_readers <= swap_queue_len)
+ goto failed;
+ new_queue = kmalloc_array(swap_queue_len + 1, sizeof(*swap_queue), gfp);
+ if (!new_queue)
+ goto failed;
+ new_ring = kmalloc(struct_size(new_ring, dev, 1), gfp);
+ if (!new_ring)
+ goto failed;
+ if (!get_swap_device_info(si))
+ goto failed;
+
+ new_ring->prio = prio;
+ new_ring->size = 1;
+ new_ring->dev[0] = si;
+ for (i = 0; i < pos; i++)
+ new_queue[i] = swap_queue[i];
+ new_queue[pos] = new_ring;
+ for (i = pos; i < swap_queue_len; i++)
+ new_queue[i + 1] = swap_queue[i];
+
+ spin_lock(&swap_queue_update_lock);
+ old_queue = swap_queue;
+ swap_queue = new_queue;
+ swap_queue_len++;
+ spin_unlock(&swap_queue_update_lock);
+ memalloc_noreclaim_restore(noreclaim_flag);
+ kfree(old_queue);
+
+ swap_queue_install_readers(*readers);
+ *readers = NULL;
+ return 0;
+
+add_to_ring:
+ ring = swap_queue[pos];
+ new_ring = kmalloc(struct_size(ring, dev, ring->size + 1), gfp);
+ if (!new_ring)
+ goto failed;
+ if (!get_swap_device_info(si))
+ goto failed;
+ spin_lock(&swap_queue_update_lock);
+ memcpy(new_ring, ring, struct_size(ring, dev, ring->size));
+ new_ring->size++;
+ new_ring->dev[new_ring->size - 1] = si;
+ old_ring = swap_queue[pos];
+ swap_queue[pos] = new_ring;
+ spin_unlock(&swap_queue_update_lock);
+ memalloc_noreclaim_restore(noreclaim_flag);
+ kfree(old_ring);
+ return 0;
+
+failed:
+ memalloc_noreclaim_restore(noreclaim_flag);
+ kfree(new_queue);
+ kfree(new_ring);
+ return err;
+}
+
+static void swap_queue_del(struct swap_info_struct *si)
+{
+ gfp_t gfp;
+ unsigned int noreclaim_flag;
+ unsigned int ring_idx, dev_idx;
+ struct swap_prio_ring *ring, *new_ring = NULL, *old_ring = NULL;
+ struct swap_prio_ring **new_queue = NULL, **old_queue = NULL;
+
+ lockdep_assert_held_write(&swapon_rwsem);
+ if (!swap_queue_find(si, &ring_idx, &dev_idx)) {
+ WARN_ON(1);
+ return;
+ }
+
+ /*
+ * Shrinking the small topology arrays is optional. Keep reader storage
+ * until a new priority is inserted or the queue is empty: per-CPU
+ * allocation must not run under this writer's noreclaim scope, which
+ * can grant backing pages reserve access.
+ */
+ gfp = GFP_NOIO | __GFP_HIGH | __GFP_NOMEMALLOC;
+ noreclaim_flag = memalloc_noreclaim_save();
+ ring = swap_queue[ring_idx];
+ if (ring->size > 1)
+ new_ring = kmalloc(struct_size(ring, dev, ring->size - 1), gfp);
+ if (ring->size == 1 && swap_queue_len > 1) {
+ new_queue = kmalloc(sizeof(*swap_queue) *
+ (swap_queue_len - 1), gfp);
+ }
+ memalloc_noreclaim_restore(noreclaim_flag);
+
+ spin_lock(&swap_queue_update_lock);
+ if (ring->size > 1) {
+ /* Shift trailing devices left to fill the gap. */
+ while (++dev_idx < ring->size)
+ ring->dev[dev_idx - 1] =
+ ring->dev[dev_idx];
+ ring->size--;
+ if (new_ring) {
+ memcpy(new_ring, ring,
+ struct_size(ring, dev, ring->size));
+ old_ring = ring;
+ swap_queue[ring_idx] = new_ring;
+ }
+ } else {
+ /* Last device in this ring: remove the ring. */
+ old_ring = ring;
+ swap_queue_len--;
+ while (++ring_idx <= swap_queue_len)
+ swap_queue[ring_idx - 1] =
+ swap_queue[ring_idx];
+ if (new_queue) {
+ memcpy(new_queue, swap_queue,
+ sizeof(*swap_queue) * swap_queue_len);
+ old_queue = swap_queue;
+ swap_queue = new_queue;
+ } else if (!swap_queue_len) {
+ old_queue = swap_queue;
+ swap_queue = NULL;
+ }
+ if (!swap_queue_len)
+ swap_queue_install_readers(NULL);
+ }
+ spin_unlock(&swap_queue_update_lock);
+
+ kfree(old_ring);
+ kfree(old_queue);
+ put_swap_device(si);
+}
+
/*
* Use the second highest bit of inuse_pages counter as the indicator
- * if one swap device is on the available plist, so the atomic can
+ * if one swap device is unavailable for allocation, so the atomic can
* still be updated arithmetically while having special data embedded.
*
* inuse_pages counter is the only thing indicating if a device should
- * be on avail_lists or not (except swapon / swapoff). By embedding the
- * off-list bit in the atomic counter, updates no longer need any lock
- * to check the list status.
+ * be in the available queue or not (except swapon / swapoff). By
+ * embedding the off-list bit in the atomic counter, updates no longer
+ * need any lock to check the list status.
*
- * This bit will be set if the device is not on the plist and not
- * usable, will be cleared if the device is on the plist.
+ * This bit will be set if the device is not in the available queue
+ * and not usable, will be cleared if the device is in the queue.
*/
#define SWAP_USAGE_OFFLIST_BIT (1UL << (BITS_PER_TYPE(atomic_long_t) - 2))
#define SWAP_USAGE_COUNTER_MASK (~SWAP_USAGE_OFFLIST_BIT)
@@ -611,11 +1032,11 @@ swap_cluster_populate(struct swap_info_struct *si,
return ci;
/*
- * Drop the device and cluster locks before retrying. Metadata allocation
- * must not recurse into swap allocation through direct reclaim: callers
- * can retain outer synchronization across cluster population. Restrict
- * this request instead of granting the task PF_MEMALLOC, which could
- * give allocator hooks and failure cleanup reserve access.
+ * Retry with the local and cluster locks dropped. The allocator can
+ * still hold swapon_rwsem for reading: direct reclaim could enter
+ * swap allocation again while a writer is waiting for this reader.
+ * Restrict this request instead of granting the task PF_MEMALLOC,
+ * which could give allocator hooks and failure cleanup reserve access.
*/
spin_unlock(&ci->lock);
if (si->flags & SWP_SOLIDSTATE)
@@ -1293,6 +1714,7 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
unsigned long pages;
spin_lock(&swap_avail_lock);
+ spin_lock(&swap_queue_update_lock);
/*
* Force remove it only for swapoff. Else, take it off-list only if
@@ -1313,9 +1735,10 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages);
}
+ swap_queue_mask(si);
plist_del(&si->avail_list, &swap_avail_head);
-
skip:
+ spin_unlock(&swap_queue_update_lock);
spin_unlock(&swap_avail_lock);
}
@@ -1326,12 +1749,12 @@ static void add_to_avail_list(struct swap_info_struct *si)
unsigned long pages;
spin_lock(&swap_avail_lock);
+ spin_lock(&swap_queue_update_lock);
/*
- * Add the device to the avail list if SWP_WRITEOK is set and
- * SWAP_USAGE_OFFLIST_BIT is still set. Swapoff clears
- * SWP_WRITEOK first, so the device won't be re-added after
- * swapoff starts unless swap_device_enable resurrects it.
+ * Mark the device as avail if SWP_WRITEOK is set. Swapoff clears
+ * SWP_WRITEOK first, so check that first so the device won't be
+ * re-added after swapoff started.
*/
if (!(si->flags & SWP_WRITEOK))
goto skip;
@@ -1342,21 +1765,23 @@ static void add_to_avail_list(struct swap_info_struct *si)
val = atomic_long_fetch_and_relaxed(~SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages);
/*
- * When device is full and device is on the plist, only one updater will
- * see (inuse_pages == si->pages) and will call del_from_avail_list. If
- * that updater happen to be here, just skip adding.
+ * When device is full and marked as available, one reader will see
+ * (inuse_pages == si->pages) and should mark it as unavailable and
+ * set SWAP_USAGE_OFFLIST_BIT. If that updater happens to be here, just
+ * skip the rest.
*/
pages = si->pages;
- if (val == pages) {
+ if ((val & SWAP_USAGE_COUNTER_MASK) == pages) {
/* Just like the cmpxchg in del_from_avail_list */
if (atomic_long_try_cmpxchg(&si->inuse_pages, &pages,
pages | SWAP_USAGE_OFFLIST_BIT))
goto skip;
}
+ swap_queue_unmask(si);
plist_add(&si->avail_list, &swap_avail_head);
-
skip:
+ spin_unlock(&swap_queue_update_lock);
spin_unlock(&swap_avail_lock);
}
@@ -1373,7 +1798,7 @@ static void swap_device_inuse_add(struct swap_info_struct *si,
/*
* If device is full, and SWAP_USAGE_OFFLIST_BIT is not set,
- * remove it from the plist.
+ * mark it unavailable.
*/
inuse_pages = atomic_long_add_return_relaxed(nr_entries, &si->inuse_pages);
if (unlikely(inuse_pages == si->pages)) {
@@ -1415,7 +1840,7 @@ static void swap_device_inuse_sub(struct swap_info_struct *si, unsigned long off
/*
* If device is not full, and SWAP_USAGE_OFFLIST_BIT is set,
- * add it back to the plist.
+ * add it back to the available queue.
*/
inuse_pages = atomic_long_sub_return_relaxed(nr_entries, &si->inuse_pages);
if (unlikely(inuse_pages & SWAP_USAGE_OFFLIST_BIT))
@@ -1438,41 +1863,44 @@ static bool get_swap_device_info(struct swap_info_struct *si)
return true;
}
-/* Rotate the device and switch to a new cluster */
-static void swap_alloc_entry(struct folio *folio)
+static int swap_alloc_entry(struct folio *folio)
{
- struct swap_info_struct *si, *next;
+ struct swap_queue_cursor cursor = {};
+ long nr_pages = folio_nr_pages(folio);
+ struct swap_info_struct *si;
+ int nr_iter, ret;
- spin_lock(&swap_avail_lock);
-start_over:
- plist_for_each_entry_safe(si, next, &swap_avail_head, avail_list) {
- /* Rotate the device and switch to a new cluster */
- plist_requeue(&si->avail_list, &swap_avail_head);
- spin_unlock(&swap_avail_lock);
- if (get_swap_device_info(si)) {
- cluster_alloc_swap_entry(si, folio);
- put_swap_device(si);
- if (folio_test_swapcache(folio))
- return;
- if (folio_test_large(folio))
- return;
+ percpu_down_read(&swapon_rwsem);
+ migrate_disable();
+ for (nr_iter = 0;; nr_iter++) {
+ si = swap_queue_get_device(nr_pages, nr_iter, &cursor);
+ if (IS_ERR(si)) {
+ ret = PTR_ERR(si);
+ if (ret == -EBUSY)
+ continue;
+ break;
+ }
+ cluster_alloc_swap_entry(si, folio);
+
+ if (folio_test_swapcache(folio)) {
+ ret = 0;
+ break;
}
- spin_lock(&swap_avail_lock);
/*
- * if we got here, it's likely that si was almost full before,
- * multiple callers probably all tried to get a page from the
- * same si and it filled up before we could get one; or, the si
- * filled up between us dropping swap_avail_lock.
- * Since we dropped the swap_avail_lock, the swap_avail_list
- * may have been modified; so if next is still in the
- * swap_avail_head list then try it, otherwise start over if we
- * have not gotten any slots.
+ * For large allocations, try every device at the same priority,
+ * but ask the caller to split instead of falling back to a lower
+ * priority ring.
*/
- if (plist_node_empty(&next->avail_list))
- goto start_over;
+ if (folio_test_large(folio)) {
+ cursor.ring_only = true;
+ continue;
+ }
}
- spin_unlock(&swap_avail_lock);
+
+ migrate_enable();
+ percpu_up_read(&swapon_rwsem);
+ return ret;
}
/*
@@ -3090,18 +3518,17 @@ static int setup_swap_extents(struct swap_info_struct *sis,
}
/*
- * Mark a fully initialized swap device writable and expose it to the
- * allocator. The caller must have resurrected its percpu ref first.
+ * Mark a fully initialized swap device writable and expose it to the allocator.
+ * The caller must have resurrected its percpu ref before entering this helper.
*/
static void __swap_device_enable(struct swap_info_struct *si)
{
lockdep_assert_held_write(&swapon_rwsem);
spin_lock(&si->lock);
- spin_lock(&swap_avail_lock);
+ spin_lock(&swap_queue_update_lock);
si->flags |= SWP_WRITEOK;
- spin_unlock(&swap_avail_lock);
+ spin_unlock(&swap_queue_update_lock);
spin_unlock(&si->lock);
-
atomic_long_add(si->pages, &nr_swap_pages);
total_swap_pages += si->pages;
plist_add(&si->list, &swap_active_head);
@@ -3234,6 +3661,11 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
return err;
}
+ percpu_down_write(&swapon_rwsem);
+ swap_queue_del(p);
+ percpu_ref_kill(&p->users);
+ percpu_up_write(&swapon_rwsem);
+
/*
* Wait for swap operations protected by get/put_swap_device()
* to complete. Because of synchronize_rcu() here, all swap
@@ -3242,7 +3674,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
* prevent folio_test_swapcache() and the following swap cache
* operations from racing with swapoff.
*/
- percpu_ref_kill(&p->users);
synchronize_rcu();
wait_for_completion(&p->comp);
@@ -3708,6 +4139,8 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
{
struct swap_info_struct *si;
+ struct swap_queue_reader __percpu *readers;
+ unsigned int nr_readers;
struct file *swap_file = NULL;
struct address_space *mapping;
struct dentry *dentry;
@@ -3894,14 +4327,33 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
si->prio = prio;
si->list.prio = -si->prio;
si->avail_list.prio = -si->prio;
- /* Publish the file, live reference and writable state together. */
- percpu_down_write(&swapon_rwsem);
- si->swap_file = swap_file;
- /* Make the reference live before exposing the writable device. */
+ /*
+ * Publish swap_file before making the percpu ref live, then add the device
+ * to the queue and make it writable under the same write-side lock. This
+ * keeps lockless ref users and /proc/swaps from observing partial state.
+ */
+ error = swap_queue_lock_for_add(prio, &readers, &nr_readers);
+ if (error) {
+ inode->i_flags &= ~S_SWAPFILE;
+ goto free_swap_zswap;
+ }
+ si->swap_file = swap_file;
percpu_ref_resurrect(&si->users);
- __swap_device_enable(si);
+ error = swap_queue_add(si, &readers, nr_readers);
+ if (error) {
+ si->swap_file = NULL;
+ percpu_ref_kill(&si->users);
+ } else {
+ __swap_device_enable(si);
+ }
percpu_up_write(&swapon_rwsem);
+ free_percpu(readers);
+ if (error) {
+ wait_for_completion(&si->comp);
+ inode->i_flags &= ~S_SWAPFILE;
+ goto free_swap_zswap;
+ }
pr_info("Adding %uk swap on %s. Priority:%d extents:%d across:%lluk %s%s%s%s\n",
K(si->pages), name->name, si->prio, nr_extents,
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 11/18] mm/swap: remove available list
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (9 preceding siblings ...)
2026-11-14 14:50 ` [RFC PATCH v4 10/18] mm/swap: add priority queue for swap device allocation Lian Wang
@ 2026-11-14 14:51 ` Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 12/18] mm/swap: bound synchronous discard outside the allocation reader Lian Wang
` (6 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:51 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
The allocator now selects devices from the priority queue. Move the
remaining block-cgroup throttle traversal from swap_avail_head to
swap_active_head under swapon_rwsem. Check SWP_WRITEOK, the off-list
state and the backing block device to retain priority-ordered target
selection.
Remove swap_avail_head, swap_avail_lock, the per-device avail_list field
and their maintenance now that no consumer needs the available plist.
Queue masking and the embedded availability state remain serialized by
swap_queue_update_lock.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/swap.h | 1 -
mm/swapfile.c | 33 ++++++---------------------------
2 files changed, 6 insertions(+), 28 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 6f32de173fb9..d31f3f023d36 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -289,7 +289,6 @@ struct swap_info_struct {
struct work_struct discard_work; /* discard worker */
struct work_struct reclaim_work; /* reclaim worker */
struct list_head discard_clusters; /* discard clusters list */
- struct plist_node avail_list; /* entry in swap_avail_head */
const struct swap_ops *ops;
};
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 21d27da95898..5c5e449e9da3 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -96,21 +96,6 @@ static const char Bad_offset[] = "Bad swap offset entry ";
*/
static PLIST_HEAD(swap_active_head);
-/*
- * all available (active, not full) swap_info_structs
- * protected with swap_avail_lock, ordered by priority.
- * This is used by folio_alloc_swap() instead of swap_active_head
- * because swap_active_head includes all swap_info_structs,
- * but folio_alloc_swap() doesn't need to look at full ones.
- * This uses its own lock instead of swapon_rwsem because when a
- * swap_info_struct changes between not-full/full, it needs to
- * add/remove itself to/from this list, but the swap_info_struct->lock
- * is held and the locking order requires swapon_rwsem to be taken
- * before any swap_info_struct->lock.
- */
-static PLIST_HEAD(swap_avail_head);
-static DEFINE_SPINLOCK(swap_avail_lock);
-
static inline struct swap_info_struct *__swap_iter(int *i, unsigned long flag)
{
lockdep_assert_held(&swapon_rwsem);
@@ -1713,7 +1698,6 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
{
unsigned long pages;
- spin_lock(&swap_avail_lock);
spin_lock(&swap_queue_update_lock);
/*
@@ -1736,10 +1720,8 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
}
swap_queue_mask(si);
- plist_del(&si->avail_list, &swap_avail_head);
skip:
spin_unlock(&swap_queue_update_lock);
- spin_unlock(&swap_avail_lock);
}
/* SWAP_USAGE_OFFLIST_BIT can only be cleared by this helper. */
@@ -1748,7 +1730,6 @@ static void add_to_avail_list(struct swap_info_struct *si)
long val;
unsigned long pages;
- spin_lock(&swap_avail_lock);
spin_lock(&swap_queue_update_lock);
/*
@@ -1779,10 +1760,8 @@ static void add_to_avail_list(struct swap_info_struct *si)
}
swap_queue_unmask(si);
- plist_add(&si->avail_list, &swap_avail_head);
skip:
spin_unlock(&swap_queue_update_lock);
- spin_unlock(&swap_avail_lock);
}
/*
@@ -3910,7 +3889,6 @@ static struct swap_info_struct *alloc_swap_info(void)
}
p->swap_extent_root = RB_ROOT;
plist_node_init(&p->list, 0);
- plist_node_init(&p->avail_list, 0);
p->flags = SWP_USED;
percpu_up_write(&swapon_rwsem);
if (defer) {
@@ -4326,7 +4304,6 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
*/
si->prio = prio;
si->list.prio = -si->prio;
- si->avail_list.prio = -si->prio;
/*
* Publish swap_file before making the percpu ref live, then add the device
@@ -4475,14 +4452,16 @@ void __folio_throttle_swaprate(struct folio *folio, gfp_t gfp)
if (current->throttle_disk)
return;
- spin_lock(&swap_avail_lock);
- plist_for_each_entry(si, &swap_avail_head, avail_list) {
- if (si->bdev) {
+ percpu_down_read(&swapon_rwsem);
+ plist_for_each_entry(si, &swap_active_head, list) {
+ if ((si->flags & SWP_WRITEOK) &&
+ !(atomic_long_read(&si->inuse_pages) &
+ SWAP_USAGE_OFFLIST_BIT) && si->bdev) {
blkcg_schedule_throttle(si->bdev->bd_disk, true);
break;
}
}
- spin_unlock(&swap_avail_lock);
+ percpu_up_read(&swapon_rwsem);
}
#endif
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 12/18] mm/swap: bound synchronous discard outside the allocation reader
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (10 preceding siblings ...)
2026-11-14 14:51 ` [RFC PATCH v4 11/18] mm/swap: remove available list Lian Wang
@ 2026-11-14 14:51 ` Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 13/18] mm/swap: drop swap active plist Lian Wang
` (5 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:51 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
The synchronous discard fallback drains a list that freeing paths can
keep replenishing. Limit each foreground step to one cluster, retaining
CLUSTER_FLAG_DISCARD while I/O is in flight and returning the cluster to
the free list only after completion. The background worker continues to
process the remaining list.
Keep synchronous discard outside the topology reader. Enter this fallback
only after a complete normal order-0 allocation walk fails. A higher-
priority device with only pending discard work does not force synchronous
I/O if a lower-priority device satisfies that walk.
Find a writable discard-capable device under the reader and take its live
reference, then release the reader before issuing I/O. Drop the reference
after completion and restart allocation with a fresh cursor, so a
concurrent topology change cannot leave stale ring state. This also sees
discard work produced by the preceding reclaim scans.
Track the swap types already processed during this folio allocation and
check each candidate at most once per fallback search. An empty check
does not consume the I/O budget. Each swap type can complete at most one
synchronous cluster discard, bounding successful retries by MAX_SWAPFILES
even if membership changes. Do not keep discarding until this task wins
a free cluster from a peer.
Large folios retain the existing same-priority retry and split boundary;
their reader-held cluster walk does not issue synchronous discard. The
budget bounds work count, not device I/O completion latency.
Link: https://lore.kernel.org/all/CAMgjq7CsYhEjvtN85XGkrONYAJxve7gG593TFeOGV-oax++kWA@mail.gmail.com/
Signed-off-by: Kairui Song <kasong@tencent.com>
Co-developed-by: Lian Wang <lianux.mm@gmail.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 175 ++++++++++++++++++++++++++++----------------------
1 file changed, 100 insertions(+), 75 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5c5e449e9da3..6b2dbaf33cb6 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -6,6 +6,7 @@
* Swap reorganised 29.12.95, Stephen Tweedie
*/
+#include <linux/bitmap.h>
#include <linux/blkdev.h>
#include <linux/mm.h>
#include <linux/sched/mm.h>
@@ -585,6 +586,26 @@ static long swap_usage_in_pages(struct swap_info_struct *si)
return atomic_long_read(&si->inuse_pages) & SWAP_USAGE_COUNTER_MASK;
}
+/*
+ * Serialize the allocation on single CPU or globally to avoid
+ * fragmentation and make the workflow easier to follow.
+ */
+static void swap_alloc_lock_device(struct swap_info_struct *si)
+{
+ if (si->flags & SWP_SOLIDSTATE)
+ local_lock(&si->percpu_cluster->lock);
+ else
+ spin_lock(&si->global_cluster->lock);
+}
+
+static void swap_alloc_unlock_device(struct swap_info_struct *si)
+{
+ if (si->flags & SWP_SOLIDSTATE)
+ local_unlock(&si->percpu_cluster->lock);
+ else
+ spin_unlock(&si->global_cluster->lock);
+}
+
/* Reclaim the swap entry anyway if possible */
#define TTRS_ANYWAY 0x1
/*
@@ -916,12 +937,8 @@ static int swap_cluster_alloc_table(struct swap_info_struct *si,
lock:
if (!locked) {
- if (si) {
- if (si->flags & SWP_SOLIDSTATE)
- local_lock(&si->percpu_cluster->lock);
- else
- spin_lock(&si->global_cluster->lock);
- }
+ if (si)
+ swap_alloc_lock_device(si);
spin_lock(&ci->lock);
}
lockdep_assert_held(&ci->lock);
@@ -1024,10 +1041,7 @@ swap_cluster_populate(struct swap_info_struct *si,
* which could give allocator hooks and failure cleanup reserve access.
*/
spin_unlock(&ci->lock);
- if (si->flags & SWP_SOLIDSTATE)
- local_unlock(&si->percpu_cluster->lock);
- else
- spin_unlock(&si->global_cluster->lock);
+ swap_alloc_unlock_device(si);
gfp = (GFP_KERNEL | __GFP_HIGH | __GFP_NOMEMALLOC) &
~__GFP_DIRECT_RECLAIM;
@@ -1134,44 +1148,40 @@ static struct swap_cluster_info *isolate_lock_cluster(
}
/*
- * Doing discard actually. After a cluster discard is finished, the cluster
- * will be added to free cluster list. Discard cluster is a bit special as
- * they don't participate in allocation or reclaim, so clusters marked as
- * CLUSTER_FLAG_DISCARD must remain off-list or on discard list.
+ * Discard one cluster. After the discard is finished, the cluster will be
+ * added to the free cluster list. Discard clusters are special because they
+ * don't participate in allocation or reclaim, so CLUSTER_FLAG_DISCARD must
+ * remain set while a cluster is either queued or being discarded.
*/
-static bool swap_do_scheduled_discard(struct swap_info_struct *si)
+static bool swap_discard_one_cluster(struct swap_info_struct *si)
{
struct swap_cluster_info *ci;
- bool ret = false;
unsigned int idx;
spin_lock(&si->lock);
- while (!list_empty(&si->discard_clusters)) {
- ci = list_first_entry(&si->discard_clusters, struct swap_cluster_info, list);
- /*
- * Delete the cluster from list to prepare for discard, but keep
- * the CLUSTER_FLAG_DISCARD flag, there could be percpu_cluster
- * pointing to it, or ran into by relocate_cluster.
- */
- list_del(&ci->list);
- idx = cluster_index(si, ci);
+ if (list_empty(&si->discard_clusters)) {
spin_unlock(&si->lock);
- discard_swap_cluster(si, idx * SWAPFILE_CLUSTER,
- SWAPFILE_CLUSTER);
-
- spin_lock(&ci->lock);
- /*
- * Discard is done, clear its flags as it's off-list, then
- * return the cluster to allocation list.
- */
- ci->flags = CLUSTER_FLAG_NONE;
- __free_cluster(si, ci);
- spin_unlock(&ci->lock);
- ret = true;
- spin_lock(&si->lock);
+ return false;
}
+
+ ci = list_first_entry(&si->discard_clusters,
+ struct swap_cluster_info, list);
+ /*
+ * Delete the cluster from the list, but keep CLUSTER_FLAG_DISCARD
+ * set while the discard is in flight. A percpu cluster may still
+ * point to it, or relocate_cluster() may encounter it.
+ */
+ list_del(&ci->list);
+ idx = cluster_index(si, ci);
spin_unlock(&si->lock);
- return ret;
+
+ discard_swap_cluster(si, idx * SWAPFILE_CLUSTER, SWAPFILE_CLUSTER);
+
+ spin_lock(&ci->lock);
+ ci->flags = CLUSTER_FLAG_NONE;
+ __free_cluster(si, ci);
+ spin_unlock(&ci->lock);
+ return true;
}
static void swap_discard_work(struct work_struct *work)
@@ -1180,7 +1190,8 @@ static void swap_discard_work(struct work_struct *work)
si = container_of(work, struct swap_info_struct, discard_work);
- swap_do_scheduled_discard(si);
+ while (swap_discard_one_cluster(si))
+ cond_resched();
}
static void swap_users_ref_free(struct percpu_ref *ref)
@@ -1609,15 +1620,11 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
if (order && !(si->flags & SWP_BLKDEV))
return 0;
- if (si->flags & SWP_SOLIDSTATE) {
- /* Fast path using per CPU cluster */
- local_lock(&si->percpu_cluster->lock);
+ swap_alloc_lock_device(si);
+ if (si->flags & SWP_SOLIDSTATE)
offset = __this_cpu_read(si->percpu_cluster->next[order]);
- } else {
- /* Serialize HDD SWAP allocation for each device. */
- spin_lock(&si->global_cluster->lock);
+ else
offset = si->global_cluster->next[order];
- }
if (offset != SWAP_ENTRY_INVALID) {
ci = swap_cluster_lock(si, offset);
@@ -1685,10 +1692,7 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
goto done;
}
done:
- if (si->flags & SWP_SOLIDSTATE)
- local_unlock(&si->percpu_cluster->lock);
- else
- spin_unlock(&si->global_cluster->lock);
+ swap_alloc_unlock_device(si);
return found;
}
@@ -1883,33 +1887,50 @@ static int swap_alloc_entry(struct folio *folio)
}
/*
- * Discard pending clusters in a synchronized way when under high pressure.
- * Return: true if any cluster is discarded.
+ * Discard outside the topology reader, holding only the selected device's
+ * live reference across I/O. Each successful retry starts a fresh queue walk.
*/
-static bool swap_sync_discard(void)
+static bool swap_discard_for_alloc(unsigned long *discarded)
{
- bool ret = false;
- struct swap_info_struct *si, *next;
+ DECLARE_BITMAP(checked, MAX_SWAPFILES) = {};
+ struct swap_info_struct *si, *candidate;
+ unsigned int i, j;
+ bool done;
+ int type;
- percpu_down_read(&swapon_rwsem);
-start_over:
- plist_for_each_entry_safe(si, next, &swap_active_head, list) {
- percpu_up_read(&swapon_rwsem);
- if (get_swap_device_info(si)) {
- if (si->flags & SWP_PAGE_DISCARD)
- ret = swap_do_scheduled_discard(si);
- put_swap_device(si);
+ for (;;) {
+ candidate = NULL;
+ percpu_down_read(&swapon_rwsem);
+ for (i = 0; i < swap_queue_len && !candidate; i++) {
+ struct swap_prio_ring *ring = swap_queue[i];
+
+ for (j = 0; j < ring->size; j++) {
+ si = swap_device_unmask_ptr(READ_ONCE(ring->dev[j]));
+ type = si->type;
+ if (test_bit(type, discarded) || test_bit(type, checked))
+ continue;
+ __set_bit(type, checked);
+ if ((READ_ONCE(si->flags) & (SWP_WRITEOK | SWP_PAGE_DISCARD)) !=
+ (SWP_WRITEOK | SWP_PAGE_DISCARD))
+ continue;
+ if (get_swap_device_info(si)) {
+ candidate = si;
+ break;
+ }
+ }
}
- if (ret)
- return true;
+ percpu_up_read(&swapon_rwsem);
+ if (!candidate)
+ return false;
- percpu_down_read(&swapon_rwsem);
- if (plist_node_empty(&next->list))
- goto start_over;
+ type = candidate->type;
+ done = swap_discard_one_cluster(candidate);
+ if (done)
+ __set_bit(type, discarded);
+ put_swap_device(candidate);
+ if (done)
+ return true;
}
- percpu_up_read(&swapon_rwsem);
-
- return false;
}
static int swap_extend_table_alloc(struct swap_info_struct *si,
@@ -2213,12 +2234,16 @@ int folio_alloc_swap(struct folio *folio)
}
}
-again:
swap_alloc_entry(folio);
if (!order && unlikely(!folio_test_swapcache(folio))) {
- if (swap_sync_discard())
- goto again;
+ DECLARE_BITMAP(discarded, MAX_SWAPFILES) = {};
+
+ while (swap_discard_for_alloc(discarded)) {
+ swap_alloc_entry(folio);
+ if (folio_test_swapcache(folio))
+ break;
+ }
}
/* Need to call this even if allocation failed, for MEMCG_SWAP_FAIL. */
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 13/18] mm/swap: drop swap active plist
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (11 preceding siblings ...)
2026-11-14 14:51 ` [RFC PATCH v4 12/18] mm/swap: bound synchronous discard outside the allocation reader Lian Wang
@ 2026-11-14 14:51 ` Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 14/18] lib/plist.c: remove requeue function Lian Wang
` (4 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:51 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Remove swap_active_head and the associated plist_node. Device identity
lookups can use the writable swap_info iterator without ordering.
Keep priority ordering for the block-cgroup swap throttle target by
walking the priority queue under the topology reader. Choose the first
unmasked, writable, non-offlist device with a backing block device.
Check OFFLIST as well as the pointer tag because full transitions can
temporarily leave an unmasked entry, and keep the explicit WRITEOK
check. The disable transition already publishes its mask before
releasing the topology writer.
This read-only lookup does not consume per-CPU allocation quotas or
change retry cursors. It avoids making the throttle disk depend on
swapon's swap-type assignment order when devices have different
priorities.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/swap.h | 1 -
mm/swapfile.c | 60 +++++++++++++++++++++++---------------------
2 files changed, 32 insertions(+), 29 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index d31f3f023d36..ce8a800751ca 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -263,7 +263,6 @@ struct swap_info_struct {
struct percpu_ref users; /* indicate and keep swap device valid. */
unsigned long flags; /* SWP_USED etc: see above */
signed short prio; /* swap priority of this type */
- struct plist_node list; /* entry in swap_active_head */
signed char type; /* strange name for an index */
unsigned int max; /* size of this swap device */
struct swap_cluster_info *cluster_info; /* array, one entry per cluster */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 6b2dbaf33cb6..bd098114ee4b 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -42,7 +42,6 @@
#include <linux/completion.h>
#include <linux/suspend.h>
#include <linux/zswap.h>
-#include <linux/plist.h>
#include <asm/tlbflush.h>
#include <linux/leafops.h>
@@ -91,12 +90,6 @@ bool swap_migration_ad_supported;
static const char Bad_file[] = "Bad swap file entry ";
static const char Bad_offset[] = "Bad swap offset entry ";
-/*
- * all active swap_info_structs
- * protected with swapon_rwsem, and ordered by priority.
- */
-static PLIST_HEAD(swap_active_head);
-
static inline struct swap_info_struct *__swap_iter(int *i, unsigned long flag)
{
lockdep_assert_held(&swapon_rwsem);
@@ -3535,7 +3528,6 @@ static void __swap_device_enable(struct swap_info_struct *si)
spin_unlock(&si->lock);
atomic_long_add(si->pages, &nr_swap_pages);
total_swap_pages += si->pages;
- plist_add(&si->list, &swap_active_head);
add_to_avail_list(si);
}
@@ -3555,9 +3547,8 @@ static int swap_device_disable(struct address_space *mapping,
int err = -EINVAL;
percpu_down_write(&swapon_rwsem);
- plist_for_each_entry(si, &swap_active_head, list) {
- if ((si->flags & SWP_WRITEOK) &&
- si->swap_file->f_mapping == mapping) {
+ for_each_avail_swap(si) {
+ if (si->swap_file->f_mapping == mapping) {
err = 0;
break;
}
@@ -3583,8 +3574,6 @@ static int swap_device_disable(struct address_space *mapping,
spin_lock(&si->lock);
del_from_avail_list(si, true);
spin_unlock(&si->lock);
-
- plist_del(&si->list, &swap_active_head);
total_swap_pages -= si->pages;
atomic_long_sub(si->pages, &nr_swap_pages);
@@ -3913,7 +3902,6 @@ static struct swap_info_struct *alloc_swap_info(void)
*/
}
p->swap_extent_root = RB_ROOT;
- plist_node_init(&p->list, 0);
p->flags = SWP_USED;
percpu_up_write(&swapon_rwsem);
if (defer) {
@@ -4323,12 +4311,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
if (swap_flags & SWAP_FLAG_PREFER)
prio = swap_flags & SWAP_FLAG_PRIO_MASK;
- /*
- * The plist prio is negated because plist ordering is
- * low-to-high, while swap ordering is high-to-low
- */
si->prio = prio;
- si->list.prio = -si->prio;
/*
* Publish swap_file before making the percpu ref live, then add the device
@@ -4454,7 +4437,33 @@ int swap_dup_entry_direct(swp_entry_t entry)
#if defined(CONFIG_MEMCG) && defined(CONFIG_BLK_CGROUP)
static bool __has_usable_swap(void)
{
- return !plist_head_empty(&swap_active_head);
+ return READ_ONCE(total_swap_pages) > 0;
+}
+
+static struct swap_info_struct *swap_queue_first_usable_bdev(void)
+{
+ struct swap_prio_ring *ring;
+ struct swap_info_struct *si;
+ unsigned int ring_idx, dev_idx;
+
+ lockdep_assert_held(&swapon_rwsem);
+ for (ring_idx = 0; ring_idx < swap_queue_len; ring_idx++) {
+ ring = swap_queue[ring_idx];
+ for (dev_idx = 0; dev_idx < ring->size; dev_idx++) {
+ si = READ_ONCE(ring->dev[dev_idx]);
+ if (swap_device_masked(si))
+ continue;
+ si = swap_device_unmask_ptr(si);
+ if (!(READ_ONCE(si->flags) & SWP_WRITEOK))
+ continue;
+ if (atomic_long_read(&si->inuse_pages) &
+ SWAP_USAGE_OFFLIST_BIT)
+ continue;
+ if (si->bdev)
+ return si;
+ }
+ }
+ return NULL;
}
void __folio_throttle_swaprate(struct folio *folio, gfp_t gfp)
@@ -4478,14 +4487,9 @@ void __folio_throttle_swaprate(struct folio *folio, gfp_t gfp)
return;
percpu_down_read(&swapon_rwsem);
- plist_for_each_entry(si, &swap_active_head, list) {
- if ((si->flags & SWP_WRITEOK) &&
- !(atomic_long_read(&si->inuse_pages) &
- SWAP_USAGE_OFFLIST_BIT) && si->bdev) {
- blkcg_schedule_throttle(si->bdev->bd_disk, true);
- break;
- }
- }
+ si = swap_queue_first_usable_bdev();
+ if (si)
+ blkcg_schedule_throttle(si->bdev->bd_disk, true);
percpu_up_read(&swapon_rwsem);
}
#endif
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 14/18] lib/plist.c: remove requeue function
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (12 preceding siblings ...)
2026-11-14 14:51 ` [RFC PATCH v4 13/18] mm/swap: drop swap active plist Lian Wang
@ 2026-11-14 14:51 ` Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 15/18] mm/swap: reuse per-CPU queue readers across priority changes Lian Wang
` (3 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:51 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
From: Kairui Song <kasong@tencent.com>
Now the last user of plist requeue is gone, this function can be
removed.
Signed-off-by: Kairui Song <kasong@tencent.com>
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
include/linux/plist.h | 2 --
lib/plist.c | 71 -------------------------------------------
2 files changed, 73 deletions(-)
diff --git a/include/linux/plist.h b/include/linux/plist.h
index 16cf4355b5c1..73d359c1a053 100644
--- a/include/linux/plist.h
+++ b/include/linux/plist.h
@@ -132,8 +132,6 @@ static inline void plist_node_init(struct plist_node *node, int prio)
extern void plist_add(struct plist_node *node, struct plist_head *head);
extern void plist_del(struct plist_node *node, struct plist_head *head);
-extern void plist_requeue(struct plist_node *node, struct plist_head *head);
-
/**
* plist_for_each - iterate over the plist
* @pos: the type * to use as a loop counter
diff --git a/lib/plist.c b/lib/plist.c
index b0273533c04d..b05ae7ffea87 100644
--- a/lib/plist.c
+++ b/lib/plist.c
@@ -142,65 +142,6 @@ void plist_del(struct plist_node *node, struct plist_head *head)
plist_check_head(head);
}
-/**
- * plist_requeue - Requeue @node at end of same-prio entries.
- *
- * This is essentially an optimized plist_del() followed by
- * plist_add(). It moves an entry already in the plist to
- * after any other same-priority entries.
- *
- * @node: &struct plist_node pointer - entry to be moved
- * @head: &struct plist_head pointer - list head
- */
-void plist_requeue(struct plist_node *node, struct plist_head *head)
-{
- struct plist_node *iter;
- struct list_head *node_next = &head->node_list;
-
- plist_check_head(head);
- BUG_ON(plist_head_empty(head));
- BUG_ON(plist_node_empty(node));
-
- if (node == plist_last(head))
- return;
-
- iter = plist_next(node);
-
- if (node->prio != iter->prio)
- return;
-
- plist_del(node, head);
-
- /*
- * After plist_del(), iter is the replacement of the node. If the node
- * was on prio_list, take shortcut to find node_next instead of looping.
- *
- * prio_list is a headless ring, so from the LAST bucket ->next wraps
- * round to the first one; in that case node_next is the list head.
- */
- if (!list_empty(&iter->prio_list)) {
- struct plist_node *first = plist_first(head);
-
- if (iter->prio_list.next == &first->prio_list)
- goto queue;
- iter = list_entry(iter->prio_list.next, struct plist_node,
- prio_list);
- node_next = &iter->node_list;
- goto queue;
- }
-
- plist_for_each_continue(iter, head) {
- if (node->prio != iter->prio) {
- node_next = &iter->node_list;
- break;
- }
- }
-queue:
- list_add_tail(&node->node_list, node_next);
-
- plist_check_head(head);
-}
-
#ifdef CONFIG_DEBUG_PLIST
#include <linux/sched.h>
#include <linux/sched/clock.h>
@@ -238,14 +179,6 @@ static void __init plist_test_check(int nr_expect)
BUG_ON(prio_pos->prio_list.next != &first->prio_list);
}
-static void __init plist_test_requeue(struct plist_node *node)
-{
- plist_requeue(node, &test_head);
-
- if (node != plist_last(&test_head))
- BUG_ON(node->prio == plist_next(node)->prio);
-}
-
static int __init plist_test(void)
{
int nr_expect = 0, i, loop;
@@ -269,10 +202,6 @@ static int __init plist_test(void)
nr_expect--;
}
plist_test_check(nr_expect);
- if (!plist_node_empty(test_node + i)) {
- plist_test_requeue(test_node + i);
- plist_test_check(nr_expect);
- }
}
for (i = 0; i < ARRAY_SIZE(test_node); i++) {
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 15/18] mm/swap: reuse per-CPU queue readers across priority changes
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (13 preceding siblings ...)
2026-11-14 14:51 ` [RFC PATCH v4 14/18] lib/plist.c: remove requeue function Lian Wang
@ 2026-11-14 14:52 ` Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 16/18] mm/swap: advance the global queue cursor directly between priority rings Lian Wang
` (2 subsequent siblings)
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:52 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
Inserting a new priority allocates a replacement per-CPU reader array
even when earlier ring removal left enough storage. An unnecessary
allocation can add swapon work or fail despite sufficient capacity.
Track the installed reader capacity and retain its high-water mark
until the queue becomes empty. Reuse existing storage when the new ring
fits, initializing every valid ring index for the new topology. Prepare
larger storage outside the writer only when growth is required and
recheck capacity under the writer before publishing.
Preserve the existing prepare/install ownership and failure rollback
protocol. Device removal continues to avoid per-CPU shrinking allocation
under the writer.
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 28 ++++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index bd098114ee4b..5e5c1aef010f 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -202,6 +202,8 @@ struct swap_queue_cursor {
};
static __percpu struct swap_queue_reader *swap_queue_readers;
+/* Keep the high-water capacity until the last priority ring is removed. */
+static unsigned int swap_queue_readers_capacity;
static inline bool swap_device_masked(struct swap_info_struct *si)
{
@@ -241,7 +243,7 @@ static bool swap_queue_needs_readers(int prio)
return false;
}
- return true;
+ return swap_queue_len >= swap_queue_readers_capacity;
}
/*
@@ -289,13 +291,15 @@ static void swap_queue_reset_readers(void)
}
}
-static void swap_queue_install_readers(struct swap_queue_reader __percpu *readers)
+static void swap_queue_install_readers(struct swap_queue_reader __percpu *readers,
+ unsigned int capacity)
{
int cpu;
lockdep_assert_held_write(&swapon_rwsem);
free_percpu(swap_queue_readers);
swap_queue_readers = readers;
+ swap_queue_readers_capacity = capacity;
if (!readers)
return;
@@ -432,7 +436,8 @@ static int swap_queue_add(struct swap_info_struct *si,
}
/* No ring at this priority: insert a new one at pos. */
- if (!*readers || nr_readers <= swap_queue_len)
+ if (swap_queue_len >= swap_queue_readers_capacity &&
+ (!*readers || nr_readers <= swap_queue_len))
goto failed;
new_queue = kmalloc_array(swap_queue_len + 1, sizeof(*swap_queue), gfp);
if (!new_queue)
@@ -460,8 +465,12 @@ static int swap_queue_add(struct swap_info_struct *si,
memalloc_noreclaim_restore(noreclaim_flag);
kfree(old_queue);
- swap_queue_install_readers(*readers);
- *readers = NULL;
+ if (swap_queue_len > swap_queue_readers_capacity) {
+ swap_queue_install_readers(*readers, nr_readers);
+ *readers = NULL;
+ } else {
+ swap_queue_reset_readers();
+ }
return 0;
add_to_ring:
@@ -504,10 +513,9 @@ static void swap_queue_del(struct swap_info_struct *si)
}
/*
- * Shrinking the small topology arrays is optional. Keep reader storage
- * until a new priority is inserted or the queue is empty: per-CPU
- * allocation must not run under this writer's noreclaim scope, which
- * can grant backing pages reserve access.
+ * Shrinking the small topology arrays is optional. Keep reader capacity
+ * until the queue is empty: per-CPU allocation must not run under this
+ * writer's noreclaim scope, which can grant backing pages reserve access.
*/
gfp = GFP_NOIO | __GFP_HIGH | __GFP_NOMEMALLOC;
noreclaim_flag = memalloc_noreclaim_save();
@@ -550,7 +558,7 @@ static void swap_queue_del(struct swap_info_struct *si)
swap_queue = NULL;
}
if (!swap_queue_len)
- swap_queue_install_readers(NULL);
+ swap_queue_install_readers(NULL, 0);
}
spin_unlock(&swap_queue_update_lock);
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 16/18] mm/swap: advance the global queue cursor directly between priority rings
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (14 preceding siblings ...)
2026-11-14 14:52 ` [RFC PATCH v4 15/18] mm/swap: reuse per-CPU queue readers across priority changes Lian Wang
@ 2026-11-14 14:52 ` Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 17/18] mm/swap: skip reader rotation for single-device " Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 18/18] mm/swap: redistribute queue readers after membership changes Lian Wang
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:52 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
The allocation walk previously passed a total retry index to the selector.
Each retry translated it back to a priority ring by scanning from the
beginning of swap_queue. Keep the ring index and the number of attempted
peers in the task-local cursor instead.
Snapshot the starting device only on the first visit to each ring and
advance each peer exactly once, including masked entries. Preserve the
large-folio same-ring boundary and the stale reader-offset correction.
The selector's complete walk is linear in the devices and priority groups;
device cluster scanning and availability updates have their own costs.
Keep the topology read lock and CPU migration protection across the walk.
The global selector uses only existing priorities and adds no per-memcg
device-selection state or ABI. Existing swap charging and limits remain
in folio_alloc_swap(), whose failure path classifies allocation errors.
The walk no longer returns an ignored internal error to that caller.
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 51 ++++++++++++++++++++++++---------------------------
1 file changed, 24 insertions(+), 27 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5e5c1aef010f..616339c5ae5b 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -195,10 +195,10 @@ struct swap_queue_reader {
};
struct swap_queue_cursor {
- bool valid;
bool ring_only;
unsigned int ring_idx;
unsigned int offset;
+ unsigned int nr_iter;
};
static __percpu struct swap_queue_reader *swap_queue_readers;
@@ -308,28 +308,32 @@ static void swap_queue_install_readers(struct swap_queue_reader __percpu *reader
swap_queue_reset_readers();
}
-static struct swap_info_struct *swap_queue_get_device(long nr_alloc, int nr_iter,
- struct swap_queue_cursor *cursor)
+static struct swap_info_struct *
+swap_queue_get_device(long nr_alloc, struct swap_queue_cursor *cursor)
{
bool rotate = false;
struct swap_info_struct *si;
struct swap_ring_iterator *ri;
struct swap_prio_ring *ring;
- unsigned int dev_idx, queue_idx;
+ unsigned int dev_idx, queue_idx, nr_iter;
- if (!swap_queue_len)
+ queue_idx = cursor->ring_idx;
+ if (queue_idx >= swap_queue_len)
return ERR_PTR(-ENOENT);
- queue_idx = 0;
- while (nr_iter >= swap_queue[queue_idx]->size) {
- nr_iter -= swap_queue[queue_idx]->size;
- if (++queue_idx >= swap_queue_len)
- return ERR_PTR(cursor->ring_only ? -E2BIG : -ENOENT);
- }
- if (cursor->ring_only && cursor->ring_idx != queue_idx)
- return ERR_PTR(-E2BIG);
-
ring = swap_queue[queue_idx];
+ if (cursor->nr_iter >= ring->size) {
+ if (cursor->ring_only)
+ return ERR_PTR(-E2BIG);
+ queue_idx = ++cursor->ring_idx;
+ if (queue_idx >= swap_queue_len)
+ return ERR_PTR(-ENOENT);
+ cursor->nr_iter = 0;
+ ring = swap_queue[queue_idx];
+ }
+
+ /* Consume masked peers too, so every walk remains bounded. */
+ nr_iter = cursor->nr_iter++;
local_lock(&swap_queue_readers->lock);
ri = this_cpu_ptr(&swap_queue_readers->ri[queue_idx]);
/*
@@ -337,9 +341,7 @@ static struct swap_info_struct *swap_queue_get_device(long nr_alloc, int nr_iter
* iterator can move between retries, but the cursor must visit every
* device in the ring exactly once before falling through.
*/
- if (!cursor->valid || cursor->ring_idx != queue_idx) {
- cursor->valid = true;
- cursor->ring_idx = queue_idx;
+ if (!nr_iter) {
if (ri->rr_counter < nr_alloc)
rotate = true;
else if (ri->offset >= ring->size)
@@ -1847,29 +1849,25 @@ static bool get_swap_device_info(struct swap_info_struct *si)
return true;
}
-static int swap_alloc_entry(struct folio *folio)
+static void swap_alloc_entry(struct folio *folio)
{
struct swap_queue_cursor cursor = {};
long nr_pages = folio_nr_pages(folio);
struct swap_info_struct *si;
- int nr_iter, ret;
percpu_down_read(&swapon_rwsem);
migrate_disable();
- for (nr_iter = 0;; nr_iter++) {
- si = swap_queue_get_device(nr_pages, nr_iter, &cursor);
+ for (;;) {
+ si = swap_queue_get_device(nr_pages, &cursor);
if (IS_ERR(si)) {
- ret = PTR_ERR(si);
- if (ret == -EBUSY)
+ if (PTR_ERR(si) == -EBUSY)
continue;
break;
}
cluster_alloc_swap_entry(si, folio);
- if (folio_test_swapcache(folio)) {
- ret = 0;
+ if (folio_test_swapcache(folio))
break;
- }
/*
* For large allocations, try every device at the same priority,
@@ -1884,7 +1882,6 @@ static int swap_alloc_entry(struct folio *folio)
migrate_enable();
percpu_up_read(&swapon_rwsem);
- return ret;
}
/*
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 17/18] mm/swap: skip reader rotation for single-device priority rings
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (15 preceding siblings ...)
2026-11-14 14:52 ` [RFC PATCH v4 16/18] mm/swap: advance the global queue cursor directly between priority rings Lian Wang
@ 2026-11-14 14:52 ` Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 18/18] mm/swap: redistribute queue readers after membership changes Lian Wang
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:52 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
A priority ring with one device cannot benefit from rotating its starting
offset or maintaining a quota. Read its only entry directly while the
caller continues to hold the topology read lock and migration protection.
Consume the cursor attempt and check the full/disabled pointer tag through
the common path. A masked singleton therefore terminates or falls through
with the same bounded behavior as a multi-device ring. Keep the existing
large-folio same-priority failure boundary.
This removes reader-local state updates for the common single-device case
without bypassing device lifetime or availability checks.
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 616339c5ae5b..0c192844b121 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -334,6 +334,11 @@ swap_queue_get_device(long nr_alloc, struct swap_queue_cursor *cursor)
/* Consume masked peers too, so every walk remains bounded. */
nr_iter = cursor->nr_iter++;
+ if (ring->size == 1) {
+ si = READ_ONCE(ring->dev[0]);
+ goto check_device;
+ }
+
local_lock(&swap_queue_readers->lock);
ri = this_cpu_ptr(&swap_queue_readers->ri[queue_idx]);
/*
@@ -363,6 +368,7 @@ swap_queue_get_device(long nr_alloc, struct swap_queue_cursor *cursor)
si = READ_ONCE(ring->dev[dev_idx]);
local_unlock(&swap_queue_readers->lock);
+check_device:
if (swap_device_masked(si))
return ERR_PTR(-EBUSY);
^ permalink raw reply related [flat|nested] 19+ messages in thread
* [RFC PATCH v4 18/18] mm/swap: redistribute queue readers after membership changes
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
` (16 preceding siblings ...)
2026-11-14 14:52 ` [RFC PATCH v4 17/18] mm/swap: skip reader rotation for single-device " Lian Wang
@ 2026-11-14 14:52 ` Lian Wang
17 siblings, 0 replies; 19+ messages in thread
From: Lian Wang @ 2026-11-14 14:52 UTC (permalink / raw)
To: linux-mm
Cc: linux-kernel, Andrew Morton, Chris Li, Nhat Pham, Baoquan He,
Barry Song, Youngjun Park, Kemeng Shi, Kairui Song, Jihan LIN,
Kunwu Chan, Kees Cook, Gustavo A. R. Silva, Thomas Gleixner,
linux-hardening
Adding a device to an existing priority ring reuses the per-CPU reader
storage. A ring that previously contained one device can therefore keep
every CPU at the old common starting offset after peers are added.
Deleting devices or rings can likewise leave offsets and quotas that
describe the previous membership.
Reset the valid per-CPU ring offsets and quotas after peer insertion and
every nonempty deletion. Run the initialization helper under the topology
writer, which excludes allocation walks, without allocating new reader
storage or adding selector hot-path work.
Use cpu-id modulo placement for a fresh best-effort rotation across all
valid rings. This does not move existing slots or guarantee strict balance
for every active CPU subset. Mask and availability transitions do not
trigger the topology reset.
Signed-off-by: Lian Wang <lianux.mm@gmail.com>
---
mm/swapfile.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 0c192844b121..eb2128132455 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -497,6 +497,7 @@ static int swap_queue_add(struct swap_info_struct *si,
spin_unlock(&swap_queue_update_lock);
memalloc_noreclaim_restore(noreclaim_flag);
kfree(old_ring);
+ swap_queue_reset_readers();
return 0;
failed:
@@ -569,6 +570,8 @@ static void swap_queue_del(struct swap_info_struct *si)
swap_queue_install_readers(NULL, 0);
}
spin_unlock(&swap_queue_update_lock);
+ if (swap_queue_len)
+ swap_queue_reset_readers();
kfree(old_ring);
kfree(old_queue);
^ permalink raw reply related [flat|nested] 19+ messages in thread
end of thread, other threads:[~2026-11-14 14:53 UTC | newest]
Thread overview: 19+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-14 14:47 [RFC PATCH v4 00/18] mm/swap: introduce priority queues for swap device allocation Lian Wang
2026-11-14 14:48 ` [RFC PATCH v4 01/18] mm/swap: remove unused parameter for reading swap header Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 02/18] mm/swap: clean up hibernation error handling Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 03/18] mm/swap: cleanup and document swap device availability flag usage Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 04/18] mm/swap: introduce swap device iteration helper Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 05/18] mm/swap: change the swapon lock into a percpu rwsem Lian Wang
2026-11-14 14:49 ` [RFC PATCH v4 06/18] mm/swap: serialize swap publication before removing swapon mutex Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 07/18] mm/swap: consolidate swap inuse accounting helpers Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 08/18] mm/swap: change back to use each swap device's percpu cluster Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 09/18] mm/swap: avoid direct reclaim when populating swap cluster metadata Lian Wang
2026-11-14 14:50 ` [RFC PATCH v4 10/18] mm/swap: add priority queue for swap device allocation Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 11/18] mm/swap: remove available list Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 12/18] mm/swap: bound synchronous discard outside the allocation reader Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 13/18] mm/swap: drop swap active plist Lian Wang
2026-11-14 14:51 ` [RFC PATCH v4 14/18] lib/plist.c: remove requeue function Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 15/18] mm/swap: reuse per-CPU queue readers across priority changes Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 16/18] mm/swap: advance the global queue cursor directly between priority rings Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 17/18] mm/swap: skip reader rotation for single-device " Lian Wang
2026-11-14 14:52 ` [RFC PATCH v4 18/18] mm/swap: redistribute queue readers after membership changes Lian Wang
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.