Skip to contentSkip to conversation

mm, swap: introduce swap table

PATCH In review Kairui Song 5 participants 56 messages May 14, 2025 – Jun 20, 2025

Subsystems (19)
Memory management MM core s390 MGLRU +15 more subsystems
lore
Share & tools
lore
#0 lore raw

cover mm, swap: introduce swap table

From: Kairui Song <[email protected]>

This is the series that implements the Swap Table idea propose in the LSF/MM/BPF topic "Integrate swap cache, swap maps with swap allocator" about one month ago [1].

With this series, swap subsystem will have a ~20-30% performance gain from basic sequential swap to heavy workloads, for both 4K and mTHP folios. The idle memory usage is already much lower, the average memory consumption is still the same or will also be even lower (with further works). And this enables many more future optimizations, with better defined swap operations.

This series is stable and mergeable on both mm-unstable and mm-stable. It's a long series so it might be challenging to review though. It has been working well with many stress tests.

You can also find the latest branch here:
https://github.com/ryncsn/linux/tree/kasong/devel/swap-table

With swap table, a table entry will be the fundamental and the only needed data structure for: swap cache, swap map, swap cgroup map. Reducing the memory usage and making the performance better, also provide more flexibility and better abstraction.

/*
* Swap table entry type and bit layouts:
*

 * NULL:     | ------------    0   -------------|
 * Shadow:   | SWAP_COUNT |---- SHADOW_VAL ---|1|
 * Folio:    | SWAP_COUNT |------ PFN -------|10|
 * Pointer:  |----------- Pointer ----------|100|

*/

This series contains many clean up and refractors due to many historical issues with the SWAP subsystem, e.g. the fuzzy workflow and definition of how swap entries are handled, and a lot of corner issues as mentioned in the LSF/MM/BPF talk.

There could be temporary increase of complicity or memory consumption in the middle of this series, but in the end it's a much simplified and sanitized in the end. And these patches have dependency on each other due to the current complex swap design. So this takes a long series.

This series cleaned up most of the issues and improved the situation with following order:
- Simplification and optimizations (Patch 1 - 3)
- Tidy up swap info and cache lookup (Patch 4 - 6)
- Introduce basic swap table infrastructure (Patch 7 - 8)
- Removed swap cache bypassing with SWP_SYNCHRONOUS_IO, enabling mTHP for more workloads (Patch 9 - 14).
- Simplify swap in synchronization with swap cache, eliminating long tailing issues and improve performance, swap can be synced with folio lock now (Patch 15 - 16).
- Make most swap operations into folio based. We now can use folio based helpers that ensures the swap entries are stable with folio lock, which also make more optimization and sanity checks doable. (Patch 17 - 18)
- Removed SWAP_HAS_CACHE. (Patch 19 - 22)
- Completely rework the swap counting using swap table, and remove COUNT_CONTINUED (Patch 23 - 27).
- Dynamic reclaim and allocation for swap table (Patch 28)

And the performance is looking great too:

vm-scalability usemem shows a great improvement:
Test using: usemem --init-time -O -y -x -n 31 1G (1G memcg, pmem as swap)

                Before:         After:
System time:    217.39s         161.59s      (-25.67%)
Throughput:     3933.58 MB/s    4975.55 MB/s (+26.48%)

(Similar results with random usemem -R)

Build kernel with defconfig on tmpfs with ZRAM:
Below results shows a test matrix using different memory cgroup limit and job numbers.

  make -j<NR>|  Total Sys Time (seconds) |   Total Time (seconds)
  (NR / Mem )|  (Before / After / Delta) | (Before / After / Delta)
  With 4k pages only:
   6 / 192M  |    5327 /  3915 / -26.5%  |    1427 /  1141 / -20.0%
  12 / 256M  |    5373 /  4009 / -25.3%  |     743 /   606 / -18.4%
  24 / 384M  |    6149 /  4523 / -26.4%  |     438 /   353 / -19.4%
  48 / 768M  |    7285 /  4521 / -37.9%  |     251 /   190 / -24.3%
  With 64k mTHP:
  24 / 512M  |    4399 /  3328 / -24.3%  |     345 /   289 / -16.2%
  48 /   1G  |    5072 /  3406 / -32.8%  |     187 /   150 / -19.7%

Memory usage is also reduced. Although this series haven't remove the swap cgroup array yet, the peak usage of one swap entry is already reduced from 12 bytes to 10 bytes. And the swap table is dynamically allocated which means the idle memory usage will be reduced by a lot.

Some other high lights and notes:

1. This series introduce a set of helpers "folio_alloc_swap", "folio_dup_swap", "folio_put_swap", "folio_free_swap*" to make most swap operations folio based, this should brought a clean border between swap and rest of mm. Also split the hibernation swap entry allocation out of ordinary swap operations.

3. This series enabled mTHP swap-in, and read ahead skipping for more workloads, as it removed the swap cache bypassing path:

We currently only do mTHP swap in and read ahead bypass for SWP_SYNCHRONOUS_IO device only when swap count of all related entries are equal to one. This makes no sense, clearly read ahead and mTHP behaviour should have nothing to do with swap count, it's only a defect due to current design that they are coupled with swap cache bypassing. This series removed that limitation while showing a major performance improvement.

This not only showed a performance gain, also should reduce mTHP fragmentation.

4. By removing the old swap cache design, now all swap cache are protected by fine grained cluster lock, this also removed the cluster shuffle algorithm should improve the performance for SWAP on HDD too (Fixing [4]). And also got rid of the many swap address_space instance design.

5. I dropped some future doable optimizations for now, e.g. the folio based helper will be an essential part for dropping the swap cgroup control map, which will improve the performance and reduce memory usage even more. It could be done later. And more folio batched operations could be done based on this. So this series is not in the best shape but already looking good enough.

Future work items:

1. More tests, and maybe some of the patches need to be split into smaller ones or need a few preparation series.

2. Integrate with Nhat Pham's Virtual swap space [2], while this series improves the performance and added a sanitized workload for SWAP, nothing changed feature wise. The swap table idea is suppose to be able to handle things like a virtual device in a cleaner way with both lower overhead and better flexibility, more work is needed to figure out a way to implement it.

3. Some helpers from this series could be very helpful for future works. E.g. the folio based swap helpers, now locking a folio will stabilize its swap entries, which could also be used to stabilize the under layer swap device's entries if a virtual device design is implemented, hence simplify the locking design. Also more entry types could be added for things like zero map or shmem.

4. The unified swap in path now already enabled mTHP swap in for swap count > 1 entries. This also making unify the read ahead of shmem / anon (as demostrated a year ago [3], that one conflicted with the standalone mTHP swapin path but now it's unified) doable now.

We can also implement a read ahead based mTHP swapin based on this too. This needs more discussion.

Kairui Song (27):
mm, swap: don't scan every fragment cluster
mm, swap: consolidate the helper for mincore
mm, swap: split readahead update out of swap cache lookup mm, swap: sanitize swap cache lookup convention
mm, swap: rearrange swap cluster definition and helpers mm, swap: tidy up swap device and cluster info helpers mm, swap: use swap table for the swap cache and switch API mm/swap: rename __read_swap_cache_async to __swapin_cache_alloc mm, swap: add a swap helper for bypassing only read ahead mm, swap: clean up and consolidate helper for mTHP swapin check mm, swap: never bypass the swap cache for SWP_SYNCHRONOUS_IO mm/shmem, swap: avoid redundant Xarray lookup during swapin mm/shmem: never bypass the swap cache for SWP_SYNCHRONOUS_IO mm, swap: split locked entry freeing into a standalone helper mm, swap: use swap cache as the swap in synchronize layer mm, swap: sanitize swap entry management workflow
mm, swap: rename and introduce folio_free_swap_cache mm, swap: clean up and improve swap entries batch freeing mm, swap: check swap table directly for checking cache mm, swap: add folio to swap cache directly on allocation mm, swap: drop the SWAP_HAS_CACHE flag
mm, swap: remove no longer needed _swap_info_get
mm, swap: implement helpers for reserving data in swap table mm/workingset: leave highest 8 bits empty for anon shadow mm, swap: minor clean up for swapon
mm, swap: use swap table to track swap count
mm, swap: implement dynamic allocation of swap table

Nhat Pham (1):
mm/shmem, swap: remove SWAP_MAP_SHMEM

 arch/s390/mm/pgtable.c |    2 +-
 include/linux/swap.h   |  119 +--
 kernel/power/swap.c    |    8 +-
 mm/filemap.c           |   20 +-
 mm/huge_memory.c       |   20 +-
 mm/madvise.c           |    2 +-
 mm/memory-failure.c    |    2 +-
 mm/memory.c            |  384 ++++-----
 mm/migrate.c           |   28 +-
 mm/mincore.c           |   49 +-
 mm/page_io.c           |   12 +-
 mm/rmap.c              |    7 +-
 mm/shmem.c             |  204 ++---
 mm/swap.h              |  316 ++++++--
 mm/swap_state.c        |  646 ++++++++-------
 mm/swap_table.h        |  231 ++++++
 mm/swapfile.c          | 1708 +++++++++++++++++-----------------------
 mm/userfaultfd.c       |    9 +-
 mm/vmscan.c            |   22 +-
 mm/workingset.c        |   39 +-
 mm/zswap.c             |   13 +-
 21 files changed, 1981 insertions(+), 1860 deletions(-)
 create mode 100644 mm/swap_table.h
Signature · 1 line
2.49.0
#1 lore raw

PATCH 1/28 mm, swap: don't scan every fragment cluster

From: Kairui Song <[email protected]>

Fragment clusters were failing high order allocation already, the reason we scan it now is that a swap entry may get freed without releasing the cache so a swap map entry will end up in HAS_CACHE only status and the cluster won't be moved back to non-full or free cluster list.

The chance is low and only happens with the device usage is low (!vm_swap_full()). This is especially unhelpful for SWP_SYNCHRONOUS_IO devices as swap cache almost always gets freed when count reaches zero for these device.

And besides, high order allocation failure isn't a critical issue. Having the scan actually slow down mTHP allocation by a lot when the fragment cluster list is long.

The HAS_CACHE issue will be fixed in a proper way later, so drop this fragment cluster scanning design.

---
 include/linux/swap.h |  1 -
 mm/swapfile.c        | 32 +++++++++-----------------------
 2 files changed, 9 insertions(+), 24 deletions(-)
include/linux/swap.h +0 -1
index bc0e1c275fc0..817e427a47d2 100644
@@ -310,7 +310,6 @@ struct swap_info_struct {
 					/* list of cluster that contains at least one free slot */
 	struct list_head frag_clusters[SWAP_NR_ORDERS];
 					/* list of cluster that are fragmented or contented */
-	atomic_long_t frag_cluster_nr[SWAP_NR_ORDERS];
 	unsigned int pages;		/* total of usable pages of swap */
 	atomic_long_t inuse_pages;	/* number of those currently in use */
 	struct swap_sequential_cluster *global_cluster; /* Use one global cluster for rotating device */
mm/swapfile.c +9 -23
index 026090bf3efe..34188714479f 100644
@@ -470,11 +470,6 @@ static void move_cluster(struct swap_info_struct *si,
 	else
 		list_move_tail(&ci->list, list);
 	spin_unlock(&si->lock);
-
-	if (ci->flags == CLUSTER_FLAG_FRAG)
-		atomic_long_dec(&si->frag_cluster_nr[ci->order]);
-	else if (new_flags == CLUSTER_FLAG_FRAG)
-		atomic_long_inc(&si->frag_cluster_nr[ci->order]);
 	ci->flags = new_flags;
 }
 
@@ -926,32 +921,25 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o
 		swap_reclaim_full_clusters(si, false);
 
 	if (order < PMD_ORDER) {
-		unsigned int frags = 0, frags_existing;
-
 		while ((ci = isolate_lock_cluster(si, &si->nonfull_clusters[order]))) {
 			found = alloc_swap_scan_cluster(si, ci, cluster_offset(si, ci),
 							order, usage);
 			if (found)
 				goto done;
-			/* Clusters failed to allocate are moved to frag_clusters */
-			frags++;
 		}
 
-		frags_existing = atomic_long_read(&si->frag_cluster_nr[order]);
-		while (frags < frags_existing &&
-		       (ci = isolate_lock_cluster(si, &si->frag_clusters[order]))) {
-			atomic_long_dec(&si->frag_cluster_nr[order]);
-			/*
-			 * Rotate the frag list to iterate, they were all
-			 * failing high order allocation or moved here due to
-			 * per-CPU usage, but they could contain newly released
-			 * reclaimable (eg. lazy-freed swap cache) slots.
-			 */
+		/*
+		 * Scan only one fragment cluster is good enough. Order 0
+		 * allocation will surely success, and mTHP allocation failure
+		 * is not critical, and scanning one cluster still keeps the
+		 * list rotated and scanned (for reclaiming HAS_CACHE).
+		 */
+		ci = isolate_lock_cluster(si, &si->frag_clusters[order]);
+		if (ci) {
 			found = alloc_swap_scan_cluster(si, ci, cluster_offset(si, ci),
-							order, usage);
+					order, usage);
 			if (found)
 				goto done;
-			frags++;
 		}
 	}
 
@@ -973,7 +961,6 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o
 		 * allocation, but reclaim may drop si->lock and race with another user.
 		 */
 		while ((ci = isolate_lock_cluster(si, &si->frag_clusters[o]))) {
-			atomic_long_dec(&si->frag_cluster_nr[o]);
 			found = alloc_swap_scan_cluster(si, ci, cluster_offset(si, ci),
 							0, usage);
 			if (found)
@@ -3234,7 +3221,6 @@ static struct swap_cluster_info *setup_clusters(struct swap_info_struct *si,
 	for (i = 0; i < SWAP_NR_ORDERS; i++) {
 		INIT_LIST_HEAD(&si->nonfull_clusters[i]);
 		INIT_LIST_HEAD(&si->frag_clusters[i]);
-		atomic_long_set(&si->frag_cluster_nr[i], 0);
 	}
 
 	/*
Signature · 1 line
2.49.0
#2 lore raw

PATCH 2/28 mm, swap: consolidate the helper for mincore

In reply to Kairui Song

From: Kairui Song <[email protected]>

mincore related logics are not used by any one else, so consolidate and move it to mincore only to simplify the code.

---
 mm/mincore.c    | 50 +++++++++++++++++++++++++++++++++++++++----------
 mm/swap.h       | 10 ----------
 mm/swap_state.c | 38 -------------------------------------
 3 files changed, 40 insertions(+), 58 deletions(-)
mm/mincore.c +40 -10
index 42d6c9c8da86..7ee88113d44c 100644
@@ -44,6 +44,36 @@ static int mincore_hugetlb(pte_t *pte, unsigned long hmask, unsigned long addr,
 	return 0;
 }
 
+static unsigned char mincore_swap(swp_entry_t entry)
+{
+	struct swap_info_struct *si;
+	struct folio *folio = NULL;
+	unsigned char present = 0;
+
+	/* There might be swapin error entries in shmem mapping. */
+	if (non_swap_entry(entry))
+		return 0;
+
+	if (!IS_ENABLED(CONFIG_SWAP)) {
+		WARN_ON_ONCE(1);
+		return 1;
+	}
+
+	/* Prevent swap device to being swapoff under us */
+	si = get_swap_device(entry);
+	if (si) {
+		folio = filemap_get_folio(swap_address_space(entry),
+					  swap_cache_index(entry));
+		put_swap_device(si);
+	}
+	if (folio) {
+		present = folio_test_uptodate(folio);
+		folio_put(folio);
+	}
+
+	return present;
+}
+
 /*
  * Later we can get more picky about what "in core" means precisely.
  * For now, simply check to see if the page is in the page cache,
@@ -61,8 +91,15 @@ static unsigned char mincore_page(struct address_space *mapping, pgoff_t index)
 	 * any other file mapping (ie. marked !present and faulted in with
 	 * tmpfs's .fault). So swapped out tmpfs mappings are tested here.
 	 */
-	folio = filemap_get_incore_folio(mapping, index);
-	if (!IS_ERR(folio)) {
+	folio = filemap_get_entry(mapping, index);
+	if (folio) {
+		if (xa_is_value(folio)) {
+			if (shmem_mapping(mapping))
+				return mincore_swap(radix_to_swp_entry(folio));
+			else
+				return 0;
+		}
+
 		present = folio_test_uptodate(folio);
 		folio_put(folio);
 	}
@@ -141,7 +178,6 @@ static int mincore_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 				vec[i] = 1;
 		} else { /* pte is a swap entry */
 			swp_entry_t entry = pte_to_swp_entry(pte);
-
 			if (non_swap_entry(entry)) {
 				/*
 				 * migration or hwpoison entries are always
@@ -149,13 +185,7 @@ static int mincore_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 				 */
 				*vec = 1;
 			} else {
-#ifdef CONFIG_SWAP
-				*vec = mincore_page(swap_address_space(entry),
-						    swap_cache_index(entry));
-#else
-				WARN_ON(1);
-				*vec = 1;
-#endif
+				*vec = mincore_swap(entry);
 			}
 		}
 		vec += step;
mm/swap.h +0 -10
index 521bf510ec75..4f85195ab83d 100644
@@ -62,9 +62,6 @@ void clear_shadow_from_swap_cache(int type, unsigned long begin,
 void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr);
 struct folio *swap_cache_get_folio(swp_entry_t entry,
 		struct vm_area_struct *vma, unsigned long addr);
-struct folio *filemap_get_incore_folio(struct address_space *mapping,
-		pgoff_t index);
-
 struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct vm_area_struct *vma, unsigned long addr,
 		struct swap_iocb **plug);
@@ -156,13 +153,6 @@ static inline struct folio *swap_cache_get_folio(swp_entry_t entry,
 	return NULL;
 }
 
-static inline
-struct folio *filemap_get_incore_folio(struct address_space *mapping,
-		pgoff_t index)
-{
-	return filemap_get_folio(mapping, index);
-}
-
 static inline void *get_shadow_from_swap_cache(swp_entry_t entry)
 {
 	return NULL;
mm/swap_state.c +0 -38
index ac4e0994931c..4117ea4e7afc 100644
@@ -324,44 +324,6 @@ struct folio *swap_cache_get_folio(swp_entry_t entry,
 	return folio;
 }
 
-/**
- * filemap_get_incore_folio - Find and get a folio from the page or swap caches.
- * @mapping: The address_space to search.
- * @index: The page cache index.
- *
- * This differs from filemap_get_folio() in that it will also look for the
- * folio in the swap cache.
- *
- * Return: The found folio or %NULL.
- */
-struct folio *filemap_get_incore_folio(struct address_space *mapping,
-		pgoff_t index)
-{
-	swp_entry_t swp;
-	struct swap_info_struct *si;
-	struct folio *folio = filemap_get_entry(mapping, index);
-
-	if (!folio)
-		return ERR_PTR(-ENOENT);
-	if (!xa_is_value(folio))
-		return folio;
-	if (!shmem_mapping(mapping))
-		return ERR_PTR(-ENOENT);
-
-	swp = radix_to_swp_entry(folio);
-	/* There might be swapin error entries in shmem mapping. */
-	if (non_swap_entry(swp))
-		return ERR_PTR(-ENOENT);
-	/* Prevent swapoff from happening to us */
-	si = get_swap_device(swp);
-	if (!si)
-		return ERR_PTR(-ENOENT);
-	index = swap_cache_index(swp);
-	folio = filemap_get_folio(swap_address_space(swp), index);
-	put_swap_device(si);
-	return folio;
-}
-
 struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated,
 		bool skip_if_exists)
Signature · 1 line
2.49.0
#3 lore raw

PATCH 3/28 mm/shmem, swap: remove SWAP_MAP_SHMEM

In reply to Kairui Song

From: Kairui Song <[email protected]>

From: Nhat Pham <[email protected]>

The SWAP_MAP_SHMEM state was introduced in the commit aaa468653b4a ("swap_info: note SWAP_MAP_SHMEM"), to quickly determine if a swap entry belongs to shmem during swapoff.

However, swapoff has since been rewritten in the commit b56a2d8af914 ("mm: rid swapoff of quadratic complexity"). Now having swap count == SWAP_MAP_SHMEM value is basically the same as having swap count == 1, and swap_shmem_alloc() behaves analogously to swap_duplicate(). The only difference of note is that swap_shmem_alloc() does not check for -ENOMEM returned from __swap_duplicate(), but it is OK because shmem never re-duplicates any swap entry it owns. This will stil be safe if we use (batched) swap_duplicate() instead.

This commit adds swap_duplicate_nr(), the batched variant of swap_duplicate(), and removes the SWAP_MAP_SHMEM state and the associated swap_shmem_alloc() helper to simplify the state machine (both mentally and in terms of actual code). We will also have an extra state/special value that can be repurposed (for swap entries that never gets re-duplicated).

---
 include/linux/swap.h | 15 +++++++--------
 mm/shmem.c           |  2 +-
 mm/swapfile.c        | 42 +++++++++++++++++-------------------------
 3 files changed, 25 insertions(+), 34 deletions(-)
include/linux/swap.h +7 -8
index 817e427a47d2..0e52ac4e817d 100644
@@ -230,7 +230,6 @@ enum {
 /* Special value in first swap_map */
 #define SWAP_MAP_MAX	0x3e	/* Max count */
 #define SWAP_MAP_BAD	0x3f	/* Note page is bad */
-#define SWAP_MAP_SHMEM	0xbf	/* Owned by shmem/tmpfs */
 
 /* Special value in each swap_map continuation */
 #define SWAP_CONT_MAX	0x7f	/* Max count */
@@ -477,8 +476,7 @@ bool folio_free_swap(struct folio *folio);
 void put_swap_folio(struct folio *folio, swp_entry_t entry);
 extern swp_entry_t get_swap_page_of_type(int);
 extern int add_swap_count_continuation(swp_entry_t, gfp_t);
-extern void swap_shmem_alloc(swp_entry_t, int);
-extern int swap_duplicate(swp_entry_t);
+extern int swap_duplicate_nr(swp_entry_t entry, int nr);
 extern int swapcache_prepare(swp_entry_t entry, int nr);
 extern void swap_free_nr(swp_entry_t entry, int nr_pages);
 extern void free_swap_and_cache_nr(swp_entry_t entry, int nr);
@@ -541,11 +539,7 @@ static inline int add_swap_count_continuation(swp_entry_t swp, gfp_t gfp_mask)
 	return 0;
 }
 
-static inline void swap_shmem_alloc(swp_entry_t swp, int nr)
-{
-}
-
-static inline int swap_duplicate(swp_entry_t swp)
+static inline int swap_duplicate_nr(swp_entry_t swp, int nr_pages)
 {
 	return 0;
 }
@@ -596,6 +590,11 @@ static inline int add_swap_extent(struct swap_info_struct *sis,
 }
 #endif /* CONFIG_SWAP */
 
+static inline int swap_duplicate(swp_entry_t entry)
+{
+	return swap_duplicate_nr(entry, 1);
+}
+
 static inline void free_swap_and_cache(swp_entry_t entry)
 {
 	free_swap_and_cache_nr(entry, 1);
mm/shmem.c +1 -1
index 99327c30507c..972bd0eca439 100644
@@ -1641,7 +1641,7 @@ static int shmem_writepage(struct page *page, struct writeback_control *wbc)
 
 	if (!folio_alloc_swap(folio, __GFP_HIGH | __GFP_NOMEMALLOC | __GFP_NOWARN)) {
 		shmem_recalc_inode(inode, 0, nr_pages);
-		swap_shmem_alloc(folio->swap, nr_pages);
+		swap_duplicate_nr(folio->swap, nr_pages);
 		shmem_delete_from_page_cache(folio, swp_to_radix_entry(folio->swap));
 
 		mutex_unlock(&shmem_swaplist_mutex);
mm/swapfile.c +17 -25

89 lines not on this page · raw message

Signature · 1 line
2.49.0
#4 lore raw

PATCH 4/28 mm, swap: split readahead update out of swap cache lookup

In reply to Kairui Song

From: Kairui Song <[email protected]>

Decouple readahead update with swap cache lookup. No feature change.

After this, swap_cache_get_folio is the only entry for getting folios from the swap cache. There are only two callers of it want to update readahead statistic.

There are only three special cases for accessing swap cache space now:
huge memory splitting, migration and shmem replacing, they directly modify the Xarray. Following commit will wrap their accesses to the swap cache with special helpers too.

---
 mm/memory.c      |  6 ++-
 mm/mincore.c     |  3 +-
 mm/shmem.c       |  5 ++-
 mm/swap.h        | 13 +++++--
 mm/swap_state.c  | 99 +++++++++++++++++++++++-------------------------
 mm/swapfile.c    | 11 +++---
 mm/userfaultfd.c |  5 +--
 7 files changed, 72 insertions(+), 70 deletions(-)
mm/memory.c +4 -2
index 5cb48f262ab0..18b5a77a0a4b 100644
@@ -4567,9 +4567,11 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	if (unlikely(!si))
 		goto out;
 
-	folio = swap_cache_get_folio(entry, vma, vmf->address);
-	if (folio)
+	folio = swap_cache_get_folio(entry);
+	if (folio) {
+		swap_update_readahead(folio, vma, vmf->address);
 		page = folio_file_page(folio, swp_offset(entry));
+	}
 	swapcache = folio;
 
 	if (!folio) {
mm/mincore.c +1 -2
index 7ee88113d44c..a57a9ee9e93d 100644
@@ -62,8 +62,7 @@ static unsigned char mincore_swap(swp_entry_t entry)
 	/* Prevent swap device to being swapoff under us */
 	si = get_swap_device(entry);
 	if (si) {
-		folio = filemap_get_folio(swap_address_space(entry),
-					  swap_cache_index(entry));
+		folio = swap_cache_get_folio(entry);
 		put_swap_device(si);
 	}
 	if (folio) {
mm/shmem.c +3 -2
index 972bd0eca439..01f29cb31c7a 100644
@@ -2259,7 +2259,9 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	}
 
 	/* Look it up and read it in.. */
-	folio = swap_cache_get_folio(swap, NULL, 0);
+	folio = swap_cache_get_folio(swap);
+	if (folio)
+		swap_update_readahead(folio, NULL, 0);
 	order = xa_get_order(&mapping->i_pages, index);
 	if (!folio) {
 		bool fallback_order0 = false;
@@ -2350,7 +2352,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 			swap = swp_entry(swp_type(swap), swp_offset(swap) + offset);
 		}
 	}
-
 alloced:
 	/* We have to do this with folio locked to prevent races */
 	folio_lock(folio);
mm/swap.h +9 -4
index 4f85195ab83d..e83109ad1456 100644
@@ -60,8 +60,7 @@ void delete_from_swap_cache(struct folio *folio);
 void clear_shadow_from_swap_cache(int type, unsigned long begin,
 				  unsigned long end);
 void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr);
-struct folio *swap_cache_get_folio(swp_entry_t entry,
-		struct vm_area_struct *vma, unsigned long addr);
+struct folio *swap_cache_get_folio(swp_entry_t entry);
 struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct vm_area_struct *vma, unsigned long addr,
 		struct swap_iocb **plug);
@@ -72,6 +71,8 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t flag,
 		struct mempolicy *mpol, pgoff_t ilx);
 struct folio *swapin_readahead(swp_entry_t entry, gfp_t flag,
 		struct vm_fault *vmf);
+void swap_update_readahead(struct folio *folio, struct vm_area_struct *vma,
+			   unsigned long addr);
 
 static inline unsigned int folio_swap_flags(struct folio *folio)
 {
@@ -138,6 +139,11 @@ static inline struct folio *swapin_readahead(swp_entry_t swp, gfp_t gfp_mask,
 	return NULL;
 }
 
+static inline void swap_update_readahead(struct folio *folio,
+		struct vm_area_struct *vma, unsigned long addr)
+{
+}
+
 static inline int swap_writepage(struct page *p, struct writeback_control *wbc)
 {
 	return 0;
@@ -147,8 +153,7 @@ static inline void swapcache_clear(struct swap_info_struct *si, swp_entry_t entr
 {
 }
 
-static inline struct folio *swap_cache_get_folio(swp_entry_t entry,
-		struct vm_area_struct *vma, unsigned long addr)
+static inline struct folio *swap_cache_get_folio(swp_entry_t entry)
 {
 	return NULL;
 }
mm/swap_state.c +48 -51

122 lines not on this page · raw message

mm/swapfile.c +5 -6
index 6b115149b845..29e918102355 100644
@@ -213,15 +213,14 @@ static int __try_to_reclaim_swap(struct swap_info_struct *si,
 				 unsigned long offset, unsigned long flags)
 {
 	swp_entry_t entry = swp_entry(si->type, offset);
-	struct address_space *address_space = swap_address_space(entry);
 	struct swap_cluster_info *ci;
 	struct folio *folio;
 	int ret, nr_pages;
 	bool need_reclaim;
 
 again:
-	folio = filemap_get_folio(address_space, swap_cache_index(entry));
-	if (IS_ERR(folio))
+	folio = swap_cache_get_folio(entry);
+	if (!folio)
 		return 0;
 
 	nr_pages = folio_nr_pages(folio);
@@ -2098,7 +2097,7 @@ static int unuse_pte_range(struct vm_area_struct *vma, pmd_t *pmd,
 		pte_unmap(pte);
 		pte = NULL;
 
-		folio = swap_cache_get_folio(entry, vma, addr);
+		folio = swap_cache_get_folio(entry);
 		if (!folio) {
 			struct vm_fault vmf = {
 				.vma = vma,
@@ -2324,8 +2323,8 @@ static int try_to_unuse(unsigned int type)
 	       (i = find_next_to_unuse(si, i)) != 0) {
 
 		entry = swp_entry(type, i);
-		folio = filemap_get_folio(swap_address_space(entry), swap_cache_index(entry));
-		if (IS_ERR(folio))
+		folio = swap_cache_get_folio(entry);
+		if (!folio)
 			continue;
 
 		/*
mm/userfaultfd.c +2 -3
index bc473ad21202..e5a0db7f3331 100644
@@ -1389,9 +1389,8 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
 		 * separately to allow proper handling.
 		 */
 		if (!src_folio)
-			folio = filemap_get_folio(swap_address_space(entry),
-					swap_cache_index(entry));
-		if (!IS_ERR_OR_NULL(folio)) {
+			folio = swap_cache_get_folio(entry);
+		if (folio) {
 			if (folio_test_large(folio)) {
 				err = -EBUSY;
 				folio_put(folio);
Signature · 1 line
2.49.0
#5 lore raw

PATCH 5/28 mm, swap: sanitize swap cache lookup convention

In reply to Kairui Song

From: Kairui Song <[email protected]>

Swap cache lookup is lock less, the returned folio could be invalidated any time before locking it. So the caller always have to lock and check the folio before use.

Introduce a helper for swap cache folio checking, document this convention, and avoid touching the folio until the folio has been verified.

And update all current users using this convention.

---
 mm/memory.c      | 31 ++++++++++++++-----------------
 mm/shmem.c       |  4 ++--
 mm/swap.h        | 21 +++++++++++++++++++++
 mm/swap_state.c  |  8 ++++++--
 mm/swapfile.c    | 10 ++++++++--
 mm/userfaultfd.c |  4 ++++
 6 files changed, 55 insertions(+), 23 deletions(-)
mm/memory.c +14 -17
index 18b5a77a0a4b..254be0e88801 100644
@@ -4568,12 +4568,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		goto out;
 
 	folio = swap_cache_get_folio(entry);
-	if (folio) {
-		swap_update_readahead(folio, vma, vmf->address);
-		page = folio_file_page(folio, swp_offset(entry));
-	}
 	swapcache = folio;
-
 	if (!folio) {
 		if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
 		    __swap_count(entry) == 1) {
@@ -4642,20 +4637,13 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		ret = VM_FAULT_MAJOR;
 		count_vm_event(PGMAJFAULT);
 		count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
-		page = folio_file_page(folio, swp_offset(entry));
-	} else if (PageHWPoison(page)) {
-		/*
-		 * hwpoisoned dirty swapcache pages are kept for killing
-		 * owner processes (which may be unknown at hwpoison time)
-		 */
-		ret = VM_FAULT_HWPOISON;
-		goto out_release;
 	}
 
 	ret |= folio_lock_or_retry(folio, vmf);
 	if (ret & VM_FAULT_RETRY)
 		goto out_release;
 
+	page = folio_file_page(folio, swp_offset(entry));
 	if (swapcache) {
 		/*
 		 * Make sure folio_free_swap() or swapoff did not release the
@@ -4664,10 +4652,20 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		 * swapcache, we need to check that the page's swap has not
 		 * changed.
 		 */
-		if (unlikely(!folio_test_swapcache(folio) ||
-			     page_swap_entry(page).val != entry.val))
+		if (!folio_swap_contains(folio, entry))
 			goto out_page;
 
+		if (PageHWPoison(page)) {
+			/*
+			 * hwpoisoned dirty swapcache pages are kept for killing
+			 * owner processes (which may be unknown at hwpoison time)
+			 */
+			ret = VM_FAULT_HWPOISON;
+			goto out_page;
+		}
+
+		swap_update_readahead(folio, vma, vmf->address);
+
 		/*
 		 * KSM sometimes has to copy on read faults, for example, if
 		 * page->index of !PageKSM() pages would be nonlinear inside the
@@ -4682,8 +4680,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 			ret = VM_FAULT_HWPOISON;
 			folio = swapcache;
 			goto out_page;
-		}
-		if (folio != swapcache)
+		} else if (folio != swapcache)
 			page = folio_page(folio, 0);
 
 		/*
mm/shmem.c +2 -2
index 01f29cb31c7a..43d9e3bf16f4 100644
@@ -2260,8 +2260,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 
 	/* Look it up and read it in.. */
 	folio = swap_cache_get_folio(swap);
-	if (folio)
-		swap_update_readahead(folio, NULL, 0);
 	order = xa_get_order(&mapping->i_pages, index);
 	if (!folio) {
 		bool fallback_order0 = false;
@@ -2362,6 +2360,8 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		error = -EEXIST;
 		goto unlock;
 	}
+	if (!skip_swapcache)
+		swap_update_readahead(folio, NULL, 0);
 	if (!folio_test_uptodate(folio)) {
 		error = -EIO;
 		goto failed;
mm/swap.h +21 -0
index e83109ad1456..34af06bf6fa4 100644
@@ -50,6 +50,22 @@ static inline pgoff_t swap_cache_index(swp_entry_t entry)
 	return swp_offset(entry) & SWAP_ADDRESS_SPACE_MASK;
 }
 
+/*
+ * Check if a folio still contains a swap entry, must be called after a
+ * swap cache lookup as the folio might have been invalidated while
+ * it's unlocked.
+ */
+static inline bool folio_swap_contains(struct folio *folio, swp_entry_t entry)
+{
+	pgoff_t index = swp_offset(entry);
+	VM_WARN_ON_ONCE(!folio_test_locked(folio));
+	if (unlikely(!folio_test_swapcache(folio)))
+		return false;
+	if (unlikely(swp_type(entry) != swp_type(folio->swap)))
+		return false;
+	return (index - swp_offset(folio->swap)) < folio_nr_pages(folio);
+}
+
 void show_swap_cache_info(void);
 void *get_shadow_from_swap_cache(swp_entry_t entry);
 int add_to_swap_cache(struct folio *folio, swp_entry_t entry,
@@ -123,6 +139,11 @@ static inline pgoff_t swap_cache_index(swp_entry_t entry)
 	return 0;
 }
 
+static inline bool folio_swap_contains(struct folio *folio, swp_entry_t entry)
+{
+	return false;
+}
+
 static inline void show_swap_cache_info(void)
 {
 }
mm/swap_state.c +6 -2
index bca201100138..07c41676486a 100644
@@ -170,7 +170,8 @@ void __delete_from_swap_cache(struct folio *folio,
  * Lookup a swap entry in the swap cache. A found folio will be returned
  * unlocked and with its refcount incremented.
  *
- * Caller must hold a reference on the swap device.
+ * Caller must hold a reference of the swap device, and check if the
+ * returned folio is still valid after locking it (e.g. folio_swap_contains).
  */
 struct folio *swap_cache_get_folio(swp_entry_t entry)
 {
@@ -339,7 +340,10 @@ struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 	for (;;) {
 		int err;
 
-		/* Check the swap cache in case the folio is already there */
+		/*
+		 * Check the swap cache first, if a cached folio is found,
+		 * return it unlocked. The caller will lock and check it.
+		 */
 		folio = swap_cache_get_folio(entry);
 		if (folio)
 			goto got_folio;
mm/swapfile.c +8 -2
index 29e918102355..aa031fd27847 100644
@@ -240,12 +240,12 @@ static int __try_to_reclaim_swap(struct swap_info_struct *si,
 	 * Offset could point to the middle of a large folio, or folio
 	 * may no longer point to the expected offset before it's locked.
 	 */
-	entry = folio->swap;
-	if (offset < swp_offset(entry) || offset >= swp_offset(entry) + nr_pages) {
+	if (!folio_swap_contains(folio, entry)) {
 		folio_unlock(folio);
 		folio_put(folio);
 		goto again;
 	}
+	entry = folio->swap;
 	offset = swp_offset(entry);
 
 	need_reclaim = ((flags & TTRS_ANYWAY) ||
@@ -2117,6 +2117,12 @@ static int unuse_pte_range(struct vm_area_struct *vma, pmd_t *pmd,
 		}
 
 		folio_lock(folio);
+		if (!folio_swap_contains(folio, entry)) {
+			folio_unlock(folio);
+			folio_put(folio);
+			continue;
+		}
+
 		folio_wait_writeback(folio);
 		ret = unuse_pte(vma, pmd, addr, entry, folio);
 		if (ret < 0) {
mm/userfaultfd.c +4 -0
index e5a0db7f3331..5b4f01aecf35 100644
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
 				goto retry;
 			}
 		}
+		if (!folio_swap_contains(src_folio, entry)) {
+			err = -EBUSY;
+			goto out;
+		}
 		err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
 				orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
 				dst_ptl, src_ptl, src_folio);
Signature · 1 line
2.49.0
#6 lore raw

PATCH 5/28 mm, swap: sanitize swap cache lookup convention

Quoted 7 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
 				goto retry;
 			}
 		}
+		if (!folio_swap_contains(src_folio, entry)) {
+			err = -EBUSY;
+			goto out;
+		}

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        } 

Also, -EBUSY is somehow incorrect error code.

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1418

Quoted 4 lines
mm/userfaultfd.c +0 -0
 		err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
 				orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
 				dst_ptl, src_ptl, src_folio);

Thanks
Barry

#7 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 2 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 8 lines

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1418

Quoted 5 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#8 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

Quoted 13 lines
Quoted 3 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 6 lines
Quoted 5 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#9 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.
double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

Quoted 17 lines
Quoted 14 lines
Quoted 4 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 6 lines
Quoted 5 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#10 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 62 lines · Kairui Song
Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

Quoted 10 lines

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.

swap_cache_get_folio() does increment the folio's refcount, but it seems this doesn't prevent do_swap_page() from freeing the swap entry after swapping in src_pte with folio A, if it's a read fault.
for write fault, folio_ref_count(folio) == (1 + folio_nr_pages(folio)) will be false:

static inline bool should_try_to_free_swap(struct folio *folio,
struct vm_area_struct *vma, unsigned int fault_flags)

{
       ...

        /*
         * If we want to map a page that's in the swapcache writable, we
         * have to detect via the refcount if we're really the exclusive
         * user. Try freeing the swapcache to get rid of the swapcache
         * reference only in case it's likely that we'll be the exlusive user.
         */
        return (fault_flags & FAULT_FLAG_WRITE) && !folio_test_ksm(folio) &&
                folio_ref_count(folio) == (1 + folio_nr_pages(folio));
}

and for swapout, __removing_mapping does check refcount as well:

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
bool reclaimed, struct mem_cgroup *target_memcg)

{
        refcount = 1 + folio_nr_pages(folio);
        if (!folio_ref_freeze(folio, refcount))
                goto cannot_free;

}

However, since __remove_mapping() occurs after pageout(), it seems this also doesn't prevent swapout from allocating a new swap entry to fill src_pte.

It seems your concern is valid—unless I'm missing something. Do you have a reproducer? If so, this will likely need a separate fix patch rather than being hidden in this patchset.

Quoted 10 lines

double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

It seems you're suggesting that a parallel swap-in allocates and adds a folio to the swap cache, but the PTE has not yet been updated from a swap entry to a present mapping?

As long as do_swap_page() adds the folio to the swap cache before updating the PTE to present, this scenario seems possible.

It seems we need to double-check:

mm/userfaultfd.c +0 -0
index bc473ad21202..976053bd2bf1 100644
@@ -1102,8 +1102,14 @@ static int move_swap_pte(struct mm_struct *mm,

struct vm_area_struct *dst_vma,

        if (src_folio) {
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);

+ } else {
+ struct folio *folio =
filemap_get_folio(swap_address_space(entry),
+ swap_cache_index(entry));
+ if (!IS_ERR_OR_NULL(folio)) {
+ double_pt_unlock(dst_ptl, src_ptl);
+ return -EAGAIN;
+ }

        }
-
        orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
 #ifdef CONFIG_MEM_SOFT_DIRTY
        orig_src_pte = pte_swp_mksoft_dirty(orig_src_pte);

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

[1] https://lore.kernel.org/linux-mm/[email protected]/

Quoted 11 lines
Quoted 11 lines
Quoted 11 lines
Quoted 6 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 1 line
Quoted 6 lines
Quoted 6 lines
Quoted 5 lines
Quoted 5 lines

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1418

Quoted 4 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#11 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 109 lines · Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:
Quoted 62 lines · Kairui Song
Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

Quoted 10 lines

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.

swap_cache_get_folio() does increment the folio's refcount, but it seems this doesn't prevent do_swap_page() from freeing the swap entry after swapping in src_pte with folio A, if it's a read fault.
for write fault, folio_ref_count(folio) == (1 + folio_nr_pages(folio)) will be false:

static inline bool should_try_to_free_swap(struct folio *folio,
struct vm_area_struct *vma, unsigned int fault_flags)

{
       ...

        /*
         * If we want to map a page that's in the swapcache writable, we
         * have to detect via the refcount if we're really the exclusive
         * user. Try freeing the swapcache to get rid of the swapcache
         * reference only in case it's likely that we'll be the exlusive user.
         */
        return (fault_flags & FAULT_FLAG_WRITE) && !folio_test_ksm(folio) &&
                folio_ref_count(folio) == (1 + folio_nr_pages(folio));
}

and for swapout, __removing_mapping does check refcount as well:

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
bool reclaimed, struct mem_cgroup *target_memcg)

{
        refcount = 1 + folio_nr_pages(folio);
        if (!folio_ref_freeze(folio, refcount))
                goto cannot_free;

}

Quoted 7 lines

However, since __remove_mapping() occurs after pageout(), it seems this also doesn't prevent swapout from allocating a new swap entry to fill src_pte.

It seems your concern is valid—unless I'm missing something. Do you have a reproducer? If so, this will likely need a separate fix patch rather than being hidden in this patchset.

Thanks for the analysis. I don't have a reproducer yet, I did some local experiments and that seems possible, but the race window is so tiny and it's very difficult to make the swap entry reuse to collide with that, I'll try more but in theory this seems possible, or at least looks very fragile.

And yeah, let's patch the kernel first if that's a real issue.

Quoted 12 lines
Quoted 11 lines

double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

Quoted 6 lines

It seems you're suggesting that a parallel swap-in allocates and adds a folio to the swap cache, but the PTE has not yet been updated from a swap entry to a present mapping?

As long as do_swap_page() adds the folio to the swap cache before updating the PTE to present, this scenario seems possible.

Yes, that's two kinds of problems here. I suspected there could be an ABA problem while working on the series, but wasn't certain. And just realised there could be another missed cache read here thanks to your review and discussion :)

Quoted 23 lines

It seems we need to double-check:

mm/userfaultfd.c +7 -1
index bc473ad21202..976053bd2bf1 100644
@@ -1102,8 +1102,14 @@ static int move_swap_pte(struct mm_struct *mm,
 struct vm_area_struct *dst_vma,
        if (src_folio) {
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
+       } else {
+               struct folio *folio =
 filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(folio)) {
+                       double_pt_unlock(dst_ptl, src_ptl);
+                       return -EAGAIN;
+               }
        }
-
        orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
Quoted 2 lines
 #ifdef CONFIG_MEM_SOFT_DIRTY
        orig_src_pte = pte_swp_mksoft_dirty(orig_src_pte);

Maybe it has to get even dirtier here to call swapcache_prepare too to cover the SYNC_IO case?

Quoted 3 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Quoted 2 lines

[1] https://lore.kernel.org/linux-mm/[email protected]/

I'll try this too.

Quoted 21 lines
Quoted 12 lines
Quoted 12 lines
Quoted 12 lines
Quoted 7 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 1 line
Quoted 6 lines
Quoted 6 lines
Quoted 5 lines
Quoted 5 lines

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1418

Quoted 4 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#12 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 119 lines · Kairui Song
Quoted 109 lines · Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:
Quoted 62 lines · Kairui Song
Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

Quoted 10 lines

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.

swap_cache_get_folio() does increment the folio's refcount, but it seems this doesn't prevent do_swap_page() from freeing the swap entry after swapping in src_pte with folio A, if it's a read fault.
for write fault, folio_ref_count(folio) == (1 + folio_nr_pages(folio)) will be false:

static inline bool should_try_to_free_swap(struct folio *folio,
struct vm_area_struct *vma, unsigned int fault_flags)

{
       ...

        /*
         * If we want to map a page that's in the swapcache writable, we
         * have to detect via the refcount if we're really the exclusive
         * user. Try freeing the swapcache to get rid of the swapcache
         * reference only in case it's likely that we'll be the exlusive user.
         */
        return (fault_flags & FAULT_FLAG_WRITE) && !folio_test_ksm(folio) &&
                folio_ref_count(folio) == (1 + folio_nr_pages(folio));
}

and for swapout, __removing_mapping does check refcount as well:

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
bool reclaimed, struct mem_cgroup *target_memcg)

{
        refcount = 1 + folio_nr_pages(folio);
        if (!folio_ref_freeze(folio, refcount))
                goto cannot_free;

}

Quoted 7 lines

However, since __remove_mapping() occurs after pageout(), it seems this also doesn't prevent swapout from allocating a new swap entry to fill src_pte.

It seems your concern is valid—unless I'm missing something. Do you have a reproducer? If so, this will likely need a separate fix patch rather than being hidden in this patchset.

Quoted 5 lines

Thanks for the analysis. I don't have a reproducer yet, I did some local experiments and that seems possible, but the race window is so tiny and it's very difficult to make the swap entry reuse to collide with that, I'll try more but in theory this seems possible, or at least looks very fragile.

I think we don't necessarily need to prove the same swap entry. As long as we can prove the original and current ptes are both swap with the same offset or not, this could be the possibility they will have the same offset. This can increase the likelihood of occurrence and help validate the conceptual model.

mm/userfaultfd.c +0 -0
index bc473ad21202..f072d4a5bcd4 100644

@@ -1090,6 +1090,10 @@ static int move_swap_pte(struct mm_struct *mm, struct vm_area_struct *dst_vma,
if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
dst_pmd, dst_pmdval)) {
+ if (src_folio && !non_swap_entry(orig_src_pte) &&
+ !non_swap_entry(ptep_get(src_pte)))
+ the case is true;
+

                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }
Quoted 67 lines

And yeah, let's patch the kernel first if that's a real issue.

Quoted 12 lines
Quoted 11 lines

double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

Quoted 6 lines

It seems you're suggesting that a parallel swap-in allocates and adds a folio to the swap cache, but the PTE has not yet been updated from a swap entry to a present mapping?

As long as do_swap_page() adds the folio to the swap cache before updating the PTE to present, this scenario seems possible.

Yes, that's two kinds of problems here. I suspected there could be an ABA problem while working on the series, but wasn't certain. And just realised there could be another missed cache read here thanks to your review and discussion :)

Quoted 23 lines

It seems we need to double-check:

mm/userfaultfd.c +7 -1
index bc473ad21202..976053bd2bf1 100644
@@ -1102,8 +1102,14 @@ static int move_swap_pte(struct mm_struct *mm,
 struct vm_area_struct *dst_vma,
        if (src_folio) {
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
+       } else {
+               struct folio *folio =
 filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(folio)) {
+                       double_pt_unlock(dst_ptl, src_ptl);
+                       return -EAGAIN;
+               }
        }
-
        orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
Quoted 2 lines
 #ifdef CONFIG_MEM_SOFT_DIRTY
        orig_src_pte = pte_swp_mksoft_dirty(orig_src_pte);

Maybe it has to get even dirtier here to call swapcache_prepare too to cover the SYNC_IO case?

Quoted 3 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Quoted 1 line
Quoted 23 lines
Quoted 1 line

[1] https://lore.kernel.org/linux-mm/[email protected]/

I'll try this too.

Quoted 19 lines
Quoted 18 lines
Quoted 18 lines
Quoted 17 lines
Quoted 7 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 6 lines
Quoted 5 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#13 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

In reply to Kairui Song
Quoted 181 lines · Kairui Song
Quoted 109 lines · Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:
Quoted 62 lines · Kairui Song
Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

Quoted 10 lines

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.

swap_cache_get_folio() does increment the folio's refcount, but it seems this doesn't prevent do_swap_page() from freeing the swap entry after swapping in src_pte with folio A, if it's a read fault.
for write fault, folio_ref_count(folio) == (1 + folio_nr_pages(folio)) will be false:

static inline bool should_try_to_free_swap(struct folio *folio,
struct vm_area_struct *vma, unsigned int fault_flags)

{
       ...

        /*
         * If we want to map a page that's in the swapcache writable, we
         * have to detect via the refcount if we're really the exclusive
         * user. Try freeing the swapcache to get rid of the swapcache
         * reference only in case it's likely that we'll be the exlusive user.
         */
        return (fault_flags & FAULT_FLAG_WRITE) && !folio_test_ksm(folio) &&
                folio_ref_count(folio) == (1 + folio_nr_pages(folio));
}

and for swapout, __removing_mapping does check refcount as well:

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
bool reclaimed, struct mem_cgroup *target_memcg)

{
        refcount = 1 + folio_nr_pages(folio);
        if (!folio_ref_freeze(folio, refcount))
                goto cannot_free;

}

Quoted 7 lines

However, since __remove_mapping() occurs after pageout(), it seems this also doesn't prevent swapout from allocating a new swap entry to fill src_pte.

It seems your concern is valid—unless I'm missing something. Do you have a reproducer? If so, this will likely need a separate fix patch rather than being hidden in this patchset.

Thanks for the analysis. I don't have a reproducer yet, I did some local experiments and that seems possible, but the race window is so tiny and it's very difficult to make the swap entry reuse to collide with that, I'll try more but in theory this seems possible, or at least looks very fragile.

And yeah, let's patch the kernel first if that's a real issue.

Quoted 12 lines
Quoted 11 lines

double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

Quoted 6 lines

It seems you're suggesting that a parallel swap-in allocates and adds a folio to the swap cache, but the PTE has not yet been updated from a swap entry to a present mapping?

As long as do_swap_page() adds the folio to the swap cache before updating the PTE to present, this scenario seems possible.

Yes, that's two kinds of problems here. I suspected there could be an ABA problem while working on the series, but wasn't certain. And just realised there could be another missed cache read here thanks to your review and discussion :)

Quoted 23 lines

It seems we need to double-check:

mm/userfaultfd.c +7 -1
index bc473ad21202..976053bd2bf1 100644
@@ -1102,8 +1102,14 @@ static int move_swap_pte(struct mm_struct *mm,
 struct vm_area_struct *dst_vma,
        if (src_folio) {
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
+       } else {
+               struct folio *folio =
 filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(folio)) {
+                       double_pt_unlock(dst_ptl, src_ptl);
+                       return -EAGAIN;
+               }
        }
-
        orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
Quoted 2 lines
 #ifdef CONFIG_MEM_SOFT_DIRTY
        orig_src_pte = pte_swp_mksoft_dirty(orig_src_pte);

Maybe it has to get even dirtier here to call swapcache_prepare too to cover the SYNC_IO case?

Quoted 1 line
Quoted 8 lines
Quoted 2 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

I attempted to reproduce the scenario where a folio is added to the swapcache after filemap_get_folio() returns NULL but before move_swap_pte() moves the swap PTE
using non-synchronized I/O. Technically, this seems possible; however, I was unable
to reproduce it, likely because the time window between swapin_readahead and taking the page table lock within do_swap_page() is too short.

Upon reconsideration, even if this situation occurs, it is not an issue because move_swap_pte() obtains both the source and destination page table locks, and *clears* the source PTE. Thus, when do_swap_page() subsequently acquires the source page table lock for src, it cannot map the new swapcache folio to the PTE since pte_same will return false.

Quoted 25 lines
Quoted 3 lines

[1] https://lore.kernel.org/linux-mm/[email protected]/

I'll try this too.

Quoted 19 lines
Quoted 18 lines
Quoted 18 lines
Quoted 17 lines
Quoted 7 lines

Also, -EBUSY is somehow incorrect error code.

Yes, thanks, I'll use EAGAIN here just like move_swap_pte.

Quoted 6 lines
Quoted 5 lines
mm/userfaultfd.c +0 -0
              err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
                              orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                              dst_ptl, src_ptl, src_folio);

Thanks
Barry

#14 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 183 lines · Barry Song
Quoted 181 lines · Kairui Song
Quoted 109 lines · Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:
Quoted 62 lines · Kairui Song
Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

Quoted 10 lines

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.

swap_cache_get_folio() does increment the folio's refcount, but it seems this doesn't prevent do_swap_page() from freeing the swap entry after swapping in src_pte with folio A, if it's a read fault.
for write fault, folio_ref_count(folio) == (1 + folio_nr_pages(folio)) will be false:

static inline bool should_try_to_free_swap(struct folio *folio,
struct vm_area_struct *vma, unsigned int fault_flags)

{
       ...

        /*
         * If we want to map a page that's in the swapcache writable, we
         * have to detect via the refcount if we're really the exclusive
         * user. Try freeing the swapcache to get rid of the swapcache
         * reference only in case it's likely that we'll be the exlusive user.
         */
        return (fault_flags & FAULT_FLAG_WRITE) && !folio_test_ksm(folio) &&
                folio_ref_count(folio) == (1 + folio_nr_pages(folio));
}

and for swapout, __removing_mapping does check refcount as well:

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
bool reclaimed, struct mem_cgroup *target_memcg)

{
        refcount = 1 + folio_nr_pages(folio);
        if (!folio_ref_freeze(folio, refcount))
                goto cannot_free;

}

Quoted 7 lines

However, since __remove_mapping() occurs after pageout(), it seems this also doesn't prevent swapout from allocating a new swap entry to fill src_pte.

It seems your concern is valid—unless I'm missing something. Do you have a reproducer? If so, this will likely need a separate fix patch rather than being hidden in this patchset.

Thanks for the analysis. I don't have a reproducer yet, I did some local experiments and that seems possible, but the race window is so tiny and it's very difficult to make the swap entry reuse to collide with that, I'll try more but in theory this seems possible, or at least looks very fragile.

And yeah, let's patch the kernel first if that's a real issue.

Quoted 12 lines
Quoted 11 lines

double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

Quoted 6 lines

It seems you're suggesting that a parallel swap-in allocates and adds a folio to the swap cache, but the PTE has not yet been updated from a swap entry to a present mapping?

As long as do_swap_page() adds the folio to the swap cache before updating the PTE to present, this scenario seems possible.

Yes, that's two kinds of problems here. I suspected there could be an ABA problem while working on the series, but wasn't certain. And just realised there could be another missed cache read here thanks to your review and discussion :)

Quoted 23 lines

It seems we need to double-check:

mm/userfaultfd.c +7 -1
index bc473ad21202..976053bd2bf1 100644
@@ -1102,8 +1102,14 @@ static int move_swap_pte(struct mm_struct *mm,
 struct vm_area_struct *dst_vma,
        if (src_folio) {
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
+       } else {
+               struct folio *folio =
 filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(folio)) {
+                       double_pt_unlock(dst_ptl, src_ptl);
+                       return -EAGAIN;
+               }
        }
-
        orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
Quoted 2 lines
 #ifdef CONFIG_MEM_SOFT_DIRTY
        orig_src_pte = pte_swp_mksoft_dirty(orig_src_pte);

Maybe it has to get even dirtier here to call swapcache_prepare too to cover the SYNC_IO case?

Quoted 1 line
Quoted 8 lines
Quoted 8 lines
Quoted 2 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Hi Barry,

Quoted 8 lines

I attempted to reproduce the scenario where a folio is added to the swapcache after filemap_get_folio() returns NULL but before move_swap_pte() moves the swap PTE
using non-synchronized I/O. Technically, this seems possible; however, I was unable
to reproduce it, likely because the time window between swapin_readahead and taking the page table lock within do_swap_page() is too short.

Thank you so much for trying this!

I have been trying to reproduce it too, and so far I didn't observe any crash or warn. I added following debug code:

static __always_inline
bool validate_dst_vma(struct vm_area_struct *dst_vma, unsigned long dst_end)
@@ -1163,6 +1167,7 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,

        pmd_t dummy_pmdval;
        pmd_t dst_pmdval;
        struct folio *src_folio = NULL;
+       struct folio *tmp_folio = NULL;
        struct anon_vma *src_anon_vma = NULL;
        struct mmu_notifier_range range;
        int err = 0;
@@ -1391,6 +1396,15 @@ static int move_pages_pte(struct mm_struct *mm,
pmd_t *dst_pmd, pmd_t *src_pmd,
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));

+ udelay(get_random_u32_below(1000));
+ tmp_folio = filemap_get_folio(swap_address_space(entry),
+ swap_cache_index(entry));
+ if (!IS_ERR_OR_NULL(tmp_folio)) {
+ if (!IS_ERR_OR_NULL(folio) && tmp_folio != folio) {
+ pr_err("UFFDIO_MOVE: UNSTABLE folio
%lx (%lx) -> %lx (%lx)\n", folio, folio->swap.val, tmp_folio, tmp_folio->swap.val);
+ }
+ folio_put(tmp_folio);
+ }

                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;

@@ -1413,6 +1427,8 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
err = move_swap_pte(mm, dst_vma, dst_addr, src_addr,
dst_pte, src_pte,
orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval, dst_ptl, src_ptl, src_folio);
+ if (tmp_folio != folio && !err)
+ pr_err("UFFDIO_MOVE: UNSTABLE folio passed

check: %lx -> %lx\n", folio, tmp_folio);
        }

And I saw these two prints are getting triggered like this (not a real issue though, just help to understand the problem)
...

[ 3127.632791] UFFDIO_MOVE: UNSTABLE folio fffffdffc334cd00 (0) ->
fffffdffc7ccac80 (51)
[ 3172.033269] UFFDIO_MOVE: UNSTABLE folio fffffdffc343bb40 (0) ->
fffffdffc3435e00 (3b)
[ 3194.425213] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d481c0 (0) ->
fffffdffc34ab8c0 (76)
[ 3194.991318] UFFDIO_MOVE: UNSTABLE folio fffffdffc34f95c0 (0) ->
fffffdffc34ab8c0 (6d)
[ 3203.467212] UFFDIO_MOVE: UNSTABLE folio fffffdffc34b13c0 (0) ->
fffffdffc34eda80 (32)
[ 3206.217820] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d297c0 (0) ->
fffffdffc38cedc0 (b)
[ 3214.913039] UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc34db140
[ 3217.066972] UFFDIO_MOVE: UNSTABLE folio fffffdffc342b5c0 (0) ->

fffffdffc3465cc0 (21)
...

The "UFFDIO_MOVE: UNSTABLE folio fffffdffc3435180 (0) -> fffffdffc3853540 (53)" worries me at first. On first look it seems the folio is indeed freed completely from the swap cache after the first lookup, so another swapout can reuse the entry. But as you mentioned __remove_mapping won't release a folio if the refcount check fails, so they must be freed by folio_free_swap or __try_to_reclaim_swap, there are many places that can happen. But these two helpers won't free a folio from swap cache if its swap count is not zero. And the folio will either be swapped out (swap count non zero), or mapped (freeing it is fine, PTE is non_swap, and another swapout will still use the same folio).

So after more investigation and dumping the pages, it's actually the second lookup (tmp_folio) seeing the entry being reused by another page table entry, after the first folio is swapped back and released. So the page table check below will always fail just fine.

But this also proves the first look up can see a completely irrelevant folio too: If the src folio is swapped out, but got swapped back and freed, then another folio B shortly got added to swap cache reuse the src folio's old swap entry, then the folio B got seen by the look up here and get freed from swap cache, then src folio got swapped out again also reusing the same entry, then we have a problem as PTE seems untouched indeed but we grabbed a wrong folio. Seems possible if I'm not wrong:

Something like this:
CPU1 CPU2
move_pages_pte
entry = pte_to_swp_entry(orig_src_pte);
| Got Swap Entry S1 from src_pte
...
<swapin src_pte, using folio A> <free folio A from swap cache freeing S1> <someone else try swap out folio B > <put folio B to swap cache using S1 >
...
folio = swap_cache_get_folio(S1)
| Got folio B here !!!
move_swap_pte
<free folio B from swap cache>
| Holding a reference doesn't pin the cache | as we have demonstrated
<Swapout folio A also using S1>
double_pt_lock
is_pte_pages_stable
| Passed because of S1 is reused
folio_move_anon_rmap(...)
| Moved invalid folio B here !!!

But this is extremely hard to reproduce though, even if doing it deliberately...

So I think a "folio_swap_contains" or equivalent check here is a good thing to have, to make it more robust and easier to understand. The checking after locking a folio has very tiny overhead and can definitely ensure the folio's swap entry is valid and stable.

The "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" here might seem problematic, but it's still not a real problem. That's the case where the swapin in src region happens after the lookup, and before the PTE lock. It will pass the PTE check without moving the folio. But the folio is guaranteed to be a completely new folio here because the folio can't be added back to the page table without holding the PTE lock, and if that happens the following PTE check here will fail.

So I think we should patch the current kernel only adding a "folio_swap_contains" equivalent check here, and maybe more comments, how do you think?

#15 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 297 lines · Kairui Song
Quoted 183 lines · Barry Song
Quoted 181 lines · Kairui Song
Quoted 109 lines · Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:
Quoted 62 lines · Kairui Song
Quoted 39 lines · Barry Song
Quoted 32 lines · Kairui Song
Quoted 19 lines · Barry Song
Quoted 8 lines

From: Kairui Song <[email protected]>

mm/userfaultfd.c +0 -0
index e5a0db7f3331..5b4f01aecf35 100644

mm/userfaultfd.c static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, · line 1415

Quoted 7 lines
mm/userfaultfd.c +4 -0
@@ -1409,6 +1409,10 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
                              goto retry;
                      }
              }
+             if (!folio_swap_contains(src_folio, entry)) {
+                     err = -EBUSY;
+                     goto out;
+             }

It seems we don't need this. In move_swap_pte(), we have been checking pte pages are stable:

Quoted 5 lines
        if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
                                 dst_pmd, dst_pmdval)) {
                double_pt_unlock(dst_ptl, src_ptl);
                return -EAGAIN;
        }

The tricky part is when swap_cache_get_folio returns the folio, both folio and ptes are unlocked. So is it possible that someone else swapped in the entries, then swapped them out again using the same entries?

Quoted 4 lines

The folio will be different here but PTEs are still the same value to they will pass the is_pte_pages_stable check, we previously saw similar races with anon fault or shmem. I think more strict checking won't hurt here.

Quoted 8 lines

This doesn't seem to be the same case as the one you fixed in do_swap_page(). Here, we're hitting the swap cache, whereas in that case, there was no one hitting the swap cache, and you used swap_prepare() to set up the cache to fix the issue.

By the way, if we're not hitting the swap cache, src_folio will be NULL. Also, it seems that folio_swap_contains(src_folio, entry) does not guard against that case either.

Ah, that's true, it should be moved inside the if (folio) {...} block above. Thanks for catching this!

Quoted 3 lines

But I suspect we won't have a problem, since we're not swapping in — we didn't read any stale data, right? Swap-in will only occur after we move the PTEs.

My concern is that a parallel swapin / swapout could result in the folio to be a completely irrelevant or invalid folio.

It's not about the dst, but in the move src side, something like:

Quoted 10 lines

CPU1 CPU2
move_pages_pte
folio = swap_cache_get_folio(...)
| Got folio A here
move_swap_pte
<swapin src_pte, using folio A> <swapout src_pte, put folio A>
| Now folio A is no longer valid. | It's very unlikely but here SWAP | could reuse the same entry as above.

swap_cache_get_folio() does increment the folio's refcount, but it seems this doesn't prevent do_swap_page() from freeing the swap entry after swapping in src_pte with folio A, if it's a read fault.
for write fault, folio_ref_count(folio) == (1 + folio_nr_pages(folio)) will be false:

static inline bool should_try_to_free_swap(struct folio *folio,
struct vm_area_struct *vma, unsigned int fault_flags)

{
       ...

        /*
         * If we want to map a page that's in the swapcache writable, we
         * have to detect via the refcount if we're really the exclusive
         * user. Try freeing the swapcache to get rid of the swapcache
         * reference only in case it's likely that we'll be the exlusive user.
         */
        return (fault_flags & FAULT_FLAG_WRITE) && !folio_test_ksm(folio) &&
                folio_ref_count(folio) == (1 + folio_nr_pages(folio));
}

and for swapout, __removing_mapping does check refcount as well:

static int __remove_mapping(struct address_space *mapping, struct folio *folio,
bool reclaimed, struct mem_cgroup *target_memcg)

{
        refcount = 1 + folio_nr_pages(folio);
        if (!folio_ref_freeze(folio, refcount))
                goto cannot_free;

}

Quoted 7 lines

However, since __remove_mapping() occurs after pageout(), it seems this also doesn't prevent swapout from allocating a new swap entry to fill src_pte.

It seems your concern is valid—unless I'm missing something. Do you have a reproducer? If so, this will likely need a separate fix patch rather than being hidden in this patchset.

Thanks for the analysis. I don't have a reproducer yet, I did some local experiments and that seems possible, but the race window is so tiny and it's very difficult to make the swap entry reuse to collide with that, I'll try more but in theory this seems possible, or at least looks very fragile.

And yeah, let's patch the kernel first if that's a real issue.

Quoted 12 lines
Quoted 11 lines

double_pt_lock
is_pte_pages_stable
| Passed because of entry reuse.
folio_move_anon_rmap(...)
| Moved invalid folio A.

And could it be possible that the swap_cache_get_folio returns NULL here, but later right before the double_pt_lock, a folio is added to swap cache? Maybe we better check the swap cache after clear and releasing dst lock, but before releasing src lock?

Quoted 6 lines

It seems you're suggesting that a parallel swap-in allocates and adds a folio to the swap cache, but the PTE has not yet been updated from a swap entry to a present mapping?

As long as do_swap_page() adds the folio to the swap cache before updating the PTE to present, this scenario seems possible.

Yes, that's two kinds of problems here. I suspected there could be an ABA problem while working on the series, but wasn't certain. And just realised there could be another missed cache read here thanks to your review and discussion :)

Quoted 23 lines

It seems we need to double-check:

mm/userfaultfd.c +7 -1
index bc473ad21202..976053bd2bf1 100644
@@ -1102,8 +1102,14 @@ static int move_swap_pte(struct mm_struct *mm,
 struct vm_area_struct *dst_vma,
        if (src_folio) {
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
+       } else {
+               struct folio *folio =
 filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(folio)) {
+                       double_pt_unlock(dst_ptl, src_ptl);
+                       return -EAGAIN;
+               }
        }
-
        orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
Quoted 2 lines
 #ifdef CONFIG_MEM_SOFT_DIRTY
        orig_src_pte = pte_swp_mksoft_dirty(orig_src_pte);

Maybe it has to get even dirtier here to call swapcache_prepare too to cover the SYNC_IO case?

Quoted 1 line
Quoted 8 lines
Quoted 8 lines
Quoted 2 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Hi Barry,

Quoted 8 lines

I attempted to reproduce the scenario where a folio is added to the swapcache after filemap_get_folio() returns NULL but before move_swap_pte() moves the swap PTE
using non-synchronized I/O. Technically, this seems possible; however, I was unable
to reproduce it, likely because the time window between swapin_readahead and taking the page table lock within do_swap_page() is too short.

Thank you so much for trying this!

I have been trying to reproduce it too, and so far I didn't observe any crash or warn. I added following debug code:

static __always_inline
bool validate_dst_vma(struct vm_area_struct *dst_vma, unsigned long dst_end)

(quoted diff) +1 -0
@@ -1163,6 +1167,7 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
        pmd_t dummy_pmdval;
        pmd_t dst_pmdval;
        struct folio *src_folio = NULL;
+       struct folio *tmp_folio = NULL;
        struct anon_vma *src_anon_vma = NULL;
        struct mmu_notifier_range range;
        int err = 0;
(quoted diff) +9 -0
@@ -1391,6 +1396,15 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));
+               udelay(get_random_u32_below(1000));
+               tmp_folio = filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(tmp_folio)) {
+                       if (!IS_ERR_OR_NULL(folio) && tmp_folio != folio) {
+                               pr_err("UFFDIO_MOVE: UNSTABLE folio
 %lx (%lx) -> %lx (%lx)\n", folio, folio->swap.val, tmp_folio,
 tmp_folio->swap.val);
+                       }
+                       folio_put(tmp_folio);
+               }
                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;
(quoted diff) +2 -0
@@ -1413,6 +1427,8 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                err = move_swap_pte(mm, dst_vma, dst_addr, src_addr,
 dst_pte, src_pte,
                                orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                                dst_ptl, src_ptl, src_folio);
+               if (tmp_folio != folio && !err)
+                       pr_err("UFFDIO_MOVE: UNSTABLE folio passed
 check: %lx -> %lx\n", folio, tmp_folio);
        }

And I saw these two prints are getting triggered like this (not a real issue though, just help to understand the problem)
...

[ 3127.632791] UFFDIO_MOVE: UNSTABLE folio fffffdffc334cd00 (0) ->
fffffdffc7ccac80 (51)
[ 3172.033269] UFFDIO_MOVE: UNSTABLE folio fffffdffc343bb40 (0) ->
fffffdffc3435e00 (3b)
[ 3194.425213] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d481c0 (0) ->
fffffdffc34ab8c0 (76)
[ 3194.991318] UFFDIO_MOVE: UNSTABLE folio fffffdffc34f95c0 (0) ->
fffffdffc34ab8c0 (6d)
[ 3203.467212] UFFDIO_MOVE: UNSTABLE folio fffffdffc34b13c0 (0) ->
fffffdffc34eda80 (32)
[ 3206.217820] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d297c0 (0) ->
fffffdffc38cedc0 (b)
[ 3214.913039] UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc34db140
[ 3217.066972] UFFDIO_MOVE: UNSTABLE folio fffffdffc342b5c0 (0) ->

fffffdffc3465cc0 (21)
...

The "UFFDIO_MOVE: UNSTABLE folio fffffdffc3435180 (0) -> fffffdffc3853540 (53)" worries me at first. On first look it seems the folio is indeed freed completely from the swap cache after the first lookup, so another swapout can reuse the entry. But as you mentioned __remove_mapping won't release a folio if the refcount check fails, so they must be freed by folio_free_swap or __try_to_reclaim_swap, there are many places that can happen. But these two helpers won't free a folio from swap cache if its swap count is not zero. And the folio will either be swapped out (swap count non zero), or mapped (freeing it is fine, PTE is non_swap, and another swapout will still use the same folio).

So after more investigation and dumping the pages, it's actually the second lookup (tmp_folio) seeing the entry being reused by another page table entry, after the first folio is swapped back and released. So the page table check below will always fail just fine.

But this also proves the first look up can see a completely irrelevant folio too: If the src folio is swapped out, but got swapped back and freed, then another folio B shortly got added to swap cache reuse the src folio's old swap entry, then the folio B got seen by the look up here and get freed from swap cache, then src folio got swapped out again also reusing the same entry, then we have a problem as PTE seems untouched indeed but we grabbed a wrong folio. Seems possible if I'm not wrong:

Quoted 7 lines

Something like this:
CPU1 CPU2
move_pages_pte
entry = pte_to_swp_entry(orig_src_pte);
| Got Swap Entry S1 from src_pte
...
<swapin src_pte, using folio A>

I’m assuming you mean <swapin src_pte, using folio B>, since I’m not sure where folio B comes from in the statement <someone else tried to swap out folio B>.

If that assumption is correct, and folio A is still in the swapcache, how could someone swap in folio B without hitting folio A? That would suggest folio A must have been removed from the swapcache earlier—right?

Quoted 34 lines

<free folio A from swap cache freeing S1> <someone else try swap out folio B > <put folio B to swap cache using S1 >
...
folio = swap_cache_get_folio(S1)
| Got folio B here !!!
move_swap_pte
<free folio B from swap cache>
| Holding a reference doesn't pin the cache | as we have demonstrated
<Swapout folio A also using S1>
double_pt_lock
is_pte_pages_stable
| Passed because of S1 is reused
folio_move_anon_rmap(...)
| Moved invalid folio B here !!!

But this is extremely hard to reproduce though, even if doing it deliberately...

So I think a "folio_swap_contains" or equivalent check here is a good thing to have, to make it more robust and easier to understand. The checking after locking a folio has very tiny overhead and can definitely ensure the folio's swap entry is valid and stable.

The "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" here might seem problematic, but it's still not a real problem. That's the case where the swapin in src region happens after the lookup, and before the PTE lock. It will pass the PTE check without moving the folio. But the folio is guaranteed to be a completely new folio here because the folio can't be added back to the page table without holding the PTE lock, and if that happens the following PTE check here will fail.

Quoted 3 lines

So I think we should patch the current kernel only adding a "folio_swap_contains" equivalent check here, and maybe more comments, how do you think?

The description appears to have some inconsistencies. Would you mind rephrasing it?

Thanks
barry

#16 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 132 lines · Barry Song
Quoted 118 lines · Kairui Song
Quoted 4 lines · Barry Song
Quoted 2 lines · Kairui Song

Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:

Quoted 8 lines
Quoted 8 lines
Quoted 2 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Hi Barry,

Quoted 8 lines

I attempted to reproduce the scenario where a folio is added to the swapcache after filemap_get_folio() returns NULL but before move_swap_pte() moves the swap PTE
using non-synchronized I/O. Technically, this seems possible; however, I was unable
to reproduce it, likely because the time window between swapin_readahead and taking the page table lock within do_swap_page() is too short.

Thank you so much for trying this!

I have been trying to reproduce it too, and so far I didn't observe any crash or warn. I added following debug code:

static __always_inline
bool validate_dst_vma(struct vm_area_struct *dst_vma, unsigned long dst_end)

(quoted diff) +1 -0
@@ -1163,6 +1167,7 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
        pmd_t dummy_pmdval;
        pmd_t dst_pmdval;
        struct folio *src_folio = NULL;
+       struct folio *tmp_folio = NULL;
        struct anon_vma *src_anon_vma = NULL;
        struct mmu_notifier_range range;
        int err = 0;
(quoted diff) +9 -0
@@ -1391,6 +1396,15 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));
+               udelay(get_random_u32_below(1000));
+               tmp_folio = filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(tmp_folio)) {
+                       if (!IS_ERR_OR_NULL(folio) && tmp_folio != folio) {
+                               pr_err("UFFDIO_MOVE: UNSTABLE folio
 %lx (%lx) -> %lx (%lx)\n", folio, folio->swap.val, tmp_folio,
 tmp_folio->swap.val);
+                       }
+                       folio_put(tmp_folio);
+               }
                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;
(quoted diff) +2 -0
@@ -1413,6 +1427,8 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                err = move_swap_pte(mm, dst_vma, dst_addr, src_addr,
 dst_pte, src_pte,
                                orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                                dst_ptl, src_ptl, src_folio);
+               if (tmp_folio != folio && !err)
+                       pr_err("UFFDIO_MOVE: UNSTABLE folio passed
 check: %lx -> %lx\n", folio, tmp_folio);
        }

And I saw these two prints are getting triggered like this (not a real issue though, just help to understand the problem)
...

[ 3127.632791] UFFDIO_MOVE: UNSTABLE folio fffffdffc334cd00 (0) ->
fffffdffc7ccac80 (51)
[ 3172.033269] UFFDIO_MOVE: UNSTABLE folio fffffdffc343bb40 (0) ->
fffffdffc3435e00 (3b)
[ 3194.425213] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d481c0 (0) ->
fffffdffc34ab8c0 (76)
[ 3194.991318] UFFDIO_MOVE: UNSTABLE folio fffffdffc34f95c0 (0) ->
fffffdffc34ab8c0 (6d)
[ 3203.467212] UFFDIO_MOVE: UNSTABLE folio fffffdffc34b13c0 (0) ->
fffffdffc34eda80 (32)
[ 3206.217820] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d297c0 (0) ->
fffffdffc38cedc0 (b)
[ 3214.913039] UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc34db140
[ 3217.066972] UFFDIO_MOVE: UNSTABLE folio fffffdffc342b5c0 (0) ->

fffffdffc3465cc0 (21)
...

The "UFFDIO_MOVE: UNSTABLE folio fffffdffc3435180 (0) -> fffffdffc3853540 (53)" worries me at first. On first look it seems the folio is indeed freed completely from the swap cache after the first lookup, so another swapout can reuse the entry. But as you mentioned __remove_mapping won't release a folio if the refcount check fails, so they must be freed by folio_free_swap or __try_to_reclaim_swap, there are many places that can happen. But these two helpers won't free a folio from swap cache if its swap count is not zero. And the folio will either be swapped out (swap count non zero), or mapped (freeing it is fine, PTE is non_swap, and another swapout will still use the same folio).

So after more investigation and dumping the pages, it's actually the second lookup (tmp_folio) seeing the entry being reused by another page table entry, after the first folio is swapped back and released. So the page table check below will always fail just fine.

But this also proves the first look up can see a completely irrelevant folio too: If the src folio is swapped out, but got swapped back and freed, then another folio B shortly got added to swap cache reuse the src folio's old swap entry, then the folio B got seen by the look up here and get freed from swap cache, then src folio got swapped out again also reusing the same entry, then we have a problem as PTE seems untouched indeed but we grabbed a wrong folio. Seems possible if I'm not wrong:

Quoted 7 lines

Something like this:
CPU1 CPU2
move_pages_pte
entry = pte_to_swp_entry(orig_src_pte);
| Got Swap Entry S1 from src_pte
...
<swapin src_pte, using folio A>

I’m assuming you mean <swapin src_pte, using folio B>, since I’m not sure where folio B comes from in the statement <someone else tried to swap out folio B>.

Quoted 6 lines

If that assumption is correct, and folio A is still in the swapcache, how could someone swap in folio B without hitting folio A? That would suggest folio A must have been removed from the swapcache earlier—right?

Quoted 2 lines

<free folio A from swap cache freeing S1> <someone else try swap out folio B >

Sorry my bad, I think I made people think folio B is related to src_pte at this point. What I actually mean is that: Another random folio B, unrelated to src_pte, could got swapped out, and using the swap entry S1.

Quoted 32 lines
Quoted 32 lines

<put folio B to swap cache using S1 >
...
folio = swap_cache_get_folio(S1)
| Got folio B here !!!
move_swap_pte
<free folio B from swap cache>
| Holding a reference doesn't pin the cache | as we have demonstrated
<Swapout folio A also using S1>
double_pt_lock
is_pte_pages_stable
| Passed because of S1 is reused
folio_move_anon_rmap(...)
| Moved invalid folio B here !!!

But this is extremely hard to reproduce though, even if doing it deliberately...

So I think a "folio_swap_contains" or equivalent check here is a good thing to have, to make it more robust and easier to understand. The checking after locking a folio has very tiny overhead and can definitely ensure the folio's swap entry is valid and stable.

The "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" here might seem problematic, but it's still not a real problem. That's the case where the swapin in src region happens after the lookup, and before the PTE lock. It will pass the PTE check without moving the folio. But the folio is guaranteed to be a completely new folio here because the folio can't be added back to the page table without holding the PTE lock, and if that happens the following PTE check here will fail.

Quoted 6 lines
Quoted 3 lines

So I think we should patch the current kernel only adding a "folio_swap_contains" equivalent check here, and maybe more comments, how do you think?

The description appears to have some inconsistencies. Would you mind rephrasing it?

Yeah, let's ignore the "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" part first, as both you and me have come into a conclusion that "filemap_get_folio() returns NULL before move_swap_pte, but a folio was added to swap cache" is OK, and this output only proves that happens.

So the problematic race is:

Here move_pages_pte is moving src_pte to dst_pte, and it begins with src_pte == swap entry S1, and S1 isn't cached.

CPU1 CPU2
move_pages_pte()

  entry = pte_to_swp_entry(orig_src_pte);
    | src_pte is absent, and got entry == S1
  ... < Somehow interrupted> ...
                                 <swapin src_pte, using folio A>
                                   | folio A is just a new allocated folio
                                   | for resolving the swap in fault.
                                 <free folio A from swap cache freeing S1>
                                   | swap in fault is resolved, src_pte
                                   | now points to folio A, so folio A
                                   | can get freed just fine.
                                   | And now S1 is free to be used
                                   | by anyone.
                                 <someone else try swap out another folio B >
                                   | Folio B is a completely unrelated
                                   | folio swapped out by random process.
                                   | (has nothing to do with src_pte)
                                   | But S1 is freed so it may use S1
                                   | as its swap entry.
                                 <put folio B to swap cache with index S1 >
                                 ...
  folio = filemap_get_folio(S1)
    | The lookup is using S1, so it
    | got folio B here !!!
  ... < Somehow interrupted> ...
                                 <free folio B from swap cache>
                                   | Folio B could fail to be swapped out
                                   | or got swapped in again, so it can
                                   | be freed by folio_free_swap or
                                   | swap cache reclaim.
                                   | CPU1 is holding a reference but it
                                   | doesn't pin the swap cache folio
                                   | as I have demonstrated with the
                                   | test C program previously.
                                   | New S1 is free to be used again.
                                 <Swapout src_pte again using S1>
                                   | No thing blocks this from happening
                                   | The swapout is still using folio A,
                                   | and src_pte == S1.
  folio_trylock(folio)
  move_swap_pte
    double_pt_lock
    is_pte_pages_stable
      | Passed because of S1 is reused so src_pte == S1.
    folio_move_anon_rmap(...)
      | Moved invalid folio B here !!!

It's a long and complex one, I don't think it's practically possible to happen in reality but in theory doable, once in a million maybe... Still we have to fix it, or did I got anything wrong here?

Quoted 3 lines

Thanks
barry

#17 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 244 lines · Kairui Song
Quoted 132 lines · Barry Song
Quoted 118 lines · Kairui Song
Quoted 4 lines · Barry Song
Quoted 2 lines · Kairui Song

Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:

Quoted 8 lines
Quoted 8 lines
Quoted 2 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Hi Barry,

Quoted 8 lines

I attempted to reproduce the scenario where a folio is added to the swapcache after filemap_get_folio() returns NULL but before move_swap_pte() moves the swap PTE
using non-synchronized I/O. Technically, this seems possible; however, I was unable
to reproduce it, likely because the time window between swapin_readahead and taking the page table lock within do_swap_page() is too short.

Thank you so much for trying this!

I have been trying to reproduce it too, and so far I didn't observe any crash or warn. I added following debug code:

static __always_inline
bool validate_dst_vma(struct vm_area_struct *dst_vma, unsigned long dst_end)

(quoted diff) +1 -0
@@ -1163,6 +1167,7 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
        pmd_t dummy_pmdval;
        pmd_t dst_pmdval;
        struct folio *src_folio = NULL;
+       struct folio *tmp_folio = NULL;
        struct anon_vma *src_anon_vma = NULL;
        struct mmu_notifier_range range;
        int err = 0;
(quoted diff) +9 -0
@@ -1391,6 +1396,15 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));
+               udelay(get_random_u32_below(1000));
+               tmp_folio = filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(tmp_folio)) {
+                       if (!IS_ERR_OR_NULL(folio) && tmp_folio != folio) {
+                               pr_err("UFFDIO_MOVE: UNSTABLE folio
 %lx (%lx) -> %lx (%lx)\n", folio, folio->swap.val, tmp_folio,
 tmp_folio->swap.val);
+                       }
+                       folio_put(tmp_folio);
+               }
                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;
(quoted diff) +2 -0
@@ -1413,6 +1427,8 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                err = move_swap_pte(mm, dst_vma, dst_addr, src_addr,
 dst_pte, src_pte,
                                orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                                dst_ptl, src_ptl, src_folio);
+               if (tmp_folio != folio && !err)
+                       pr_err("UFFDIO_MOVE: UNSTABLE folio passed
 check: %lx -> %lx\n", folio, tmp_folio);
        }

And I saw these two prints are getting triggered like this (not a real issue though, just help to understand the problem)
...

[ 3127.632791] UFFDIO_MOVE: UNSTABLE folio fffffdffc334cd00 (0) ->
fffffdffc7ccac80 (51)
[ 3172.033269] UFFDIO_MOVE: UNSTABLE folio fffffdffc343bb40 (0) ->
fffffdffc3435e00 (3b)
[ 3194.425213] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d481c0 (0) ->
fffffdffc34ab8c0 (76)
[ 3194.991318] UFFDIO_MOVE: UNSTABLE folio fffffdffc34f95c0 (0) ->
fffffdffc34ab8c0 (6d)
[ 3203.467212] UFFDIO_MOVE: UNSTABLE folio fffffdffc34b13c0 (0) ->
fffffdffc34eda80 (32)
[ 3206.217820] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d297c0 (0) ->
fffffdffc38cedc0 (b)
[ 3214.913039] UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc34db140
[ 3217.066972] UFFDIO_MOVE: UNSTABLE folio fffffdffc342b5c0 (0) ->

fffffdffc3465cc0 (21)
...

The "UFFDIO_MOVE: UNSTABLE folio fffffdffc3435180 (0) -> fffffdffc3853540 (53)" worries me at first. On first look it seems the folio is indeed freed completely from the swap cache after the first lookup, so another swapout can reuse the entry. But as you mentioned __remove_mapping won't release a folio if the refcount check fails, so they must be freed by folio_free_swap or __try_to_reclaim_swap, there are many places that can happen. But these two helpers won't free a folio from swap cache if its swap count is not zero. And the folio will either be swapped out (swap count non zero), or mapped (freeing it is fine, PTE is non_swap, and another swapout will still use the same folio).

So after more investigation and dumping the pages, it's actually the second lookup (tmp_folio) seeing the entry being reused by another page table entry, after the first folio is swapped back and released. So the page table check below will always fail just fine.

But this also proves the first look up can see a completely irrelevant folio too: If the src folio is swapped out, but got swapped back and freed, then another folio B shortly got added to swap cache reuse the src folio's old swap entry, then the folio B got seen by the look up here and get freed from swap cache, then src folio got swapped out again also reusing the same entry, then we have a problem as PTE seems untouched indeed but we grabbed a wrong folio. Seems possible if I'm not wrong:

Quoted 7 lines

Something like this:
CPU1 CPU2
move_pages_pte
entry = pte_to_swp_entry(orig_src_pte);
| Got Swap Entry S1 from src_pte
...
<swapin src_pte, using folio A>

I’m assuming you mean <swapin src_pte, using folio B>, since I’m not sure where folio B comes from in the statement <someone else tried to swap out folio B>.

Quoted 6 lines

If that assumption is correct, and folio A is still in the swapcache, how could someone swap in folio B without hitting folio A? That would suggest folio A must have been removed from the swapcache earlier—right?

Quoted 2 lines

<free folio A from swap cache freeing S1> <someone else try swap out folio B >

Sorry my bad, I think I made people think folio B is related to src_pte at this point. What I actually mean is that: Another random folio B, unrelated to src_pte, could got swapped out, and using the swap entry S1.

Quoted 32 lines
Quoted 32 lines

<put folio B to swap cache using S1 >
...
folio = swap_cache_get_folio(S1)
| Got folio B here !!!
move_swap_pte
<free folio B from swap cache>
| Holding a reference doesn't pin the cache | as we have demonstrated
<Swapout folio A also using S1>
double_pt_lock
is_pte_pages_stable
| Passed because of S1 is reused
folio_move_anon_rmap(...)
| Moved invalid folio B here !!!

But this is extremely hard to reproduce though, even if doing it deliberately...

So I think a "folio_swap_contains" or equivalent check here is a good thing to have, to make it more robust and easier to understand. The checking after locking a folio has very tiny overhead and can definitely ensure the folio's swap entry is valid and stable.

The "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" here might seem problematic, but it's still not a real problem. That's the case where the swapin in src region happens after the lookup, and before the PTE lock. It will pass the PTE check without moving the folio. But the folio is guaranteed to be a completely new folio here because the folio can't be added back to the page table without holding the PTE lock, and if that happens the following PTE check here will fail.

Quoted 6 lines
Quoted 3 lines

So I think we should patch the current kernel only adding a "folio_swap_contains" equivalent check here, and maybe more comments, how do you think?

The description appears to have some inconsistencies. Would you mind rephrasing it?

Yeah, let's ignore the "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" part first, as both you and me have come into a conclusion that "filemap_get_folio() returns NULL before move_swap_pte, but a folio was added to swap cache" is OK, and this output only proves that happens.

So the problematic race is:

Here move_pages_pte is moving src_pte to dst_pte, and it begins with src_pte == swap entry S1, and S1 isn't cached.

CPU1 CPU2
move_pages_pte()

  entry = pte_to_swp_entry(orig_src_pte);
    | src_pte is absent, and got entry == S1
  ... < Somehow interrupted> ...
                                 <swapin src_pte, using folio A>
                                   | folio A is just a new allocated folio
                                   | for resolving the swap in fault.
                                 <free folio A from swap cache freeing S1>
                                   | swap in fault is resolved, src_pte
                                   | now points to folio A, so folio A
                                   | can get freed just fine.
                                   | And now S1 is free to be used
                                   | by anyone.
                                 <someone else try swap out another folio B >
                                   | Folio B is a completely unrelated
                                   | folio swapped out by random process.
                                   | (has nothing to do with src_pte)
                                   | But S1 is freed so it may use S1
                                   | as its swap entry.
                                 <put folio B to swap cache with index S1 >
                                 ...
  folio = filemap_get_folio(S1)
    | The lookup is using S1, so it
    | got folio B here !!!
  ... < Somehow interrupted> ...
                                 <free folio B from swap cache>
                                   | Folio B could fail to be swapped out
                                   | or got swapped in again, so it can
                                   | be freed by folio_free_swap or
                                   | swap cache reclaim.
                                   | CPU1 is holding a reference but it
                                   | doesn't pin the swap cache folio
                                   | as I have demonstrated with the
                                   | test C program previously.
                                   | New S1 is free to be used again.
                                 <Swapout src_pte again using S1>
                                   | No thing blocks this from happening
                                   | The swapout is still using folio A,
                                   | and src_pte == S1.
  folio_trylock(folio)
  move_swap_pte
    double_pt_lock
    is_pte_pages_stable
      | Passed because of S1 is reused so src_pte == S1.
    folio_move_anon_rmap(...)
      | Moved invalid folio B here !!!
Quoted 3 lines

It's a long and complex one, I don't think it's practically possible to happen in reality but in theory doable, once in a million maybe... Still we have to fix it, or did I got anything wrong here?

Hi Barry,

I managed to reproduce this issue, by hacking the kernel a bit (Only adding only delay to increase the race window, and adding a WARN to indicate the problem)

1. Applying following patch for kernel:
===

mm/userfaultfd.c +1 -0
index bc473ad21202..1d710adf9839 100644
@@ -15,6 +15,7 @@
 #include <linux/mmu_notifier.h>
 #include <linux/hugetlb.h>
 #include <linux/shmem_fs.h>
+#include <linux/delay.h>
 #include <asm/tlbflush.h>
 #include <asm/tlb.h>
 #include "internal.h"
@@ -1100,6 +1101,10 @@ static int move_swap_pte(struct mm_struct *mm,

struct vm_area_struct *dst_vma,

         * occur and hit the swapcache after moving the PTE.
         */
        if (src_folio) {

+ if (WARN_ON(src_folio->swap.val !=
pte_to_swp_entry(orig_src_pte).val))
+ pr_err("Moving folio %lx (folio->swap = %lx),
orig_src_pte = %lx\n",
+ (unsigned long)src_folio, src_folio->swap.val,

+                              pte_to_swp_entry(orig_src_pte).val);
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
        }

@@ -1388,9 +1393,13 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
* folios in the swapcache. This issue needs to be resolved
* separately to allow proper handling.
*/
+ pr_err("DEBUG: Will do the lookup using entry %lx,
wait 3s...\n", entry.val);

+               mdelay(1000 * 3);
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));
+               pr_err("DEBUG: Got folio value %lx, wait 3s...\n",
(unsigned long)folio);
+               mdelay(1000 * 3);
                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;

2. Save following program in userspace (didn't bother with error check for simpler code):
===
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>

#define PAGE_SIZE 4096
/* Need to consume all slots so define the swap device size here */ #define SWAP_DEVICE_SIZE (PAGE_SIZE * 1024 - 1)

char *src, *race, *dst, *place_holder;
int uffd;

void read_in(char *p) {
    /* This test program initials memory with 0xAA to bypass zeromap */
    while (*((volatile char*)p) != 0xAA);
}
void *reader_thread(void *arg) {
    /* Test requires kernel to wait upon uffd move */
    read_in(dst);
    return NULL;
}
void *fault_handler_thread(void *arg) {
    int ret;
    struct uffd_msg msg;
    struct uffdio_move move;
    struct pollfd pollfd = { .fd = uffd, .events = POLLIN };
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

    while (1) {
        poll(&pollfd, 1, -1);
        read(uffd, &msg, sizeof(msg));

        move.src = (unsigned long)src + (msg.arg.pagefault.address -
            (unsigned long)dst);
        move.dst = msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
        move.len = PAGE_SIZE;
        move.mode = 0;

        ioctl(uffd, UFFDIO_MOVE, &move);
    }
    return NULL;
}
int main() {
    pthread_t fault_handler_thr, reader_thr;
    struct uffdio_api uffdio_api = { .api = UFFD_API, .features = 0 };
    struct uffdio_register uffdio_register;

    src = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE |
MAP_ANONYMOUS, -1, 0);
    dst = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE |
MAP_ANONYMOUS, -1, 0);
    memset(src, 0xAA, PAGE_SIZE);
    madvise(src, PAGE_SIZE, MADV_PAGEOUT);

    /* Consume all slots on swap device left only one entry (S1) */
    place_holder = mmap(NULL, SWAP_DEVICE_SIZE - 1, PROT_READ |
PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memset(place_holder, 0xAA, SWAP_DEVICE_SIZE - 1);
    madvise(place_holder, SWAP_DEVICE_SIZE - 1, MADV_PAGEOUT);

    /* Setup uffd handler and dst reader */
    uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    ioctl(uffd, UFFDIO_API, &uffdio_api);
    uffdio_register.range.start = (unsigned long)dst;
    uffdio_register.range.len = PAGE_SIZE;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);
    pthread_create(&fault_handler_thr, NULL, fault_handler_thread, NULL);
    pthread_create(&reader_thr, NULL, reader_thread, NULL);

    /* Wait for UFFDIO to start */
    sleep(1);

    /* Release src folio (A) from swap, freeing the entry S1 */
    read_in(src);

    /* Swapout another race folio (B) using S1 */
    race = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED |
MAP_ANONYMOUS, -1, 0);
    memset(race, 0xAA, PAGE_SIZE);
    madvise(race, PAGE_SIZE, MADV_PAGEOUT);

    /* Wait for UFFDIO swap lookup to see the race folio (B) */
    sleep(3);

    /* Free the race folio (B) from swap */
    read_in(race);
    /* And swap out src folio (A) again, using S1 */
    madvise(src, PAGE_SIZE, MADV_PAGEOUT);

    /* Kernel should have moved a wrong folio by now */

    pthread_join(reader_thr, NULL);
    pthread_cancel(fault_handler_thr);
    pthread_join(fault_handler_thr, NULL);
    munmap(race, PAGE_SIZE);
    munmap(src, PAGE_SIZE);
    munmap(dst, PAGE_SIZE);
    close(uffd);

    return 0;
}

3. Run the test with (ensure no other swap device is mounted and current dir is on a block device):
===
dd if=/dev/zero of=swap.img bs=1M count=1; mkswap swap.img; swapon swap.img; gcc test-uffd.c && ./a.out

Then we get the WARN:

[  348.200587] ------------[ cut here ]------------
[  348.200599] WARNING: CPU: 7 PID: 1856 at mm/userfaultfd.c:1104
move_pages_pte+0xdb8/0x11a0
[  348.207544] Modules linked in: loop
[  348.209401] CPU: 7 UID: 0 PID: 1856 Comm: a.out Kdump: loaded Not
tainted 6.15.0-rc6ptch-00381-g99f00d7c6c6f-dirty #304
PREEMPT(voluntary)
[  348.214579] Hardware name: QEMU QEMU Virtual Machine, BIOS
edk2-stable202408-prebuilt.qemu.org 08/13/2024
[  348.218656] pstate: 81400005 (Nzcv daif +PAN -UAO -TCO +DIT -SSBS BTYPE=--)
[  348.222013] pc : move_pages_pte+0xdb8/0x11a0
[  348.224062] lr : move_pages_pte+0x928/0x11a0
[  348.225881] sp : ffff800088b2b8f0
[  348.227360] x29: ffff800088b2b970 x28: 0000000000000000 x27: 0000ffffbc920000
[  348.230228] x26: fffffdffc335e4a8 x25: 0000000000000001 x24: fffffdffc3e4dd40
[  348.233159] x23: 080000010d792403 x22: ffff0000cd792900 x21: ffff0000c5a6d2c0
[  348.236339] x20: fffffdffc335e4a8 x19: 0000000000001004 x18: 0000000000000006
[  348.239269] x17: 0000ffffbc920000 x16: 0000ffffbc922fff x15: 0000000000000003
[  348.242703] x14: ffff8000812c3b68 x13: 0000000000000003 x12: 0000000000000003
[  348.245947] x11: 0000000000000000 x10: ffff800081e4feb8 x9 : 0000000000000001
[  348.249284] x8 : 0000000000000000 x7 : 6f696c6f6620746f x6 : 47203a4755424544
[  348.252071] x5 : ffff8000815789e3 x4 : ffff8000815789e5 x3 : 0000000000000000
[  348.255358] x2 : ffff0001fed2aef0 x1 : 0000000000000000 x0 : fffffdffc335e4a8
[  348.258134] Call trace:
[  348.259468]  move_pages_pte+0xdb8/0x11a0 (P)
[  348.261348]  move_pages+0x3c0/0x738
[  348.262987]  userfaultfd_ioctl+0x3d8/0x1f98
[  348.264916]  __arm64_sys_ioctl+0x88/0xd0
[  348.266779]  invoke_syscall+0x64/0xec
[  348.268347]  el0_svc_common+0xa8/0xd8
[  348.269967]  do_el0_svc+0x1c/0x28
[  348.271711]  el0_svc+0x40/0xe0
[  348.273345]  el0t_64_sync_handler+0x78/0x108
[  348.274821]  el0t_64_sync+0x19c/0x1a0
[  348.276117] ---[ end trace 0000000000000000 ]---
[  348.278638] Moving folio fffffdffc3e4dd40 (folio->swap = 0), orig_src_pte = 1

That's the new added WARN, but the test program also hung with D forever, and with errors with other tests like:

[  406.893936] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0
type:MM_ANONPAGES val:-1
[  406.894071] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0

type:MM_SHMEMPAGES val:1

Because the kernel just moved the wrong folio, so unmap takes forever looking for the missing folio, and counting went wrong too.

So this race is real. It's extremely unlikely to happen because it requires multiple collisions of multiple tiny race windows, however it's not impossible.

I'll post a fix very soon.

#18 lore raw

Re: [PATCH 05/28] mm, swap: sanitize swap cache lookup convention

Quoted 473 lines · Kairui Song
Quoted 244 lines · Kairui Song
Quoted 132 lines · Barry Song
Quoted 118 lines · Kairui Song
Quoted 4 lines · Barry Song
Quoted 2 lines · Kairui Song

Barry Song <[email protected]> 于 2025年5月21日周三 06:33写道:

Quoted 8 lines
Quoted 8 lines
Quoted 2 lines

Let me run test case [1] to check whether this ever happens. I guess I need to hack kernel a bit to always add folio to swapcache even for SYNC IO.

That will cause quite a performance regression I think. Good thing is, that's exactly the problem this series is solving by dropping the SYNC IO swapin path and never bypassing the swap cache, while improving the performance, eliminating things like this. One more reason to justify the approach :)

Hi Barry,

Quoted 8 lines

I attempted to reproduce the scenario where a folio is added to the swapcache after filemap_get_folio() returns NULL but before move_swap_pte() moves the swap PTE
using non-synchronized I/O. Technically, this seems possible; however, I was unable
to reproduce it, likely because the time window between swapin_readahead and taking the page table lock within do_swap_page() is too short.

Thank you so much for trying this!

I have been trying to reproduce it too, and so far I didn't observe any crash or warn. I added following debug code:

static __always_inline
bool validate_dst_vma(struct vm_area_struct *dst_vma, unsigned long dst_end)

(quoted diff) +1 -0
@@ -1163,6 +1167,7 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
        pmd_t dummy_pmdval;
        pmd_t dst_pmdval;
        struct folio *src_folio = NULL;
+       struct folio *tmp_folio = NULL;
        struct anon_vma *src_anon_vma = NULL;
        struct mmu_notifier_range range;
        int err = 0;
(quoted diff) +9 -0
@@ -1391,6 +1396,15 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));
+               udelay(get_random_u32_below(1000));
+               tmp_folio = filemap_get_folio(swap_address_space(entry),
+                                       swap_cache_index(entry));
+               if (!IS_ERR_OR_NULL(tmp_folio)) {
+                       if (!IS_ERR_OR_NULL(folio) && tmp_folio != folio) {
+                               pr_err("UFFDIO_MOVE: UNSTABLE folio
 %lx (%lx) -> %lx (%lx)\n", folio, folio->swap.val, tmp_folio,
 tmp_folio->swap.val);
+                       }
+                       folio_put(tmp_folio);
+               }
                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;
(quoted diff) +2 -0
@@ -1413,6 +1427,8 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                err = move_swap_pte(mm, dst_vma, dst_addr, src_addr,
 dst_pte, src_pte,
                                orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
                                dst_ptl, src_ptl, src_folio);
+               if (tmp_folio != folio && !err)
+                       pr_err("UFFDIO_MOVE: UNSTABLE folio passed
 check: %lx -> %lx\n", folio, tmp_folio);
        }

And I saw these two prints are getting triggered like this (not a real issue though, just help to understand the problem)
...

[ 3127.632791] UFFDIO_MOVE: UNSTABLE folio fffffdffc334cd00 (0) ->
fffffdffc7ccac80 (51)
[ 3172.033269] UFFDIO_MOVE: UNSTABLE folio fffffdffc343bb40 (0) ->
fffffdffc3435e00 (3b)
[ 3194.425213] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d481c0 (0) ->
fffffdffc34ab8c0 (76)
[ 3194.991318] UFFDIO_MOVE: UNSTABLE folio fffffdffc34f95c0 (0) ->
fffffdffc34ab8c0 (6d)
[ 3203.467212] UFFDIO_MOVE: UNSTABLE folio fffffdffc34b13c0 (0) ->
fffffdffc34eda80 (32)
[ 3206.217820] UFFDIO_MOVE: UNSTABLE folio fffffdffc7d297c0 (0) ->
fffffdffc38cedc0 (b)
[ 3214.913039] UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc34db140
[ 3217.066972] UFFDIO_MOVE: UNSTABLE folio fffffdffc342b5c0 (0) ->

fffffdffc3465cc0 (21)
...

The "UFFDIO_MOVE: UNSTABLE folio fffffdffc3435180 (0) -> fffffdffc3853540 (53)" worries me at first. On first look it seems the folio is indeed freed completely from the swap cache after the first lookup, so another swapout can reuse the entry. But as you mentioned __remove_mapping won't release a folio if the refcount check fails, so they must be freed by folio_free_swap or __try_to_reclaim_swap, there are many places that can happen. But these two helpers won't free a folio from swap cache if its swap count is not zero. And the folio will either be swapped out (swap count non zero), or mapped (freeing it is fine, PTE is non_swap, and another swapout will still use the same folio).

So after more investigation and dumping the pages, it's actually the second lookup (tmp_folio) seeing the entry being reused by another page table entry, after the first folio is swapped back and released. So the page table check below will always fail just fine.

But this also proves the first look up can see a completely irrelevant folio too: If the src folio is swapped out, but got swapped back and freed, then another folio B shortly got added to swap cache reuse the src folio's old swap entry, then the folio B got seen by the look up here and get freed from swap cache, then src folio got swapped out again also reusing the same entry, then we have a problem as PTE seems untouched indeed but we grabbed a wrong folio. Seems possible if I'm not wrong:

Quoted 7 lines

Something like this:
CPU1 CPU2
move_pages_pte
entry = pte_to_swp_entry(orig_src_pte);
| Got Swap Entry S1 from src_pte
...
<swapin src_pte, using folio A>

I’m assuming you mean <swapin src_pte, using folio B>, since I’m not sure where folio B comes from in the statement <someone else tried to swap out folio B>.

Quoted 6 lines

If that assumption is correct, and folio A is still in the swapcache, how could someone swap in folio B without hitting folio A? That would suggest folio A must have been removed from the swapcache earlier—right?

Quoted 2 lines

<free folio A from swap cache freeing S1> <someone else try swap out folio B >

Sorry my bad, I think I made people think folio B is related to src_pte at this point. What I actually mean is that: Another random folio B, unrelated to src_pte, could got swapped out, and using the swap entry S1.

Quoted 32 lines
Quoted 32 lines

<put folio B to swap cache using S1 >
...
folio = swap_cache_get_folio(S1)
| Got folio B here !!!
move_swap_pte
<free folio B from swap cache>
| Holding a reference doesn't pin the cache | as we have demonstrated
<Swapout folio A also using S1>
double_pt_lock
is_pte_pages_stable
| Passed because of S1 is reused
folio_move_anon_rmap(...)
| Moved invalid folio B here !!!

But this is extremely hard to reproduce though, even if doing it deliberately...

So I think a "folio_swap_contains" or equivalent check here is a good thing to have, to make it more robust and easier to understand. The checking after locking a folio has very tiny overhead and can definitely ensure the folio's swap entry is valid and stable.

The "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" here might seem problematic, but it's still not a real problem. That's the case where the swapin in src region happens after the lookup, and before the PTE lock. It will pass the PTE check without moving the folio. But the folio is guaranteed to be a completely new folio here because the folio can't be added back to the page table without holding the PTE lock, and if that happens the following PTE check here will fail.

Quoted 6 lines
Quoted 3 lines

So I think we should patch the current kernel only adding a "folio_swap_contains" equivalent check here, and maybe more comments, how do you think?

The description appears to have some inconsistencies. Would you mind rephrasing it?

Yeah, let's ignore the "UFFDIO_MOVE: UNSTABLE folio passed check: 0 -> fffffdffc385fb00" part first, as both you and me have come into a conclusion that "filemap_get_folio() returns NULL before move_swap_pte, but a folio was added to swap cache" is OK, and this output only proves that happens.

So the problematic race is:

Here move_pages_pte is moving src_pte to dst_pte, and it begins with src_pte == swap entry S1, and S1 isn't cached.

CPU1 CPU2
move_pages_pte()

  entry = pte_to_swp_entry(orig_src_pte);
    | src_pte is absent, and got entry == S1
  ... < Somehow interrupted> ...
                                 <swapin src_pte, using folio A>
                                   | folio A is just a new allocated folio
                                   | for resolving the swap in fault.
                                 <free folio A from swap cache freeing S1>
                                   | swap in fault is resolved, src_pte
                                   | now points to folio A, so folio A
                                   | can get freed just fine.
                                   | And now S1 is free to be used
                                   | by anyone.
                                 <someone else try swap out another folio B >
                                   | Folio B is a completely unrelated
                                   | folio swapped out by random process.
                                   | (has nothing to do with src_pte)
                                   | But S1 is freed so it may use S1
                                   | as its swap entry.
                                 <put folio B to swap cache with index S1 >
                                 ...
  folio = filemap_get_folio(S1)
    | The lookup is using S1, so it
    | got folio B here !!!
  ... < Somehow interrupted> ...
                                 <free folio B from swap cache>
                                   | Folio B could fail to be swapped out
                                   | or got swapped in again, so it can
                                   | be freed by folio_free_swap or
                                   | swap cache reclaim.
                                   | CPU1 is holding a reference but it
                                   | doesn't pin the swap cache folio
                                   | as I have demonstrated with the
                                   | test C program previously.
                                   | New S1 is free to be used again.
                                 <Swapout src_pte again using S1>
                                   | No thing blocks this from happening
                                   | The swapout is still using folio A,
                                   | and src_pte == S1.
  folio_trylock(folio)
  move_swap_pte
    double_pt_lock
    is_pte_pages_stable
      | Passed because of S1 is reused so src_pte == S1.
    folio_move_anon_rmap(...)
      | Moved invalid folio B here !!!
Quoted 3 lines

It's a long and complex one, I don't think it's practically possible to happen in reality but in theory doable, once in a million maybe... Still we have to fix it, or did I got anything wrong here?

Hi Barry,

I managed to reproduce this issue, by hacking the kernel a bit (Only adding only delay to increase the race window, and adding a WARN to indicate the problem)

1. Applying following patch for kernel:
===

mm/userfaultfd.c +5 -0
index bc473ad21202..1d710adf9839 100644
@@ -15,6 +15,7 @@
 #include <linux/mmu_notifier.h>
 #include <linux/hugetlb.h>
 #include <linux/shmem_fs.h>
+#include <linux/delay.h>
 #include <asm/tlbflush.h>
 #include <asm/tlb.h>
 #include "internal.h"
@@ -1100,6 +1101,10 @@ static int move_swap_pte(struct mm_struct *mm,
 struct vm_area_struct *dst_vma,
         * occur and hit the swapcache after moving the PTE.
         */
        if (src_folio) {
+               if (WARN_ON(src_folio->swap.val !=
 pte_to_swp_entry(orig_src_pte).val))
+                       pr_err("Moving folio %lx (folio->swap = %lx),
 orig_src_pte = %lx\n",
+                              (unsigned long)src_folio, src_folio->swap.val,
+                              pte_to_swp_entry(orig_src_pte).val);
                folio_move_anon_rmap(src_folio, dst_vma);
                src_folio->index = linear_page_index(dst_vma, dst_addr);
        }
mm/userfaultfd.c +4 -0
@@ -1388,9 +1393,13 @@ static int move_pages_pte(struct mm_struct *mm,
 pmd_t *dst_pmd, pmd_t *src_pmd,
                 * folios in the swapcache. This issue needs to be resolved
                 * separately to allow proper handling.
                 */
+               pr_err("DEBUG: Will do the lookup using entry %lx,
 wait 3s...\n", entry.val);
+               mdelay(1000 * 3);
                if (!src_folio)
                        folio = filemap_get_folio(swap_address_space(entry),
                                        swap_cache_index(entry));
+               pr_err("DEBUG: Got folio value %lx, wait 3s...\n",
 (unsigned long)folio);
+               mdelay(1000 * 3);
                if (!IS_ERR_OR_NULL(folio)) {
                        if (folio_test_large(folio)) {
                                err = -EBUSY;

2. Save following program in userspace (didn't bother with error check for simpler code):
===
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>

#define PAGE_SIZE 4096
/* Need to consume all slots so define the swap device size here */ #define SWAP_DEVICE_SIZE (PAGE_SIZE * 1024 - 1)

char *src, *race, *dst, *place_holder;
int uffd;

void read_in(char *p) {
    /* This test program initials memory with 0xAA to bypass zeromap */
    while (*((volatile char*)p) != 0xAA);
}
void *reader_thread(void *arg) {
    /* Test requires kernel to wait upon uffd move */
    read_in(dst);
    return NULL;
}
void *fault_handler_thread(void *arg) {
    int ret;
    struct uffd_msg msg;
    struct uffdio_move move;
    struct pollfd pollfd = { .fd = uffd, .events = POLLIN };
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

    while (1) {
        poll(&pollfd, 1, -1);
        read(uffd, &msg, sizeof(msg));

        move.src = (unsigned long)src + (msg.arg.pagefault.address -
            (unsigned long)dst);
        move.dst = msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
        move.len = PAGE_SIZE;
        move.mode = 0;

        ioctl(uffd, UFFDIO_MOVE, &move);
    }
    return NULL;
}
int main() {
    pthread_t fault_handler_thr, reader_thr;
    struct uffdio_api uffdio_api = { .api = UFFD_API, .features = 0 };
    struct uffdio_register uffdio_register;

    src = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE |
MAP_ANONYMOUS, -1, 0);
    dst = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE |
MAP_ANONYMOUS, -1, 0);
    memset(src, 0xAA, PAGE_SIZE);
    madvise(src, PAGE_SIZE, MADV_PAGEOUT);

    /* Consume all slots on swap device left only one entry (S1) */
    place_holder = mmap(NULL, SWAP_DEVICE_SIZE - 1, PROT_READ |
PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memset(place_holder, 0xAA, SWAP_DEVICE_SIZE - 1);
    madvise(place_holder, SWAP_DEVICE_SIZE - 1, MADV_PAGEOUT);

    /* Setup uffd handler and dst reader */
    uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    ioctl(uffd, UFFDIO_API, &uffdio_api);
    uffdio_register.range.start = (unsigned long)dst;
    uffdio_register.range.len = PAGE_SIZE;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);
    pthread_create(&fault_handler_thr, NULL, fault_handler_thread, NULL);
    pthread_create(&reader_thr, NULL, reader_thread, NULL);

    /* Wait for UFFDIO to start */
    sleep(1);

    /* Release src folio (A) from swap, freeing the entry S1 */
    read_in(src);

    /* Swapout another race folio (B) using S1 */
    race = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED |
MAP_ANONYMOUS, -1, 0);
    memset(race, 0xAA, PAGE_SIZE);
    madvise(race, PAGE_SIZE, MADV_PAGEOUT);

    /* Wait for UFFDIO swap lookup to see the race folio (B) */
    sleep(3);

    /* Free the race folio (B) from swap */
    read_in(race);
    /* And swap out src folio (A) again, using S1 */
    madvise(src, PAGE_SIZE, MADV_PAGEOUT);

    /* Kernel should have moved a wrong folio by now */

    pthread_join(reader_thr, NULL);
    pthread_cancel(fault_handler_thr);
    pthread_join(fault_handler_thr, NULL);
    munmap(race, PAGE_SIZE);
    munmap(src, PAGE_SIZE);
    munmap(dst, PAGE_SIZE);
    close(uffd);

    return 0;
}

3. Run the test with (ensure no other swap device is mounted and current dir is on a block device):
===
dd if=/dev/zero of=swap.img bs=1M count=1; mkswap swap.img; swapon swap.img; gcc test-uffd.c && ./a.out

Then we get the WARN:

[  348.200587] ------------[ cut here ]------------
[  348.200599] WARNING: CPU: 7 PID: 1856 at mm/userfaultfd.c:1104
move_pages_pte+0xdb8/0x11a0
[  348.207544] Modules linked in: loop
[  348.209401] CPU: 7 UID: 0 PID: 1856 Comm: a.out Kdump: loaded Not
tainted 6.15.0-rc6ptch-00381-g99f00d7c6c6f-dirty #304
PREEMPT(voluntary)
[  348.214579] Hardware name: QEMU QEMU Virtual Machine, BIOS
edk2-stable202408-prebuilt.qemu.org 08/13/2024
[  348.218656] pstate: 81400005 (Nzcv daif +PAN -UAO -TCO +DIT -SSBS BTYPE=--)
[  348.222013] pc : move_pages_pte+0xdb8/0x11a0
[  348.224062] lr : move_pages_pte+0x928/0x11a0
[  348.225881] sp : ffff800088b2b8f0
[  348.227360] x29: ffff800088b2b970 x28: 0000000000000000 x27: 0000ffffbc920000
[  348.230228] x26: fffffdffc335e4a8 x25: 0000000000000001 x24: fffffdffc3e4dd40
[  348.233159] x23: 080000010d792403 x22: ffff0000cd792900 x21: ffff0000c5a6d2c0
[  348.236339] x20: fffffdffc335e4a8 x19: 0000000000001004 x18: 0000000000000006
[  348.239269] x17: 0000ffffbc920000 x16: 0000ffffbc922fff x15: 0000000000000003
[  348.242703] x14: ffff8000812c3b68 x13: 0000000000000003 x12: 0000000000000003
[  348.245947] x11: 0000000000000000 x10: ffff800081e4feb8 x9 : 0000000000000001
[  348.249284] x8 : 0000000000000000 x7 : 6f696c6f6620746f x6 : 47203a4755424544
[  348.252071] x5 : ffff8000815789e3 x4 : ffff8000815789e5 x3 : 0000000000000000
[  348.255358] x2 : ffff0001fed2aef0 x1 : 0000000000000000 x0 : fffffdffc335e4a8
[  348.258134] Call trace:
[  348.259468]  move_pages_pte+0xdb8/0x11a0 (P)
[  348.261348]  move_pages+0x3c0/0x738
[  348.262987]  userfaultfd_ioctl+0x3d8/0x1f98
[  348.264916]  __arm64_sys_ioctl+0x88/0xd0
[  348.266779]  invoke_syscall+0x64/0xec
[  348.268347]  el0_svc_common+0xa8/0xd8
[  348.269967]  do_el0_svc+0x1c/0x28
[  348.271711]  el0_svc+0x40/0xe0
[  348.273345]  el0t_64_sync_handler+0x78/0x108
[  348.274821]  el0t_64_sync+0x19c/0x1a0
[  348.276117] ---[ end trace 0000000000000000 ]---
[  348.278638] Moving folio fffffdffc3e4dd40 (folio->swap = 0), orig_src_pte = 1

That's the new added WARN, but the test program also hung with D forever, and with errors with other tests like:

[  406.893936] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0
type:MM_ANONPAGES val:-1
[  406.894071] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0

type:MM_SHMEMPAGES val:1

Quoted 8 lines

Because the kernel just moved the wrong folio, so unmap takes forever looking for the missing folio, and counting went wrong too.

So this race is real. It's extremely unlikely to happen because it requires multiple collisions of multiple tiny race windows, however it's not impossible.

I'll post a fix very soon.

On second thought, the "filemap_get_folio() returns NULL before move_swap_pte, but a folio was added to swap cache" case is also buggy. It can also be reproduced with the program above with slight modification:

test-uffd-same-folio.c 2025-05-30 19:04:13.826078271 +0000 renamed from test-uffd.c 2025-05-30 08:34:00.485206529 +0000 +1 -1
@@ -83,20 +83,20 @@
     /* Release src folio (A) from swap, freeing the entry S1 */
     read_in(src);
 
-    /* Swapout another race folio (B) using S1 */
+    /* Swapout and free another race folio (B) forcing reclaiming S1

and folio (A) */
race = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED |

MAP_ANONYMOUS, -1, 0);
     memset(race, 0xAA, PAGE_SIZE);
     madvise(race, PAGE_SIZE, MADV_PAGEOUT);

+ read_in(race);
+ printf("RECLAMING A?\n");

- /* Wait for UFFDIO swap lookup to see the race folio (B) */
+ /* Wait for UFFDIO swap lookup to see NULL */

     sleep(3);

- /* Free the race folio (B) from swap */

-    read_in(race);
     /* And swap out src folio (A) again, using S1 */
     madvise(src, PAGE_SIZE, MADV_PAGEOUT);

- /* Kernel should have moved a wrong folio by now */
+ /* Kernel should have moved folio (A) but it didn't */

     pthread_join(reader_thr, NULL);
     pthread_cancel(fault_handler_thr);

I'll fix them together.

#19 lore raw

PATCH 6/28 mm, swap: rearrange swap cluster definition and helpers

In reply to Kairui Song

From: Kairui Song <[email protected]>

No feature change, move all cluster related definition and helpers to mm/swap.h, also tidy up and add a "swap_" prefix for all cluster lock/unlock helpers, so they can be better used outside of swap files.

---
 include/linux/swap.h |  34 ---------------
 mm/swap.h            |  62 ++++++++++++++++++++++++++
 mm/swapfile.c        | 102 +++++++++++++------------------------------
 3 files changed, 92 insertions(+), 106 deletions(-)
include/linux/swap.h +0 -34
index 0e52ac4e817d..1e7d9d55c39a 100644
@@ -234,40 +234,6 @@ enum {
 /* Special value in each swap_map continuation */
 #define SWAP_CONT_MAX	0x7f	/* Max count */
 
-/*
- * We use this to track usage of a cluster. A cluster is a block of swap disk
- * space with SWAPFILE_CLUSTER pages long and naturally aligns in disk. All
- * free clusters are organized into a list. We fetch an entry from the list to
- * get a free cluster.
- *
- * The flags field determines if a cluster is free. This is
- * protected by cluster lock.
- */
-struct swap_cluster_info {
-	spinlock_t lock;	/*
-				 * Protect swap_cluster_info fields
-				 * other than list, and swap_info_struct->swap_map
-				 * elements corresponding to the swap cluster.
-				 */
-	u16 count;
-	u8 flags;
-	u8 order;
-	struct list_head list;
-};
-
-/* All on-list cluster must have a non-zero flag. */
-enum swap_cluster_flags {
-	CLUSTER_FLAG_NONE = 0, /* For temporary off-list cluster */
-	CLUSTER_FLAG_FREE,
-	CLUSTER_FLAG_NONFULL,
-	CLUSTER_FLAG_FRAG,
-	/* Clusters with flags above are allocatable */
-	CLUSTER_FLAG_USABLE = CLUSTER_FLAG_FRAG,
-	CLUSTER_FLAG_FULL,
-	CLUSTER_FLAG_DISCARD,
-	CLUSTER_FLAG_MAX,
-};
-
 /*
  * The first page in the swap file is the swap header, which is always marked
  * bad to prevent it from being allocated as an entry. This also prevents the
mm/swap.h +62 -0

72 lines not on this page · raw message

mm/swapfile.c +30 -72

276 lines not on this page · raw message

Signature · 1 line
2.49.0
#20 lore raw

Re: [PATCH 06/28] mm, swap: rearrange swap cluster definition and helpers

mm/swapfile.c static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o · line 893

Quoted 8 lines

From: Kairui Song <[email protected]>

mm/swapfile.c +0 -2
@@ -889,10 +849,8 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o
 		/* Serialize HDD SWAP allocation for each device. */
 		spin_lock(&si->global_cluster_lock);
 		offset = si->global_cluster->next[order];
-		if (offset == SWAP_ENTRY_INVALID)
-			goto new_cluster;

We are implicitly dropping this. Does it mean the current code is wrong? Do we need some clarification about this?

mm/swapfile.c static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o · line 856

Quoted 6 lines
mm/swapfile.c +1 -1
-		ci = lock_cluster(si, offset);
+		ci = swap_lock_cluster(si, offset);
 		/* Cluster could have been used by another order */
 		if (cluster_is_usable(ci, order)) {
 			if (cluster_is_empty(ci))

Thanks
Barry

#21 lore raw

Re: [PATCH 06/28] mm, swap: rearrange swap cluster definition and helpers

Quoted 12 lines · Barry Song

mm/swapfile.c static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o · line 893

Quoted 9 lines

From: Kairui Song <[email protected]>

mm/swapfile.c +0 -2
@@ -889,10 +849,8 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o
              /* Serialize HDD SWAP allocation for each device. */
              spin_lock(&si->global_cluster_lock);
              offset = si->global_cluster->next[order];
-             if (offset == SWAP_ENTRY_INVALID)
-                     goto new_cluster;

We are implicitly dropping this. Does it mean the current code is wrong? Do we need some clarification about this?

Sorry, my bad, this change has nothing to do with this commit, I'll drop this change in the next version.

Quoted 10 lines

mm/swapfile.c static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si, int o · line 856

Quoted 7 lines
mm/swapfile.c +1 -1
-             ci = lock_cluster(si, offset);
+             ci = swap_lock_cluster(si, offset);
              /* Cluster could have been used by another order */
              if (cluster_is_usable(ci, order)) {
                      if (cluster_is_empty(ci))

Thanks
Barry

#22 lore raw

PATCH 7/28 mm, swap: tidy up swap device and cluster info helpers

In reply to Kairui Song

From: Kairui Song <[email protected]>

swp_swap_info is the common helper used for retrieving swap info in many places. It has an internal check that may lead to a NULL return value, but almost none of its caller checks the return value, and the internal check is pointless. In fact, most of these callers already ensure the entry is valid during that period and never expect a NULL value.

Tidy this up. If the caller ensures the swap entry / type is valid and device is pinned, use swp_info / swp_type_info instead, which has more debug checks and lower overhead as they are inline.

Caller that may expect a NULL value could use swp_get_info / swp_type_get_info instead.

No feature change as the rearranged codes have no effect since they were mostly ignored in some way, some new sanity checks are added for debug built to catch potential misuse.

---
 include/linux/swap.h |  6 ------
 mm/memory.c          |  2 +-
 mm/page_io.c         | 12 ++++++------
 mm/swap.h            | 32 ++++++++++++++++++++++++++++++--
 mm/swap_state.c      |  4 ++--
 mm/swapfile.c        | 35 ++++++++++++++++++-----------------
 6 files changed, 57 insertions(+), 34 deletions(-)
include/linux/swap.h +0 -6
index 1e7d9d55c39a..4239852fd203 100644
@@ -453,7 +453,6 @@ extern sector_t swapdev_block(int, pgoff_t);
 extern int __swap_count(swp_entry_t entry);
 extern bool swap_entry_swapped(struct swap_info_struct *si, swp_entry_t entry);
 extern int swp_swapcount(swp_entry_t entry);
-struct swap_info_struct *swp_swap_info(swp_entry_t entry);
 struct backing_dev_info;
 extern int init_swap_address_space(unsigned int type, unsigned long nr_pages);
 extern void exit_swap_address_space(unsigned int type);
@@ -466,11 +465,6 @@ static inline void put_swap_device(struct swap_info_struct *si)
 }
 
 #else /* CONFIG_SWAP */
-static inline struct swap_info_struct *swp_swap_info(swp_entry_t entry)
-{
-	return NULL;
-}
-
 static inline struct swap_info_struct *get_swap_device(swp_entry_t entry)
 {
 	return NULL;
mm/memory.c +1 -1
index 254be0e88801..cc1f6891cf99 100644
@@ -4315,7 +4315,7 @@ static struct folio *__alloc_swap_folio(struct vm_fault *vmf)
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
 static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
 {
-	struct swap_info_struct *si = swp_swap_info(entry);
+	struct swap_info_struct *si = swp_info(entry);
 	pgoff_t offset = swp_offset(entry);
 	int i;
 
mm/page_io.c +6 -6
index 4bce19df557b..eaf6319b81ab 100644
@@ -204,7 +204,7 @@ static bool is_folio_zero_filled(struct folio *folio)
 static void swap_zeromap_folio_set(struct folio *folio)
 {
 	struct obj_cgroup *objcg = get_obj_cgroup_from_folio(folio);
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 	int nr_pages = folio_nr_pages(folio);
 	swp_entry_t entry;
 	unsigned int i;
@@ -223,7 +223,7 @@ static void swap_zeromap_folio_set(struct folio *folio)
 
 static void swap_zeromap_folio_clear(struct folio *folio)
 {
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 	swp_entry_t entry;
 	unsigned int i;
 
@@ -375,7 +375,7 @@ static void sio_write_complete(struct kiocb *iocb, long ret)
 static void swap_writepage_fs(struct folio *folio, struct writeback_control *wbc)
 {
 	struct swap_iocb *sio = NULL;
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 	struct file *swap_file = sis->swap_file;
 	loff_t pos = swap_dev_pos(folio->swap);
 
@@ -452,7 +452,7 @@ static void swap_writepage_bdev_async(struct folio *folio,
 
 void __swap_writepage(struct folio *folio, struct writeback_control *wbc)
 {
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 
 	VM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio);
 	/*
@@ -543,7 +543,7 @@ static bool swap_read_folio_zeromap(struct folio *folio)
 
 static void swap_read_folio_fs(struct folio *folio, struct swap_iocb **plug)
 {
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 	struct swap_iocb *sio = NULL;
 	loff_t pos = swap_dev_pos(folio->swap);
 
@@ -614,7 +614,7 @@ static void swap_read_folio_bdev_async(struct folio *folio,
 
 void swap_read_folio(struct folio *folio, struct swap_iocb **plug)
 {
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 	bool synchronous = sis->flags & SWP_SYNCHRONOUS_IO;
 	bool workingset = folio_test_workingset(folio);
 	unsigned long pflags;
mm/swap.h +30 -2
index 38d37d241f1c..4982e6c2ad95 100644
@@ -13,6 +13,8 @@ extern int page_cluster;
 #define swap_entry_order(order)	0
 #endif
 
+extern struct swap_info_struct *swap_info[];
+
 /*
  * We use this to track usage of a cluster. A cluster is a block of swap disk
  * space with SWAPFILE_CLUSTER pages long and naturally aligns in disk. All
@@ -51,9 +53,29 @@ enum swap_cluster_flags {
 #include <linux/swapops.h> /* for swp_offset */
 #include <linux/blk_types.h> /* for bio_end_io_t */
 
+/*
+ * All swp_* function callers must ensure the entry is valid, and hold the
+ * swap device reference or pin the device in other ways. E.g, a locked
+ * folio in the swap cache makes sure its entries (folio->swap) are valid
+ * and won't be freed, the device is also pinned by its entries.
+ */
+static inline struct swap_info_struct *swp_type_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;
+}
+
+static inline struct swap_info_struct *swp_info(swp_entry_t entry)
+{
+	return swp_type_info(swp_type(entry));
+}
+
 static inline struct swap_cluster_info *swp_offset_cluster(
 		struct swap_info_struct *si, pgoff_t offset)
 {
+	VM_WARN_ON_ONCE(percpu_ref_is_zero(&si->users)); /* race with swapoff */
 	return &si->cluster_info[offset / SWAPFILE_CLUSTER];
 }
 
@@ -62,6 +84,7 @@ static inline struct swap_cluster_info *swap_lock_cluster(
 		unsigned long offset)
 {
 	struct swap_cluster_info *ci = swp_offset_cluster(si, offset);
+	VM_WARN_ON_ONCE(percpu_ref_is_zero(&si->users)); /* race with swapoff */
 	spin_lock(&ci->lock);
 	return ci;
 }
@@ -154,7 +177,7 @@ void swap_update_readahead(struct folio *folio, struct vm_area_struct *vma,
 
 static inline unsigned int folio_swap_flags(struct folio *folio)
 {
-	return swp_swap_info(folio->swap)->flags;
+	return swp_info(folio->swap)->flags;
 }
 
 /*
@@ -165,7 +188,7 @@ static inline unsigned int folio_swap_flags(struct folio *folio)
 static inline int swap_zeromap_batch(swp_entry_t entry, int max_nr,
 		bool *is_zeromap)
 {
-	struct swap_info_struct *sis = swp_swap_info(entry);
+	struct swap_info_struct *sis = swp_info(entry);
 	unsigned long start = swp_offset(entry);
 	unsigned long end = start + max_nr;
 	bool first_bit;
@@ -184,6 +207,11 @@ static inline int swap_zeromap_batch(swp_entry_t entry, int max_nr,
 
 #else /* CONFIG_SWAP */
 struct swap_iocb;
+static inline struct swap_info_struct *swp_info(swp_entry_t entry)
+{
+	return NULL;
+}
+
 static inline void swap_read_folio(struct folio *folio, struct swap_iocb **plug)
 {
 }
mm/swap_state.c +2 -2
index 07c41676486a..db9efa64f64e 100644
@@ -330,7 +330,7 @@ struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated,
 		bool skip_if_exists)
 {
-	struct swap_info_struct *si = swp_swap_info(entry);
+	struct swap_info_struct *si = swp_info(entry);
 	struct folio *folio;
 	struct folio *new_folio = NULL;
 	struct folio *result = NULL;
@@ -554,7 +554,7 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
 	unsigned long offset = entry_offset;
 	unsigned long start_offset, end_offset;
 	unsigned long mask;
-	struct swap_info_struct *si = swp_swap_info(entry);
+	struct swap_info_struct *si = swp_info(entry);
 	struct blk_plug plug;
 	struct swap_iocb *splug = NULL;
 	bool page_allocated;
mm/swapfile.c +18 -17
index ba3fd99eb5fa..2f834069b7ad 100644
@@ -102,7 +102,7 @@ static PLIST_HEAD(swap_active_head);
 static struct plist_head *swap_avail_heads;
 static DEFINE_SPINLOCK(swap_avail_lock);
 
-static struct swap_info_struct *swap_info[MAX_SWAPFILES];
+struct swap_info_struct *swap_info[MAX_SWAPFILES];
 
 static DEFINE_MUTEX(swapon_mutex);
 
@@ -124,14 +124,20 @@ static DEFINE_PER_CPU(struct percpu_swap_cluster, percpu_swap_cluster) = {
 	.lock = INIT_LOCAL_LOCK(),
 };
 
-static struct swap_info_struct *swap_type_to_swap_info(int type)
+/* May return NULL on invalid type, caller must check for NULL return */
+static struct swap_info_struct *swp_type_get_info(int type)
 {
 	if (type >= MAX_SWAPFILES)
 		return NULL;
-
 	return READ_ONCE(swap_info[type]); /* rcu_dereference() */
 }
 
+/* May return NULL on invalid entry, caller must check for NULL return */
+static struct swap_info_struct *swp_get_info(swp_entry_t entry)
+{
+	return swp_type_get_info(swp_type(entry));
+}
+
 static inline unsigned char swap_count(unsigned char ent)
 {
 	return ent & ~SWAP_HAS_CACHE;	/* may include COUNT_CONTINUED flag */
@@ -343,7 +349,7 @@ offset_to_swap_extent(struct swap_info_struct *sis, unsigned long offset)
 
 sector_t swap_folio_sector(struct folio *folio)
 {
-	struct swap_info_struct *sis = swp_swap_info(folio->swap);
+	struct swap_info_struct *sis = swp_info(folio->swap);
 	struct swap_extent *se;
 	sector_t sector;
 	pgoff_t offset;
@@ -1272,7 +1278,7 @@ static struct swap_info_struct *_swap_info_get(swp_entry_t entry)
 
 	if (!entry.val)
 		goto out;
-	si = swp_swap_info(entry);
+	si = swp_get_info(entry);
 	if (!si)
 		goto bad_nofile;
 	if (data_race(!(si->flags & SWP_USED)))
@@ -1381,7 +1387,7 @@ struct swap_info_struct *get_swap_device(swp_entry_t entry)
 
 	if (!entry.val)
 		goto out;
-	si = swp_swap_info(entry);
+	si = swp_get_info(entry);
 	if (!si)
 		goto bad_nofile;
 	if (!get_swap_device_info(si))
@@ -1560,7 +1566,7 @@ void put_swap_folio(struct folio *folio, swp_entry_t entry)
 
 int __swap_count(swp_entry_t entry)
 {
-	struct swap_info_struct *si = swp_swap_info(entry);
+	struct swap_info_struct *si = swp_info(entry);
 	pgoff_t offset = swp_offset(entry);
 
 	return swap_count(si->swap_map[offset]);
@@ -1791,7 +1797,7 @@ void free_swap_and_cache_nr(swp_entry_t entry, int nr)
 
 swp_entry_t get_swap_page_of_type(int type)
 {
-	struct swap_info_struct *si = swap_type_to_swap_info(type);
+	struct swap_info_struct *si = swp_type_get_info(type);
 	unsigned long offset;
 	swp_entry_t entry = {0};
 
@@ -1872,7 +1878,7 @@ int find_first_swap(dev_t *device)
  */
 sector_t swapdev_block(int type, pgoff_t offset)
 {
-	struct swap_info_struct *si = swap_type_to_swap_info(type);
+	struct swap_info_struct *si = swp_type_get_info(type);
 	struct swap_extent *se;
 
 	if (!si || !(si->flags & SWP_WRITEOK))
@@ -2801,7 +2807,7 @@ static void *swap_start(struct seq_file *swap, loff_t *pos)
 	if (!l)
 		return SEQ_START_TOKEN;
 
-	for (type = 0; (si = swap_type_to_swap_info(type)); type++) {
+	for (type = 0; (si = swp_type_get_info(type)); type++) {
 		if (!(si->flags & SWP_USED) || !si->swap_map)
 			continue;
 		if (!--l)
@@ -2822,7 +2828,7 @@ static void *swap_next(struct seq_file *swap, void *v, loff_t *pos)
 		type = si->type + 1;
 
 	++(*pos);
-	for (; (si = swap_type_to_swap_info(type)); type++) {
+	for (; (si = swp_type_get_info(type)); type++) {
 		if (!(si->flags & SWP_USED) || !si->swap_map)
 			continue;
 		return si;
@@ -3483,7 +3489,7 @@ static int __swap_duplicate(swp_entry_t entry, unsigned char usage, int nr)
 	unsigned char has_cache;
 	int err, i;
 
-	si = swp_swap_info(entry);
+	si = swp_get_info(entry);
 	if (WARN_ON_ONCE(!si)) {
 		pr_err("%s%08lx\n", Bad_file, entry.val);
 		return -EINVAL;
@@ -3598,11 +3604,6 @@ void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr)
 	swap_entries_put_cache(si, entry, nr);
 }
 
-struct swap_info_struct *swp_swap_info(swp_entry_t entry)
-{
-	return swap_type_to_swap_info(swp_type(entry));
-}
-
 /*
  * add_swap_count_continuation - called when a swap count is duplicated
  * beyond SWAP_MAP_MAX, it allocates a new page and links that to the entry's
Signature · 1 line
2.49.0
#23 lore raw

PATCH 8/28 mm, swap: use swap table for the swap cache and switch API

In reply to Kairui Song

From: Kairui Song <[email protected]>

Introduce basic swap table infrastructures, which is now just a fix-sized flat array inside each swap cluster, with access wrappers.

Each table entry is an opaque atomic long, which could be in 3 types:
a shadow type (XA_VALUE), a folio type (pointer), or NULL.

In this first step, it only supports storing a folio or shadow, and it is a drop-in replacement for the swap cache's underlying structure.

This commit converts all swap cache users to use new sets of APIs. Swap cache lookups (swap_cache_get_*) are still lock-less and require a pin on the swap device to prevent the memory from being freed, which is unchanged as before.

All swap cache updates will now be protected by the swap cluster lock, which is either handled by the new helpers internally, or the caller has to lock the cluster before using __swap_cache_* functions. The cluster lock also replaces where Xarray locks were used.

At this point it looks like just a downgrade from Xarray to flat array. Later commits will implement a fully cluster based unified swap table, with dynamic allocation, which should reduce the memory usage while making the performance even better.

---
 include/linux/swap.h |   2 -
 mm/filemap.c         |  20 +--
 mm/huge_memory.c     |  20 ++-
 mm/memory.c          |   2 +-
 mm/migrate.c         |  28 ++--
 mm/shmem.c           |  25 +---
 mm/swap.h            | 133 +++++++++++++------
 mm/swap_state.c      | 308 +++++++++++++++++++++----------------------
 mm/swap_table.h      | 103 +++++++++++++++
 mm/swapfile.c        | 108 ++++++++-------
 mm/vmscan.c          |  21 ++-
 mm/zswap.c           |   7 +-
 12 files changed, 473 insertions(+), 304 deletions(-)
 create mode 100644 mm/swap_table.h
include/linux/swap.h +0 -2
index 4239852fd203..58230f3e15e6 100644
@@ -454,8 +454,6 @@ extern int __swap_count(swp_entry_t entry);
 extern bool swap_entry_swapped(struct swap_info_struct *si, swp_entry_t entry);
 extern int swp_swapcount(swp_entry_t entry);
 struct backing_dev_info;
-extern int init_swap_address_space(unsigned int type, unsigned long nr_pages);
-extern void exit_swap_address_space(unsigned int type);
 extern struct swap_info_struct *get_swap_device(swp_entry_t entry);
 sector_t swap_folio_sector(struct folio *folio);
 
mm/filemap.c +7 -13
index 09d005848f0d..6840cd817ed3 100644
@@ -4403,23 +4403,17 @@ static void filemap_cachestat(struct address_space *mapping,
 #ifdef CONFIG_SWAP /* implies CONFIG_MMU */
 			if (shmem_mapping(mapping)) {
 				/* shmem file - in swap cache */
+				struct swap_info_struct *si;
 				swp_entry_t swp = radix_to_swp_entry(folio);
 
-				/* swapin error results in poisoned entry */
-				if (non_swap_entry(swp))
+				/* prevent swapoff from releasing the device */
+				si = get_swap_device(swp);
+				if (!si)
 					goto resched;
 
-				/*
-				 * Getting a swap entry from the shmem
-				 * inode means we beat
-				 * shmem_unuse(). rcu_read_lock()
-				 * ensures swapoff waits for us before
-				 * freeing the swapper space. However,
-				 * we can race with swapping and
-				 * invalidation, so there might not be
-				 * a shadow in the swapcache (yet).
-				 */
-				shadow = get_shadow_from_swap_cache(swp);
+				shadow = swap_cache_get_shadow(swp);
+				put_swap_device(si);
+
 				if (!shadow)
 					goto resched;
 			}
mm/huge_memory.c +9 -11
index d3e66136e41a..126cf217293c 100644
@@ -3457,9 +3457,9 @@ static int __split_unmapped_folio(struct folio *folio, int new_order,
 		bool uniform_split)
 {
 	struct lruvec *lruvec;
-	struct address_space *swap_cache = NULL;
 	struct folio *origin_folio = folio;
 	struct folio *next_folio = folio_next(folio);
+	struct swap_cluster_info *ci = NULL;
 	struct folio *new_folio;
 	struct folio *next;
 	int order = folio_order(folio);
@@ -3476,8 +3476,7 @@ static int __split_unmapped_folio(struct folio *folio, int new_order,
 		if (!uniform_split || new_order != 0)
 			return -EINVAL;
 
-		swap_cache = swap_address_space(folio->swap);
-		xa_lock(&swap_cache->i_pages);
+		ci = swap_lock_folio_cluster(folio);
 	}
 
 	if (folio_test_anon(folio))
@@ -3566,7 +3565,7 @@ static int __split_unmapped_folio(struct folio *folio, int new_order,
 				continue;
 
 			folio_ref_unfreeze(release, 1 +
-					((mapping || swap_cache) ?
+					((mapping || ci) ?
 						folio_nr_pages(release) : 0));
 
 			lru_add_split_folio(origin_folio, release, lruvec,
@@ -3584,10 +3583,9 @@ static int __split_unmapped_folio(struct folio *folio, int new_order,
 			} else if (mapping) {
 				__xa_store(&mapping->i_pages,
 						release->index, release, 0);
-			} else if (swap_cache) {
-				__xa_store(&swap_cache->i_pages,
-						swap_cache_index(release->swap),
-						release, 0);
+			} else if (ci) {
+				__swap_cache_override_folio(ci, release->swap,
+							    origin_folio, release);
 			}
 		}
 	}
@@ -3599,12 +3597,12 @@ static int __split_unmapped_folio(struct folio *folio, int new_order,
 	 * see stale page cache entries.
 	 */
 	folio_ref_unfreeze(origin_folio, 1 +
-		((mapping || swap_cache) ? folio_nr_pages(origin_folio) : 0));
+		((mapping || ci) ? folio_nr_pages(origin_folio) : 0));
 
 	unlock_page_lruvec(lruvec);
 
-	if (swap_cache)
-		xa_unlock(&swap_cache->i_pages);
+	if (ci)
+		swap_unlock_cluster(ci);
 	if (mapping)
 		xa_unlock(&mapping->i_pages);
 
mm/memory.c +1 -1
index cc1f6891cf99..f2897d9059f2 100644
@@ -4603,7 +4603,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 
 				memcg1_swapin(entry, nr_pages);
 
-				shadow = get_shadow_from_swap_cache(entry);
+				shadow = swap_cache_get_shadow(entry);
 				if (shadow)
 					workingset_refault(folio, shadow);
 
mm/migrate.c +17 -11
index 784ac2256d08..dad428b1a78f 100644
@@ -458,10 +458,10 @@ static int __folio_migrate_mapping(struct address_space *mapping,
 		struct folio *newfolio, struct folio *folio, int expected_count)
 {
 	XA_STATE(xas, &mapping->i_pages, folio_index(folio));
+	struct swap_cluster_info *ci = NULL;
 	struct zone *oldzone, *newzone;
 	int dirty;
 	long nr = folio_nr_pages(folio);
-	long entries, i;
 
 	if (!mapping) {
 		/* Take off deferred split queue while frozen and memcg set */
@@ -487,9 +487,16 @@ static int __folio_migrate_mapping(struct address_space *mapping,
 	oldzone = folio_zone(folio);
 	newzone = folio_zone(newfolio);
 
-	xas_lock_irq(&xas);
+	if (folio_test_swapcache(folio))
+		ci = swap_lock_folio_cluster_irq(folio);
+	else
+		xas_lock_irq(&xas);
+
 	if (!folio_ref_freeze(folio, expected_count)) {
-		xas_unlock_irq(&xas);
+		if (ci)
+			swap_unlock_cluster(ci);
+		else
+			xas_unlock_irq(&xas);
 		return -EAGAIN;
 	}
 
@@ -510,9 +517,6 @@ static int __folio_migrate_mapping(struct address_space *mapping,
 	if (folio_test_swapcache(folio)) {
 		folio_set_swapcache(newfolio);
 		newfolio->private = folio_get_private(folio);
-		entries = nr;
-	} else {
-		entries = 1;
 	}
 
 	/* Move dirty while folio refs frozen and newfolio not yet exposed */
@@ -522,11 +526,10 @@ static int __folio_migrate_mapping(struct address_space *mapping,
 		folio_set_dirty(newfolio);
 	}
 
-	/* Swap cache still stores N entries instead of a high-order entry */
-	for (i = 0; i < entries; i++) {
+	if (folio_test_swapcache(folio))
+		WARN_ON_ONCE(__swap_cache_replace_folio(ci, folio->swap, folio, newfolio));
+	else
 		xas_store(&xas, newfolio);
-		xas_next(&xas);
-	}
 
 	/*
 	 * Drop cache reference from old folio by unfreezing
@@ -535,8 +538,11 @@ static int __folio_migrate_mapping(struct address_space *mapping,
 	 */
 	folio_ref_unfreeze(folio, expected_count - nr);
 
-	xas_unlock(&xas);
 	/* Leave irq disabled to prevent preemption while updating stats */
+	if (ci)
+		swap_unlock_cluster(ci);
+	else
+		xas_unlock(&xas);
 
 	/*
 	 * If moved to a different zone then also account
mm/shmem.c +6 -19
index 43d9e3bf16f4..0da9e06eaee8 100644
@@ -2009,7 +2009,7 @@ static struct folio *shmem_swap_alloc_folio(struct inode *inode,
 	new->swap = entry;
 
 	memcg1_swapin(entry, nr_pages);
-	shadow = get_shadow_from_swap_cache(entry);
+	shadow = swap_cache_get_shadow(entry);
 	if (shadow)
 		workingset_refault(new, shadow);
 	folio_add_lru(new);
@@ -2038,13 +2038,11 @@ static int shmem_replace_folio(struct folio **foliop, gfp_t gfp,
 				struct shmem_inode_info *info, pgoff_t index,
 				struct vm_area_struct *vma)
 {
+	struct swap_cluster_info *ci;
 	struct folio *new, *old = *foliop;
 	swp_entry_t entry = old->swap;
-	struct address_space *swap_mapping = swap_address_space(entry);
-	pgoff_t swap_index = swap_cache_index(entry);
-	XA_STATE(xas, &swap_mapping->i_pages, swap_index);
 	int nr_pages = folio_nr_pages(old);
-	int error = 0, i;
+	int error = 0;
 
 	/*
 	 * We have arrived here because our zones are constrained, so don't
@@ -2073,25 +2071,14 @@ static int shmem_replace_folio(struct folio **foliop, gfp_t gfp,
 	new->swap = entry;
 	folio_set_swapcache(new);
 
-	/* Swap cache still stores N entries instead of a high-order entry */
-	xa_lock_irq(&swap_mapping->i_pages);
-	for (i = 0; i < nr_pages; i++) {
-		void *item = xas_load(&xas);
-
-		if (item != old) {
-			error = -ENOENT;
-			break;
-		}
-
-		xas_store(&xas, new);
-		xas_next(&xas);
-	}
+	ci = swap_lock_folio_cluster_irq(old);
+	error = __swap_cache_replace_folio(ci, entry, old, new);
 	if (!error) {
 		mem_cgroup_replace_folio(old, new);
 		shmem_update_stats(new, nr_pages);
 		shmem_update_stats(old, -nr_pages);
 	}
-	xa_unlock_irq(&swap_mapping->i_pages);
+	swap_unlock_cluster(ci);
 
 	if (unlikely(error)) {
 		/*
mm/swap.h +96 -37

209 lines not on this page · raw message

mm/swap_state.c +150 -158

402 lines not on this page · raw message

mm/swap_table.h new file +103 -0

103 lines not on this page · raw message

mm/swapfile.c +64 -44

227 lines not on this page · raw message

mm/vmscan.c +15 -6
index 7d6d1ce3921e..7b5f41b4147b 100644
@@ -743,13 +743,19 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
 {
 	int refcount;
 	void *shadow = NULL;
+	struct swap_cluster_info *ci;
 
 	BUG_ON(!folio_test_locked(folio));
 	BUG_ON(mapping != folio_mapping(folio));
 
-	if (!folio_test_swapcache(folio))
+	if (folio_test_swapcache(folio)) {
+		ci = swap_lock_cluster_irq(swp_info(folio->swap),
+					   swp_offset(folio->swap));
+	} else {
 		spin_lock(&mapping->host->i_lock);
-	xa_lock_irq(&mapping->i_pages);
+		xa_lock_irq(&mapping->i_pages);
+	}
+
 	/*
 	 * The non racy check for a busy folio.
 	 *
@@ -789,9 +795,9 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
 
 		if (reclaimed && !mapping_exiting(mapping))
 			shadow = workingset_eviction(folio, target_memcg);
-		__delete_from_swap_cache(folio, swap, shadow);
+		__swap_cache_del_folio(swap, folio, shadow);
 		memcg1_swapout(folio, swap);
-		xa_unlock_irq(&mapping->i_pages);
+		swap_unlock_cluster_irq(ci);
 		put_swap_folio(folio, swap);
 	} else {
 		void (*free_folio)(struct folio *);
@@ -829,9 +835,12 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
 	return 1;
 
 cannot_free:
-	xa_unlock_irq(&mapping->i_pages);
-	if (!folio_test_swapcache(folio))
+	if (folio_test_swapcache(folio)) {
+		swap_unlock_cluster_irq(ci);
+	} else {
+		xa_unlock_irq(&mapping->i_pages);
 		spin_unlock(&mapping->host->i_lock);
+	}
 	return 0;
 }
 
mm/zswap.c +5 -2
index 455e9425c5f5..af954bda0b02 100644
@@ -233,10 +233,13 @@ static bool zswap_has_pool;
 * helpers and fwd declarations
 **********************************/
 
+/* One swap address space for each 64M swap space */
+#define ZSWAP_ADDRESS_SPACE_SHIFT 14
+#define ZSWAP_ADDRESS_SPACE_PAGES (1 << ZSWAP_ADDRESS_SPACE_SHIFT)
 static inline struct xarray *swap_zswap_tree(swp_entry_t swp)
 {
 	return &zswap_trees[swp_type(swp)][swp_offset(swp)
-		>> SWAP_ADDRESS_SPACE_SHIFT];
+		>> ZSWAP_ADDRESS_SPACE_SHIFT];
 }
 
 #define zswap_pool_debug(msg, p)				\
@@ -1741,7 +1744,7 @@ int zswap_swapon(int type, unsigned long nr_pages)
 	struct xarray *trees, *tree;
 	unsigned int nr, i;
 
-	nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES);
+	nr = DIV_ROUND_UP(nr_pages, ZSWAP_ADDRESS_SPACE_PAGES);
 	trees = kvcalloc(nr, sizeof(*tree), GFP_KERNEL);
 	if (!trees) {
 		pr_err("alloc failed, zswap disabled for swap type %d\n", type);
Signature · 1 line
2.49.0
#24 lore raw

PATCH 9/28 mm/swap: rename __read_swap_cache_async to __swapin_cache_alloc

In reply to Kairui Song

From: Kairui Song <[email protected]>

__read_swap_cache_async is widely used to allocate and ensure a folio is in swapcache, or get the folio if a folio is already there.

It's not async, and it's not doing any read. Rename it to better present its usage.

---
 mm/swap.h       |  2 +-
 mm/swap_state.c | 20 ++++++++++----------
 mm/swapfile.c   |  2 +-
 mm/zswap.c      |  4 ++--
 4 files changed, 14 insertions(+), 14 deletions(-)
mm/swap.h +1 -1
index 30cd257aecbb..fec7d6e751ae 100644
@@ -210,7 +210,7 @@ void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr);
 struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct vm_area_struct *vma, unsigned long addr,
 		struct swap_iocb **plug);
-struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_flags,
+struct folio *__swapin_cache_alloc(swp_entry_t entry, gfp_t gfp_flags,
 		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated,
 		bool skip_if_exists);
 struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t flag,
mm/swap_state.c +10 -10
index bef9633533ec..fe71706e29d9 100644
@@ -353,7 +353,7 @@ void swap_update_readahead(struct folio *folio,
 	}
 }
 
-struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
+struct folio *__swapin_cache_alloc(swp_entry_t entry, gfp_t gfp_mask,
 		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated,
 		bool skip_if_exists)
 {
@@ -403,12 +403,12 @@ struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 			goto put_and_return;
 
 		/*
-		 * Protect against a recursive call to __read_swap_cache_async()
+		 * Protect against a recursive call to __swapin_cache_alloc()
 		 * on the same entry waiting forever here because SWAP_HAS_CACHE
 		 * is set but the folio is not the swap cache yet. This can
 		 * happen today if mem_cgroup_swapin_charge_folio() below
 		 * triggers reclaim through zswap, which may call
-		 * __read_swap_cache_async() in the writeback path.
+		 * __swapin_cache_alloc() in the writeback path.
 		 */
 		if (skip_if_exists)
 			goto put_and_return;
@@ -417,7 +417,7 @@ struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		 * We might race against __swap_cache_del_folio(), and
 		 * stumble across a swap_map entry whose SWAP_HAS_CACHE
 		 * has not yet been cleared.  Or race against another
-		 * __read_swap_cache_async(), which has set SWAP_HAS_CACHE
+		 * __swapin_cache_alloc(), which has set SWAP_HAS_CACHE
 		 * in swap_map, but not yet added its folio to swap cache.
 		 */
 		schedule_timeout_uninterruptible(1);
@@ -464,7 +464,7 @@ struct folio *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
  * the swap entry is no longer in use.
  *
  * get/put_swap_device() aren't needed to call this function, because
- * __read_swap_cache_async() call them and swap_read_folio() holds the
+ * __swapin_cache_alloc() call them and swap_read_folio() holds the
  * swap cache folio lock.
  */
 struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
@@ -482,7 +482,7 @@ struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		return NULL;
 
 	mpol = get_vma_policy(vma, addr, 0, &ilx);
-	folio = __read_swap_cache_async(entry, gfp_mask, mpol, ilx,
+	folio = __swapin_cache_alloc(entry, gfp_mask, mpol, ilx,
 					&page_allocated, false);
 	mpol_cond_put(mpol);
 
@@ -600,7 +600,7 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
 	blk_start_plug(&plug);
 	for (offset = start_offset; offset <= end_offset ; offset++) {
 		/* Ok, do the async read-ahead now */
-		folio = __read_swap_cache_async(
+		folio = __swapin_cache_alloc(
 				swp_entry(swp_type(entry), offset),
 				gfp_mask, mpol, ilx, &page_allocated, false);
 		if (!folio)
@@ -619,7 +619,7 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
 	lru_add_drain();	/* Push any new pages onto the LRU now */
 skip:
 	/* The page was likely read above, so no need for plugging here */
-	folio = __read_swap_cache_async(entry, gfp_mask, mpol, ilx,
+	folio = __swapin_cache_alloc(entry, gfp_mask, mpol, ilx,
 					&page_allocated, false);
 	if (unlikely(page_allocated))
 		swap_read_folio(folio, NULL);
@@ -714,7 +714,7 @@ static struct folio *swap_vma_readahead(swp_entry_t targ_entry, gfp_t gfp_mask,
 			continue;
 		pte_unmap(pte);
 		pte = NULL;
-		folio = __read_swap_cache_async(entry, gfp_mask, mpol, ilx,
+		folio = __swapin_cache_alloc(entry, gfp_mask, mpol, ilx,
 						&page_allocated, false);
 		if (!folio)
 			continue;
@@ -734,7 +734,7 @@ static struct folio *swap_vma_readahead(swp_entry_t targ_entry, gfp_t gfp_mask,
 	lru_add_drain();
 skip:
 	/* The folio was likely read above, so no need for plugging here */
-	folio = __read_swap_cache_async(targ_entry, gfp_mask, mpol, targ_ilx,
+	folio = __swapin_cache_alloc(targ_entry, gfp_mask, mpol, targ_ilx,
 					&page_allocated, false);
 	if (unlikely(page_allocated))
 		swap_read_folio(folio, NULL);
mm/swapfile.c +1 -1
index aaf7d21eaecb..62af67b6f7c2 100644
@@ -1390,7 +1390,7 @@ static unsigned char swap_entry_put_locked(struct swap_info_struct *si,
  *   CPU1				CPU2
  *   do_swap_page()
  *     ...				swapoff+swapon
- *     __read_swap_cache_async()
+ *     __swapin_cache_alloc()
  *       swapcache_prepare()
  *         __swap_duplicate()
  *           // check swap_map
mm/zswap.c +2 -2
index af954bda0b02..87aebeee11ef 100644
@@ -1084,8 +1084,8 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
 		return -EEXIST;
 
 	mpol = get_task_policy(current);
-	folio = __read_swap_cache_async(swpentry, GFP_KERNEL, mpol,
-			NO_INTERLEAVE_INDEX, &folio_was_allocated, true);
+	folio = __swapin_cache_alloc(swpentry, GFP_KERNEL, mpol,
+				NO_INTERLEAVE_INDEX, &folio_was_allocated, true);
 	put_swap_device(si);
 	if (!folio)
 		return -ENOMEM;
Signature · 1 line
2.49.0
#25 lore raw

PATCH 10/28 mm, swap: add a swap helper for bypassing only read ahead

In reply to Kairui Song

From: Kairui Song <[email protected]>

The swap cache now has a very low overhead, bypassing it is not helpful anymore. To prepare for unifying the swap in path, introduce a new helper that only bypasses read ahead and does not bypass the swap cache.

---
 mm/swap.h       |   6 ++
 mm/swap_state.c | 158 ++++++++++++++++++++++++++++++------------------
 2 files changed, 105 insertions(+), 59 deletions(-)
mm/swap.h +6 -0
index fec7d6e751ae..aab6bf9c3a8a 100644
@@ -217,6 +217,7 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t flag,
 		struct mempolicy *mpol, pgoff_t ilx);
 struct folio *swapin_readahead(swp_entry_t entry, gfp_t flag,
 		struct vm_fault *vmf);
+struct folio *swapin_entry(swp_entry_t entry, struct folio *folio);
 void swap_update_readahead(struct folio *folio, struct vm_area_struct *vma,
 			   unsigned long addr);
 
@@ -303,6 +304,11 @@ static inline struct folio *swapin_readahead(swp_entry_t swp, gfp_t gfp_mask,
 	return NULL;
 }
 
+static inline struct folio *swapin_entry(swp_entry_t ent, struct folio *folio)
+{
+	return NULL;
+}
+
 static inline void swap_update_readahead(struct folio *folio,
 		struct vm_area_struct *vma, unsigned long addr)
 {
mm/swap_state.c +99 -59

195 lines not on this page · raw message

Signature · 1 line
2.49.0
#26 lore raw

PATCH 11/28 mm, swap: clean up and consolidate helper for mTHP swapin check

In reply to Kairui Song

From: Kairui Song <[email protected]>

Move all mTHP swapin check into can_swapin_thp and use it for both pre IO check and post IO check. This way the code is more consolidated and make later commit easier to maintain.

Also clean up the comments while at it. The current comment of non_swapcache_batch is not correct: swap in bypassing swap cache won't reach the swap device as long as the entry is cached, because it still sets the SWAP_HAS_CACHE flag. If the folio is already in swap cache, raced swap in will either fail due to -EEXIST with swapcache_prepare, or see the cached folio.

The real reason this non_swapcache_batch is needed is that if a smaller folio is in the swap cache but not mapped, mTHP swapin will be blocked forever as it won't see the folio due to index offset, nor it can set the SWAP_HAS_CACHE bit, so it has to fallback to order 0 swap in.

---
 mm/memory.c | 90 ++++++++++++++++++++++++-----------------------------
 1 file changed, 41 insertions(+), 49 deletions(-)
mm/memory.c +41 -49
index f2897d9059f2..1b6e192de6ec 100644
@@ -4319,12 +4319,6 @@ static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
 	pgoff_t offset = swp_offset(entry);
 	int i;
 
-	/*
-	 * While allocating a large folio and doing swap_read_folio, which is
-	 * the case the being faulted pte doesn't have swapcache. We need to
-	 * ensure all PTEs have no cache as well, otherwise, we might go to
-	 * swap devices while the content is in swapcache.
-	 */
 	for (i = 0; i < max_nr; i++) {
 		if ((si->swap_map[offset + i] & SWAP_HAS_CACHE))
 			return i;
@@ -4334,34 +4328,30 @@ static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
 }
 
 /*
- * Check if the PTEs within a range are contiguous swap entries
- * and have consistent swapcache, zeromap.
+ * Check if the page table is still suitable for large folio swap in.
+ * @vmf: The fault triggering the swap-in.
+ * @ptep: Pointer to the PTE that should be the head of the swap in folio.
+ * @addr: The address corresponding to the PTE.
+ * @nr_pages: Number of pages of the folio that suppose to be swapped in.
  */
-static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep, int nr_pages)
+static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep,
+			   unsigned long addr, unsigned int nr_pages)
 {
-	unsigned long addr;
-	swp_entry_t entry;
-	int idx;
-	pte_t pte;
-
-	addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
-	idx = (vmf->address - addr) / PAGE_SIZE;
-	pte = ptep_get(ptep);
+	pte_t pte = ptep_get(ptep);
+	unsigned long addr_end = addr + (PAGE_SIZE * nr_pages);
+	unsigned long pte_offset = (vmf->address - addr) / PAGE_SIZE;
 
-	if (!pte_same(pte, pte_move_swp_offset(vmf->orig_pte, -idx)))
+	VM_WARN_ON_ONCE(!IS_ALIGNED(addr, PAGE_SIZE) ||
+			addr > vmf->address || addr_end <= vmf->address);
+	if (unlikely(addr < max(addr & PMD_MASK, vmf->vma->vm_start) ||
+		     addr_end > pmd_addr_end(addr, vmf->vma->vm_end)))
 		return false;
-	entry = pte_to_swp_entry(pte);
-	if (swap_pte_batch(ptep, nr_pages, pte) != nr_pages)
-		return false;
-
 	/*
-	 * swap_read_folio() can't handle the case a large folio is hybridly
-	 * from different backends. And they are likely corner cases. Similar
-	 * things might be added once zswap support large folios.
+	 * All swap entries must from the same swap device, in same
+	 * cgroup, with same exclusiveness, only differs in offset.
 	 */
-	if (unlikely(swap_zeromap_batch(entry, nr_pages, NULL) != nr_pages))
-		return false;
-	if (unlikely(non_swapcache_batch(entry, nr_pages) != nr_pages))
+	if (!pte_same(pte, pte_move_swp_offset(vmf->orig_pte, -pte_offset)) ||
+	    swap_pte_batch(ptep, nr_pages, pte) != nr_pages)
 		return false;
 
 	return true;
@@ -4441,13 +4431,24 @@ static struct folio *alloc_swap_folio(struct vm_fault *vmf)
 	 * completely swap entries with contiguous swap offsets.
 	 */
 	order = highest_order(orders);
-	while (orders) {
-		addr = ALIGN_DOWN(vmf->address, PAGE_SIZE << order);
-		if (can_swapin_thp(vmf, pte + pte_index(addr), 1 << order))
-			break;
-		order = next_order(&orders, order);
+	for (; orders; order = next_order(&orders, order)) {
+		unsigned long nr_pages = 1 << order;
+		swp_entry_t swap_entry = { .val = ALIGN_DOWN(entry.val, nr_pages) };
+		addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
+		if (!can_swapin_thp(vmf, pte + pte_index(addr), addr, nr_pages))
+			continue;
+		/*
+		 * If there is already a smaller folio in cache, it will
+		 * conflict with the larger folio in the swap cache layer
+		 * and block the swap in.
+		 */
+		if (unlikely(non_swapcache_batch(swap_entry, nr_pages) != nr_pages))
+			continue;
+		/* Zero map doesn't work with large folio yet. */
+		if (unlikely(swap_zeromap_batch(swap_entry, nr_pages, NULL) != nr_pages))
+			continue;
+		break;
 	}
-
 	pte_unmap_unlock(pte, ptl);
 
 	/* Try allocating the highest of the remaining orders. */
@@ -4731,27 +4732,18 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	page_idx = 0;
 	address = vmf->address;
 	ptep = vmf->pte;
+
 	if (folio_test_large(folio) && folio_test_swapcache(folio)) {
-		int nr = folio_nr_pages(folio);
+		unsigned long nr = folio_nr_pages(folio);
 		unsigned long idx = folio_page_idx(folio, page);
-		unsigned long folio_start = address - idx * PAGE_SIZE;
-		unsigned long folio_end = folio_start + nr * PAGE_SIZE;
-		pte_t *folio_ptep;
-		pte_t folio_pte;
+		unsigned long folio_address = address - idx * PAGE_SIZE;
+		pte_t *folio_ptep = vmf->pte - idx;
 
-		if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
-			goto check_folio;
-		if (unlikely(folio_end > pmd_addr_end(address, vma->vm_end)))
-			goto check_folio;
-
-		folio_ptep = vmf->pte - idx;
-		folio_pte = ptep_get(folio_ptep);
-		if (!pte_same(folio_pte, pte_move_swp_offset(vmf->orig_pte, -idx)) ||
-		    swap_pte_batch(folio_ptep, nr, folio_pte) != nr)
+		if (!can_swapin_thp(vmf, folio_ptep, folio_address, nr))
 			goto check_folio;
 
 		page_idx = idx;
-		address = folio_start;
+		address = folio_address;
 		ptep = folio_ptep;
 		nr_pages = nr;
 		entry = folio->swap;
Signature · 1 line
2.49.0
#27 lore raw

Re: [PATCH 11/28] mm, swap: clean up and consolidate helper for mTHP swapin check

Hi,

Quoted 5 lines · Kairui Song

From: Kairui Song <[email protected]>

Move all mTHP swapin check into can_swapin_thp and use it for both pre IO check and post IO check. This way the code is more consolidated and make later commit easier to maintain.

From what I can see, can_swapin_thp is gated behind CONFIG_TRANSPARENT_HUGEPAGE and this fails to build when it's not enabled.

Quoted 138 lines

Also clean up the comments while at it. The current comment of non_swapcache_batch is not correct: swap in bypassing swap cache won't reach the swap device as long as the entry is cached, because it still sets the SWAP_HAS_CACHE flag. If the folio is already in swap cache, raced swap in will either fail due to -EEXIST with swapcache_prepare, or see the cached folio.

The real reason this non_swapcache_batch is needed is that if a smaller folio is in the swap cache but not mapped, mTHP swapin will be blocked forever as it won't see the folio due to index offset, nor it can set the SWAP_HAS_CACHE bit, so it has to fallback to order 0 swap in.

---
 mm/memory.c | 90 ++++++++++++++++++++++++-----------------------------
 1 file changed, 41 insertions(+), 49 deletions(-)
mm/memory.c +39 -41
index f2897d9059f2..1b6e192de6ec 100644
@@ -4319,12 +4319,6 @@ static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
 	pgoff_t offset = swp_offset(entry);
 	int i;
 
-	/*
-	 * While allocating a large folio and doing swap_read_folio, which is
-	 * the case the being faulted pte doesn't have swapcache. We need to
-	 * ensure all PTEs have no cache as well, otherwise, we might go to
-	 * swap devices while the content is in swapcache.
-	 */
 	for (i = 0; i < max_nr; i++) {
 		if ((si->swap_map[offset + i] & SWAP_HAS_CACHE))
 			return i;
@@ -4334,34 +4328,30 @@ static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
 }
 
 /*
- * Check if the PTEs within a range are contiguous swap entries
- * and have consistent swapcache, zeromap.
+ * Check if the page table is still suitable for large folio swap in.
+ * @vmf: The fault triggering the swap-in.
+ * @ptep: Pointer to the PTE that should be the head of the swap in folio.
+ * @addr: The address corresponding to the PTE.
+ * @nr_pages: Number of pages of the folio that suppose to be swapped in.
  */
-static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep, int nr_pages)
+static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep,
+			   unsigned long addr, unsigned int nr_pages)
 {
-	unsigned long addr;
-	swp_entry_t entry;
-	int idx;
-	pte_t pte;
-
-	addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
-	idx = (vmf->address - addr) / PAGE_SIZE;
-	pte = ptep_get(ptep);
+	pte_t pte = ptep_get(ptep);
+	unsigned long addr_end = addr + (PAGE_SIZE * nr_pages);
+	unsigned long pte_offset = (vmf->address - addr) / PAGE_SIZE;
 
-	if (!pte_same(pte, pte_move_swp_offset(vmf->orig_pte, -idx)))
+	VM_WARN_ON_ONCE(!IS_ALIGNED(addr, PAGE_SIZE) ||
+			addr > vmf->address || addr_end <= vmf->address);
+	if (unlikely(addr < max(addr & PMD_MASK, vmf->vma->vm_start) ||
+		     addr_end > pmd_addr_end(addr, vmf->vma->vm_end)))
 		return false;
-	entry = pte_to_swp_entry(pte);
-	if (swap_pte_batch(ptep, nr_pages, pte) != nr_pages)
-		return false;
-
 	/*
-	 * swap_read_folio() can't handle the case a large folio is hybridly
-	 * from different backends. And they are likely corner cases. Similar
-	 * things might be added once zswap support large folios.
+	 * All swap entries must from the same swap device, in same
+	 * cgroup, with same exclusiveness, only differs in offset.
 	 */
-	if (unlikely(swap_zeromap_batch(entry, nr_pages, NULL) != nr_pages))
-		return false;
-	if (unlikely(non_swapcache_batch(entry, nr_pages) != nr_pages))
+	if (!pte_same(pte, pte_move_swp_offset(vmf->orig_pte, -pte_offset)) ||
+	    swap_pte_batch(ptep, nr_pages, pte) != nr_pages)
 		return false;
 
 	return true;
@@ -4441,13 +4431,24 @@ static struct folio *alloc_swap_folio(struct vm_fault *vmf)
 	 * completely swap entries with contiguous swap offsets.
 	 */
 	order = highest_order(orders);
-	while (orders) {
-		addr = ALIGN_DOWN(vmf->address, PAGE_SIZE << order);
-		if (can_swapin_thp(vmf, pte + pte_index(addr), 1 << order))
-			break;
-		order = next_order(&orders, order);
+	for (; orders; order = next_order(&orders, order)) {
+		unsigned long nr_pages = 1 << order;
+		swp_entry_t swap_entry = { .val = ALIGN_DOWN(entry.val, nr_pages) };
+		addr = ALIGN_DOWN(vmf->address, nr_pages * PAGE_SIZE);
+		if (!can_swapin_thp(vmf, pte + pte_index(addr), addr, nr_pages))
+			continue;
+		/*
+		 * If there is already a smaller folio in cache, it will
+		 * conflict with the larger folio in the swap cache layer
+		 * and block the swap in.
+		 */
+		if (unlikely(non_swapcache_batch(swap_entry, nr_pages) != nr_pages))
+			continue;
+		/* Zero map doesn't work with large folio yet. */
+		if (unlikely(swap_zeromap_batch(swap_entry, nr_pages, NULL) != nr_pages))
+			continue;
+		break;
 	}
-
 	pte_unmap_unlock(pte, ptl);
 
 	/* Try allocating the highest of the remaining orders. */
@@ -4731,27 +4732,18 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	page_idx = 0;
 	address = vmf->address;
 	ptep = vmf->pte;
+
 	if (folio_test_large(folio) && folio_test_swapcache(folio)) {
-		int nr = folio_nr_pages(folio);
+		unsigned long nr = folio_nr_pages(folio);
 		unsigned long idx = folio_page_idx(folio, page);
-		unsigned long folio_start = address - idx * PAGE_SIZE;
-		unsigned long folio_end = folio_start + nr * PAGE_SIZE;
-		pte_t *folio_ptep;
-		pte_t folio_pte;
+		unsigned long folio_address = address - idx * PAGE_SIZE;
+		pte_t *folio_ptep = vmf->pte - idx;
 
-		if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
-			goto check_folio;

mm/memory.c vm_fault_t do_swap_page(struct vm_fault *vmf) · line 4744

Quoted 10 lines
mm/memory.c +1 -7
-		if (unlikely(folio_end > pmd_addr_end(address, vma->vm_end)))
-			goto check_folio;
-
-		folio_ptep = vmf->pte - idx;
-		folio_pte = ptep_get(folio_ptep);
-		if (!pte_same(folio_pte, pte_move_swp_offset(vmf->orig_pte, -idx)) ||
-		    swap_pte_batch(folio_ptep, nr, folio_pte) != nr)
 
+		if (!can_swapin_thp(vmf, folio_ptep, folio_address, nr))
 			goto check_folio;

At this point we're outside CONFIG_TRANSPARENT_HUGEPAGE.

Quoted 10 lines
mm/memory.c +1 -1
 		page_idx = idx;
-		address = folio_start;
+		address = folio_address;
 		ptep = folio_ptep;
 		nr_pages = nr;
 		entry = folio->swap;
Signature · 1 line
2.49.0

Regards,
Klara Modin

#28 lore raw

Re: [PATCH 11/28] mm, swap: clean up and consolidate helper for mTHP swapin check

Quoted 6 lines · Klara Modin

Hi,

Quoted 2 lines · Kairui Song

From: Kairui Song <[email protected]>

Quoted 7 lines
Quoted 3 lines

Move all mTHP swapin check into can_swapin_thp and use it for both pre IO check and post IO check. This way the code is more consolidated and make later commit easier to maintain.

From what I can see, can_swapin_thp is gated behind CONFIG_TRANSPARENT_HUGEPAGE and this fails to build when it's not enabled.

Thanks for the review.

Right, I might have to add an empty one for
!CONFIG_TRANSPARENT_HUGEPAGE to pass the build.

#29 lore raw

Re: [PATCH 11/28] mm, swap: clean up and consolidate helper for mTHP swapin check

In reply to Kairui Song
Quoted 19 lines

From: Kairui Song <[email protected]>

mm/memory.c +6 -2
-static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep, int nr_pages)
+static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep,
+			   unsigned long addr, unsigned int nr_pages)
 
+	if (unlikely(addr < max(addr & PMD_MASK, vmf->vma->vm_start) ||
+		     addr_end > pmd_addr_end(addr, vmf->vma->vm_end)))
@@ -4731,27 +4732,18 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	page_idx = 0;
 	address = vmf->address;
 	ptep = vmf->pte;
+
 	if (folio_test_large(folio) && folio_test_swapcache(folio)) {
-		int nr = folio_nr_pages(folio);
+		unsigned long nr = folio_nr_pages(folio);

mm/memory.c vm_fault_t do_swap_page(struct vm_fault *vmf) · line 4743

Quoted 10 lines
mm/memory.c +2 -6
 		unsigned long idx = folio_page_idx(folio, page);
-		unsigned long folio_start = address - idx * PAGE_SIZE;
-		unsigned long folio_end = folio_start + nr * PAGE_SIZE;
-		pte_t *folio_ptep;
-		pte_t folio_pte;
+		unsigned long folio_address = address - idx * PAGE_SIZE;
+		pte_t *folio_ptep = vmf->pte - idx;
 
-		if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
-			goto check_folio;

We are handling a corner case a large folio is remapped to an unaligned address. For example,

A 64KiB mTHP at address: XGB + 2MB +4KB,

Its start address will be XGB + 2MB - 60KB which is another PMD.

The previous code will return false; now your can_swapin_thp() will return true as you are using XGB + 2MB - 60KB as the argument "addr" in can_swapin_thp().

mm/memory.c vm_fault_t do_swap_page(struct vm_fault *vmf) · line 4742

Quoted 8 lines
mm/memory.c +1 -7
-		if (unlikely(folio_end > pmd_addr_end(address, vma->vm_end)))
-			goto check_folio;
-
-		folio_ptep = vmf->pte - idx;
-		folio_pte = ptep_get(folio_ptep);
-		if (!pte_same(folio_pte, pte_move_swp_offset(vmf->orig_pte, -idx)) ||
-		    swap_pte_batch(folio_ptep, nr, folio_pte) != nr)
+		if (!can_swapin_thp(vmf, folio_ptep, folio_address, nr))

Thanks
Barry

#30 lore raw

Re: [PATCH 11/28] mm, swap: clean up and consolidate helper for mTHP swapin check

Quoted 34 lines · Barry Song
Quoted 20 lines

From: Kairui Song <[email protected]>

mm/memory.c +6 -2
-static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep, int nr_pages)
+static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep,
+                        unsigned long addr, unsigned int nr_pages)
 
+     if (unlikely(addr < max(addr & PMD_MASK, vmf->vma->vm_start) ||
+                  addr_end > pmd_addr_end(addr, vmf->vma->vm_end)))
@@ -4731,27 +4732,18 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
      page_idx = 0;
      address = vmf->address;
      ptep = vmf->pte;
+
      if (folio_test_large(folio) && folio_test_swapcache(folio)) {
-             int nr = folio_nr_pages(folio);
+             unsigned long nr = folio_nr_pages(folio);

mm/memory.c vm_fault_t do_swap_page(struct vm_fault *vmf) · line 4743

Quoted 10 lines
mm/memory.c +2 -6
              unsigned long idx = folio_page_idx(folio, page);
-             unsigned long folio_start = address - idx * PAGE_SIZE;
-             unsigned long folio_end = folio_start + nr * PAGE_SIZE;
-             pte_t *folio_ptep;
-             pte_t folio_pte;
+             unsigned long folio_address = address - idx * PAGE_SIZE;
+             pte_t *folio_ptep = vmf->pte - idx;
 
-             if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
-                     goto check_folio;

We are handling a corner case a large folio is remapped to an unaligned address. For example,

Quoted 6 lines

A 64KiB mTHP at address: XGB + 2MB +4KB,

Its start address will be XGB + 2MB - 60KB which is another PMD.

The previous code will return false; now your can_swapin_thp() will return true as you are using XGB + 2MB - 60KB as the argument "addr" in can_swapin_thp().

Thanks very much for the info and explanation.

You are right, I need to keep using vmf->address in can_swapin_thp:

if (unlikely(addr < max(vmf->address & PMD_MASK, vmf->vma->vm_start) ||

     addr_end > pmd_addr_end(vmf->address, vmf->vma->vm_end)))

return false;

But one thing I'm not so sure is how that happens? And there isn't an address checking in the direct swapin mTHP check above?

#31 lore raw

Re: [PATCH 11/28] mm, swap: clean up and consolidate helper for mTHP swapin check

Quoted 45 lines · Kairui Song
Quoted 34 lines · Barry Song
Quoted 20 lines

From: Kairui Song <[email protected]>

mm/memory.c +6 -2
-static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep, int nr_pages)
+static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep,
+                        unsigned long addr, unsigned int nr_pages)
 
+     if (unlikely(addr < max(addr & PMD_MASK, vmf->vma->vm_start) ||
+                  addr_end > pmd_addr_end(addr, vmf->vma->vm_end)))
@@ -4731,27 +4732,18 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
      page_idx = 0;
      address = vmf->address;
      ptep = vmf->pte;
+
      if (folio_test_large(folio) && folio_test_swapcache(folio)) {
-             int nr = folio_nr_pages(folio);
+             unsigned long nr = folio_nr_pages(folio);

mm/memory.c vm_fault_t do_swap_page(struct vm_fault *vmf) · line 4743

Quoted 10 lines
mm/memory.c +2 -6
              unsigned long idx = folio_page_idx(folio, page);
-             unsigned long folio_start = address - idx * PAGE_SIZE;
-             unsigned long folio_end = folio_start + nr * PAGE_SIZE;
-             pte_t *folio_ptep;
-             pte_t folio_pte;
+             unsigned long folio_address = address - idx * PAGE_SIZE;
+             pte_t *folio_ptep = vmf->pte - idx;
 
-             if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
-                     goto check_folio;

We are handling a corner case a large folio is remapped to an unaligned address. For example,

Quoted 6 lines

A 64KiB mTHP at address: XGB + 2MB +4KB,

Its start address will be XGB + 2MB - 60KB which is another PMD.

The previous code will return false; now your can_swapin_thp() will return true as you are using XGB + 2MB - 60KB as the argument "addr" in can_swapin_thp().

Thanks very much for the info and explanation.

Quoted 8 lines

You are right, I need to keep using vmf->address in can_swapin_thp:

if (unlikely(addr < max(vmf->address & PMD_MASK, vmf->vma->vm_start) ||

     addr_end > pmd_addr_end(vmf->address, vmf->vma->vm_end)))

return false;

But one thing I'm not so sure is how that happens? And there isn't an address checking in the direct swapin mTHP check above?

In page faults, we always make the start address aligned with PAGE_SIZE * nr_pages.
but for a mremap, we can't actually control the dst address.

so the original code can exclude this case for direct mTHP swapin by the below you are
dropping:

- if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
- goto check_folio;
- if (unlikely(folio_end > pmd_addr_end(address, vma->vm_end)))
- goto check_folio;

Thanks
Barry

#32 lore raw

PATCH 12/28 mm, swap: never bypass the swap cache for SWP_SYNCHRONOUS_IO

In reply to Kairui Song

From: Kairui Song <[email protected]>

Now the overhead of the swap cache is trivial to none, bypassing the swap cache is no longer a valid optimization.

This commit is more than code simplification, it changes the swap in behaviour in multiple ways:

We used to rely on SWP_SYNCHRONOUS_IO && __swap_count(entry) == 1 as The indicator to bypass the swap cache and read ahead, in many workload bypassing read ahead is the more helpful part for SWP_SYNCHRONOUS_IO devices as they have extreme low latency the read ahead isn't helpful.

The SWP_SYNCHRONOUS_IO && __swap_count(entry) == 1 is not a good indicator in the first place: obviously, read ahead has nothing to do with swap count, that's more of a workaround due to the limitation of current implementation that read ahead bypassing is strictly coupled with swap cache bypassing. Swap count > 1 can't bypass the swap cache because that will result in redundant IO or wasted CPU time.

So the first change with this commit is that read ahead is now always disabled for SWP_SYNCHRONOUS_IO devices, this is a good thing as these devices have extreme low latency, and queued IO do not affect them (ZRAM, RAMDISK), so read ahead isn't helpful.

The second thing here is that this enabled mTHP swap in for all faults on SWP_SYNCHRONOUS_IO devices. Previously, the mTHP swap is also coupled with swap cache bypassing. But again clearly, it doesn't make much sense that mTHP's ref count affects its swap in behavior.

And to catch potential issues with mTHP swap in, especially with page exclusiveness, more debug sanity checks and comments are added. But the code is still simpler with reduced LOC.

For a real mTHP workload, this may cause more serious thrashing, this isn't a problem with this commit but a generic mTHP issue. For a 4K workload, this commit boosts the performance:

---
 mm/memory.c | 267 +++++++++++++++++++++++-----------------------------
 1 file changed, 116 insertions(+), 151 deletions(-)
mm/memory.c +116 -151
index 1b6e192de6ec..0b41d15c6d7a 100644
@@ -87,6 +87,7 @@
 #include <asm/tlbflush.h>
 
 #include "pgalloc-track.h"
+#include "swap_table.h"
 #include "internal.h"
 #include "swap.h"
 
@@ -4477,7 +4478,33 @@ static struct folio *alloc_swap_folio(struct vm_fault *vmf)
 }
 #endif /* CONFIG_TRANSPARENT_HUGEPAGE */
 
-static DECLARE_WAIT_QUEUE_HEAD(swapcache_wq);
+/* Check if a folio should be exclusive, with sanity tests */
+static bool check_swap_exclusive(struct folio *folio, swp_entry_t entry,
+				 pte_t *ptep, unsigned int fault_nr)
+{
+	pgoff_t offset = swp_offset(entry);
+	struct page *page = folio_file_page(folio, offset);
+
+	if (!pte_swp_exclusive(ptep_get(ptep)))
+		return false;
+
+	/* For exclusive swapin, it must not be mapped */
+	if (fault_nr == 1)
+		VM_WARN_ON_ONCE_PAGE(atomic_read(&page->_mapcount) != -1, page);
+	else
+		VM_WARN_ON_ONCE_FOLIO(folio_mapped(folio), folio);
+	/*
+	 * Check if swap count is consistent with exclusiveness. The folio
+	 * and PTL lock keeps the swap count stable.
+	 */
+	if (IS_ENABLED(CONFIG_VM_DEBUG)) {
+		for (int i = 0; i < fault_nr; i++) {
+			VM_WARN_ON_FOLIO(__swap_count(entry) != 1, folio);
+			entry.val++;
+		}
+	}
+	return true;
+}
 
 /*
  * We enter with non-exclusive mmap_lock (to exclude vma changes,
@@ -4490,17 +4517,14 @@ static DECLARE_WAIT_QUEUE_HEAD(swapcache_wq);
 vm_fault_t do_swap_page(struct vm_fault *vmf)
 {
 	struct vm_area_struct *vma = vmf->vma;
-	struct folio *swapcache, *folio = NULL;
-	DECLARE_WAITQUEUE(wait, current);
+	struct folio *swapcache = NULL, *folio;
 	struct page *page;
 	struct swap_info_struct *si = NULL;
 	rmap_t rmap_flags = RMAP_NONE;
-	bool need_clear_cache = false;
 	bool exclusive = false;
 	swp_entry_t entry;
 	pte_t pte;
 	vm_fault_t ret = 0;
-	void *shadow = NULL;
 	int nr_pages;
 	unsigned long page_idx;
 	unsigned long address;
@@ -4571,56 +4595,18 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	folio = swap_cache_get_folio(entry);
 	swapcache = folio;
 	if (!folio) {
-		if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
-		    __swap_count(entry) == 1) {
-			/* skip swapcache */
+		if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) {
 			folio = alloc_swap_folio(vmf);
 			if (folio) {
-				__folio_set_locked(folio);
-				__folio_set_swapbacked(folio);
-
-				nr_pages = folio_nr_pages(folio);
-				if (folio_test_large(folio))
-					entry.val = ALIGN_DOWN(entry.val, nr_pages);
-				/*
-				 * Prevent parallel swapin from proceeding with
-				 * the cache flag. Otherwise, another thread
-				 * may finish swapin first, free the entry, and
-				 * swapout reusing the same entry. It's
-				 * undetectable as pte_same() returns true due
-				 * to entry reuse.
-				 */
-				if (swapcache_prepare(entry, nr_pages)) {
-					/*
-					 * Relax a bit to prevent rapid
-					 * repeated page faults.
-					 */
-					add_wait_queue(&swapcache_wq, &wait);
-					schedule_timeout_uninterruptible(1);
-					remove_wait_queue(&swapcache_wq, &wait);
-					goto out_page;
-				}
-				need_clear_cache = true;
-
-				memcg1_swapin(entry, nr_pages);
-
-				shadow = swap_cache_get_shadow(entry);
-				if (shadow)
-					workingset_refault(folio, shadow);
-
-				folio_add_lru(folio);
-
-				/* To provide entry to swap_read_folio() */
-				folio->swap = entry;
-				swap_read_folio(folio, NULL);
-				folio->private = NULL;
+				swapcache = swapin_entry(entry, folio);
+				if (swapcache != folio)
+					folio_put(folio);
 			}
 		} else {
-			folio = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
-						vmf);
-			swapcache = folio;
+			swapcache = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf);
 		}
 
+		folio = swapcache;
 		if (!folio) {
 			/*
 			 * Back out if somebody else faulted in this pte
@@ -4644,57 +4630,56 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	if (ret & VM_FAULT_RETRY)
 		goto out_release;
 
+	/*
+	 * Make sure folio_free_swap() or swapoff did not release the
+	 * swapcache from under us.  The page pin, and pte_same test
+	 * below, are not enough to exclude that.  Even if it is still
+	 * swapcache, we need to check that the page's swap has not
+	 * changed.
+	 */
+	if (!folio_swap_contains(folio, entry))
+		goto out_page;
 	page = folio_file_page(folio, swp_offset(entry));
-	if (swapcache) {
-		/*
-		 * Make sure folio_free_swap() or swapoff did not release the
-		 * swapcache from under us.  The page pin, and pte_same test
-		 * below, are not enough to exclude that.  Even if it is still
-		 * swapcache, we need to check that the page's swap has not
-		 * changed.
-		 */
-		if (!folio_swap_contains(folio, entry))
-			goto out_page;
 
-		if (PageHWPoison(page)) {
-			/*
-			 * hwpoisoned dirty swapcache pages are kept for killing
-			 * owner processes (which may be unknown at hwpoison time)
-			 */
-			ret = VM_FAULT_HWPOISON;
-			goto out_page;
-		}
-
-		swap_update_readahead(folio, vma, vmf->address);
+	/*
+	 * hwpoisoned dirty swapcache pages are kept for killing
+	 * owner processes (which may be unknown at hwpoison time)
+	 */
+	if (PageHWPoison(page)) {
+		ret = VM_FAULT_HWPOISON;
+		goto out_page;
+	}
 
-		/*
-		 * KSM sometimes has to copy on read faults, for example, if
-		 * page->index of !PageKSM() pages would be nonlinear inside the
-		 * anon VMA -- PageKSM() is lost on actual swapout.
-		 */
-		folio = ksm_might_need_to_copy(folio, vma, vmf->address);
-		if (unlikely(!folio)) {
-			ret = VM_FAULT_OOM;
-			folio = swapcache;
-			goto out_page;
-		} else if (unlikely(folio == ERR_PTR(-EHWPOISON))) {
-			ret = VM_FAULT_HWPOISON;
-			folio = swapcache;
-			goto out_page;
-		} else if (folio != swapcache)
-			page = folio_page(folio, 0);
+	swap_update_readahead(folio, vma, vmf->address);
 
-		/*
-		 * If we want to map a page that's in the swapcache writable, we
-		 * have to detect via the refcount if we're really the exclusive
-		 * owner. Try removing the extra reference from the local LRU
-		 * caches if required.
-		 */
-		if ((vmf->flags & FAULT_FLAG_WRITE) && folio == swapcache &&
-		    !folio_test_ksm(folio) && !folio_test_lru(folio))
-			lru_add_drain();
+	/*
+	 * KSM sometimes has to copy on read faults, for example, if
+	 * page->index of !PageKSM() pages would be nonlinear inside the
+	 * anon VMA -- PageKSM() is lost on actual swapout.
+	 */
+	folio = ksm_might_need_to_copy(folio, vma, vmf->address);
+	if (unlikely(!folio)) {
+		ret = VM_FAULT_OOM;
+		folio = swapcache;
+		goto out_page;
+	} else if (unlikely(folio == ERR_PTR(-EHWPOISON))) {
+		ret = VM_FAULT_HWPOISON;
+		folio = swapcache;
+		goto out_page;
+	} else if (folio != swapcache) {
+		page = folio_file_page(folio, swp_offset(entry));
 	}
 
+	/*
+	 * If we want to map a page that's in the swapcache writable, we
+	 * have to detect via the refcount if we're really the exclusive
+	 * owner. Try removing the extra reference from the local LRU
+	 * caches if required.
+	 */
+	if ((vmf->flags & FAULT_FLAG_WRITE) && folio == swapcache &&
+	    !folio_test_ksm(folio) && !folio_test_lru(folio))
+		lru_add_drain();
+
 	folio_throttle_swaprate(folio, GFP_KERNEL);
 
 	/*
@@ -4710,44 +4695,41 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		goto out_nomap;
 	}
 
-	/* allocated large folios for SWP_SYNCHRONOUS_IO */
-	if (folio_test_large(folio) && !folio_test_swapcache(folio)) {
-		unsigned long nr = folio_nr_pages(folio);
-		unsigned long folio_start = ALIGN_DOWN(vmf->address, nr * PAGE_SIZE);
-		unsigned long idx = (vmf->address - folio_start) / PAGE_SIZE;
-		pte_t *folio_ptep = vmf->pte - idx;
-		pte_t folio_pte = ptep_get(folio_ptep);
-
-		if (!pte_same(folio_pte, pte_move_swp_offset(vmf->orig_pte, -idx)) ||
-		    swap_pte_batch(folio_ptep, nr, folio_pte) != nr)
-			goto out_nomap;
-
-		page_idx = idx;
-		address = folio_start;
-		ptep = folio_ptep;
-		goto check_folio;
-	}
-
 	nr_pages = 1;
 	page_idx = 0;
 	address = vmf->address;
 	ptep = vmf->pte;
 
-	if (folio_test_large(folio) && folio_test_swapcache(folio)) {
+	if (folio_test_large(folio)) {
 		unsigned long nr = folio_nr_pages(folio);
 		unsigned long idx = folio_page_idx(folio, page);
-		unsigned long folio_address = address - idx * PAGE_SIZE;
+		unsigned long folio_address = vmf->address - idx * PAGE_SIZE;
 		pte_t *folio_ptep = vmf->pte - idx;
 
-		if (!can_swapin_thp(vmf, folio_ptep, folio_address, nr))
+		if (can_swapin_thp(vmf, folio_ptep, folio_address, nr)) {
+			page_idx = idx;
+			address = folio_address;
+			ptep = folio_ptep;
+			nr_pages = nr;
+			entry = folio->swap;
+			page = &folio->page;
 			goto check_folio;
-
-		page_idx = idx;
-		address = folio_address;
-		ptep = folio_ptep;
-		nr_pages = nr;
-		entry = folio->swap;
-		page = &folio->page;
+		}
+		/*
+		 * If it's a fresh large folio in the swap cache but the
+		 * page table supporting it is gone, drop it and fallback
+		 * to order 0 swap in again.
+		 *
+		 * The folio must be clean, nothing should have touched
+		 * it, shmem removes the folio from swap cache upon
+		 * swapin, and anon flag won't be gone once set.
+		 * TODO: We might want to split or partially map it.
+		 */
+		if (!folio_test_anon(folio)) {
+			WARN_ON_ONCE(folio_test_dirty(folio));
+			delete_from_swap_cache(folio);
+			goto out_nomap;
+		}
 	}
 
 check_folio:
@@ -4767,7 +4749,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	 * the swap entry concurrently) for certainly exclusive pages.
 	 */
 	if (!folio_test_ksm(folio)) {
-		exclusive = pte_swp_exclusive(vmf->orig_pte);
+		exclusive = check_swap_exclusive(folio, entry, ptep, nr_pages);
 		if (folio != swapcache) {
 			/*
 			 * We have a fresh page that is not exposed to the
@@ -4805,15 +4787,6 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	 */
 	arch_swap_restore(folio_swap(entry, folio), folio);
 
-	/*
-	 * Remove the swap entry and conditionally try to free up the swapcache.
-	 * We're already holding a reference on the page but haven't mapped it
-	 * yet.
-	 */
-	swap_free_nr(entry, nr_pages);
-	if (should_try_to_free_swap(folio, vma, vmf->flags))
-		folio_free_swap(folio);
-
 	add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
 	add_mm_counter(vma->vm_mm, MM_SWAPENTS, -nr_pages);
 	pte = mk_pte(page, vma->vm_page_prot);
@@ -4849,14 +4822,8 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		folio_add_new_anon_rmap(folio, vma, address, RMAP_EXCLUSIVE);
 		folio_add_lru_vma(folio, vma);
 	} else if (!folio_test_anon(folio)) {
-		/*
-		 * We currently only expect small !anon folios which are either
-		 * fully exclusive or fully shared, or new allocated large
-		 * folios which are fully exclusive. If we ever get large
-		 * folios within swapcache here, we have to be careful.
-		 */
-		VM_WARN_ON_ONCE(folio_test_large(folio) && folio_test_swapcache(folio));
-		VM_WARN_ON_FOLIO(!folio_test_locked(folio), folio);
+		VM_WARN_ON_ONCE_FOLIO(folio_nr_pages(folio) != nr_pages, folio);
+		VM_WARN_ON_ONCE_FOLIO(folio_mapped(folio), folio);
 		folio_add_new_anon_rmap(folio, vma, address, rmap_flags);
 	} else {
 		folio_add_anon_rmap_ptes(folio, page, nr_pages, vma, address,
@@ -4869,7 +4836,16 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	arch_do_swap_page_nr(vma->vm_mm, vma, address,
 			pte, pte, nr_pages);
 
+	/*
+	 * Remove the swap entry and conditionally try to free up the
+	 * swapcache then unlock the folio. Do this after the PTEs are
+	 * set, so raced faults will see updated PTEs.
+	 */
+	swap_free_nr(entry, nr_pages);
+	if (should_try_to_free_swap(folio, vma, vmf->flags))
+		folio_free_swap(folio);
 	folio_unlock(folio);
+
 	if (folio != swapcache && swapcache) {
 		/*
 		 * Hold the lock to avoid the swap entry to be reused
@@ -4896,12 +4872,6 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	if (vmf->pte)
 		pte_unmap_unlock(vmf->pte, vmf->ptl);
 out:
-	/* Clear the swap cache pin for direct swapin after PTL unlock */
-	if (need_clear_cache) {
-		swapcache_clear(si, entry, nr_pages);
-		if (waitqueue_active(&swapcache_wq))
-			wake_up(&swapcache_wq);
-	}
 	if (si)
 		put_swap_device(si);
 	return ret;
@@ -4916,11 +4886,6 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		folio_unlock(swapcache);
 		folio_put(swapcache);
 	}
-	if (need_clear_cache) {
-		swapcache_clear(si, entry, nr_pages);
-		if (waitqueue_active(&swapcache_wq))
-			wake_up(&swapcache_wq);
-	}
 	if (si)
 		put_swap_device(si);
 	return ret;
Signature · 1 line
2.49.0
#33 lore raw

PATCH 13/28 mm/shmem, swap: avoid redundant Xarray lookup during swapin

In reply to Kairui Song

From: Kairui Song <[email protected]>

Currently shmem calls xa_get_order multiple times to get the swap radix entry order. This can be combined with the swap entry value checking (shmem_confirm_swap) to avoid the duplicated lookup, which should improve the performance.

This also provides the helper need for later commits.

---
 mm/shmem.c | 67 ++++++++++++++++++++++++++++++------------------------
 1 file changed, 37 insertions(+), 30 deletions(-)
mm/shmem.c +37 -30
index 0da9e06eaee8..da80a8faa39e 100644
@@ -509,11 +509,26 @@ static int shmem_replace_entry(struct address_space *mapping,
  *
  * Checking folio is not enough: by the time a swapcache folio is locked, it
  * might be reused, and again be swapcache, using the same swap as before.
+ *
+ * Check if the swap entry is still in the shmem mapping and get its order,
+ * return -1 if it's no longer valid.
  */
-static bool shmem_confirm_swap(struct address_space *mapping,
-			       pgoff_t index, swp_entry_t swap)
+static int shmem_check_swap_entry(struct address_space *mapping, pgoff_t index,
+				  swp_entry_t swap)
 {
-	return xa_load(&mapping->i_pages, index) == swp_to_radix_entry(swap);
+	XA_STATE(xas, &mapping->i_pages, index);
+	int order = -1;
+	void *entry;
+
+	rcu_read_lock();
+	do {
+		entry = xas_load(&xas);
+		if (entry == swp_to_radix_entry(swap))
+			order = xas_get_order(&xas);
+	} while (xas_retry(&xas, entry));
+	rcu_read_unlock();
+
+	return order;
 }
 
 /*
@@ -2238,16 +2253,17 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		return -EIO;
 
 	si = get_swap_device(swap);
-	if (!si) {
-		if (!shmem_confirm_swap(mapping, index, swap))
-			return -EEXIST;
-		else
-			return -EINVAL;
+	order = shmem_check_swap_entry(mapping, index, swap);
+	if (order < 0) {
+		if (si)
+			put_swap_device(si);
+		return -EEXIST;
 	}
+	if (!si)
+		return -EINVAL;
 
 	/* Look it up and read it in.. */
 	folio = swap_cache_get_folio(swap);
-	order = xa_get_order(&mapping->i_pages, index);
 	if (!folio) {
 		bool fallback_order0 = false;
 
@@ -2303,7 +2319,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		 */
 		if (split_order > 0) {
 			pgoff_t offset = index - round_down(index, 1 << split_order);
-
 			swap = swp_entry(swp_type(swap), swp_offset(swap) + offset);
 		}
 
@@ -2325,25 +2340,20 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 			error = split_order;
 			goto failed;
 		}
-
-		/*
-		 * If the large swap entry has already been split, it is
-		 * necessary to recalculate the new swap entry based on
-		 * the old order alignment.
-		 */
-		if (split_order > 0) {
-			pgoff_t offset = index - round_down(index, 1 << split_order);
-
-			swap = swp_entry(swp_type(swap), swp_offset(swap) + offset);
-		}
 	}
 alloced:
 	/* We have to do this with folio locked to prevent races */
 	folio_lock(folio);
-	if ((!skip_swapcache && !folio_test_swapcache(folio)) ||
-	    folio->swap.val != swap.val ||
-	    !shmem_confirm_swap(mapping, index, swap) ||
-	    xa_get_order(&mapping->i_pages, index) != folio_order(folio)) {
+	if (!skip_swapcache && !folio_swap_contains(folio, swap)) {
+		error = -EEXIST;
+		goto unlock;
+	}
+
+	nr_pages = folio_nr_pages(folio);
+	index = round_down(index, nr_pages);
+	swap = swp_entry(swp_type(swap), round_down(swp_offset(swap), nr_pages));
+
+	if (folio_order(folio) != shmem_check_swap_entry(mapping, index, swap)) {
 		error = -EEXIST;
 		goto unlock;
 	}
@@ -2354,7 +2364,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		goto failed;
 	}
 	folio_wait_writeback(folio);
-	nr_pages = folio_nr_pages(folio);
 
 	/*
 	 * Some architectures may have to restore extra metadata to the
@@ -2368,8 +2377,7 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 			goto failed;
 	}
 
-	error = shmem_add_to_page_cache(folio, mapping,
-					round_down(index, nr_pages),
+	error = shmem_add_to_page_cache(folio, mapping, index,
 					swp_to_radix_entry(swap), gfp);
 	if (error)
 		goto failed;
@@ -2392,7 +2400,7 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	*foliop = folio;
 	return 0;
 failed:
-	if (!shmem_confirm_swap(mapping, index, swap))
+	if (shmem_check_swap_entry(mapping, index, swap) < 0)
 		error = -EEXIST;
 	if (error == -EIO)
 		shmem_set_folio_swapin_error(inode, index, folio, swap,
@@ -2405,7 +2413,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		folio_put(folio);
 	}
 	put_swap_device(si);
-
 	return error;
 }
 
Signature · 1 line
2.49.0
#34 lore raw

PATCH 14/28 mm/shmem: never bypass the swap cache for SWP_SYNCHRONOUS_IO

In reply to Kairui Song

From: Kairui Song <[email protected]>

Now the overhead of the swap cache is trivial to none, bypassing the swap cache is no longer a valid optimization.

So remove the cache bypass swap path for simplification. Many helpers and functions can be dropped now.

---
 mm/shmem.c    | 109 ++++++++++++++++++--------------------------------
 mm/swap.h     |   4 --
 mm/swapfile.c |  35 +++++-----------
 3 files changed, 48 insertions(+), 100 deletions(-)
mm/shmem.c +39 -70
index da80a8faa39e..e87eff03c08b 100644
@@ -899,7 +899,9 @@ static int shmem_add_to_page_cache(struct folio *folio,
 				   pgoff_t index, void *expected, gfp_t gfp)
 {
 	XA_STATE_ORDER(xas, &mapping->i_pages, index, folio_order(folio));
-	long nr = folio_nr_pages(folio);
+	unsigned long nr = folio_nr_pages(folio);
+	swp_entry_t iter, swap;
+	void *entry;
 
 	VM_BUG_ON_FOLIO(index != round_down(index, nr), folio);
 	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
@@ -912,13 +914,19 @@ static int shmem_add_to_page_cache(struct folio *folio,
 	gfp &= GFP_RECLAIM_MASK;
 	folio_throttle_swaprate(folio, gfp);
 
+	if (expected)
+		swap = iter = radix_to_swp_entry(expected);
+
 	do {
 		xas_lock_irq(&xas);
-		if (expected != xas_find_conflict(&xas)) {
-			xas_set_err(&xas, -EEXIST);
-			goto unlock;
+		xas_for_each_conflict(&xas, entry) {
+			if (!expected || entry != swp_to_radix_entry(iter)) {
+				xas_set_err(&xas, -EEXIST);
+				goto unlock;
+			}
+			iter.val += 1 << xas_get_order(&xas);
 		}
-		if (expected && xas_find_conflict(&xas)) {
+		if (expected && iter.val - nr != swap.val) {
 			xas_set_err(&xas, -EEXIST);
 			goto unlock;
 		}
@@ -1973,14 +1981,12 @@ static struct folio *shmem_alloc_and_add_folio(struct vm_fault *vmf,
 	return ERR_PTR(error);
 }
 
-static struct folio *shmem_swap_alloc_folio(struct inode *inode,
+static struct folio *shmem_swapin_folio_order(struct inode *inode,
 		struct vm_area_struct *vma, pgoff_t index,
 		swp_entry_t entry, int order, gfp_t gfp)
 {
 	struct shmem_inode_info *info = SHMEM_I(inode);
-	struct folio *new;
-	void *shadow;
-	int nr_pages;
+	struct folio *new, *swapcache;
 
 	/*
 	 * We have arrived here because our zones are constrained, so don't
@@ -1995,41 +2001,19 @@ static struct folio *shmem_swap_alloc_folio(struct inode *inode,
 
 	new = shmem_alloc_folio(gfp, order, info, index);
 	if (!new)
-		return ERR_PTR(-ENOMEM);
+		return NULL;
 
-	nr_pages = folio_nr_pages(new);
 	if (mem_cgroup_swapin_charge_folio(new, vma ? vma->vm_mm : NULL,
-					   gfp, entry)) {
+				gfp, entry)) {
 		folio_put(new);
-		return ERR_PTR(-ENOMEM);
+		return NULL;
 	}
 
-	/*
-	 * Prevent parallel swapin from proceeding with the swap cache flag.
-	 *
-	 * Of course there is another possible concurrent scenario as well,
-	 * that is to say, the swap cache flag of a large folio has already
-	 * been set by swapcache_prepare(), while another thread may have
-	 * already split the large swap entry stored in the shmem mapping.
-	 * In this case, shmem_add_to_page_cache() will help identify the
-	 * concurrent swapin and return -EEXIST.
-	 */
-	if (swapcache_prepare(entry, nr_pages)) {
+	swapcache = swapin_entry(entry, new);
+	if (swapcache != new)
 		folio_put(new);
-		return ERR_PTR(-EEXIST);
-	}
 
-	__folio_set_locked(new);
-	__folio_set_swapbacked(new);
-	new->swap = entry;
-
-	memcg1_swapin(entry, nr_pages);
-	shadow = swap_cache_get_shadow(entry);
-	if (shadow)
-		workingset_refault(new, shadow);
-	folio_add_lru(new);
-	swap_read_folio(new, NULL);
-	return new;
+	return swapcache;
 }
 
 /*
@@ -2122,8 +2106,7 @@ static int shmem_replace_folio(struct folio **foliop, gfp_t gfp,
 }
 
 static void shmem_set_folio_swapin_error(struct inode *inode, pgoff_t index,
-					 struct folio *folio, swp_entry_t swap,
-					 bool skip_swapcache)
+					 struct folio *folio, swp_entry_t swap)
 {
 	struct address_space *mapping = inode->i_mapping;
 	swp_entry_t swapin_error;
@@ -2139,8 +2122,7 @@ static void shmem_set_folio_swapin_error(struct inode *inode, pgoff_t index,
 
 	nr_pages = folio_nr_pages(folio);
 	folio_wait_writeback(folio);
-	if (!skip_swapcache)
-		delete_from_swap_cache(folio);
+	delete_from_swap_cache(folio);
 	/*
 	 * Don't treat swapin error folio as alloced. Otherwise inode->i_blocks
 	 * won't be 0 when inode is released and thus trigger WARN_ON(i_blocks)
@@ -2241,7 +2223,6 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	struct shmem_inode_info *info = SHMEM_I(inode);
 	struct swap_info_struct *si;
 	struct folio *folio = NULL;
-	bool skip_swapcache = false;
 	swp_entry_t swap;
 	int error, nr_pages, order, split_order;
 
@@ -2283,25 +2264,16 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 				  !zswap_never_enabled()))
 			fallback_order0 = true;
 
-		/* Skip swapcache for synchronous device. */
+		/* Try mTHP swapin for synchronous device. */
 		if (!fallback_order0 && data_race(si->flags & SWP_SYNCHRONOUS_IO)) {
-			folio = shmem_swap_alloc_folio(inode, vma, index, swap, order, gfp);
-			if (!IS_ERR(folio)) {
-				skip_swapcache = true;
+			folio = shmem_swapin_folio_order(inode, vma, index, swap, order, gfp);
+			if (folio)
 				goto alloced;
-			}
-
-			/*
-			 * Fallback to swapin order-0 folio unless the swap entry
-			 * already exists.
-			 */
-			error = PTR_ERR(folio);
-			folio = NULL;
-			if (error == -EEXIST)
-				goto failed;
 		}
 
 		/*
+		 * Fallback to swapin order-0 folio.
+		 *
 		 * Now swap device can only swap in order 0 folio, then we
 		 * should split the large swap entry stored in the pagecache
 		 * if necessary.
@@ -2338,13 +2310,15 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		split_order = shmem_split_large_entry(inode, index, swap, gfp);
 		if (split_order < 0) {
 			error = split_order;
+			folio_put(folio);
+			folio = NULL;
 			goto failed;
 		}
 	}
 alloced:
 	/* We have to do this with folio locked to prevent races */
 	folio_lock(folio);
-	if (!skip_swapcache && !folio_swap_contains(folio, swap)) {
+	if (!folio_swap_contains(folio, swap)) {
 		error = -EEXIST;
 		goto unlock;
 	}
@@ -2353,12 +2327,15 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	index = round_down(index, nr_pages);
 	swap = swp_entry(swp_type(swap), round_down(swp_offset(swap), nr_pages));
 
-	if (folio_order(folio) != shmem_check_swap_entry(mapping, index, swap)) {
+	/*
+	 * Swap must go through swap cache layer, only the split may happen
+	 * without locking the swap cache.
+	 */
+	if (folio_order(folio) < shmem_check_swap_entry(mapping, index, swap)) {
 		error = -EEXIST;
 		goto unlock;
 	}
-	if (!skip_swapcache)
-		swap_update_readahead(folio, NULL, 0);
+	swap_update_readahead(folio, NULL, 0);
 	if (!folio_test_uptodate(folio)) {
 		error = -EIO;
 		goto failed;
@@ -2387,12 +2364,7 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	if (sgp == SGP_WRITE)
 		folio_mark_accessed(folio);
 
-	if (skip_swapcache) {
-		folio->swap.val = 0;
-		swapcache_clear(si, swap, nr_pages);
-	} else {
-		delete_from_swap_cache(folio);
-	}
+	delete_from_swap_cache(folio);
 	folio_mark_dirty(folio);
 	swap_free_nr(swap, nr_pages);
 	put_swap_device(si);
@@ -2403,11 +2375,8 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	if (shmem_check_swap_entry(mapping, index, swap) < 0)
 		error = -EEXIST;
 	if (error == -EIO)
-		shmem_set_folio_swapin_error(inode, index, folio, swap,
-					     skip_swapcache);
+		shmem_set_folio_swapin_error(inode, index, folio, swap);
 unlock:
-	if (skip_swapcache)
-		swapcache_clear(si, swap, folio_nr_pages(folio));
 	if (folio) {
 		folio_unlock(folio);
 		folio_put(folio);
mm/swap.h +0 -4
index aab6bf9c3a8a..cad24a3abda8 100644
@@ -319,10 +319,6 @@ static inline int swap_writepage(struct page *p, struct writeback_control *wbc)
 	return 0;
 }
 
-static inline void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr)
-{
-}
-
 static inline struct folio *swap_cache_get_folio(swp_entry_t entry)
 {
 	return NULL;
mm/swapfile.c +9 -26
index 62af67b6f7c2..d3abd2149f8e 100644
@@ -1430,22 +1430,6 @@ struct swap_info_struct *get_swap_device(swp_entry_t entry)
 	return NULL;
 }
 
-static void swap_entries_put_cache(struct swap_info_struct *si,
-				   swp_entry_t entry, int nr)
-{
-	unsigned long offset = swp_offset(entry);
-	struct swap_cluster_info *ci;
-
-	ci = swap_lock_cluster(si, offset);
-	if (swap_only_has_cache(si, offset, nr)) {
-		swap_entries_free(si, ci, entry, nr);
-	} else {
-		for (int i = 0; i < nr; i++, entry.val++)
-			swap_entry_put_locked(si, ci, entry, SWAP_HAS_CACHE);
-	}
-	swap_unlock_cluster(ci);
-}
-
 static bool swap_entries_put_map(struct swap_info_struct *si,
 				 swp_entry_t entry, int nr)
 {
@@ -1578,13 +1562,21 @@ void swap_free_nr(swp_entry_t entry, int nr_pages)
 void put_swap_folio(struct folio *folio, swp_entry_t entry)
 {
 	struct swap_info_struct *si;
+	struct swap_cluster_info *ci;
+	unsigned long offset = swp_offset(entry);
 	int size = 1 << swap_entry_order(folio_order(folio));
 
 	si = _swap_info_get(entry);
 	if (!si)
 		return;
 
-	swap_entries_put_cache(si, entry, size);
+	ci = swap_lock_cluster(si, offset);
+	if (swap_only_has_cache(si, offset, size))
+		swap_entries_free(si, ci, entry, size);
+	else
+		for (int i = 0; i < size; i++, entry.val++)
+			swap_entry_put_locked(si, ci, entry, SWAP_HAS_CACHE);
+	swap_unlock_cluster(ci);
 }
 
 int __swap_count(swp_entry_t entry)
@@ -3615,15 +3607,6 @@ int swapcache_prepare(swp_entry_t entry, int nr)
 	return __swap_duplicate(entry, SWAP_HAS_CACHE, nr);
 }
 
-/*
- * Caller should ensure entries belong to the same folio so
- * the entries won't span cross cluster boundary.
- */
-void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr)
-{
-	swap_entries_put_cache(si, entry, nr);
-}
-
 /*
  * add_swap_count_continuation - called when a swap count is duplicated
  * beyond SWAP_MAP_MAX, it allocates a new page and links that to the entry's
Signature · 1 line
2.49.0
#35 lore raw

PATCH 15/28 mm, swap: split locked entry freeing into a standalone helper

In reply to Kairui Song

From: Kairui Song <[email protected]>

No feature change, split the common logic into a stand alone helper to be reused later.

---
 mm/swapfile.c | 61 +++++++++++++++++++++++++++------------------------
 1 file changed, 32 insertions(+), 29 deletions(-)
mm/swapfile.c +32 -29
index d3abd2149f8e..d01dc0646db9 100644
@@ -3492,26 +3492,14 @@ void si_swapinfo(struct sysinfo *val)
  * - swap-cache reference is requested but the entry is not used. -> ENOENT
  * - swap-mapped reference requested but needs continued swap count. -> ENOMEM
  */
-static int __swap_duplicate(swp_entry_t entry, unsigned char usage, int nr)
+static int swap_dup_entries(struct swap_info_struct *si,
+			    struct swap_cluster_info *ci,
+			    unsigned long offset,
+			    unsigned char usage, int nr)
 {
-	struct swap_info_struct *si;
-	struct swap_cluster_info *ci;
-	unsigned long offset;
-	unsigned char count;
-	unsigned char has_cache;
-	int err, i;
-
-	si = swp_get_info(entry);
-	if (WARN_ON_ONCE(!si)) {
-		pr_err("%s%08lx\n", Bad_file, entry.val);
-		return -EINVAL;
-	}
-
-	offset = swp_offset(entry);
-	VM_WARN_ON(nr > SWAPFILE_CLUSTER - offset % SWAPFILE_CLUSTER);
-	ci = swap_lock_cluster(si, offset);
+	int i;
+	unsigned char count, has_cache;
 
-	err = 0;
 	for (i = 0; i < nr; i++) {
 		count = si->swap_map[offset + i];
 
@@ -3520,24 +3508,20 @@ static int __swap_duplicate(swp_entry_t entry, unsigned char usage, int nr)
 		 * swap entry could be SWAP_MAP_BAD. Check here with lock held.
 		 */
 		if (unlikely(swap_count(count) == SWAP_MAP_BAD)) {
-			err = -ENOENT;
-			goto unlock_out;
+			return -ENOENT;
 		}
 
 		has_cache = count & SWAP_HAS_CACHE;
 		count &= ~SWAP_HAS_CACHE;
 
 		if (!count && !has_cache) {
-			err = -ENOENT;
+			return -ENOENT;
 		} else if (usage == SWAP_HAS_CACHE) {
 			if (has_cache)
-				err = -EEXIST;
+				return -EEXIST;
 		} else if ((count & ~COUNT_CONTINUED) > SWAP_MAP_MAX) {
-			err = -EINVAL;
+			return -EINVAL;
 		}
-
-		if (err)
-			goto unlock_out;
 	}
 
 	for (i = 0; i < nr; i++) {
@@ -3556,15 +3540,34 @@ static int __swap_duplicate(swp_entry_t entry, unsigned char usage, int nr)
 			 * Don't need to rollback changes, because if
 			 * usage == 1, there must be nr == 1.
 			 */
-			err = -ENOMEM;
-			goto unlock_out;
+			return -ENOMEM;
 		}
 
 		WRITE_ONCE(si->swap_map[offset + i], count | has_cache);
 	}
 
-unlock_out:
+	return 0;
+}
+
+static int __swap_duplicate(swp_entry_t entry, unsigned char usage, int nr)
+{
+	struct swap_info_struct *si;
+	struct swap_cluster_info *ci;
+	unsigned long offset;
+	int err;
+
+	si = swp_get_info(entry);
+	if (WARN_ON_ONCE(!si)) {
+		pr_err("%s%08lx\n", Bad_file, entry.val);
+		return -EINVAL;
+	}
+
+	offset = swp_offset(entry);
+	VM_WARN_ON(nr > SWAPFILE_CLUSTER - offset % SWAPFILE_CLUSTER);
+	ci = swap_lock_cluster(si, offset);
+	err = swap_dup_entries(si, ci, offset, usage, nr);
 	swap_unlock_cluster(ci);
+
 	return err;
 }
 
Signature · 1 line
2.49.0
#36 lore raw

PATCH 16/28 mm, swap: use swap cache as the swap in synchronize layer

In reply to Kairui Song

From: Kairui Song <[email protected]>

Current swap synchronization is mostly based on the swap_map's SWAP_HAS_CACHE bit. Whoever sets the bit first does the actual work to swap in a folio.

This has been causing many issues as it's just a poor implementation of a bit lock based on a busy loop. The busy loop is relaxed with a schedule_timeout_uninterruptible(1), which is ugly and causes more long tailing or other performance issues. Beside, the abuse of SWAP_HAS_CACHE has been causing trouble for maintenance.

We have just removed all swap in paths bypassing the swap cache, so now we can just resolve the swap synchronization with the swap cache layer directly (similar to page cache). Whoever adds a folio into the swap cache first takes care of the real IO. Raced threads will see the newly inserted folio from others, they can simply wait on the folio lock.

This way, the race swap in is synchronized with a proper lock. This both simplifies the logic and should improve the performance, and eliminated issues like the workaround in commit 01626a1823024 ("mm: avoid unconditional one-tick sleep when swapcache_prepare fails"), or the "skip_if_exists" from commit a65b0e7607ccb ("zswap: make shrinking memcg-aware").

---
 include/linux/swap.h |   6 ---
 mm/swap.h            |  17 ++++--
 mm/swap_state.c      | 120 +++++++++++++++++--------------------------
 mm/swapfile.c        |  32 ++++++------
 mm/vmscan.c          |   1 -
 mm/zswap.c           |   2 +-
 6 files changed, 76 insertions(+), 102 deletions(-)
include/linux/swap.h +0 -6
index 58230f3e15e6..2da769cdc663 100644
@@ -443,7 +443,6 @@ void put_swap_folio(struct folio *folio, swp_entry_t entry);
 extern swp_entry_t get_swap_page_of_type(int);
 extern int add_swap_count_continuation(swp_entry_t, gfp_t);
 extern int swap_duplicate_nr(swp_entry_t entry, int nr);
-extern int swapcache_prepare(swp_entry_t entry, int nr);
 extern void swap_free_nr(swp_entry_t entry, int nr_pages);
 extern void free_swap_and_cache_nr(swp_entry_t entry, int nr);
 int swap_type_of(dev_t device, sector_t offset);
@@ -502,11 +501,6 @@ static inline int swap_duplicate_nr(swp_entry_t swp, int nr_pages)
 	return 0;
 }
 
-static inline int swapcache_prepare(swp_entry_t swp, int nr)
-{
-	return 0;
-}
-
 static inline void swap_free_nr(swp_entry_t entry, int nr_pages)
 {
 }
mm/swap.h +12 -5
index cad24a3abda8..2abfb40fc7ec 100644
@@ -135,6 +135,13 @@ static inline void swap_unlock_cluster_irq(struct swap_cluster_info *ci)
 	spin_unlock_irq(&ci->lock);
 }
 
+extern int __swap_cache_set_entry(struct swap_info_struct *si,
+				  struct swap_cluster_info *ci,
+				  unsigned long offset);
+extern void __swap_cache_put_entries(struct swap_info_struct *si,
+				     struct swap_cluster_info *ci,
+				     swp_entry_t entry, unsigned int size);
+
 /* linux/mm/page_io.c */
 int sio_pool_init(void);
 struct swap_iocb;
@@ -158,8 +165,8 @@ static inline struct address_space *swap_address_space(swp_entry_t entry)
 
 /* Below helpers requires the caller to pin the swap device. */
 extern struct folio *swap_cache_get_folio(swp_entry_t entry);
-extern int swap_cache_add_folio(swp_entry_t entry,
-				struct folio *folio, void **shadow);
+extern struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
+					  void **shadow, bool swapin);
 extern void *swap_cache_get_shadow(swp_entry_t entry);
 /* Below helpers requires the caller to lock the swap cluster. */
 extern void __swap_cache_del_folio(swp_entry_t entry,
@@ -211,8 +218,7 @@ struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct vm_area_struct *vma, unsigned long addr,
 		struct swap_iocb **plug);
 struct folio *__swapin_cache_alloc(swp_entry_t entry, gfp_t gfp_flags,
-		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated,
-		bool skip_if_exists);
+		struct mempolicy *mpol, pgoff_t ilx, bool *new_page_allocated);
 struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t flag,
 		struct mempolicy *mpol, pgoff_t ilx);
 struct folio *swapin_readahead(swp_entry_t entry, gfp_t flag,
@@ -324,7 +330,8 @@ static inline struct folio *swap_cache_get_folio(swp_entry_t entry)
 	return NULL;
 }
 
-static inline int swap_cache_add_folio(swp_entry_t end, struct folio *folio, void **shadow)
+static inline int swap_cache_add_folio(swp_entry_t end, struct folio *folio,
+				       void **shadow, bool swapin)
 {
 	return -EINVAL;
 }
mm/swap_state.c +47 -73

226 lines not on this page · raw message

mm/swapfile.c +16 -16
index d01dc0646db9..8909d1655432 100644
@@ -1283,7 +1283,7 @@ int folio_alloc_swap(struct folio *folio, gfp_t gfp)
 	if (!entry.val)
 		return -ENOMEM;
 
-	if (swap_cache_add_folio(entry, folio, NULL))
+	if (WARN_ON(swap_cache_add_folio(entry, folio, NULL, false) != folio))
 		goto out_free;
 
 	atomic_long_sub(size, &nr_swap_pages);
@@ -1556,6 +1556,17 @@ void swap_free_nr(swp_entry_t entry, int nr_pages)
 	}
 }
 
+void __swap_cache_put_entries(struct swap_info_struct *si,
+			      struct swap_cluster_info *ci,
+			      swp_entry_t entry, unsigned int size)
+{
+	if (swap_only_has_cache(si, swp_offset(entry), size))
+		swap_entries_free(si, ci, entry, size);
+	else
+		for (int i = 0; i < size; i++, entry.val++)
+			swap_entry_put_locked(si, ci, entry, SWAP_HAS_CACHE);
+}
+
 /*
  * Called after dropping swapcache to decrease refcnt to swap entries.
  */
@@ -1571,11 +1582,7 @@ void put_swap_folio(struct folio *folio, swp_entry_t entry)
 		return;
 
 	ci = swap_lock_cluster(si, offset);
-	if (swap_only_has_cache(si, offset, size))
-		swap_entries_free(si, ci, entry, size);
-	else
-		for (int i = 0; i < size; i++, entry.val++)
-			swap_entry_put_locked(si, ci, entry, SWAP_HAS_CACHE);
+	__swap_cache_put_entries(si, ci, entry, size);
 	swap_unlock_cluster(ci);
 }
 
@@ -3597,17 +3604,10 @@ int swap_duplicate_nr(swp_entry_t entry, int nr)
 	return err;
 }
 
-/*
- * @entry: first swap entry from which we allocate nr swap cache.
- *
- * Called when allocating swap cache for existing swap entries,
- * This can return error codes. Returns 0 at success.
- * -EEXIST means there is a swap cache.
- * Note: return code is different from swap_duplicate().
- */
-int swapcache_prepare(swp_entry_t entry, int nr)
+int __swap_cache_set_entry(struct swap_info_struct *si,
+			   struct swap_cluster_info *ci, unsigned long offset)
 {
-	return __swap_duplicate(entry, SWAP_HAS_CACHE, nr);
+	return swap_dup_entries(si, ci, offset, SWAP_HAS_CACHE, 1);
 }
 
 /*
mm/vmscan.c +0 -1
index 7b5f41b4147b..8b5498cae0d5 100644
@@ -798,7 +798,6 @@ static int __remove_mapping(struct address_space *mapping, struct folio *folio,
 		__swap_cache_del_folio(swap, folio, shadow);
 		memcg1_swapout(folio, swap);
 		swap_unlock_cluster_irq(ci);
-		put_swap_folio(folio, swap);
 	} else {
 		void (*free_folio)(struct folio *);
 
mm/zswap.c +1 -1
index 87aebeee11ef..65c1aff5c4a4 100644
@@ -1085,7 +1085,7 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
 
 	mpol = get_task_policy(current);
 	folio = __swapin_cache_alloc(swpentry, GFP_KERNEL, mpol,
-				NO_INTERLEAVE_INDEX, &folio_was_allocated, true);
+				     NO_INTERLEAVE_INDEX, &folio_was_allocated);
 	put_swap_device(si);
 	if (!folio)
 		return -ENOMEM;
Signature · 1 line
2.49.0
#37 lore raw

PATCH 17/28 mm, swap: sanitize swap entry management workflow

In reply to Kairui Song

From: Kairui Song <[email protected]>

The current swap entry allocation / freeing workflow has never had a clear definition. Which makes it hard to debug or add new optimizations.

This commit introduces a proper definition of how swap entries would be allocated and freed. Now, most operations are folio-based, so they will Never exceed one swap cluster, and we now have a cleaner border between swap and the rest of mm, making it much easier to follow and debug, especially with sanity checks. Also making more optimization possible later:

Swap entry should be allocated and free, either with a folio bound, or from the page table / mapping directly.

When swap entries are bound with a folio (folio in the swap cache and locked), these entries could be considered stable, won't be freed completely and the swap device can't be swapped offed too.

(Hibernation subsystem is a bit different, see below)

For swap operations from the mm (folio/page table) side:

- folio_alloc_swap() - Allocation (swapout)
This allocates one or a set of continuous swap entries for one folio and bind the folio with it by adding it to the swap cache.
Context: The folio must be locked.

- folio_dup_swap() - On folio unmap (swapout)
This increases the ref count of swap entries allocated to a folio.
Context: The folio must be locked and in the swap cache.

- folio_put_swap() - On folio map (swap in)
This decreases the ref count of swap entries allocated to a folio.
Context: The folio must be locked and in the swap cache.

NOTE: this won't remove the folio from the swap cache, as the swap cache is lazy freed. The allocator can reclaim clean swap caches, and reduces IO or allocator overhead. The lazy freeing of the swap cache could be further optimized later. (see folio_free_swap below *)

- do_put_swap_entries() - On page table zapping / shmem truncate This decreases the ref count of swap entries as the page table gets zapped, or mapping gets truncated.
Context: The caller must ensure the entries won't be freed completely during the period, which is currently done by holding the page table lock (zapping) or mapping cmpxchg (shmem) to prevent swap in or concurrent free.

- do_dup_swap_entry() - On page table copy (fork)
This increases the ref count of the swap entry as the page table gets copied.
Context: The caller must ensure the entry won't be freed during The period, which is currently done by holding the page table lock to prevent a swap-in or concurrent free.

* There is already a folio_free_swap(), it's a bit special as it will try to free the swap entries pinned by a folio only if all entries' count have dropped to zero. So it can be called after folio_put_swap() have dropped all swap ref counts. It can be better optimized and maybe merged into folio_put_swap() later.

For hibernation, two special helpers are here:

- get_swap_page_of_type() - Allocate one entry from one device.
- free_swap_page_of_entry() - Free one entry allocated by above helper.

All hibernation entries are exclusive to the hibernation subsystem and should not interact with ordinary swap routines.

---
 arch/s390/mm/pgtable.c |   2 +-
 include/linux/swap.h   |  57 ++++++++---------
 kernel/power/swap.c    |   8 ++-
 mm/madvise.c           |   2 +-
 mm/memory.c            |  20 +++---
 mm/rmap.c              |   7 ++-
 mm/shmem.c             |   8 +--
 mm/swap.h              |  29 +++++++++
 mm/swapfile.c          | 135 +++++++++++++++++++++++++++++------------
 9 files changed, 176 insertions(+), 92 deletions(-)
arch/s390/mm/pgtable.c +1 -1
index 9901934284ec..c402552bc8f3 100644
@@ -715,7 +715,7 @@ static void ptep_zap_swap_entry(struct mm_struct *mm, swp_entry_t entry)
 
 		dec_mm_counter(mm, mm_counter(folio));
 	}
-	free_swap_and_cache(entry);
+	do_put_swap_entries(entry, 1);
 }
 
 void ptep_zap_unused(struct mm_struct *mm, unsigned long addr,
include/linux/swap.h +24 -33

99 lines not on this page · raw message

kernel/power/swap.c +5 -3
index 80ff5f933a62..f94c4ea350cf 100644
@@ -182,7 +182,7 @@ sector_t alloc_swapdev_block(int swap)
 	offset = swp_offset(get_swap_page_of_type(swap));
 	if (offset) {
 		if (swsusp_extents_insert(offset))
-			swap_free(swp_entry(swap, offset));
+			free_swap_page_of_entry(swp_entry(swap, offset));
 		else
 			return swapdev_block(swap, offset);
 	}
@@ -197,6 +197,7 @@ sector_t alloc_swapdev_block(int swap)
 
 void free_all_swap_pages(int swap)
 {
+	unsigned long offset;
 	struct rb_node *node;
 
 	while ((node = swsusp_extents.rb_node)) {
@@ -204,8 +205,9 @@ void free_all_swap_pages(int swap)
 
 		ext = rb_entry(node, struct swsusp_extent, node);
 		rb_erase(node, &swsusp_extents);
-		swap_free_nr(swp_entry(swap, ext->start),
-			     ext->end - ext->start + 1);
+
+		for (offset = ext->start; offset < ext->end; offset++)
+			free_swap_page_of_entry(swp_entry(swap, offset));
 
 		kfree(ext);
 	}
mm/madvise.c +1 -1
index 8433ac9b27e0..36c62353d184 100644
@@ -697,7 +697,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 				max_nr = (end - addr) / PAGE_SIZE;
 				nr = swap_pte_batch(pte, max_nr, ptent);
 				nr_swap -= nr;
-				free_swap_and_cache_nr(entry, nr);
+				do_put_swap_entries(entry, nr);
 				clear_not_present_full_ptes(mm, addr, pte, nr, tlb->fullmm);
 			} else if (is_hwpoison_entry(entry) ||
 				   is_poisoned_swp_entry(entry)) {
mm/memory.c +11 -9
index 0b41d15c6d7a..c000e39b3eb2 100644
@@ -804,7 +804,7 @@ copy_nonpresent_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	swp_entry_t entry = pte_to_swp_entry(orig_pte);
 
 	if (likely(!non_swap_entry(entry))) {
-		if (swap_duplicate(entry) < 0)
+		if (do_dup_swap_entry(entry) < 0)
 			return -EIO;
 
 		/* make sure dst_mm is on swapoff's mmlist. */
@@ -1625,7 +1625,7 @@ static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,
 
 		nr = swap_pte_batch(pte, max_nr, ptent);
 		rss[MM_SWAPENTS] -= nr;
-		free_swap_and_cache_nr(entry, nr);
+		do_put_swap_entries(entry, nr);
 	} else if (is_migration_entry(entry)) {
 		struct folio *folio = pfn_swap_entry_folio(entry);
 
@@ -4783,7 +4783,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	/*
 	 * Some architectures may have to restore extra metadata to the page
 	 * when reading from swap. This metadata may be indexed by swap entry
-	 * so this must be called before swap_free().
+	 * so this must be called before folio_put_swap().
 	 */
 	arch_swap_restore(folio_swap(entry, folio), folio);
 
@@ -4821,13 +4821,17 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	if (unlikely(folio != swapcache && swapcache)) {
 		folio_add_new_anon_rmap(folio, vma, address, RMAP_EXCLUSIVE);
 		folio_add_lru_vma(folio, vma);
+		folio_put_swap(swapcache, NULL);
 	} else if (!folio_test_anon(folio)) {
 		VM_WARN_ON_ONCE_FOLIO(folio_nr_pages(folio) != nr_pages, folio);
 		VM_WARN_ON_ONCE_FOLIO(folio_mapped(folio), folio);
 		folio_add_new_anon_rmap(folio, vma, address, rmap_flags);
+		folio_put_swap(folio, NULL);
 	} else {
+		VM_WARN_ON_ONCE(nr_pages != 1 && nr_pages != folio_nr_pages(folio));
 		folio_add_anon_rmap_ptes(folio, page, nr_pages, vma, address,
-					rmap_flags);
+					 rmap_flags);
+		folio_put_swap(folio, nr_pages == folio_nr_pages(folio) ? NULL : page);
 	}
 
 	VM_BUG_ON(!folio_test_anon(folio) ||
@@ -4837,11 +4841,9 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 			pte, pte, nr_pages);
 
 	/*
-	 * Remove the swap entry and conditionally try to free up the
-	 * swapcache then unlock the folio. Do this after the PTEs are
-	 * set, so raced faults will see updated PTEs.
+	 * Conditionally try to free up the swapcache and unlock the folio
+	 * after the PTEs are set, so raced faults will see updated PTEs.
 	 */
-	swap_free_nr(entry, nr_pages);
 	if (should_try_to_free_swap(folio, vma, vmf->flags))
 		folio_free_swap(folio);
 	folio_unlock(folio);
@@ -4851,7 +4853,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		 * Hold the lock to avoid the swap entry to be reused
 		 * until we take the PT lock for the pte_same() check
 		 * (to avoid false positives from pte_same). For
-		 * further safety release the lock after the swap_free
+		 * further safety release the lock after the folio_put_swap
 		 * so that the swap count won't change under a
 		 * parallel locked swapcache.
 		 */
mm/rmap.c +4 -3
index fb63d9256f09..d2195ebb4c35 100644
@@ -83,6 +83,7 @@
 #include <trace/events/migrate.h>
 
 #include "internal.h"
+#include "swap.h"
 
 static struct kmem_cache *anon_vma_cachep;
 static struct kmem_cache *anon_vma_chain_cachep;
@@ -2141,7 +2142,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 				goto discard;
 			}
 
-			if (swap_duplicate(entry) < 0) {
+			if (folio_dup_swap(folio, subpage) < 0) {
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
@@ -2152,7 +2153,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			 * so we'll not check/care.
 			 */
 			if (arch_unmap_one(mm, vma, address, pteval) < 0) {
-				swap_free(entry);
+				folio_put_swap(folio, subpage);
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
@@ -2160,7 +2161,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			/* See folio_try_share_anon_rmap(): clear PTE first. */
 			if (anon_exclusive &&
 			    folio_try_share_anon_rmap_pte(folio, subpage)) {
-				swap_free(entry);
+				folio_put_swap(folio, subpage);
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
mm/shmem.c +4 -4
index e87eff03c08b..0d23c1c12204 100644
@@ -981,7 +981,7 @@ static long shmem_free_swap(struct address_space *mapping,
 	old = xa_cmpxchg_irq(&mapping->i_pages, index, radswap, NULL, 0);
 	if (old != radswap)
 		return 0;
-	free_swap_and_cache_nr(radix_to_swp_entry(radswap), 1 << order);
+	do_put_swap_entries(radix_to_swp_entry(radswap), 1 << order);
 
 	return 1 << order;
 }
@@ -1663,8 +1663,8 @@ static int shmem_writepage(struct page *page, struct writeback_control *wbc)
 		list_add(&info->swaplist, &shmem_swaplist);
 
 	if (!folio_alloc_swap(folio, __GFP_HIGH | __GFP_NOMEMALLOC | __GFP_NOWARN)) {
+		folio_dup_swap(folio, NULL);
 		shmem_recalc_inode(inode, 0, nr_pages);
-		swap_duplicate_nr(folio->swap, nr_pages);
 		shmem_delete_from_page_cache(folio, swp_to_radix_entry(folio->swap));
 
 		mutex_unlock(&shmem_swaplist_mutex);
@@ -2122,6 +2122,7 @@ static void shmem_set_folio_swapin_error(struct inode *inode, pgoff_t index,
 
 	nr_pages = folio_nr_pages(folio);
 	folio_wait_writeback(folio);
+	folio_put_swap(folio, NULL);
 	delete_from_swap_cache(folio);
 	/*
 	 * Don't treat swapin error folio as alloced. Otherwise inode->i_blocks
@@ -2129,7 +2130,6 @@ static void shmem_set_folio_swapin_error(struct inode *inode, pgoff_t index,
 	 * in shmem_evict_inode().
 	 */
 	shmem_recalc_inode(inode, -nr_pages, -nr_pages);
-	swap_free_nr(swap, nr_pages);
 }
 
 static int shmem_split_large_entry(struct inode *inode, pgoff_t index,
@@ -2364,9 +2364,9 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	if (sgp == SGP_WRITE)
 		folio_mark_accessed(folio);
 
+	folio_put_swap(folio, NULL);
 	delete_from_swap_cache(folio);
 	folio_mark_dirty(folio);
-	swap_free_nr(swap, nr_pages);
 	put_swap_device(si);
 
 	*foliop = folio;
mm/swap.h +29 -0
index 2abfb40fc7ec..4c4a71081895 100644
@@ -142,6 +142,20 @@ extern void __swap_cache_put_entries(struct swap_info_struct *si,
 				     struct swap_cluster_info *ci,
 				     swp_entry_t entry, unsigned int size);
 
+/*
+ * All swap entries starts getting allocated by folio_alloc_swap(),
+ * and the folio will be added to swap cache.
+ *
+ * Swap out (pageout) unmaps a folio and increased the swap table entry
+ * count with folio_dup_swap.
+ *
+ * Swap in maps a folio in swap cache and decrease the swap table entry
+ * count with folio_put_swap.
+ */
+int folio_alloc_swap(struct folio *folio, gfp_t gfp_mask);
+int folio_dup_swap(struct folio *folio, struct page *subpage);
+void folio_put_swap(struct folio *folio, struct page *subpage);
+
 /* linux/mm/page_io.c */
 int sio_pool_init(void);
 struct swap_iocb;
@@ -272,9 +286,24 @@ static inline struct swap_info_struct *swp_info(swp_entry_t entry)
 	return NULL;
 }
 
+static inline int folio_alloc_swap(struct folio *folio, gfp_t gfp)
+{
+	return -EINVAL;
+}
+
+static inline int folio_dup_swap(struct folio *folio, struct page *page)
+{
+	return -EINVAL;
+}
+
+static inline void folio_put_swap(struct folio *folio, struct page *page)
+{
+}
+
 static inline void swap_read_folio(struct folio *folio, struct swap_iocb **plug)
 {
 }
+
 static inline void swap_write_unplug(struct swap_iocb *sio)
 {
 }
mm/swapfile.c +97 -38

232 lines not on this page · raw message

Signature · 1 line
2.49.0
#38 lore raw

PATCH 18/28 mm, swap: rename and introduce folio_free_swap_cache

In reply to Kairui Song

From: Kairui Song <[email protected]>

We now have folio_alloc_swap, folio_dup_swap, folio_put_swap, and folio_free_swap (which is actually try to free). Also rename delete_from_swap_cache to folio_free_swap_cache, because swap cache will always be the last reference of a folio bounded entry now. Freeing the swap cache will also attempt to free the swap entries.

---
 mm/memory-failure.c |  2 +-
 mm/memory.c         |  2 +-
 mm/shmem.c          |  4 ++--
 mm/swap.h           | 14 +++++++++-----
 mm/swap_state.c     | 12 ------------
 mm/swapfile.c       | 23 +++++++++++++++++++++--
 mm/zswap.c          |  2 +-
 7 files changed, 35 insertions(+), 24 deletions(-)
mm/memory-failure.c +1 -1
index b91a33fb6c69..ba96aaf96e83 100644
@@ -1185,7 +1185,7 @@ static int me_swapcache_clean(struct page_state *ps, struct page *p)
 	struct folio *folio = page_folio(p);
 	int ret;
 
-	delete_from_swap_cache(folio);
+	folio_free_swap_cache(folio);
 
 	ret = delete_from_lru_cache(folio) ? MF_FAILED : MF_RECOVERED;
 	folio_unlock(folio);
mm/memory.c +1 -1
index c000e39b3eb2..a70624a55aa2 100644
@@ -4727,7 +4727,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		 */
 		if (!folio_test_anon(folio)) {
 			WARN_ON_ONCE(folio_test_dirty(folio));
-			delete_from_swap_cache(folio);
+			folio_free_swap_cache(folio);
 			goto out_nomap;
 		}
 	}
mm/shmem.c +2 -2
index 0d23c1c12204..c7475629365c 100644
@@ -2123,7 +2123,7 @@ static void shmem_set_folio_swapin_error(struct inode *inode, pgoff_t index,
 	nr_pages = folio_nr_pages(folio);
 	folio_wait_writeback(folio);
 	folio_put_swap(folio, NULL);
-	delete_from_swap_cache(folio);
+	folio_free_swap_cache(folio);
 	/*
 	 * Don't treat swapin error folio as alloced. Otherwise inode->i_blocks
 	 * won't be 0 when inode is released and thus trigger WARN_ON(i_blocks)
@@ -2365,7 +2365,7 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 		folio_mark_accessed(folio);
 
 	folio_put_swap(folio, NULL);
-	delete_from_swap_cache(folio);
+	folio_free_swap_cache(folio);
 	folio_mark_dirty(folio);
 	put_swap_device(si);
 
mm/swap.h +9 -5
index 4c4a71081895..467996dafbae 100644
@@ -151,10 +151,15 @@ extern void __swap_cache_put_entries(struct swap_info_struct *si,
  *
  * Swap in maps a folio in swap cache and decrease the swap table entry
  * count with folio_put_swap.
+ *
+ * Swap uses lazy free, so a folio may stay in swap cache for a long time
+ * and pin the swap entry. folio_free_swap_cache and folio_free_swap can
+ * be used to reclaim the swap cache.
  */
 int folio_alloc_swap(struct folio *folio, gfp_t gfp_mask);
 int folio_dup_swap(struct folio *folio, struct page *subpage);
 void folio_put_swap(struct folio *folio, struct page *subpage);
+void folio_free_swap_cache(struct folio *folio);
 
 /* linux/mm/page_io.c */
 int sio_pool_init(void);
@@ -226,7 +231,6 @@ static inline bool folio_swap_contains(struct folio *folio, swp_entry_t entry)
 }
 
 void show_swap_cache_info(void);
-void delete_from_swap_cache(struct folio *folio);
 void swapcache_clear(struct swap_info_struct *si, swp_entry_t entry, int nr);
 struct folio *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
 		struct vm_area_struct *vma, unsigned long addr,
@@ -300,6 +304,10 @@ static inline void folio_put_swap(struct folio *folio, struct page *page)
 {
 }
 
+static inline void folio_free_swap_cache(struct folio *folio)
+{
+}
+
 static inline void swap_read_folio(struct folio *folio, struct swap_iocb **plug)
 {
 }
@@ -387,10 +395,6 @@ static inline void *swap_cache_get_shadow(swp_entry_t end)
 	return NULL;
 }
 
-static inline void delete_from_swap_cache(struct folio *folio)
-{
-}
-
 static inline unsigned int folio_swap_flags(struct folio *folio)
 {
 	return 0;
mm/swap_state.c +0 -12
index 715aff5aca57..c8bb16835612 100644
@@ -216,18 +216,6 @@ void __swap_cache_del_folio(swp_entry_t entry,
 	__swap_cache_put_entries(si, ci, entry, nr_pages);
 }
 
-void delete_from_swap_cache(struct folio *folio)
-{
-	struct swap_cluster_info *ci;
-	swp_entry_t entry = folio->swap;
-
-	ci = swap_lock_cluster(swp_info(entry), swp_offset(entry));
-	__swap_cache_del_folio(entry, folio, NULL);
-	swap_unlock_cluster(ci);
-
-	folio_ref_sub(folio, folio_nr_pages(folio));
-}
-
 /*
  * Caller must hold a reference on the swap device, and check if the
  * returned folio is still valid after locking it (e.g. folio_swap_contains).
mm/swapfile.c +21 -2
index daf7810bcb28..0a8b36ecbf08 100644
@@ -273,7 +273,7 @@ static int __try_to_reclaim_swap(struct swap_info_struct *si,
 	if (!need_reclaim)
 		goto out_unlock;
 
-	delete_from_swap_cache(folio);
+	folio_free_swap_cache(folio);
 	folio_set_dirty(folio);
 	ret = nr_pages;
 out_unlock:
@@ -1354,6 +1354,25 @@ void folio_put_swap(struct folio *folio, struct page *subpage)
 	swap_entries_put_map(swp_info(entry), entry, nr_pages);
 }
 
+/*
+ * folio_free_swap_cache() - Remove the folio from swap cache, and free
+ * all entires with zero count.
+ *
+ * NOTE: if the folio is dirty and any of its swap entries' count is not
+ * zero, freeing the swap cache without write back may cause data loss.
+ */
+void folio_free_swap_cache(struct folio *folio)
+{
+	struct swap_cluster_info *ci;
+	swp_entry_t entry = folio->swap;
+
+	ci = swap_lock_cluster(swp_info(entry), swp_offset(entry));
+	__swap_cache_del_folio(entry, folio, NULL);
+	swap_unlock_cluster(ci);
+
+	folio_ref_sub(folio, folio_nr_pages(folio));
+}
+
 static struct swap_info_struct *_swap_info_get(swp_entry_t entry)
 {
 	struct swap_info_struct *si;
@@ -1787,7 +1806,7 @@ bool folio_free_swap(struct folio *folio)
 	if (folio_swapped(folio))
 		return false;
 
-	delete_from_swap_cache(folio);
+	folio_free_swap_cache(folio);
 	folio_set_dirty(folio);
 	return true;
 }
mm/zswap.c +1 -1
index 65c1aff5c4a4..6bac50bc2bf5 100644
@@ -1141,7 +1141,7 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
 
 out:
 	if (ret && ret != -EEXIST) {
-		delete_from_swap_cache(folio);
+		folio_free_swap_cache(folio);
 		folio_unlock(folio);
 	}
 	folio_put(folio);
Signature · 1 line
2.49.0
#39 lore raw

PATCH 19/28 mm, swap: clean up and improve swap entries batch freeing

In reply to Kairui Song

From: Kairui Song <[email protected]>

Introduce a helper to free up all the continuous entries that has only one last swap entry count left and has no cache.

Compared to the current design, which scans the whole region first, then frees it only if the whole region is filled with the same count, this new helper avoids the two-pass scan, and will batch-free more entries when fragmented, also more robust with sanity checks. And check the swap table directly for the cache status instead of looking at swap_map here.

Also rename related functions to better present their usage.

This simplifies the code and prepares for follow up commits to clean up the freeing of swap entries even more.

---
 mm/swapfile.c | 165 ++++++++++++++++++++------------------------------
 1 file changed, 67 insertions(+), 98 deletions(-)
mm/swapfile.c +67 -98
index 0a8b36ecbf08..ef233466725e 100644
@@ -54,14 +54,16 @@
 static bool swap_count_continued(struct swap_info_struct *, pgoff_t,
 				 unsigned char);
 static void free_swap_count_continuations(struct swap_info_struct *);
-static void swap_entries_free(struct swap_info_struct *si,
+static void swap_free_entries(struct swap_info_struct *si,
 			      struct swap_cluster_info *ci,
-			      swp_entry_t entry, unsigned int nr_pages);
+			      unsigned long start, unsigned int nr_pages);
 static void swap_range_alloc(struct swap_info_struct *si,
 			     unsigned int nr_entries);
 static int __swap_duplicate(swp_entry_t entry, unsigned char usage, int nr);
-static bool swap_entries_put_map(struct swap_info_struct *si,
-				 swp_entry_t entry, int nr);
+static unsigned char swap_put_entry_locked(struct swap_info_struct *si,
+					   struct swap_cluster_info *ci,
+					   swp_entry_t entry,
+					   unsigned char usage);
 static bool folio_swapcache_freeable(struct folio *folio);
 
 static DEFINE_SPINLOCK(swap_lock);
@@ -193,25 +195,6 @@ static bool swap_only_has_cache(struct swap_info_struct *si,
 	return true;
 }
 
-static bool swap_is_last_map(struct swap_info_struct *si,
-		unsigned long offset, int nr_pages, bool *has_cache)
-{
-	unsigned char *map = si->swap_map + offset;
-	unsigned char *map_end = map + nr_pages;
-	unsigned char count = *map;
-
-	if (swap_count(count) != 1)
-		return false;
-
-	while (++map < map_end) {
-		if (*map != count)
-			return false;
-	}
-
-	*has_cache = !!(count & SWAP_HAS_CACHE);
-	return true;
-}
-
 /*
  * returns number of pages in the folio that backs the swap entry. If positive,
  * the folio was reclaimed. If negative, the folio was not reclaimed. If 0, no
@@ -1237,6 +1220,56 @@ static bool swap_alloc_slow(swp_entry_t *entry,
 	return false;
 }
 
+/*
+ * Put the ref count of entries, caller must ensure the entries'
+ * swap table count are not zero. This won't free up the swap cache.
+ */
+static bool swap_put_entries(struct swap_info_struct *si,
+			     unsigned long start, int nr)
+{
+	unsigned long offset = start, end = start + nr, cluster_end;
+	unsigned long head = SWAP_ENTRY_INVALID;
+	struct swap_cluster_info *ci;
+	bool has_cache = false;
+	unsigned int count;
+	swp_te_t swp_te;
+next_cluster:
+	ci = swap_lock_cluster(si, offset);
+	cluster_end = min(cluster_offset(si, ci) + SWAPFILE_CLUSTER, end);
+	do {
+		swp_te = __swap_table_get(ci, offset);
+		count = si->swap_map[offset];
+		if (WARN_ON_ONCE(!swap_count(count))) {
+			goto skip;
+		} else if (swp_te_is_folio(swp_te)) {
+			VM_WARN_ON_ONCE(!(count & SWAP_HAS_CACHE));
+			/* Let the swap cache (folio) handle the final free */
+			has_cache = true;
+		} else if (count == 1) {
+			/* Free up continues last ref entries in batch */
+			head = head ? head : offset;
+			continue;
+		}
+		swap_put_entry_locked(si, ci, swp_entry(si->type, offset), 1);
+skip:
+		if (head) {
+			swap_free_entries(si, ci, head, offset - head);
+			head = SWAP_ENTRY_INVALID;
+		}
+	} while (++offset < cluster_end);
+
+	if (head) {
+		swap_free_entries(si, ci, head, offset - head);
+		head = SWAP_ENTRY_INVALID;
+	}
+
+	swap_unlock_cluster(ci);
+	if (unlikely(cluster_end < end))
+		goto next_cluster;
+
+	return has_cache;
+}
+
 /**
  * folio_alloc_swap - allocate swap space for a folio
  * @folio: folio we want to move to swap
@@ -1351,7 +1384,7 @@ void folio_put_swap(struct folio *folio, struct page *subpage)
 		nr_pages = 1;
 	}
 
-	swap_entries_put_map(swp_info(entry), entry, nr_pages);
+	swap_put_entries(swp_info(entry), swp_offset(entry), nr_pages);
 }
 
 /*
@@ -1407,7 +1440,7 @@ static struct swap_info_struct *_swap_info_get(swp_entry_t entry)
 	return NULL;
 }
 
-static unsigned char swap_entry_put_locked(struct swap_info_struct *si,
+static unsigned char swap_put_entry_locked(struct swap_info_struct *si,
 					   struct swap_cluster_info *ci,
 					   swp_entry_t entry,
 					   unsigned char usage)
@@ -1438,7 +1471,7 @@ static unsigned char swap_entry_put_locked(struct swap_info_struct *si,
 	if (usage)
 		WRITE_ONCE(si->swap_map[offset], usage);
 	else
-		swap_entries_free(si, ci, entry, 1);
+		swap_free_entries(si, ci, offset, 1);
 
 	return usage;
 }
@@ -1509,70 +1542,6 @@ struct swap_info_struct *get_swap_device(swp_entry_t entry)
 	return NULL;
 }
 
-static bool swap_entries_put_map(struct swap_info_struct *si,
-				 swp_entry_t entry, int nr)
-{
-	unsigned long offset = swp_offset(entry);
-	struct swap_cluster_info *ci;
-	bool has_cache = false;
-	unsigned char count;
-	int i;
-
-	if (nr <= 1)
-		goto fallback;
-	count = swap_count(data_race(si->swap_map[offset]));
-	if (count != 1)
-		goto fallback;
-
-	ci = swap_lock_cluster(si, offset);
-	if (!swap_is_last_map(si, offset, nr, &has_cache)) {
-		goto locked_fallback;
-	}
-	if (!has_cache)
-		swap_entries_free(si, ci, entry, nr);
-	else
-		for (i = 0; i < nr; i++)
-			WRITE_ONCE(si->swap_map[offset + i], SWAP_HAS_CACHE);
-	swap_unlock_cluster(ci);
-
-	return has_cache;
-
-fallback:
-	ci = swap_lock_cluster(si, offset);
-locked_fallback:
-	for (i = 0; i < nr; i++, entry.val++) {
-		count = swap_entry_put_locked(si, ci, entry, 1);
-		if (count == SWAP_HAS_CACHE)
-			has_cache = true;
-	}
-	swap_unlock_cluster(ci);
-	return has_cache;
-}
-
-/*
- * Only functions with "_nr" suffix are able to free entries spanning
- * cross multi clusters, so ensure the range is within a single cluster
- * when freeing entries with functions without "_nr" suffix.
- */
-static bool swap_entries_put_map_nr(struct swap_info_struct *si,
-				    swp_entry_t entry, int nr)
-{
-	int cluster_nr, cluster_rest;
-	unsigned long offset = swp_offset(entry);
-	bool has_cache = false;
-
-	cluster_rest = SWAPFILE_CLUSTER - offset % SWAPFILE_CLUSTER;
-	while (nr) {
-		cluster_nr = min(nr, cluster_rest);
-		has_cache |= swap_entries_put_map(si, entry, cluster_nr);
-		cluster_rest = SWAPFILE_CLUSTER;
-		nr -= cluster_nr;
-		entry.val += cluster_nr;
-	}
-
-	return has_cache;
-}
-
 /*
  * Check if it's the last ref of swap entry in the freeing path.
  */
@@ -1585,11 +1554,11 @@ static inline bool __maybe_unused swap_is_last_ref(unsigned char count)
  * Drop the last ref of swap entries, caller have to ensure all entries
  * belong to the same cgroup and cluster.
  */
-static void swap_entries_free(struct swap_info_struct *si,
+static void swap_free_entries(struct swap_info_struct *si,
 			      struct swap_cluster_info *ci,
-			      swp_entry_t entry, unsigned int nr_pages)
+			      unsigned long offset, unsigned int nr_pages)
 {
-	unsigned long offset = swp_offset(entry);
+	swp_entry_t entry = swp_entry(si->type, offset);
 	unsigned char *map = si->swap_map + offset;
 	unsigned char *map_end = map + nr_pages;
 
@@ -1622,10 +1591,10 @@ void __swap_cache_put_entries(struct swap_info_struct *si,
 			      swp_entry_t entry, unsigned int size)
 {
 	if (swap_only_has_cache(si, swp_offset(entry), size))
-		swap_entries_free(si, ci, entry, size);
+		swap_free_entries(si, ci, swp_offset(entry), size);
 	else
 		for (int i = 0; i < size; i++, entry.val++)
-			swap_entry_put_locked(si, ci, entry, SWAP_HAS_CACHE);
+			swap_put_entry_locked(si, ci, entry, SWAP_HAS_CACHE);
 }
 
 /*
@@ -1843,7 +1812,7 @@ void do_put_swap_entries(swp_entry_t entry, int nr)
 	/*
 	 * First free all entries in the range.
 	 */
-	any_only_cache = swap_entries_put_map_nr(si, entry, nr);
+	any_only_cache = swap_put_entries(swp_info(entry), swp_offset(entry), nr);
 
 	/*
 	 * Short-circuit the below loop if none of the entries had their
@@ -1917,7 +1886,7 @@ void free_swap_page_of_entry(swp_entry_t entry)
 	if (!si)
 		return;
 	ci = swap_lock_cluster(si, offset);
-	WARN_ON(swap_count(swap_entry_put_locked(si, ci, entry, 1)));
+	WARN_ON(swap_count(swap_put_entry_locked(si, ci, entry, 1)));
 	/* It might got added to swap cache accidentally by read ahead */
 	__try_to_reclaim_swap(si, offset, TTRS_ANYWAY);
 	swap_unlock_cluster(ci);
@@ -3805,7 +3774,7 @@ int add_swap_count_continuation(swp_entry_t entry, gfp_t gfp_mask)
  * into, carry if so, or else fail until a new continuation page is allocated;
  * when the original swap_map count is decremented from 0 with continuation,
  * borrow from the continuation and report whether it still holds more.
- * Called while __swap_duplicate() or caller of swap_entry_put_locked()
+ * Called while __swap_duplicate() or caller of swap_put_entry_locked()
  * holds cluster lock.
  */
 static bool swap_count_continued(struct swap_info_struct *si,
Signature · 1 line
2.49.0
#40 lore raw

PATCH 20/28 mm, swap: check swap table directly for checking cache

In reply to Kairui Song

From: Kairui Song <[email protected]>

Instead of looking at the swap map, check swap table directly to tell if a swap entry has cache. Prepare for remove SWAP_HAS_CACHE.

---
 mm/memory.c     | 12 +++++------
 mm/swap.h       |  6 ++++++
 mm/swap_state.c | 11 ++++++++++
 mm/swapfile.c   | 54 +++++++++++++++++++++++--------------------------
 4 files changed, 48 insertions(+), 35 deletions(-)
mm/memory.c +6 -6
index a70624a55aa2..a9a548575e72 100644
@@ -4314,15 +4314,15 @@ static struct folio *__alloc_swap_folio(struct vm_fault *vmf)
 }
 
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
-static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
+static inline int non_swapcache_batch(swp_entry_t entry, unsigned int max_nr)
 {
-	struct swap_info_struct *si = swp_info(entry);
-	pgoff_t offset = swp_offset(entry);
-	int i;
+	unsigned int i;
 
 	for (i = 0; i < max_nr; i++) {
-		if ((si->swap_map[offset + i] & SWAP_HAS_CACHE))
-			return i;
+		/* Page table lock pins the swap entries / swap device */
+		if (swap_cache_check_folio(entry))
+			break;
+		entry.val++;
 	}
 
 	return i;
mm/swap.h +6 -0
index 467996dafbae..2ae4624a0e48 100644
@@ -186,6 +186,7 @@ static inline struct address_space *swap_address_space(swp_entry_t entry)
 extern struct folio *swap_cache_get_folio(swp_entry_t entry);
 extern struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
 					  void **shadow, bool swapin);
+extern bool swap_cache_check_folio(swp_entry_t entry);
 extern void *swap_cache_get_shadow(swp_entry_t entry);
 /* Below helpers requires the caller to lock the swap cluster. */
 extern void __swap_cache_del_folio(swp_entry_t entry,
@@ -395,6 +396,11 @@ static inline void *swap_cache_get_shadow(swp_entry_t end)
 	return NULL;
 }
 
+static inline bool swap_cache_check_folio(swp_entry_t entry)
+{
+	return false;
+}
+
 static inline unsigned int folio_swap_flags(struct folio *folio)
 {
 	return 0;
mm/swap_state.c +11 -0
index c8bb16835612..ea6a1741db5c 100644
@@ -266,6 +266,17 @@ struct folio *swap_cache_get_folio(swp_entry_t entry)
 	return folio;
 }
 
+/*
+ * Check if a swap entry has folio cached, may return false positive.
+ * Caller must hold a reference of the swap device or pin it in other ways.
+ */
+bool swap_cache_check_folio(swp_entry_t entry)
+{
+	swp_te_t swp_te;
+	swp_te = __swap_table_get(swp_cluster(entry), swp_offset(entry));
+	return swp_te_is_folio(swp_te);
+}
+
 /*
  * If we are the only user, then try to free up the swap cache.
  *
mm/swapfile.c +25 -29

123 lines not on this page · raw message

Signature · 1 line
2.49.0
#41 lore raw

Re: [PATCH 20/28] mm, swap: check swap table directly for checking cache

Quoted 4 lines · Kairui Song

From: Kairui Song <[email protected]>

Instead of looking at the swap map, check swap table directly to tell if a swap entry has cache. Prepare for remove SWAP_HAS_CACHE.

But you actually check both the swap table entry and swap map entry in this patch, or do I miss anything?

E.g

if (!swap_count(si->swap_map[offset]) && swp_te_is_folio(swp_te))

Quoted 89 lines
---
 mm/memory.c     | 12 +++++------
 mm/swap.h       |  6 ++++++
 mm/swap_state.c | 11 ++++++++++
 mm/swapfile.c   | 54 +++++++++++++++++++++++--------------------------
 4 files changed, 48 insertions(+), 35 deletions(-)
mm/memory.c +6 -6
index a70624a55aa2..a9a548575e72 100644
@@ -4314,15 +4314,15 @@ static struct folio *__alloc_swap_folio(struct vm_fault *vmf)
 }
 
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
-static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
+static inline int non_swapcache_batch(swp_entry_t entry, unsigned int max_nr)
 {
-	struct swap_info_struct *si = swp_info(entry);
-	pgoff_t offset = swp_offset(entry);
-	int i;
+	unsigned int i;
 
 	for (i = 0; i < max_nr; i++) {
-		if ((si->swap_map[offset + i] & SWAP_HAS_CACHE))
-			return i;
+		/* Page table lock pins the swap entries / swap device */
+		if (swap_cache_check_folio(entry))
+			break;
+		entry.val++;
 	}
 
 	return i;
mm/swap.h +6 -0
index 467996dafbae..2ae4624a0e48 100644
@@ -186,6 +186,7 @@ static inline struct address_space *swap_address_space(swp_entry_t entry)
 extern struct folio *swap_cache_get_folio(swp_entry_t entry);
 extern struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
 					  void **shadow, bool swapin);
+extern bool swap_cache_check_folio(swp_entry_t entry);
 extern void *swap_cache_get_shadow(swp_entry_t entry);
 /* Below helpers requires the caller to lock the swap cluster. */
 extern void __swap_cache_del_folio(swp_entry_t entry,
@@ -395,6 +396,11 @@ static inline void *swap_cache_get_shadow(swp_entry_t end)
 	return NULL;
 }
 
+static inline bool swap_cache_check_folio(swp_entry_t entry)
+{
+	return false;
+}
+
 static inline unsigned int folio_swap_flags(struct folio *folio)
 {
 	return 0;
mm/swap_state.c +11 -0
index c8bb16835612..ea6a1741db5c 100644
@@ -266,6 +266,17 @@ struct folio *swap_cache_get_folio(swp_entry_t entry)
 	return folio;
 }
 
+/*
+ * Check if a swap entry has folio cached, may return false positive.
+ * Caller must hold a reference of the swap device or pin it in other ways.
+ */
+bool swap_cache_check_folio(swp_entry_t entry)
+{
+	swp_te_t swp_te;
+	swp_te = __swap_table_get(swp_cluster(entry), swp_offset(entry));
+	return swp_te_is_folio(swp_te);
+}
+
 /*
  * If we are the only user, then try to free up the swap cache.
  *
mm/swapfile.c +0 -0
index ef233466725e..0f2a499ff2c9 100644
@@ -181,15 +181,19 @@ static long swap_usage_in_pages(struct swap_info_struct *si)
 #define TTRS_FULL		0x4
 
 static bool swap_only_has_cache(struct swap_info_struct *si,

mm/swapfile.c static long swap_usage_in_pages(struct swap_info_struct *si) · line 192

Quoted 10 lines
mm/swapfile.c +4 -1
-			      unsigned long offset, int nr_pages)
+				struct swap_cluster_info *ci,
+				unsigned long offset, int nr_pages)
 {
 	unsigned char *map = si->swap_map + offset;
 	unsigned char *map_end = map + nr_pages;
+	swp_te_t entry;
 
 	do {
+		entry = __swap_table_get(ci, offset);

entry is not used in swap_only_has_cache() in this patch.

Quoted 8 lines
mm/swapfile.c +2 -1
 		VM_BUG_ON(!(*map & SWAP_HAS_CACHE));
-		if (*map != SWAP_HAS_CACHE)
+		if (*map)
 			return false;
+		offset++;
 	} while (++map < map_end);
 
 	return true;

......snip...

#42 lore raw

Re: [PATCH 20/28] mm, swap: check swap table directly for checking cache

Quoted 9 lines · Baoquan He
Quoted 4 lines · Kairui Song

From: Kairui Song <[email protected]>

Instead of looking at the swap map, check swap table directly to tell if a swap entry has cache. Prepare for remove SWAP_HAS_CACHE.

But you actually check both the swap table entry and swap map entry in this patch, or do I miss anything?

Hi, Baoquan

Quoted 4 lines

E.g

if (!swap_count(si->swap_map[offset]) && swp_te_is_folio(swp_te))

Yes, the count info is still in the swap_map now, I'm only converting the HAS_CACHE check to use swp_te_t here. We'll remove swap_map in later patches and use the swp_te_t solely to get both info.

The reason some checks are added to check the swap_count is that:
Before this patch, swap_map[offset] == SWAP_HAS_CACHE implies the count is zero too. So if HAS_CACHE is moved to swp_te_t, we still need to check the count separately. The overhead will be gone very soon in a later patch.

Quoted 98 lines
Quoted 98 lines
---
 mm/memory.c     | 12 +++++------
 mm/swap.h       |  6 ++++++
 mm/swap_state.c | 11 ++++++++++
 mm/swapfile.c   | 54 +++++++++++++++++++++++--------------------------
 4 files changed, 48 insertions(+), 35 deletions(-)
mm/memory.c +6 -6
index a70624a55aa2..a9a548575e72 100644
@@ -4314,15 +4314,15 @@ static struct folio *__alloc_swap_folio(struct vm_fault *vmf)
 }
 
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
-static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
+static inline int non_swapcache_batch(swp_entry_t entry, unsigned int max_nr)
 {
-     struct swap_info_struct *si = swp_info(entry);
-     pgoff_t offset = swp_offset(entry);
-     int i;
+     unsigned int i;
 
      for (i = 0; i < max_nr; i++) {
-             if ((si->swap_map[offset + i] & SWAP_HAS_CACHE))
-                     return i;
+             /* Page table lock pins the swap entries / swap device */
+             if (swap_cache_check_folio(entry))
+                     break;
+             entry.val++;
      }
 
      return i;
mm/swap.h +6 -0
index 467996dafbae..2ae4624a0e48 100644
@@ -186,6 +186,7 @@ static inline struct address_space *swap_address_space(swp_entry_t entry)
 extern struct folio *swap_cache_get_folio(swp_entry_t entry);
 extern struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
                                        void **shadow, bool swapin);
+extern bool swap_cache_check_folio(swp_entry_t entry);
 extern void *swap_cache_get_shadow(swp_entry_t entry);
 /* Below helpers requires the caller to lock the swap cluster. */
 extern void __swap_cache_del_folio(swp_entry_t entry,
@@ -395,6 +396,11 @@ static inline void *swap_cache_get_shadow(swp_entry_t end)
      return NULL;
 }
 
+static inline bool swap_cache_check_folio(swp_entry_t entry)
+{
+     return false;
+}
+
 static inline unsigned int folio_swap_flags(struct folio *folio)
 {
      return 0;
mm/swap_state.c +11 -0
index c8bb16835612..ea6a1741db5c 100644
@@ -266,6 +266,17 @@ struct folio *swap_cache_get_folio(swp_entry_t entry)
      return folio;
 }
 
+/*
+ * Check if a swap entry has folio cached, may return false positive.
+ * Caller must hold a reference of the swap device or pin it in other ways.
+ */
+bool swap_cache_check_folio(swp_entry_t entry)
+{
+     swp_te_t swp_te;
+     swp_te = __swap_table_get(swp_cluster(entry), swp_offset(entry));
+     return swp_te_is_folio(swp_te);
+}
+
 /*
  * If we are the only user, then try to free up the swap cache.
  *
mm/swapfile.c +3 -1
index ef233466725e..0f2a499ff2c9 100644
@@ -181,15 +181,19 @@ static long swap_usage_in_pages(struct swap_info_struct *si)
 #define TTRS_FULL            0x4
 
 static bool swap_only_has_cache(struct swap_info_struct *si,
-                           unsigned long offset, int nr_pages)
+                             struct swap_cluster_info *ci,
+                             unsigned long offset, int nr_pages)
 {
      unsigned char *map = si->swap_map + offset;
      unsigned char *map_end = map + nr_pages;
+     swp_te_t entry;
 
Quoted 4 lines

mm/swapfile.c static long swap_usage_in_pages(struct swap_info_struct *si) · line 192

Quoted 2 lines
mm/swapfile.c +1 -0
      do {
+             entry = __swap_table_get(ci, offset);

entry is not used in swap_only_has_cache() in this patch.

Thanks, it used in a later patch so I must move it here accidently during a rebase, will defer this change to later patch.

Quoted 12 lines
Quoted 9 lines
mm/swapfile.c +2 -1
              VM_BUG_ON(!(*map & SWAP_HAS_CACHE));
-             if (*map != SWAP_HAS_CACHE)
+             if (*map)
                      return false;
+             offset++;
      } while (++map < map_end);
 
      return true;

......snip...

#43 lore raw

Re: [PATCH 20/28] mm, swap: check swap table directly for checking cache

Quoted 14 lines · Kairui Song
Quoted 9 lines · Baoquan He
Quoted 4 lines · Kairui Song

From: Kairui Song <[email protected]>

Instead of looking at the swap map, check swap table directly to tell if a swap entry has cache. Prepare for remove SWAP_HAS_CACHE.

But you actually check both the swap table entry and swap map entry in this patch, or do I miss anything?

Hi, Baoquan

Quoted 1 line
Quoted 7 lines
Quoted 3 lines

E.g

if (!swap_count(si->swap_map[offset]) && swp_te_is_folio(swp_te))

Yes, the count info is still in the swap_map now, I'm only converting the HAS_CACHE check to use swp_te_t here. We'll remove swap_map in later patches and use the swp_te_t solely to get both info.

Ah, I see it now. That's why the subject is saying it's checking swap table for checking cache. Then it's fine to me, even though it's a little confusing.

Quoted 6 lines

The reason some checks are added to check the swap_count is that:
Before this patch, swap_map[offset] == SWAP_HAS_CACHE implies the count is zero too. So if HAS_CACHE is moved to swp_te_t, we still need to check the count separately. The overhead will be gone very soon in a later patch.

Got it, that sounds great, thanks.

#44 lore raw

PATCH 21/28 mm, swap: add folio to swap cache directly on allocation

In reply to Kairui Song

From: Kairui Song <[email protected]>

All swap allocations are folio based now (except for hibernation), and swap cache is protected by cluster lock too. So insert the folio directly in to the swap cache upon allocation while holding the cluster to avoid problems caused by dropping and re-acquiring the lock.

---
 mm/swap.h       |   8 ++--
 mm/swap_state.c |  48 +++++++++++++++----
 mm/swapfile.c   | 122 ++++++++++++++++++++----------------------------
 3 files changed, 93 insertions(+), 85 deletions(-)
mm/swap.h +5 -3
index 2ae4624a0e48..b042609e6eb2 100644
@@ -185,7 +185,10 @@ static inline struct address_space *swap_address_space(swp_entry_t entry)
 /* Below helpers requires the caller to pin the swap device. */
 extern struct folio *swap_cache_get_folio(swp_entry_t entry);
 extern struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
-					  void **shadow, bool swapin);
+					  void **shadow);
+extern void __swap_cache_add_folio(swp_entry_t entry,
+				   struct swap_cluster_info *ci,
+				   struct folio *folio);
 extern bool swap_cache_check_folio(swp_entry_t entry);
 extern void *swap_cache_get_shadow(swp_entry_t entry);
 /* Below helpers requires the caller to lock the swap cluster. */
@@ -368,8 +371,7 @@ static inline struct folio *swap_cache_get_folio(swp_entry_t entry)
 	return NULL;
 }
 
-static inline int swap_cache_add_folio(swp_entry_t end, struct folio *folio,
-				       void **shadow, bool swapin)
+static inline int swap_cache_add_folio(swp_entry_t end, struct folio *folio, void **shadow)
 {
 	return -EINVAL;
 }
mm/swap_state.c +38 -10

73 lines not on this page · raw message

mm/swapfile.c +50 -72

266 lines not on this page · raw message

Signature · 1 line
2.49.0
#45 lore raw

PATCH 22/28 mm, swap: drop the SWAP_HAS_CACHE flag

In reply to Kairui Song

From: Kairui Song <[email protected]>

Now swap cache is managed with swap table, users will check if an entry is cached by looking at the swap table type directly. SWAP_HAS_CACHE is only used to pin a entry temporarily so it won't be used by anyone else.

Previous commits have converted all places checking SWAP_HAS_CACHE to check the swap table directly, now the only place still sets SWAP_HAS_CACHE is on folio freeing.

Freeing a cached entry will set its swap map to SWAP_HAS_CACHE first, keep the entry pinned with SWAP_HAS_CACHE, then free it.

Now as the swap has become the mandatory layer and managed by swap table, and all users are checking the swap table directly, this can be much
simplified: when removing a folio from swap cache, free all it's entry that have zero count directly instead of doing a temporarily pin.

After above change, SWAP_HAS_CACHE no longer have any users, remove all related logic and helpers.

---
 include/linux/swap.h |   1 -
 mm/swap.h            |  12 ++-
 mm/swap_state.c      |  22 ++++--
 mm/swapfile.c        | 184 +++++++++++--------------------------------
 4 files changed, 67 insertions(+), 152 deletions(-)
include/linux/swap.h +0 -1
index adac6d51da05..60b126918399 100644
@@ -224,7 +224,6 @@ enum {
 #define COMPACT_CLUSTER_MAX SWAP_CLUSTER_MAX
 
 /* Bit flag in swap_map */
-#define SWAP_HAS_CACHE	0x40	/* Flag page is cached, in first swap_map */
 #define COUNT_CONTINUED	0x80	/* Flag swap_map continuation for full count */
 
 /* Special value in first swap_map */
mm/swap.h +5 -7
index b042609e6eb2..7cbfca39225f 100644
@@ -135,13 +135,6 @@ static inline void swap_unlock_cluster_irq(struct swap_cluster_info *ci)
 	spin_unlock_irq(&ci->lock);
 }
 
-extern int __swap_cache_set_entry(struct swap_info_struct *si,
-				  struct swap_cluster_info *ci,
-				  unsigned long offset);
-extern void __swap_cache_put_entries(struct swap_info_struct *si,
-				     struct swap_cluster_info *ci,
-				     swp_entry_t entry, unsigned int size);
-
 /*
  * All swap entries starts getting allocated by folio_alloc_swap(),
  * and the folio will be added to swap cache.
@@ -161,6 +154,11 @@ int folio_dup_swap(struct folio *folio, struct page *subpage);
 void folio_put_swap(struct folio *folio, struct page *subpage);
 void folio_free_swap_cache(struct folio *folio);
 
+/* For internal use */
+extern void __swap_free_entries(struct swap_info_struct *si,
+			      struct swap_cluster_info *ci,
+			      unsigned long offset, unsigned int nr_pages);
+
 /* linux/mm/page_io.c */
 int sio_pool_init(void);
 struct swap_iocb;
mm/swap_state.c +17 -5
index 9e7d40215958..2b145c0f7773 100644
@@ -169,7 +169,7 @@ struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
 			existing = swp_te_folio(exist);
 			goto out_failed;
 		}
-		if (__swap_cache_set_entry(si, ci, offset))
+		if (!__swap_count(swp_entry(si->type, offset)))
 			goto out_failed;
 		if (shadow && swp_te_is_shadow(exist))
 			*shadow = swp_te_shadow(exist);
@@ -191,10 +191,8 @@ struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
 	 * We may lose shadow here due to raced swapin, which is rare and OK,
 	 * caller better keep the previous returned shadow.
 	 */
-	while (offset-- > start) {
+	while (offset-- > start)
 		__swap_table_set_shadow(ci, offset, NULL);
-		__swap_cache_put_entries(si, ci, swp_entry(si->type, offset), 1);
-	}
 	swap_unlock_cluster(ci);
 
 	/*
@@ -219,6 +217,7 @@ void __swap_cache_del_folio(swp_entry_t entry,
 	pgoff_t offset, start, end;
 	struct swap_info_struct *si;
 	struct swap_cluster_info *ci;
+	bool folio_swapped = false, need_free = false;
 	unsigned long nr_pages = folio_nr_pages(folio);
 
 	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
@@ -235,13 +234,26 @@ void __swap_cache_del_folio(swp_entry_t entry,
 		exist = __swap_table_get(ci, offset);
 		VM_WARN_ON_ONCE(swp_te_folio(exist) != folio);
 		__swap_table_set_shadow(ci, offset, shadow);
+		if (__swap_count(swp_entry(si->type, offset)))
+			folio_swapped = true;
+		else
+			need_free = true;
 	} while (++offset < end);
 
 	folio->swap.val = 0;
 	folio_clear_swapcache(folio);
 	node_stat_mod_folio(folio, NR_FILE_PAGES, -nr_pages);
 	lruvec_stat_mod_folio(folio, NR_SWAPCACHE, -nr_pages);
-	__swap_cache_put_entries(si, ci, entry, nr_pages);
+
+	if (!folio_swapped) {
+		__swap_free_entries(si, ci, start, nr_pages);
+	} else if (need_free) {
+		offset = start;
+		do {
+			if (!__swap_count(swp_entry(si->type, offset)))
+				__swap_free_entries(si, ci, offset, 1);
+		} while (++offset < end);
+	}
 }
 
 /*
mm/swapfile.c +45 -139

426 lines not on this page · raw message

Signature · 1 line
2.49.0
#46 lore raw

PATCH 23/28 mm, swap: remove no longer needed _swap_info_get

In reply to Kairui Song

From: Kairui Song <[email protected]>

There is now only two users of _swap_info_get after consolidating these callers, folio_try_reclaim_swap and swp_swapcount.

folio_try_reclaim_swap holds the folio lock and the folio is in swap cache, _swap_info_get is redundant.

For swp_swapcount, _swap_info_get is insufficient as the swap entry is no pinned so the device could be swapped off anytime, it should use get_swap_device instead.

And after these change, _swap_info_get is no longer used, we can safely remove it.

---
 mm/swapfile.c | 63 +++++++++++++--------------------------------------
 1 file changed, 16 insertions(+), 47 deletions(-)
mm/swapfile.c +16 -47
index c2154f19c21b..28bb0a74e4a6 100644
@@ -1364,35 +1364,6 @@ void folio_free_swap_cache(struct folio *folio)
 	folio_ref_sub(folio, folio_nr_pages(folio));
 }
 
-static struct swap_info_struct *_swap_info_get(swp_entry_t entry)
-{
-	struct swap_info_struct *si;
-	unsigned long offset;
-
-	if (!entry.val)
-		goto out;
-	si = swp_get_info(entry);
-	if (!si)
-		goto bad_nofile;
-	if (data_race(!(si->flags & SWP_USED)))
-		goto bad_device;
-	offset = swp_offset(entry);
-	if (offset >= si->max)
-		goto bad_offset;
-	return si;
-
-bad_offset:
-	pr_err("%s: %s%08lx\n", __func__, Bad_offset, entry.val);
-	goto out;
-bad_device:
-	pr_err("%s: %s%08lx\n", __func__, Unused_file, entry.val);
-	goto out;
-bad_nofile:
-	pr_err("%s: %s%08lx\n", __func__, Bad_file, entry.val);
-out:
-	return NULL;
-}
-
 static unsigned char swap_put_entry_locked(struct swap_info_struct *si,
 					   struct swap_cluster_info *ci,
 					   swp_entry_t entry)
@@ -1549,7 +1520,7 @@ int swp_swapcount(swp_entry_t entry)
 	pgoff_t offset;
 	unsigned char *map;
 
-	si = _swap_info_get(entry);
+	si = get_swap_device(entry);
 	if (!si)
 		return 0;
 
@@ -1579,6 +1550,7 @@ int swp_swapcount(swp_entry_t entry)
 	} while (tmp_count & COUNT_CONTINUED);
 out:
 	swap_unlock_cluster(ci);
+	put_swap_device(si);
 	return count;
 }
 
@@ -1610,26 +1582,10 @@ static bool swap_page_trans_huge_swapped(struct swap_info_struct *si,
 	return ret;
 }
 
-static bool folio_swapped(struct folio *folio)
-{
-	swp_entry_t entry = folio->swap;
-	struct swap_info_struct *si = _swap_info_get(entry);
-
-	if (!si)
-		return false;
-
-	if (!IS_ENABLED(CONFIG_THP_SWAP) || likely(!folio_test_large(folio)))
-		return swap_entry_swapped(si, entry);
-
-	return swap_page_trans_huge_swapped(si, entry, folio_order(folio));
-}
-
 static bool folio_swapcache_freeable(struct folio *folio)
 {
 	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
 
-	if (!folio_test_swapcache(folio))
-		return false;
 	if (folio_test_writeback(folio))
 		return false;
 
@@ -1665,9 +1621,22 @@ static bool folio_swapcache_freeable(struct folio *folio)
  */
 bool folio_free_swap(struct folio *folio)
 {
+	bool swapped;
+	struct swap_info_struct *si;
+	swp_entry_t entry = folio->swap;
+
+	if (!folio_test_swapcache(folio))
+		return false;
 	if (!folio_swapcache_freeable(folio))
 		return false;
-	if (folio_swapped(folio))
+
+	si = swp_info(entry);
+	if (!IS_ENABLED(CONFIG_THP_SWAP) || !folio_test_large(folio))
+		swapped = swap_entry_swapped(si, entry);
+	else
+		swapped = swap_page_trans_huge_swapped(si, entry,
+						       folio_order(folio));
+	if (swapped)
 		return false;
 
 	folio_free_swap_cache(folio);
Signature · 1 line
2.49.0
#47 lore raw

PATCH 24/28 mm, swap: implement helpers for reserving data in swap table

In reply to Kairui Song

From: Kairui Song <[email protected]>

To prepare for using swap table as the unified swap layer, introduce macro and helpers for storing multiple kind of data in an swap table entry.

---
 mm/swap_table.h | 130 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 119 insertions(+), 11 deletions(-)
mm/swap_table.h +119 -11
index 69a074339444..9356004d211a 100644
@@ -5,9 +5,41 @@
 #include "swap.h"
 
 /*
- * Swap table entry could be a pointer (folio), a XA_VALUE (shadow), or NULL.
+ * Swap table entry type and bit layouts:
+ *
+ * NULL:     | ------------    0   -------------|
+ * Shadow:   | SWAP_COUNT |---- SHADOW_VAL ---|1|
+ * Folio:    | SWAP_COUNT |------ PFN -------|10|
+ * Pointer:  |----------- Pointer ----------|100|
+ *
+ * Usage:
+ * - NULL: Swap Entry is unused.
+ *
+ * - Shadow: Swap Entry is used and not cached (swapped out).
+ *   It's reusing XA_VALUE format to be compatible with workingset
+ *   shadows. SHADOW_VAL part could be all 0.
+ *
+ * - Folio: Swap Entry is in cache.
+ *
+ * - Pointer: Unused yet. Because only the last three bit of a pointer
+ *   is usable so now `100` is reserved for potential pointer use.
  */
 
+#define ENTRY_COUNT_BITS	BITS_PER_BYTE
+#define ENTRY_SHADOW_MARK	0b1UL
+#define ENTRY_PFN_MARK		0b10UL
+#define ENTRY_PFN_LOW_MASK	0b11UL
+#define ENTRY_PFN_SHIFT		2
+#define ENTRY_PFN_MASK		((~0UL) >> ENTRY_COUNT_BITS)
+#define ENTRY_COUNT_MASK	(~((~0UL) >> ENTRY_COUNT_BITS))
+#define ENTRY_COUNT_SHIFT	(BITS_PER_LONG - BITS_PER_BYTE)
+#define ENTRY_COUNT_MAX		((1 << ENTRY_COUNT_BITS) - 2)
+#define ENTRY_COUNT_BAD		((1 << ENTRY_COUNT_BITS) - 1) /* ENTRY_BAD */
+#define ENTRY_BAD		(~0UL)
+
+/* For shadow offset calculation */
+#define SWAP_COUNT_SHIFT	ENTRY_COUNT_BITS
+
 /*
  * Helpers for casting one type of info into a swap table entry.
  */
@@ -19,17 +51,27 @@ static inline swp_te_t null_swp_te(void)
 
 static inline swp_te_t folio_swp_te(struct folio *folio)
 {
-	BUILD_BUG_ON(sizeof(swp_te_t) != sizeof(void *));
-	swp_te_t swp_te = { .counter = (unsigned long)folio };
+	BUILD_BUG_ON((MAX_POSSIBLE_PHYSMEM_BITS - PAGE_SHIFT) >
+		     (BITS_PER_LONG - ENTRY_PFN_SHIFT - ENTRY_COUNT_BITS));
+	swp_te_t swp_te = {
+		.counter = (folio_pfn(folio) << ENTRY_PFN_SHIFT) | ENTRY_PFN_MARK
+	};
 	return swp_te;
 }
 
 static inline swp_te_t shadow_swp_te(void *shadow)
 {
-	BUILD_BUG_ON((BITS_PER_XA_VALUE + 1) !=
-		     BITS_PER_BYTE * sizeof(swp_te_t));
-	VM_WARN_ON_ONCE(shadow && !xa_is_value(shadow));
 	swp_te_t swp_te = { .counter = ((unsigned long)shadow) };
+	BUILD_BUG_ON((BITS_PER_XA_VALUE + 1) != BITS_PER_BYTE * sizeof(swp_te_t));
+	BUILD_BUG_ON((unsigned long)xa_mk_value(0) != ENTRY_SHADOW_MARK);
+	VM_WARN_ON_ONCE(shadow && !xa_is_value(shadow));
+	swp_te.counter |= ENTRY_SHADOW_MARK;
+	return swp_te;
+}
+
+static inline swp_te_t bad_swp_te(void)
+{
+	swp_te_t swp_te = { .counter = ENTRY_BAD };
 	return swp_te;
 }
 
@@ -43,7 +85,7 @@ static inline bool swp_te_is_null(swp_te_t swp_te)
 
 static inline bool swp_te_is_folio(swp_te_t swp_te)
 {
-	return !xa_is_value((void *)swp_te.counter) && !swp_te_is_null(swp_te);
+	return ((swp_te.counter & ENTRY_PFN_LOW_MASK) == ENTRY_PFN_MARK);
 }
 
 static inline bool swp_te_is_shadow(swp_te_t swp_te)
@@ -51,19 +93,63 @@ static inline bool swp_te_is_shadow(swp_te_t swp_te)
 	return xa_is_value((void *)swp_te.counter);
 }
 
+static inline bool swp_te_is_valid_shadow(swp_te_t swp_te)
+{
+	/* The shadow could be empty, just for holding the swap count */
+	return xa_is_value((void *)swp_te.counter) &&
+	       xa_to_value((void *)swp_te.counter);
+}
+
+static inline bool swp_te_is_bad(swp_te_t swp_te)
+{
+	return swp_te.counter == ENTRY_BAD;
+}
+
+static inline bool __swp_te_is_countable(swp_te_t ent)
+{
+	return (swp_te_is_shadow(ent) || swp_te_is_folio(ent) ||
+		swp_te_is_null(ent));
+}
+
 /*
  * Helpers for retrieving info from swap table.
  */
 static inline struct folio *swp_te_folio(swp_te_t swp_te)
 {
 	VM_WARN_ON(!swp_te_is_folio(swp_te));
-	return (void *)swp_te.counter;
+	return pfn_folio((swp_te.counter & ENTRY_PFN_MASK) >> ENTRY_PFN_SHIFT);
 }
 
 static inline void *swp_te_shadow(swp_te_t swp_te)
 {
 	VM_WARN_ON(!swp_te_is_shadow(swp_te));
-	return (void *)swp_te.counter;
+	return (void *)(swp_te.counter & ~ENTRY_COUNT_MASK);
+}
+
+static inline unsigned char swp_te_get_count(swp_te_t swp_te)
+{
+	VM_WARN_ON(!__swp_te_is_countable(swp_te));
+	return ((swp_te.counter & ENTRY_COUNT_MASK) >> ENTRY_COUNT_SHIFT);
+}
+
+static inline unsigned char swp_te_try_get_count(swp_te_t swp_te)
+{
+	if (__swp_te_is_countable(swp_te))
+		return swp_te_get_count(swp_te);
+	return 0;
+}
+
+static inline swp_te_t swp_te_set_count(swp_te_t swp_te,
+					unsigned char count)
+{
+	VM_BUG_ON(!__swp_te_is_countable(swp_te));
+	VM_BUG_ON(count > ENTRY_COUNT_MAX);
+
+	swp_te.counter &= ~ENTRY_COUNT_MASK;
+	swp_te.counter |= ((unsigned long)count) << ENTRY_COUNT_SHIFT;
+	VM_BUG_ON(swp_te_get_count(swp_te) != count);
+
+	return swp_te;
 }
 
 /*
@@ -87,17 +173,39 @@ static inline swp_te_t __swap_table_get(struct swap_cluster_info *ci, pgoff_t of
 static inline void __swap_table_set_folio(struct swap_cluster_info *ci, pgoff_t off,
 					  struct folio *folio)
 {
-	__swap_table_set(ci, off, folio_swp_te(folio));
+	swp_te_t swp_te;
+	unsigned char count;
+
+	swp_te = __swap_table_get(ci, off);
+	count = swp_te_get_count(swp_te);
+	swp_te = swp_te_set_count(folio_swp_te(folio), count);
+
+	__swap_table_set(ci, off, swp_te);
 }
 
 static inline void __swap_table_set_shadow(struct swap_cluster_info *ci, pgoff_t off,
 					   void *shadow)
 {
-	__swap_table_set(ci, off, shadow_swp_te(shadow));
+	swp_te_t swp_te;
+	unsigned char count;
+
+	swp_te = __swap_table_get(ci, off);
+	count = swp_te_get_count(swp_te);
+	swp_te = swp_te_set_count(shadow_swp_te(shadow), count);
+
+	__swap_table_set(ci, off, swp_te);
 }
 
 static inline void __swap_table_set_null(struct swap_cluster_info *ci, pgoff_t off)
 {
 	__swap_table_set(ci, off, null_swp_te());
 }
+
+static inline void __swap_table_set_count(struct swap_cluster_info *ci, pgoff_t off,
+					  unsigned char count)
+{
+	swp_te_t swp_te;
+	swp_te = swp_te_set_count(__swap_table_get(ci, off), count);
+	__swap_table_set(ci, off, swp_te);
+}
 #endif
Signature · 1 line
2.49.0
#48 lore raw

Re: [PATCH 24/28] mm, swap: implement helpers for reserving data in swap table

Hi,

Quoted 59 lines · Kairui Song

From: Kairui Song <[email protected]>

To prepare for using swap table as the unified swap layer, introduce macro and helpers for storing multiple kind of data in an swap table entry.

---
 mm/swap_table.h | 130 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 119 insertions(+), 11 deletions(-)
mm/swap_table.h +33 -1
index 69a074339444..9356004d211a 100644
@@ -5,9 +5,41 @@
 #include "swap.h"
 
 /*
- * Swap table entry could be a pointer (folio), a XA_VALUE (shadow), or NULL.
+ * Swap table entry type and bit layouts:
+ *
+ * NULL:     | ------------    0   -------------|
+ * Shadow:   | SWAP_COUNT |---- SHADOW_VAL ---|1|
+ * Folio:    | SWAP_COUNT |------ PFN -------|10|
+ * Pointer:  |----------- Pointer ----------|100|
+ *
+ * Usage:
+ * - NULL: Swap Entry is unused.
+ *
+ * - Shadow: Swap Entry is used and not cached (swapped out).
+ *   It's reusing XA_VALUE format to be compatible with workingset
+ *   shadows. SHADOW_VAL part could be all 0.
+ *
+ * - Folio: Swap Entry is in cache.
+ *
+ * - Pointer: Unused yet. Because only the last three bit of a pointer
+ *   is usable so now `100` is reserved for potential pointer use.
  */
 
+#define ENTRY_COUNT_BITS	BITS_PER_BYTE
+#define ENTRY_SHADOW_MARK	0b1UL
+#define ENTRY_PFN_MARK		0b10UL
+#define ENTRY_PFN_LOW_MASK	0b11UL
+#define ENTRY_PFN_SHIFT		2
+#define ENTRY_PFN_MASK		((~0UL) >> ENTRY_COUNT_BITS)
+#define ENTRY_COUNT_MASK	(~((~0UL) >> ENTRY_COUNT_BITS))
+#define ENTRY_COUNT_SHIFT	(BITS_PER_LONG - BITS_PER_BYTE)
+#define ENTRY_COUNT_MAX		((1 << ENTRY_COUNT_BITS) - 2)
+#define ENTRY_COUNT_BAD		((1 << ENTRY_COUNT_BITS) - 1) /* ENTRY_BAD */
+#define ENTRY_BAD		(~0UL)
+
+/* For shadow offset calculation */
+#define SWAP_COUNT_SHIFT	ENTRY_COUNT_BITS
+
 /*
  * Helpers for casting one type of info into a swap table entry.
  */

mm/swap_table.h static inline swp_te_t null_swp_te(void) · line 56

Quoted 8 lines
mm/swap_table.h +2 -2
@@ -19,17 +51,27 @@ static inline swp_te_t null_swp_te(void)
 
 static inline swp_te_t folio_swp_te(struct folio *folio)
 {
-	BUILD_BUG_ON(sizeof(swp_te_t) != sizeof(void *));
-	swp_te_t swp_te = { .counter = (unsigned long)folio };
 
+	BUILD_BUG_ON((MAX_POSSIBLE_PHYSMEM_BITS - PAGE_SHIFT) >
+		     (BITS_PER_LONG - ENTRY_PFN_SHIFT - ENTRY_COUNT_BITS));

MAX_POSSIBLE_PHYSMEM_BITS does not seem to be available on all arches/configs.

E.g. zsmalloc seems to set it to MAX_PHYSMEM_BITS when this is the case but I don't know if that applies here.

Quoted 145 lines
mm/swap_table.h +84 -8
+	swp_te_t swp_te = {
+		.counter = (folio_pfn(folio) << ENTRY_PFN_SHIFT) | ENTRY_PFN_MARK
+	};
 	return swp_te;
 }
 
 static inline swp_te_t shadow_swp_te(void *shadow)
 {
-	BUILD_BUG_ON((BITS_PER_XA_VALUE + 1) !=
-		     BITS_PER_BYTE * sizeof(swp_te_t));
-	VM_WARN_ON_ONCE(shadow && !xa_is_value(shadow));
 	swp_te_t swp_te = { .counter = ((unsigned long)shadow) };
+	BUILD_BUG_ON((BITS_PER_XA_VALUE + 1) != BITS_PER_BYTE * sizeof(swp_te_t));
+	BUILD_BUG_ON((unsigned long)xa_mk_value(0) != ENTRY_SHADOW_MARK);
+	VM_WARN_ON_ONCE(shadow && !xa_is_value(shadow));
+	swp_te.counter |= ENTRY_SHADOW_MARK;
+	return swp_te;
+}
+
+static inline swp_te_t bad_swp_te(void)
+{
+	swp_te_t swp_te = { .counter = ENTRY_BAD };
 	return swp_te;
 }
@@ -43,7 +85,7 @@ static inline bool swp_te_is_null(swp_te_t swp_te)
 
 static inline bool swp_te_is_folio(swp_te_t swp_te)
 {
-	return !xa_is_value((void *)swp_te.counter) && !swp_te_is_null(swp_te);
+	return ((swp_te.counter & ENTRY_PFN_LOW_MASK) == ENTRY_PFN_MARK);
 }
 
 static inline bool swp_te_is_shadow(swp_te_t swp_te)
@@ -51,19 +93,63 @@ static inline bool swp_te_is_shadow(swp_te_t swp_te)
 	return xa_is_value((void *)swp_te.counter);
 }
 
+static inline bool swp_te_is_valid_shadow(swp_te_t swp_te)
+{
+	/* The shadow could be empty, just for holding the swap count */
+	return xa_is_value((void *)swp_te.counter) &&
+	       xa_to_value((void *)swp_te.counter);
+}
+
+static inline bool swp_te_is_bad(swp_te_t swp_te)
+{
+	return swp_te.counter == ENTRY_BAD;
+}
+
+static inline bool __swp_te_is_countable(swp_te_t ent)
+{
+	return (swp_te_is_shadow(ent) || swp_te_is_folio(ent) ||
+		swp_te_is_null(ent));
+}
+
 /*
  * Helpers for retrieving info from swap table.
  */
 static inline struct folio *swp_te_folio(swp_te_t swp_te)
 {
 	VM_WARN_ON(!swp_te_is_folio(swp_te));
-	return (void *)swp_te.counter;
+	return pfn_folio((swp_te.counter & ENTRY_PFN_MASK) >> ENTRY_PFN_SHIFT);
 }
 
 static inline void *swp_te_shadow(swp_te_t swp_te)
 {
 	VM_WARN_ON(!swp_te_is_shadow(swp_te));
-	return (void *)swp_te.counter;
+	return (void *)(swp_te.counter & ~ENTRY_COUNT_MASK);
+}
+
+static inline unsigned char swp_te_get_count(swp_te_t swp_te)
+{
+	VM_WARN_ON(!__swp_te_is_countable(swp_te));
+	return ((swp_te.counter & ENTRY_COUNT_MASK) >> ENTRY_COUNT_SHIFT);
+}
+
+static inline unsigned char swp_te_try_get_count(swp_te_t swp_te)
+{
+	if (__swp_te_is_countable(swp_te))
+		return swp_te_get_count(swp_te);
+	return 0;
+}
+
+static inline swp_te_t swp_te_set_count(swp_te_t swp_te,
+					unsigned char count)
+{
+	VM_BUG_ON(!__swp_te_is_countable(swp_te));
+	VM_BUG_ON(count > ENTRY_COUNT_MAX);
+
+	swp_te.counter &= ~ENTRY_COUNT_MASK;
+	swp_te.counter |= ((unsigned long)count) << ENTRY_COUNT_SHIFT;
+	VM_BUG_ON(swp_te_get_count(swp_te) != count);
+
+	return swp_te;
 }
 
 /*
@@ -87,17 +173,39 @@ static inline swp_te_t __swap_table_get(struct swap_cluster_info *ci, pgoff_t of
 static inline void __swap_table_set_folio(struct swap_cluster_info *ci, pgoff_t off,
 					  struct folio *folio)
 {
-	__swap_table_set(ci, off, folio_swp_te(folio));
+	swp_te_t swp_te;
+	unsigned char count;
+
+	swp_te = __swap_table_get(ci, off);
+	count = swp_te_get_count(swp_te);
+	swp_te = swp_te_set_count(folio_swp_te(folio), count);
+
+	__swap_table_set(ci, off, swp_te);
 }
 
 static inline void __swap_table_set_shadow(struct swap_cluster_info *ci, pgoff_t off,
 					   void *shadow)
 {
-	__swap_table_set(ci, off, shadow_swp_te(shadow));
+	swp_te_t swp_te;
+	unsigned char count;
+
+	swp_te = __swap_table_get(ci, off);
+	count = swp_te_get_count(swp_te);
+	swp_te = swp_te_set_count(shadow_swp_te(shadow), count);
+
+	__swap_table_set(ci, off, swp_te);
 }
 
 static inline void __swap_table_set_null(struct swap_cluster_info *ci, pgoff_t off)
 {
 	__swap_table_set(ci, off, null_swp_te());
 }
+
+static inline void __swap_table_set_count(struct swap_cluster_info *ci, pgoff_t off,
+					  unsigned char count)
+{
+	swp_te_t swp_te;
+	swp_te = swp_te_set_count(__swap_table_get(ci, off), count);
+	__swap_table_set(ci, off, swp_te);
+}
 #endif
Signature · 1 line
2.49.0

Regards,
Klara Modin

#49 lore raw

Re: [PATCH 24/28] mm, swap: implement helpers for reserving data in swap table

Quoted 69 lines · Klara Modin

Hi,

Quoted 64 lines · Kairui Song

From: Kairui Song <[email protected]>

To prepare for using swap table as the unified swap layer, introduce macro and helpers for storing multiple kind of data in an swap table entry.

---
 mm/swap_table.h | 130 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 119 insertions(+), 11 deletions(-)
mm/swap_table.h +33 -3
index 69a074339444..9356004d211a 100644
@@ -5,9 +5,41 @@
 #include "swap.h"
 
 /*
- * Swap table entry could be a pointer (folio), a XA_VALUE (shadow), or NULL.
+ * Swap table entry type and bit layouts:
+ *
+ * NULL:     | ------------    0   -------------|
+ * Shadow:   | SWAP_COUNT |---- SHADOW_VAL ---|1|
+ * Folio:    | SWAP_COUNT |------ PFN -------|10|
+ * Pointer:  |----------- Pointer ----------|100|
+ *
+ * Usage:
+ * - NULL: Swap Entry is unused.
+ *
+ * - Shadow: Swap Entry is used and not cached (swapped out).
+ *   It's reusing XA_VALUE format to be compatible with workingset
+ *   shadows. SHADOW_VAL part could be all 0.
+ *
+ * - Folio: Swap Entry is in cache.
+ *
+ * - Pointer: Unused yet. Because only the last three bit of a pointer
+ *   is usable so now `100` is reserved for potential pointer use.
  */
 
+#define ENTRY_COUNT_BITS     BITS_PER_BYTE
+#define ENTRY_SHADOW_MARK    0b1UL
+#define ENTRY_PFN_MARK               0b10UL
+#define ENTRY_PFN_LOW_MASK   0b11UL
+#define ENTRY_PFN_SHIFT              2
+#define ENTRY_PFN_MASK               ((~0UL) >> ENTRY_COUNT_BITS)
+#define ENTRY_COUNT_MASK     (~((~0UL) >> ENTRY_COUNT_BITS))
+#define ENTRY_COUNT_SHIFT    (BITS_PER_LONG - BITS_PER_BYTE)
+#define ENTRY_COUNT_MAX              ((1 << ENTRY_COUNT_BITS) - 2)
+#define ENTRY_COUNT_BAD              ((1 << ENTRY_COUNT_BITS) - 1) /* ENTRY_BAD */
+#define ENTRY_BAD            (~0UL)
+
+/* For shadow offset calculation */
+#define SWAP_COUNT_SHIFT     ENTRY_COUNT_BITS
+
 /*
  * Helpers for casting one type of info into a swap table entry.
  */
@@ -19,17 +51,27 @@ static inline swp_te_t null_swp_te(void)
 
 static inline swp_te_t folio_swp_te(struct folio *folio)
 {
-     BUILD_BUG_ON(sizeof(swp_te_t) != sizeof(void *));
-     swp_te_t swp_te = { .counter = (unsigned long)folio };
Quoted 9 lines

mm/swap_table.h static inline swp_te_t null_swp_te(void) · line 55

Quoted 2 lines
mm/swap_table.h +2 -0
+     BUILD_BUG_ON((MAX_POSSIBLE_PHYSMEM_BITS - PAGE_SHIFT) >
+                  (BITS_PER_LONG - ENTRY_PFN_SHIFT - ENTRY_COUNT_BITS));

MAX_POSSIBLE_PHYSMEM_BITS does not seem to be available on all arches/configs.

E.g. zsmalloc seems to set it to MAX_PHYSMEM_BITS when this is the case but I don't know if that applies here.

Thanks, I think I'll just copy the snip from zsmalloc, it's basically doing the same check to ensure there are still enough bits left after embedding a PFN in a LONG.

#50 lore raw

PATCH 25/28 mm/workingset: leave highest 8 bits empty for anon shadow

In reply to Kairui Song

From: Kairui Song <[email protected]>

Swap table entry will need 8 bits reserved for swap count, so anon shadow should have 8 bits remain 0.

This should be OK for foreseeable future, take 52 bits physical address space as example: for 4K pages, there would be at most 40 bits for addressable pages. Currently we have 36 bits available (with NODES_SHIFT set to 10, this can be decreased for more bits), so in worst case refault distance compare will be done for every 64K sized bucket.

This commit may increases the bucket size to 16M, which should be fine as the workingset size will be way larger than the bucket size for such large machines.

For MGLRU 28 bits can track a huge amount of gens already, there should be no problem either.

And the 8 bits can be changed to 6 or even fewer bits later.

---
 mm/swap_table.h |  1 +
 mm/workingset.c | 39 ++++++++++++++++++++++++++-------------
 2 files changed, 27 insertions(+), 13 deletions(-)
mm/swap_table.h +1 -0
index 9356004d211a..afb2953d408a 100644
@@ -65,6 +65,7 @@ static inline swp_te_t shadow_swp_te(void *shadow)
 	BUILD_BUG_ON((BITS_PER_XA_VALUE + 1) != BITS_PER_BYTE * sizeof(swp_te_t));
 	BUILD_BUG_ON((unsigned long)xa_mk_value(0) != ENTRY_SHADOW_MARK);
 	VM_WARN_ON_ONCE(shadow && !xa_is_value(shadow));
+	VM_WARN_ON((unsigned long)shadow & ENTRY_COUNT_MASK);
 	swp_te.counter |= ENTRY_SHADOW_MARK;
 	return swp_te;
 }
mm/workingset.c +26 -13
index 6e7f4cb1b9a7..86a549a17ae1 100644
@@ -16,6 +16,7 @@
 #include <linux/dax.h>
 #include <linux/fs.h>
 #include <linux/mm.h>
+#include "swap_table.h"
 #include "internal.h"
 
 /*
@@ -184,7 +185,9 @@
 #define EVICTION_SHIFT	((BITS_PER_LONG - BITS_PER_XA_VALUE) +	\
 			 WORKINGSET_SHIFT + NODES_SHIFT + \
 			 MEM_CGROUP_ID_SHIFT)
+#define EVICTION_SHIFT_ANON	(EVICTION_SHIFT + SWAP_COUNT_SHIFT)
 #define EVICTION_MASK	(~0UL >> EVICTION_SHIFT)
+#define EVICTION_MASK_ANON	(~0UL >> EVICTION_SHIFT_ANON)
 
 /*
  * Eviction timestamps need to be able to cover the full range of
@@ -194,12 +197,16 @@
  * that case, we have to sacrifice granularity for distance, and group
  * evictions into coarser buckets by shaving off lower timestamp bits.
  */
-static unsigned int bucket_order __read_mostly;
+static unsigned int bucket_order[ANON_AND_FILE] __read_mostly;
 
 static void *pack_shadow(int memcgid, pg_data_t *pgdat, unsigned long eviction,
-			 bool workingset)
+			 bool workingset, bool file)
 {
-	eviction &= EVICTION_MASK;
+	if (file)
+		eviction &= EVICTION_MASK;
+	else
+		eviction &= EVICTION_MASK_ANON;
+
 	eviction = (eviction << MEM_CGROUP_ID_SHIFT) | memcgid;
 	eviction = (eviction << NODES_SHIFT) | pgdat->node_id;
 	eviction = (eviction << WORKINGSET_SHIFT) | workingset;
@@ -244,7 +251,8 @@ static void *lru_gen_eviction(struct folio *folio)
 	struct mem_cgroup *memcg = folio_memcg(folio);
 	struct pglist_data *pgdat = folio_pgdat(folio);
 
-	BUILD_BUG_ON(LRU_GEN_WIDTH + LRU_REFS_WIDTH > BITS_PER_LONG - EVICTION_SHIFT);
+	BUILD_BUG_ON(LRU_GEN_WIDTH + LRU_REFS_WIDTH >
+		     BITS_PER_LONG - max(EVICTION_SHIFT, EVICTION_SHIFT_ANON));
 
 	lruvec = mem_cgroup_lruvec(memcg, pgdat);
 	lrugen = &lruvec->lrugen;
@@ -254,7 +262,7 @@ static void *lru_gen_eviction(struct folio *folio)
 	hist = lru_hist_from_seq(min_seq);
 	atomic_long_add(delta, &lrugen->evicted[hist][type][tier]);
 
-	return pack_shadow(mem_cgroup_id(memcg), pgdat, token, workingset);
+	return pack_shadow(mem_cgroup_id(memcg), pgdat, token, workingset, type);
 }
 
 /*
@@ -381,6 +389,7 @@ void workingset_age_nonresident(struct lruvec *lruvec, unsigned long nr_pages)
 void *workingset_eviction(struct folio *folio, struct mem_cgroup *target_memcg)
 {
 	struct pglist_data *pgdat = folio_pgdat(folio);
+	int file = folio_is_file_lru(folio);
 	unsigned long eviction;
 	struct lruvec *lruvec;
 	int memcgid;
@@ -397,10 +406,10 @@ void *workingset_eviction(struct folio *folio, struct mem_cgroup *target_memcg)
 	/* XXX: target_memcg can be NULL, go through lruvec */
 	memcgid = mem_cgroup_id(lruvec_memcg(lruvec));
 	eviction = atomic_long_read(&lruvec->nonresident_age);
-	eviction >>= bucket_order;
+	eviction >>= bucket_order[file];
 	workingset_age_nonresident(lruvec, folio_nr_pages(folio));
 	return pack_shadow(memcgid, pgdat, eviction,
-				folio_test_workingset(folio));
+			   folio_test_workingset(folio), folio_is_file_lru(folio));
 }
 
 /**
@@ -438,7 +447,7 @@ bool workingset_test_recent(void *shadow, bool file, bool *workingset,
 
 	rcu_read_lock();
 	unpack_shadow(shadow, &memcgid, &pgdat, &eviction, workingset);
-	eviction <<= bucket_order;
+	eviction <<= bucket_order[file];
 
 	/*
 	 * Look up the memcg associated with the stored ID. It might
@@ -780,8 +789,8 @@ static struct lock_class_key shadow_nodes_key;
 
 static int __init workingset_init(void)
 {
+	unsigned int timestamp_bits, timestamp_bits_anon;
 	struct shrinker *workingset_shadow_shrinker;
-	unsigned int timestamp_bits;
 	unsigned int max_order;
 	int ret = -ENOMEM;
 
@@ -794,11 +803,15 @@ static int __init workingset_init(void)
 	 * double the initial memory by using totalram_pages as-is.
 	 */
 	timestamp_bits = BITS_PER_LONG - EVICTION_SHIFT;
+	timestamp_bits_anon = BITS_PER_LONG - EVICTION_SHIFT_ANON;
 	max_order = fls_long(totalram_pages() - 1);
-	if (max_order > timestamp_bits)
-		bucket_order = max_order - timestamp_bits;
-	pr_info("workingset: timestamp_bits=%d max_order=%d bucket_order=%u\n",
-	       timestamp_bits, max_order, bucket_order);
+	if (max_order > (BITS_PER_LONG - EVICTION_SHIFT))
+		bucket_order[WORKINGSET_FILE] = max_order - timestamp_bits;
+	if (max_order > timestamp_bits_anon)
+		bucket_order[WORKINGSET_ANON] = max_order - timestamp_bits_anon;
+	pr_info("workingset: timestamp_bits=%d (anon: %d) max_order=%d bucket_order=%u (anon: %d)\n",
+		timestamp_bits, timestamp_bits_anon, max_order,
+		bucket_order[WORKINGSET_FILE], bucket_order[WORKINGSET_ANON]);
 
 	workingset_shadow_shrinker = shrinker_alloc(SHRINKER_NUMA_AWARE |
 						    SHRINKER_MEMCG_AWARE,
Signature · 1 line
2.49.0
#51 lore raw

PATCH 26/28 mm, swap: minor clean up for swapon

In reply to Kairui Song

From: Kairui Song <[email protected]>

Setup cluster info first as now it's the most basic info for swap device now, and clean up swap_map setting. There is no need to pass the them as argument for multiple functions, they are never set to NULL once allocated, so just set it once.

---
 mm/swapfile.c | 62 +++++++++++++++++++++------------------------------
 1 file changed, 26 insertions(+), 36 deletions(-)
mm/swapfile.c +26 -36
index 28bb0a74e4a6..c50cbf6578d3 100644
@@ -2440,8 +2440,6 @@ static int swap_node(struct swap_info_struct *si)
 }
 
 static void setup_swap_info(struct swap_info_struct *si, int prio,
-			    unsigned char *swap_map,
-			    struct swap_cluster_info *cluster_info,
 			    unsigned long *zeromap)
 {
 	int i;
@@ -2465,8 +2463,6 @@ static void setup_swap_info(struct swap_info_struct *si, int prio,
 				si->avail_lists[i].prio = -si->prio;
 		}
 	}
-	si->swap_map = swap_map;
-	si->cluster_info = cluster_info;
 	si->zeromap = zeromap;
 }
 
@@ -2493,13 +2489,11 @@ static void _enable_swap_info(struct swap_info_struct *si)
 }
 
 static void enable_swap_info(struct swap_info_struct *si, int prio,
-				unsigned char *swap_map,
-				struct swap_cluster_info *cluster_info,
 				unsigned long *zeromap)
 {
 	spin_lock(&swap_lock);
 	spin_lock(&si->lock);
-	setup_swap_info(si, prio, swap_map, cluster_info, zeromap);
+	setup_swap_info(si, prio, zeromap);
 	spin_unlock(&si->lock);
 	spin_unlock(&swap_lock);
 	/*
@@ -2517,7 +2511,7 @@ static void reinsert_swap_info(struct swap_info_struct *si)
 {
 	spin_lock(&swap_lock);
 	spin_lock(&si->lock);
-	setup_swap_info(si, si->prio, si->swap_map, si->cluster_info, si->zeromap);
+	setup_swap_info(si, si->prio, si->zeromap);
 	_enable_swap_info(si);
 	spin_unlock(&si->lock);
 	spin_unlock(&swap_lock);
@@ -2541,13 +2535,13 @@ static void wait_for_allocation(struct swap_info_struct *si)
 	}
 }
 
-static void free_cluster_info(struct swap_cluster_info *cluster_info,
-			      unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
+				   unsigned long max)
 {
-	int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
-
+	int i, nr_clusters = DIV_ROUND_UP(max, SWAPFILE_CLUSTER);
 	if (!cluster_info)
 		return;
+	VM_WARN_ON(!nr_clusters);
 	for (i = 0; i < nr_clusters; i++)
 		cluster_table_free(&cluster_info[i]);
 	kvfree(cluster_info);
@@ -2580,7 +2574,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 	struct swap_info_struct *p = NULL;
 	unsigned char *swap_map;
 	unsigned long *zeromap;
-	struct swap_cluster_info *cluster_info;
 	struct file *swap_file, *victim;
 	struct address_space *mapping;
 	struct inode *inode;
@@ -2687,14 +2680,13 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
 
 	swap_file = p->swap_file;
 	p->swap_file = NULL;
-	p->max = 0;
 	swap_map = p->swap_map;
 	p->swap_map = NULL;
 	zeromap = p->zeromap;
 	p->zeromap = NULL;
-	cluster_info = p->cluster_info;
-	free_cluster_info(cluster_info, p->max);
+	free_swap_cluster_info(p->cluster_info, p->max);
 	p->cluster_info = NULL;
+	p->max = 0;
 	spin_unlock(&p->lock);
 	spin_unlock(&swap_lock);
 	arch_swap_invalidate_area(p->type);
@@ -3067,7 +3059,6 @@ static int setup_swap_map_and_extents(struct swap_info_struct *si,
 
 	if (nr_good_pages) {
 		swap_map[0] = SWAP_MAP_BAD;
-		si->max = maxpages;
 		si->pages = nr_good_pages;
 		nr_extents = setup_swap_extents(si, span);
 		if (nr_extents < 0)
@@ -3089,13 +3080,12 @@ static int setup_swap_map_and_extents(struct swap_info_struct *si,
 #define SWAP_CLUSTER_COLS						\
 	max_t(unsigned int, SWAP_CLUSTER_INFO_COLS, SWAP_CLUSTER_SPACE_COLS)
 
-static struct swap_cluster_info *setup_clusters(struct swap_info_struct *si,
-						union swap_header *swap_header,
-						unsigned long maxpages)
+static int setup_swap_clusters_info(struct swap_info_struct *si,
+				    union swap_header *swap_header,
+				    unsigned long maxpages)
 {
 	unsigned long nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
 	struct swap_cluster_info *cluster_info;
-	int err = -ENOMEM;
 	unsigned long i;
 
 	cluster_info = kvcalloc(nr_clusters, sizeof(*cluster_info), GFP_KERNEL);
@@ -3151,11 +3141,12 @@ static struct swap_cluster_info *setup_clusters(struct swap_info_struct *si,
 			list_add_tail(&ci->list, &si->free_clusters);
 		}
 	}
-	return cluster_info;
+	si->cluster_info = cluster_info;
+	return 0;
 err_free:
-	free_cluster_info(cluster_info, maxpages);
+	free_swap_cluster_info(cluster_info, maxpages);
 err:
-	return ERR_PTR(err);
+	return -ENOMEM;
 }
 
 SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
@@ -3173,7 +3164,6 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
 	unsigned long maxpages;
 	unsigned char *swap_map = NULL;
 	unsigned long *zeromap = NULL;
-	struct swap_cluster_info *cluster_info = NULL;
 	struct folio *folio = NULL;
 	struct inode *inode = NULL;
 	bool inced_nr_rotate_swap = false;
@@ -3241,13 +3231,19 @@ 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);
+	si->max = maxpages;
 	if (unlikely(!maxpages)) {
 		error = -EINVAL;
 		goto bad_swap_unlock_inode;
 	}
 
+	error = setup_swap_clusters_info(si, swap_header, maxpages);
+	if (error)
+		goto bad_swap_unlock_inode;
+
 	/* OK, set up the swap map and apply the bad block list */
 	swap_map = vzalloc(maxpages);
+	si->swap_map = swap_map;
 	if (!swap_map) {
 		error = -ENOMEM;
 		goto bad_swap_unlock_inode;
@@ -3288,13 +3284,6 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
 		inced_nr_rotate_swap = true;
 	}
 
-	cluster_info = setup_clusters(si, swap_header, maxpages);
-	if (IS_ERR(cluster_info)) {
-		error = PTR_ERR(cluster_info);
-		cluster_info = NULL;
-		goto bad_swap_unlock_inode;
-	}
-
 	if ((swap_flags & SWAP_FLAG_DISCARD) &&
 	    si->bdev && bdev_max_discard_sectors(si->bdev)) {
 		/*
@@ -3345,7 +3334,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
 	prio = -1;
 	if (swap_flags & SWAP_FLAG_PREFER)
 		prio = swap_flags & SWAP_FLAG_PRIO_MASK;
-	enable_swap_info(si, prio, swap_map, cluster_info, zeromap);
+	enable_swap_info(si, prio, zeromap);
 
 	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,
@@ -3375,10 +3364,11 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
 	si->swap_file = NULL;
 	si->flags = 0;
 	spin_unlock(&swap_lock);
-	vfree(swap_map);
+	vfree(si->swap_map);
+	si->swap_map = NULL;
+	free_swap_cluster_info(si->cluster_info, si->max);
+	si->cluster_info = NULL;
 	kvfree(zeromap);
-	if (cluster_info)
-		free_cluster_info(cluster_info, maxpages);
 	if (inced_nr_rotate_swap)
 		atomic_dec(&nr_rotate_swap);
 	if (swap_file)
Signature · 1 line
2.49.0
#52 lore raw

PATCH 27/28 mm, swap: use swap table to track swap count

In reply to Kairui Song

From: Kairui Song <[email protected]>

Now all the data is ready, switch to use swap table only.

Introduce a new set of functions based on swap table for manipulating swap counts:

- swap_dup_entry_locked
Increase the swap count of one swap entry. The entry must be allocated by folio_alloc_swap.

- swap_dup_entries
Increase the swap count of a set of swap entries. The entries must be allocated by folio_alloc_swap.

- swap_put_entry_locked (already exist but rewritten) Decrease the swap count of one swap entry. The entry must be allocated by folio_alloc_swap.
This won't free the entries completely if they are bounded to a folio in swap cache, the entries will be freed when swap cache is freed.

- swap_put_entries (already exist but rewritten)
Decrease the swap count of a set of swap entries. The entries must be allocated by folio_alloc_swap.
This won't free the entries completely if they are bounded to a folio in swap cache, the entries will be freed when swap cache is freed.

And use these helper to replace all existing callers. This helps to simplify the count tracking by a lot, and the swap_map is gone.

---
 include/linux/swap.h |  11 +-
 mm/memory.c          |   2 +-
 mm/swap.h            |   9 +-
 mm/swap_state.c      |  19 +-
 mm/swapfile.c        | 671 +++++++++++++++----------------------------
 5 files changed, 253 insertions(+), 459 deletions(-)
include/linux/swap.h +2 -9
index 60b126918399..b9796bb9e7e7 100644
@@ -264,8 +264,7 @@ struct swap_info_struct {
 	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;		/* extent of the swap_map */
-	unsigned char *swap_map;	/* vmalloc'ed array of usage counts */
+	unsigned int	max;		/* size of this swap device */
 	unsigned long *zeromap;		/* kvmalloc'ed bitmap to track zero pages */
 	struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
 	struct list_head free_clusters; /* free clusters list */
@@ -284,7 +283,7 @@ struct swap_info_struct {
 	struct completion comp;		/* seldom referenced */
 	spinlock_t lock;		/*
 					 * protect map scan related fields like
-					 * swap_map, lowest_bit, highest_bit,
+					 * lowest_bit, highest_bit,
 					 * inuse_pages, cluster_next,
 					 * cluster_nr, lowest_alloc,
 					 * highest_alloc, free/discard cluster
@@ -437,7 +436,6 @@ static inline long get_nr_swap_pages(void)
 
 extern void si_swapinfo(struct sysinfo *);
 void put_swap_folio(struct folio *folio, swp_entry_t entry);
-extern int add_swap_count_continuation(swp_entry_t, gfp_t);
 int swap_type_of(dev_t device, sector_t offset);
 int find_first_swap(dev_t *device);
 extern unsigned int count_swap_pages(int, int);
@@ -502,11 +500,6 @@ static inline void free_swap_cache(struct folio *folio)
 {
 }
 
-static inline int add_swap_count_continuation(swp_entry_t swp, gfp_t gfp_mask)
-{
-	return 0;
-}
-
 static inline int do_dup_swap_entry(swp_entry_t ent)
 {
 	return 0;
mm/memory.c +1 -1
index a9a548575e72..a33f860317f5 100644
@@ -1207,7 +1207,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 
 	if (ret == -EIO) {
 		VM_WARN_ON_ONCE(!entry.val);
-		if (add_swap_count_continuation(entry, GFP_KERNEL) < 0) {
+		if (swap_retry_table_alloc(entry, GFP_KERNEL) < 0) {
 			ret = -ENOMEM;
 			goto out;
 		}
mm/swap.h +8 -1
index 7cbfca39225f..228195e54c9d 100644
@@ -39,6 +39,7 @@ struct swap_cluster_info {
 	u8 flags;
 	u8 order;
 	swp_te_t *table;
+	unsigned long *extend_table; /* Only used for extended swap count */
 	struct list_head list;
 };
 
@@ -135,6 +136,8 @@ static inline void swap_unlock_cluster_irq(struct swap_cluster_info *ci)
 	spin_unlock_irq(&ci->lock);
 }
 
+extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
+
 /*
  * All swap entries starts getting allocated by folio_alloc_swap(),
  * and the folio will be added to swap cache.
@@ -198,7 +201,6 @@ extern int __swap_cache_replace_folio(struct swap_cluster_info *ci,
 extern void __swap_cache_override_folio(struct swap_cluster_info *ci,
 					swp_entry_t entry, struct folio *old,
 					struct folio *new);
-extern void __swap_cache_clear_shadow(swp_entry_t entry, int nr_ents);
 
 /*
  * Return the swap device position of the swap entry.
@@ -364,6 +366,11 @@ static inline int swap_writepage(struct page *p, struct writeback_control *wbc)
 	return 0;
 }
 
+static inline int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
+{
+	return -EINVAL;
+}
+
 static inline struct folio *swap_cache_get_folio(swp_entry_t entry)
 {
 	return NULL;
mm/swap_state.c +3 -16
index 2b145c0f7773..b08d26e7dda5 100644
@@ -169,7 +169,7 @@ struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
 			existing = swp_te_folio(exist);
 			goto out_failed;
 		}
-		if (!__swap_count(swp_entry(si->type, offset)))
+		if (!swp_te_get_count(exist))
 			goto out_failed;
 		if (shadow && swp_te_is_shadow(exist))
 			*shadow = swp_te_shadow(exist);
@@ -234,7 +234,7 @@ void __swap_cache_del_folio(swp_entry_t entry,
 		exist = __swap_table_get(ci, offset);
 		VM_WARN_ON_ONCE(swp_te_folio(exist) != folio);
 		__swap_table_set_shadow(ci, offset, shadow);
-		if (__swap_count(swp_entry(si->type, offset)))
+		if (swp_te_get_count(exist))
 			folio_swapped = true;
 		else
 			need_free = true;
@@ -250,7 +250,7 @@ void __swap_cache_del_folio(swp_entry_t entry,
 	} else if (need_free) {
 		offset = start;
 		do {
-			if (!__swap_count(swp_entry(si->type, offset)))
+			if (!swp_te_get_count(__swap_table_get(ci, offset)))
 				__swap_free_entries(si, ci, offset, 1);
 		} while (++offset < end);
 	}
@@ -270,19 +270,6 @@ void *swap_cache_get_shadow(swp_entry_t entry)
 	return swp_te_is_shadow(swp_te) ? swp_te_shadow(swp_te) : NULL;
 }
 
-void __swap_cache_clear_shadow(swp_entry_t entry, int nr_ents)
-{
-	struct swap_cluster_info *ci;
-	pgoff_t offset = swp_offset(entry), end;
-
-	ci = swp_offset_cluster(swp_info(entry), offset);
-	end = offset + nr_ents;
-	do {
-		WARN_ON_ONCE(swp_te_is_folio(__swap_table_get(ci, offset)));
-		__swap_table_set_null(ci, offset);
-	} while (++offset < end);
-}
-
 /*
  * Lookup a swap entry in the swap cache. A found folio will be returned
  * unlocked and with its refcount incremented.
mm/swapfile.c +239 -432

1,010 lines not on this page · raw message

Signature · 1 line
2.49.0
#53 lore raw

PATCH 28/28 mm, swap: implement dynamic allocation of swap table

In reply to Kairui Song

From: Kairui Song <[email protected]>

Now swap table is cluster based, which means free clusters can free its table since no one should modify it.

There could be speculative readers, like swap cache look up, protect them by making them RCU safe. All swap table should be filled with null entries before free, so such readers will either see a NULL pointer or a null filled table being lazy freed.

On allocation, allocate the table when a cluster is used by any order.

This way, we can reduce the memory usage of large swap device significantly.

This idea to dynamically release unused swap cluster data was initially suggested by Chris Li while proposing the cluster swap allocator and I found it suits the swap table idea very well.

---
 mm/swap.h       |   8 +-
 mm/swap_state.c |  11 +--
 mm/swap_table.h |  25 ++++-
 mm/swapfile.c   | 241 +++++++++++++++++++++++++++++++++++-------------
 4 files changed, 213 insertions(+), 72 deletions(-)
mm/swap.h +7 -1
index 228195e54c9d..dfe9fc1552e8 100644
@@ -24,6 +24,12 @@ extern struct swap_info_struct *swap_info[];
  */
 typedef atomic_long_t swp_te_t;
 
+/* A typical flat array as swap table */
+struct swap_table_flat {
+	swp_te_t entries[SWAPFILE_CLUSTER];
+	struct rcu_head rcu;
+};
+
 /*
  * We use this to track usage of a cluster. A cluster is a block of swap disk
  * space with SWAPFILE_CLUSTER pages long and naturally aligns in disk. All
@@ -38,7 +44,7 @@ struct swap_cluster_info {
 	u16 count;
 	u8 flags;
 	u8 order;
-	swp_te_t *table;
+	swp_te_t __rcu *table;
 	unsigned long *extend_table; /* Only used for extended swap count */
 	struct list_head list;
 };
mm/swap_state.c +5 -6
index b08d26e7dda5..dd14e110f273 100644
@@ -163,6 +163,8 @@ struct folio *swap_cache_add_folio(swp_entry_t entry, struct folio *folio,
 	existing = NULL;
 	si = swp_info(entry);
 	ci = swap_lock_cluster(si, offset);
+	if (!ci->table)
+		goto out_failed;
 	do {
 		exist = __swap_table_get(ci, offset);
 		if (unlikely(swp_te_is_folio(exist))) {
@@ -263,10 +265,8 @@ void __swap_cache_del_folio(swp_entry_t entry,
 void *swap_cache_get_shadow(swp_entry_t entry)
 {
 	swp_te_t swp_te;
-
 	pgoff_t offset = swp_offset(entry);
-	swp_te = __swap_table_get(swp_cluster(entry), offset);
-
+	swp_te = swap_table_try_get(swp_cluster(entry), offset);
 	return swp_te_is_shadow(swp_te) ? swp_te_shadow(swp_te) : NULL;
 }
 
@@ -281,8 +281,7 @@ struct folio *swap_cache_get_folio(swp_entry_t entry)
 {
 	swp_te_t swp_te;
 	struct folio *folio;
-	swp_te = __swap_table_get(swp_cluster(entry), swp_offset(entry));
-
+	swp_te = swap_table_try_get(swp_cluster(entry), swp_offset(entry));
 	if (!swp_te_is_folio(swp_te))
 		return NULL;
 
@@ -300,7 +299,7 @@ struct folio *swap_cache_get_folio(swp_entry_t entry)
 bool swap_cache_check_folio(swp_entry_t entry)
 {
 	swp_te_t swp_te;
-	swp_te = __swap_table_get(swp_cluster(entry), swp_offset(entry));
+	swp_te = swap_table_try_get(swp_cluster(entry), swp_offset(entry));
 	return swp_te_is_folio(swp_te);
 }
 
mm/swap_table.h +22 -3
index afb2953d408a..6f0b80fee03c 100644
@@ -2,6 +2,7 @@
 #ifndef _MM_SWAP_TABLE_H
 #define _MM_SWAP_TABLE_H
 
+#include <linux/rcupdate.h>
 #include "swap.h"
 
 /*
@@ -161,13 +162,31 @@ static inline void __swap_table_set(struct swap_cluster_info *ci, pgoff_t off,
 				    swp_te_t swp_te)
 {
 	atomic_long_set(&ci->table[off % SWAPFILE_CLUSTER], swp_te.counter);
+	lockdep_assert_held(&ci->lock);
+	swp_te_t *table = rcu_dereference_protected(ci->table, true);
+	atomic_long_set(&table[off % SWAPFILE_CLUSTER], swp_te.counter);
+}
+
+static inline swp_te_t swap_table_try_get(struct swap_cluster_info *ci, pgoff_t off)
+{
+	swp_te_t swp_te;
+	rcu_read_lock();
+	swp_te_t *table = rcu_dereference_check(ci->table,
+						lockdep_is_held(&ci->lock));
+	if (table)
+		swp_te.counter = atomic_long_read(&table[off % SWAPFILE_CLUSTER]);
+	else
+		swp_te = null_swp_te();
+	rcu_read_unlock();
+	return swp_te;
 }
 
 static inline swp_te_t __swap_table_get(struct swap_cluster_info *ci, pgoff_t off)
 {
-	swp_te_t swp_te = {
-		.counter = atomic_long_read(&ci->table[off % SWAPFILE_CLUSTER])
-	};
+	swp_te_t swp_te;
+	swp_te_t *table = rcu_dereference_check(ci->table,
+						lockdep_is_held(&ci->lock));
+	swp_te.counter = atomic_long_read(&table[off % SWAPFILE_CLUSTER]);
 	return swp_te;
 }
 
mm/swapfile.c +179 -62

457 lines not on this page · raw message

Signature · 1 line
2.49.0
maintainer
#54 lore raw

Re: [PATCH 28/28] mm, swap: implement dynamic allocation of swap table

Quoted 16 lines · Kairui Song

From: Kairui Song <[email protected]>

Now swap table is cluster based, which means free clusters can free its table since no one should modify it.

There could be speculative readers, like swap cache look up, protect them by making them RCU safe. All swap table should be filled with null entries before free, so such readers will either see a NULL pointer or a null filled table being lazy freed.

On allocation, allocate the table when a cluster is used by any order.

This way, we can reduce the memory usage of large swap device significantly.

Quoted 6 lines

This idea to dynamically release unused swap cluster data was initially suggested by Chris Li while proposing the cluster swap allocator and I found it suits the swap table idea very well.

Nice optimization!

However, please correct me if I'm wrong - but we are only dynamically allocating the swap table with this patch. What we are getting here is the dynamic allocation of the swap entries' metadata (through the swap table), which my virtual swap prototype already provides. The cluster metadata struct (struct swap_cluster_info) itself is statically allocated still (at swapon time), correct? That will not work for a large virtual swap space :( So unfortunately, even with this swap table series, swap virtualization is still not trivial - definitely not as trivial as a new swap device type...

Reading your physical swapfile allocator gives me some ideas though - let me build it into my prototype :) I'll send it out once it's ready.

#55 lore raw

Re: [PATCH 28/28] mm, swap: implement dynamic allocation of swap table

Quoted 18 lines · Nhat Pham
Quoted 16 lines · Kairui Song

From: Kairui Song <[email protected]>

Now swap table is cluster based, which means free clusters can free its table since no one should modify it.

There could be speculative readers, like swap cache look up, protect them by making them RCU safe. All swap table should be filled with null entries before free, so such readers will either see a NULL pointer or a null filled table being lazy freed.

On allocation, allocate the table when a cluster is used by any order.

This way, we can reduce the memory usage of large swap device significantly.

Quoted 8 lines
Quoted 6 lines

This idea to dynamically release unused swap cluster data was initially suggested by Chris Li while proposing the cluster swap allocator and I found it suits the swap table idea very well.

Nice optimization!

Thanks!

Quoted 7 lines

However, please correct me if I'm wrong - but we are only dynamically allocating the swap table with this patch. What we are getting here is the dynamic allocation of the swap entries' metadata (through the swap table), which my virtual swap prototype already provides. The cluster metadata struct (struct swap_cluster_info) itself is statically allocated still (at swapon time), correct?

That's true for now, but noticing the static data is much smaller and unified now, and that enables more work in the following ways:
(I didn't include it in the series because it is getting too long already..)

The static data is only 48 bytes per 2M swap space, so for example if you have a 1TB swap device / space, it's only 20M in total, previously it would be at least 768M (could be much higher as I'm only counting swap_map and cgroup array here).
Now the memory overhead is 0.0019% of the swap space.

And the static data is now only an intermediate cluster table, and only used in one place (si->cluster_info), so reallocating is doable now:
Readers of the actual swap table are protected by RCU and won't modifying the cluster metadata, the only updater of cluster metadata is allocation/freeing, and they can be organized in better ways to allow the cluster data to be reallocated.

And due to the low memory overhead of cluster metadata, it's totally acceptable to preallocate a much larger space now, for example we can always preallocate a 4TB space on boot, tha't 80M in total. Might seems not that trivial, but there is another planned series to make the vmalloc space dynamic too, leverage the page table directly, so the 20M per TB overhead can be avoided too. Not sure if it will be needed though, the overhead is so tiny already.

So in summary what I have in mind is we can either:
- Extend the cluster data when it's not enough (or getting fragmented), since the table data is still accessible during the reallocate and copied data is minimal, so it shouldn't be a heavy lifting operation.
- Preallocate a larger amount of cluster data on swapon, the overhead is still very controllable.
- (Once we have a dynamic vmalloc) preallocate a super large space for swap and allocate each page when needed.

These ideas can be somehow combined, or related to each other.

Quoted 8 lines

That will not work for a
large virtual swap space :( So unfortunately, even with this swap table series, swap virtualization is still not trivial - definitely not as trivial as a new swap device type...

Reading your physical swapfile allocator gives me some ideas though - let me build it into my prototype :) I'll send it out once it's ready.

Yeah, a virtual swap is definitely not trivial, instead it's challenging and very important, just like you have demonstrated.
It requires quite some work other than just metadata level things, I never expected it to be just as simple as a "just another swap table entry type" :)
What I meant is that to be done with minimal overhead and better flexibility, swap needs better infrastructures, which this series is working on.