[RESEND RFC PATCH v2 03/13] mm/swap: cleanup and document swap device availability flag usage
From: Lian Wang (ProcessMission)
Date: Sat Aug 29 2026 - 03:49:43 EST
From: Kairui Song <kasong@xxxxxxxxxxx>
Rework swap device flag and metadata handling and swapon/swapoff to
be cleaner and better documented, in preparation for locking cleanup.
Consolidate existing routines and introduce swap_device_enable() and
swap_device_disable() as the clean boundary of exposing or isolating a
swap device. swap_device_enable() sets the proper flags and exposes the
device as an allocation candidate, while swap_device_disable() clears
related flags and ensures no more allocations will happen.
Keep the mapping lookup and disable transition in the same swap_lock
critical section. Otherwise, a concurrent swapoff can release the
selected slot and swapon can reuse it for a different device before the
first syscall disables the swap_info_struct it found. Drain the cluster
allocators in the same helper after dropping swap_lock, so the identity
transition remains atomic without holding the lock over all cluster
locks.
Add comment blocks documenting the lifetime and locking rules for swap
device flags and their locking conventions.
Apart from closing that lifecycle race, the remaining changes are code
rearrangement and documentation.
Signed-off-by: Kairui Song <kasong@xxxxxxxxxxx>
Co-developed-by: Lian Wang (ProcessMission) <lianux.mm@xxxxxxxxx>
Signed-off-by: Lian Wang (ProcessMission) <lianux.mm@xxxxxxxxx>
Tested-by: Kunwu Chan <kunwu.chan@xxxxxxxxx>
---
include/linux/swap.h | 27 ++++--
mm/swapfile.c | 207 ++++++++++++++++++++-----------------------
2 files changed, 115 insertions(+), 119 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 5658a1634b85..d9e535cd07c5 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -194,6 +194,22 @@ 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). The 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 both swap_lock and swap_avail_lock, clearing
+ * this flag also waits 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? */
@@ -262,14 +278,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 46772d0e3e68..d7115b9195a6 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1199,28 +1199,20 @@ 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 {
+ atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages);
}
plist_del(&si->avail_list, &swap_avail_head);
@@ -1230,21 +1222,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;
@@ -1300,7 +1292,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,
@@ -1354,7 +1346,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;
@@ -2977,58 +2969,87 @@ 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)
+/*
+ * 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)
{
- atomic_long_add(si->pages, &nr_swap_pages);
- total_swap_pages += si->pages;
+ spin_lock(&swap_lock);
- assert_spin_locked(&swap_lock);
+ spin_lock(&swap_avail_lock);
+ si->flags |= SWP_WRITEOK;
+ spin_unlock(&swap_avail_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 back to available list */
- add_to_avail_list(si, true);
+ add_to_avail_list(si);
}
-/*
- * 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.
- */
-static void enable_swap_info(struct swap_info_struct *si)
+static int swap_device_disable(struct address_space *mapping,
+ struct swap_info_struct **swap_info)
{
- percpu_ref_resurrect(&si->users);
- spin_lock(&swap_lock);
- spin_lock(&si->lock);
- _enable_swap_info(si);
- spin_unlock(&si->lock);
- spin_unlock(&swap_lock);
-}
+ struct swap_info_struct *si;
+ struct swap_cluster_info *ci;
+ unsigned long offset, end;
+ int err = -EINVAL;
-static void reinsert_swap_info(struct swap_info_struct *si)
-{
spin_lock(&swap_lock);
- spin_lock(&si->lock);
- _enable_swap_info(si);
- spin_unlock(&si->lock);
- spin_unlock(&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;
-/*
- * 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;
+ /*
+ * Refuse swapoff while the device is pinned for hibernation.
+ */
+ if (si->flags & SWP_HIBERNATION) {
+ err = -EBUSY;
+ goto unlock;
+ }
- BUG_ON(si->flags & SWP_WRITEOK);
+ if (security_vm_enough_memory_mm(current->mm, si->pages)) {
+ err = -ENOMEM;
+ goto unlock;
+ }
+ vm_unacct_memory(si->pages);
+ spin_lock(&swap_avail_lock);
+ si->flags &= ~SWP_WRITEOK;
+ spin_unlock(&swap_avail_lock);
+
+ plist_del(&si->list, &swap_active_head);
+ total_swap_pages -= si->pages;
+ atomic_long_sub(si->pages, &nr_swap_pages);
+
+ end = ALIGN(si->max, SWAPFILE_CLUSTER);
+unlock:
+ spin_unlock(&swap_lock);
+ if (err)
+ return err;
+
+ del_from_avail_list(si, true);
+
+ /*
+ * 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,
@@ -3073,7 +3094,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;
@@ -3082,7 +3102,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;
@@ -3095,44 +3115,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);
@@ -3140,8 +3127,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;
}
/*
@@ -3201,13 +3188,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
@@ -3629,7 +3613,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))
@@ -3794,7 +3778,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,
--
2.55.0