Skip to contentSkip to conversation

Optimize anonymous large folio unmapping

PATCH In review Dev Jain 4 participants 56 messages May 26 – Jul 9

Subsystems (7)
lore
Share & tools
lore

Dev Jain

author
#0 lore raw

v4 cover Optimize anonymous large folio unmapping

Speed up unmapping of anonymous large folios by clearing the ptes, and setting swap ptes, in one go.

The following benchmark (stolen from Barry at [1]) is used to measure the time taken to swapout 256M worth of memory backed by 64K large folios:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>

#define SIZE_MB 256
#define SIZE_BYTES (SIZE_MB * 1024 * 1024)

 int main() {
     void *addr = mmap(NULL, SIZE_BYTES, PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
     if (addr == MAP_FAILED) {
         perror("mmap failed");
         return 1;
     }

     memset(addr, 0, SIZE_BYTES);

     struct timespec start, end;
     clock_gettime(CLOCK_MONOTONIC, &start);

     if (madvise(addr, SIZE_BYTES, MADV_PAGEOUT) != 0) {
         perror("madvise(MADV_PAGEOUT) failed");
         munmap(addr, SIZE_BYTES);
         return 1;
     }

     clock_gettime(CLOCK_MONOTONIC, &end);

     long duration_ns = (end.tv_sec - start.tv_sec) * 1e9 +
                        (end.tv_nsec - start.tv_nsec);
     printf("madvise(MADV_PAGEOUT) took %ld ns (%.3f ms)\n",
            duration_ns, duration_ns / 1e6);

     munmap(addr, SIZE_BYTES);
     return 0;

}

Performance as measured on a Linux VM on Apple M3 (arm64):

Vanilla - Mean: 37401913 ns, std dev: 12%
Patched - Mean: 17420282 ns, std dev: 11%

No regression observed on 4K folios.

Performance as measured on bare metal x86:

Vanilla - mean: 54986286 ns, std dev: 1.5%
Patched - mean: 51930795 ns, std dev: 3%

Interestingly, no obvious improvement is observed on x86, hinting that the benefit lies mainly in the reduction of ptep_get() calls and the reduction of TLB flushes during contpte-unfolding, on arm64.

No regression is observed on 4K folios on x86 too.

---
Applies on mm-unstable (444fc9435e57).

v3->v4:
- Add patch 1 (page->folio conversion), patch 2 (add try_to_unmap_hugetlb_one), patch 6 and 11 (subpage -> page renaming), and patch 10 (refactoring anon unmap first)
- Drop patch 1 of v3, which is now in mm-hotfixes
- Patch 3: do set_ptes() restoration at call site
- Patch 4: rename the function, move to memory.c, rename pte->ptep, pteval->pte
- Patch 7,8: Remove redundant stub, add kerneldoc
- Patch 9: remove helper, do a loop in-line

v2->v3:
Mostly a resend after merge window. Some minor changes:

- Match kerneldoc parameter with function parameter (pte -> ptep)
- Mention change BUG->WARN in patch description
- Rename walk_done -> exit_walk in patch 2
-
v1->v2:
- Keep nr_pages as unsigned long
- Add patch 2
- Rename some functions, make return type bool for functions returning 0/1
- Drop page_vma_mapped_walk_jump - this is implicitly handled
- Drop likely()
- Add folio_dup/put_swap_pages, do subpage -> page
- Shorten the kerneldoc to remove unnecessary information - keep it aligned with analogous functions
- Put clear_pages_anon_exclusive to mm.h
- Some more refactoring in last patch with finish_folio_unmap

Dev Jain (12):
mm/rmap: convert page -> folio for hwpoison checks mm/rmap: Add try_to_unmap_hugetlb_one
mm/rmap: refactor some code around lazyfree folio unmapping mm/memory: Batch set uffd-wp markers during zapping mm/rmap: batch unmap folios belonging to uffd-wp VMAs mm/swap: rename subpage->page in folio_dup_swap/folio_put_swap mm/swapfile: Add batched version of folio_dup_swap mm/swapfile: Add batched version of folio_put_swap mm/rmap: Add batched version of folio_try_share_anon_rmap_pte mm/rmap: refactor anon folio unmap in try_to_unmap_one mm/mprotect: drop 'sub' from page_anon_exclusive_sub_batch mm/rmap: enable batch unmapping of anonymous folios

 include/linux/mm.h        |   4 +
 include/linux/mm_inline.h |  53 -----
 include/linux/rmap.h      |  52 +++--
 mm/internal.h             |  17 ++
 mm/memory.c               |  87 +++++--
 mm/mprotect.c             |  19 +-
 mm/rmap.c                 | 475 ++++++++++++++++++++++++--------------
 mm/shmem.c                |   8 +-
 mm/swap.h                 |  36 ++-
 mm/swapfile.c             |  31 ++-
 10 files changed, 470 insertions(+), 312 deletions(-)
Signature · 1 line
2.34.1
author
#1 lore raw

PATCH v4 1/12 mm/rmap: convert page -> folio for hwpoison checks

If any page of a folio is poisoned, try_to_unmap() is called with the intent to remove any mappings pointing to any part of the folio.

Therefore, an hwpoison check on a single page does not make sense (although everything works out because the memory-failure path sets hwpoison on the head page too). Convert these checks to check whether the folio has at least one hwpoisoned page.

While at it, convert VM_BUG_* to VM_WARN_*.

---
 mm/rmap.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)
mm/rmap.c +5 -4
index fb3c351f8c458..430c91c8fe2ae 100644
@@ -2116,10 +2116,11 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			bool anon = folio_test_anon(folio);
 
 			/*
-			 * The try_to_unmap() is only passed a hugetlb page
-			 * in the case where the hugetlb page is poisoned.
+			 * The try_to_unmap() is only passed a hugetlb folio
+			 * in the case where the hugetlb folio contains a
+			 * poisoned page.
 			 */
-			VM_BUG_ON_PAGE(!PageHWPoison(subpage), subpage);
+			VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);
 			/*
 			 * huge_pmd_unshare may unmap an entire PMD page.
 			 * There is no way of knowing exactly which PMDs may
@@ -2198,7 +2199,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 		/* Update high watermark before we lower rss */
 		update_hiwater_rss(mm);
 
-		if (PageHWPoison(subpage) && (flags & TTU_HWPOISON)) {
+		if (folio_contain_hwpoisoned_page(folio) && (flags & TTU_HWPOISON)) {
 			pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
 			if (folio_test_hugetlb(folio)) {
 				hugetlb_count_sub(folio_nr_pages(folio), mm);
Signature · 1 line
2.34.1
#2 lore raw

Re: [PATCH v4 01/12] mm/rmap: convert page -> folio for hwpoison checks

Quoted 7 lines · Dev Jain

If any page of a folio is poisoned, try_to_unmap() is called with the intent to remove any mappings pointing to any part of the folio.

Therefore, an hwpoison check on a single page does not make sense (although everything works out because the memory-failure path sets hwpoison on the head page too). Convert these checks to check whether the folio has at least one hwpoisoned page.

I think the reasoning is different and should better be described:

1) For hugetlb, we will always get subpage == head page (single PTE -> first PFN).

try_memory_failure_hugetlb()->try_memory_failure_hugetlb()->hugetlb_update_hwpoison() will do the folio_test_set_hwpoison().

folio_test_set_hwpoison() is the same as setting it on the head page.

For hugetlb, subpage==headpage

That's why PageHWPoison(subpage) used to work.

2) try_to_unmap() cannot handle large folios (besides hugetlb folios).

unmap_poisoned_folio() spells that out. That's why subpage==headpage right now.

That's why PageHWPoison(subpage) used to work.

Quoted 14 lines

While at it, convert VM_BUG_* to VM_WARN_*.

---
 mm/rmap.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)
mm/rmap.c +0 -0
index fb3c351f8c458..430c91c8fe2ae 100644
@@ -2116,10 +2116,11 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			bool anon = folio_test_anon(folio);

mm/rmap.c static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma, · line 2123

Quoted 10 lines
mm/rmap.c +4 -3
 
 			/*
-			 * The try_to_unmap() is only passed a hugetlb page
-			 * in the case where the hugetlb page is poisoned.
+			 * The try_to_unmap() is only passed a hugetlb folio
+			 * in the case where the hugetlb folio contains a
+			 * poisoned page.
 			 */
-			VM_BUG_ON_PAGE(!PageHWPoison(subpage), subpage);
+			VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);

I think we could also just use folio_test_hwpoison() here.

mm/rmap.c static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma, · line 2205

Quoted 12 lines
mm/rmap.c +1 -1
 			/*
 			 * huge_pmd_unshare may unmap an entire PMD page.
 			 * There is no way of knowing exactly which PMDs may
@@ -2198,7 +2199,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 		/* Update high watermark before we lower rss */
 		update_hiwater_rss(mm);
 
-		if (PageHWPoison(subpage) && (flags & TTU_HWPOISON)) {
+		if (folio_contain_hwpoisoned_page(folio) && (flags & TTU_HWPOISON)) {
 			pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
 			if (folio_test_hugetlb(folio)) {
 				hugetlb_count_sub(folio_nr_pages(folio), mm);

And here. Which might be cleaner, because folio_contain_hwpoisoned_page() is so far never called on hugetlb folios.

Because folio_set_has_hwpoisoned() is only ever used on THPs, not hugetlb. It would currently work, but the expectation so far is that these primitives are for small folios+thp, not hugetlb.

You could add a comment here, that with TTU_HWPOISON, we only ever expect small folios or hugetlb folios for now.

Signature · 3 lines
Cheers,

David
author
#3 lore raw

Re: [PATCH v4 01/12] mm/rmap: convert page -> folio for hwpoison checks

Quoted 18 lines · David Hildenbrand (Arm)
Quoted 7 lines · Dev Jain

If any page of a folio is poisoned, try_to_unmap() is called with the intent to remove any mappings pointing to any part of the folio.

Therefore, an hwpoison check on a single page does not make sense (although everything works out because the memory-failure path sets hwpoison on the head page too). Convert these checks to check whether the folio has at least one hwpoisoned page.

I think the reasoning is different and should better be described:

1) For hugetlb, we will always get subpage == head page (single PTE -> first PFN).

try_memory_failure_hugetlb()->try_memory_failure_hugetlb()->hugetlb_update_hwpoison() will do the folio_test_set_hwpoison().

folio_test_set_hwpoison() is the same as setting it on the head page.

Quoted 8 lines

For hugetlb, subpage==headpage

That's why PageHWPoison(subpage) used to work.

2) try_to_unmap() cannot handle large folios (besides hugetlb folios).

unmap_poisoned_folio() spells that out. That's why subpage==headpage right now.

Should have mentioned: subpage != headpage for contpte mapped hugetlb folios on arm64.

But I think even in that case, hwpoison is not set on the specific pfn, it is set only on the head page. So I think folio_test_hwpoison() would suffice.

Quoted 54 lines

That's why PageHWPoison(subpage) used to work.

Quoted 14 lines

While at it, convert VM_BUG_* to VM_WARN_*.

---
 mm/rmap.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)
mm/rmap.c +0 -0
index fb3c351f8c458..430c91c8fe2ae 100644
@@ -2116,10 +2116,11 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			bool anon = folio_test_anon(folio);

mm/rmap.c static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma, · line 2123

Quoted 10 lines
mm/rmap.c +4 -3
 
 			/*
-			 * The try_to_unmap() is only passed a hugetlb page
-			 * in the case where the hugetlb page is poisoned.
+			 * The try_to_unmap() is only passed a hugetlb folio
+			 * in the case where the hugetlb folio contains a
+			 * poisoned page.
 			 */
-			VM_BUG_ON_PAGE(!PageHWPoison(subpage), subpage);
+			VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);

I think we could also just use folio_test_hwpoison() here.

mm/rmap.c static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma, · line 2205

Quoted 12 lines
mm/rmap.c +1 -1
 			/*
 			 * huge_pmd_unshare may unmap an entire PMD page.
 			 * There is no way of knowing exactly which PMDs may
@@ -2198,7 +2199,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 		/* Update high watermark before we lower rss */
 		update_hiwater_rss(mm);
 
-		if (PageHWPoison(subpage) && (flags & TTU_HWPOISON)) {
+		if (folio_contain_hwpoisoned_page(folio) && (flags & TTU_HWPOISON)) {
 			pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
 			if (folio_test_hugetlb(folio)) {
 				hugetlb_count_sub(folio_nr_pages(folio), mm);

And here. Which might be cleaner, because folio_contain_hwpoisoned_page() is so far never called on hugetlb folios.

Because folio_set_has_hwpoisoned() is only ever used on THPs, not hugetlb. It would currently work, but the expectation so far is that these primitives are for small folios+thp, not hugetlb.

You could add a comment here, that with TTU_HWPOISON, we only ever expect small folios or hugetlb folios for now.

#4 lore raw

Re: [PATCH v4 01/12] mm/rmap: convert page -> folio for hwpoison checks

Quoted 26 lines · Dev Jain
Quoted 23 lines · David Hildenbrand (Arm)
Quoted 7 lines · Dev Jain

If any page of a folio is poisoned, try_to_unmap() is called with the intent to remove any mappings pointing to any part of the folio.

Therefore, an hwpoison check on a single page does not make sense (although everything works out because the memory-failure path sets hwpoison on the head page too). Convert these checks to check whether the folio has at least one hwpoisoned page.

I think the reasoning is different and should better be described:

1) For hugetlb, we will always get subpage == head page (single PTE -> first PFN).

try_memory_failure_hugetlb()->try_memory_failure_hugetlb()->hugetlb_update_hwpoison() will do the folio_test_set_hwpoison().

folio_test_set_hwpoison() is the same as setting it on the head page.

For hugetlb, subpage==headpage

That's why PageHWPoison(subpage) used to work.

Quoted 6 lines
Quoted 3 lines

2) try_to_unmap() cannot handle large folios (besides hugetlb folios).

unmap_poisoned_folio() spells that out. That's why subpage==headpage right now.

Should have mentioned: subpage != headpage for contpte mapped hugetlb folios on arm64.

No, hugetlb code always walks "logical" PTEs, not physical ones. So it will walk each hugetlb folio only once, and always hit the head page.

That is, for cont-pte/pmd, it will always return the first PTE in the cont-pte group.

See page_vma_mapped_walk()->hugetlb_walk machinery.

Signature · 3 lines
Cheers,

David
author
#5 lore raw

PATCH v4 2/12 mm/rmap: Add try_to_unmap_hugetlb_one

In reply to Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

To understand the correctness of the refactoring, the following points are noted:

1. try_to_unmap() is called for hugetlb folios only when they are hwpoisoned.

2. A hugetlb VMA cannot be mlocked.

3. The pvmw API sets pvmw.pte to the base of the hugetlb folio (pvmw.pmd is NULL).

4. We won't ever process a softleaf entry that encodes a hugetlb folio; hugetlb folios are never swapped out, migration entries will be skipped (PVMW_MIGRATION not passed) and device-exclusive does not work for hugetlb.

5. uffd-wp bit is lost when converting pvmw.pte to hwpoison softleaf (therefore no need to call pte_install_uffd_wp_if_needed after clearing pvmw.pte)

6. TTU_HWPOISON is always present; for it to not be present, either folio has to be in swapcache, or mapping_can_writeback() is true (see unmap_poisoned_folio), none of which is true for hugetlb folios.

7. hugetlb uses separate counters from normal rss counters, therefore update_highwater_rss() need not be called.

While at it:

- Change VM_BUG_* to VM_WARN_*.

- Do not declare variables which are only used once

- Assert that the subpage derived by the pvmw walk is exactly the head page. This is because try_to_unmap() does not remember the specific subpage which was hwpoisoned, and, since we cannot munmap/mremap across a hugetlb folio partially, the first pte mapping the hugetlb folio (in case of a contpte or contpmd mapped folio) cannot ever point to an intermediate page.

---
 mm/rmap.c | 203 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 121 insertions(+), 82 deletions(-)
mm/rmap.c +121 -82
index 430c91c8fe2ae..06ab1158d4cd1 100644
@@ -1978,6 +1978,121 @@ static inline unsigned int folio_unmap_pte_batch(struct folio *folio,
 				     FPB_RESPECT_WRITE | FPB_RESPECT_SOFT_DIRTY);
 }
 
+static bool __try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, struct page_vma_mapped_walk *pvmw,
+		struct mmu_notifier_range *range, enum ttu_flags flags)
+{
+	unsigned long hsz = huge_page_size(hstate_vma(vma));
+	unsigned long address = pvmw->address;
+	struct mm_struct *mm = vma->vm_mm;
+	struct page *subpage;
+	bool ret = true;
+	pte_t pteval;
+
+	if (!page_vma_mapped_walk(pvmw))
+		return true;
+
+	pteval = ptep_get(pvmw->pte);
+	VM_WARN_ON(!pte_present(pteval));
+	subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));
+	VM_WARN_ON(folio_page(folio, 0) != subpage);
+
+	/*
+	 * huge_pmd_unshare may unmap an entire PMD page. There is no way of
+	 * knowing exactly which PMDs may be cached for this mm, so we must
+	 * flush them all. start/end were already adjusted above to cover this
+	 * range.
+	 */
+	flush_cache_range(vma, range->start, range->end);
+
+	/*
+	 * To call huge_pmd_unshare, i_mmap_rwsem must be held in write mode.
+	 * Caller needs to explicitly do this outside rmap routines.
+	 *
+	 * We also must hold hugetlb vma_lock in write mode. Lock order dictates
+	 * acquiring vma_lock BEFORE i_mmap_rwsem. We can only try lock here and
+	 * fail if unsuccessful.
+	 */
+	if (!folio_test_anon(folio)) {
+		struct mmu_gather tlb;
+
+		VM_WARN_ON(!(flags & TTU_RMAP_LOCKED));
+		if (!hugetlb_vma_trylock_write(vma)) {
+			ret = false;
+			goto walk_done;
+		}
+
+		tlb_gather_mmu_vma(&tlb, vma);
+		if (huge_pmd_unshare(&tlb, vma, address, pvmw->pte)) {
+			hugetlb_vma_unlock_write(vma);
+			huge_pmd_unshare_flush(&tlb, vma);
+			tlb_finish_mmu(&tlb);
+			/*
+			 * The PMD table was unmapped, consequently unmapping
+			 * the folio.
+			 */
+			goto walk_done;
+		}
+		hugetlb_vma_unlock_write(vma);
+		tlb_finish_mmu(&tlb);
+	}
+	pteval = huge_ptep_clear_flush(vma, address, pvmw->pte);
+	if (pte_dirty(pteval))
+		folio_mark_dirty(folio);
+
+	VM_WARN_ON(!(flags & TTU_HWPOISON));
+	pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
+	hugetlb_count_sub(folio_nr_pages(folio), mm);
+	set_huge_pte_at(mm, address, pvmw->pte, pteval, hsz);
+	hugetlb_remove_rmap(folio);
+	folio_put_refs(folio, 1);
+
+walk_done:
+	page_vma_mapped_walk_done(pvmw);
+	return ret;
+}
+
+static bool try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, unsigned long address, void *arg)
+{
+	DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
+	struct mmu_notifier_range range;
+	enum ttu_flags flags = (enum ttu_flags)(long)arg;
+	bool ret;
+
+	/*
+	 * The try_to_unmap() is only passed a hugetlb folio in the case
+	 * where the hugetlb folio contains a poisoned page.
+	 */
+	VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);
+
+	/*
+	 * When racing against e.g. zap_pte_range() on another cpu,
+	 * in between its ptep_get_and_clear_full() and folio_remove_rmap_*(),
+	 * try_to_unmap() may return before folio_mapped() has become false,
+	 * if page table locking is skipped: use TTU_SYNC to wait for that.
+	 */
+	if (flags & TTU_SYNC)
+		pvmw.flags = PVMW_SYNC;
+
+	/*
+	 * For hugetlb, it could be much worse than THP if we need pud
+	 * invalidation in the case of pmd sharing.
+	 *
+	 * Note that the folio can not be freed in this function as call of
+	 * try_to_unmap() must hold a reference on the folio.
+	 */
+	range.end = vma_address_end(&pvmw);
+	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
+				address, range.end);
+	adjust_range_if_pmd_sharing_possible(vma, &range.start, &range.end);
+	mmu_notifier_invalidate_range_start(&range);
+	ret = __try_to_unmap_hugetlb_one(folio, vma, &pvmw, &range,
+					 flags);
+	mmu_notifier_invalidate_range_end(&range);
+	return ret;
+}
+
 /*
  * @arg: enum ttu_flags will be passed to this argument
  */
@@ -1993,7 +2108,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 	enum ttu_flags flags = (enum ttu_flags)(long)arg;
 	unsigned long nr_pages = 1, end_addr;
 	unsigned long pfn;
-	unsigned long hsz = 0;
 	int ptes = 0;
 
 	/*
@@ -2007,8 +2121,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 
 	/*
 	 * For THP, we have to assume the worse case ie pmd for invalidation.
-	 * For hugetlb, it could be much worse if we need to do pud
-	 * invalidation in the case of pmd sharing.
 	 *
 	 * Note that the folio can not be freed in this function as call of
 	 * try_to_unmap() must hold a reference on the folio.
@@ -2016,17 +2128,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 	range.end = vma_address_end(&pvmw);
 	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
 				address, range.end);
-	if (folio_test_hugetlb(folio)) {
-		/*
-		 * If sharing is possible, start and end will be adjusted
-		 * accordingly.
-		 */
-		adjust_range_if_pmd_sharing_possible(vma, &range.start,
-						     &range.end);
-
-		/* We need the huge page size for set_huge_pte_at() */
-		hsz = huge_page_size(hstate_vma(vma));
-	}
 	mmu_notifier_invalidate_range_start(&range);
 
 	while (page_vma_mapped_walk(&pvmw)) {
@@ -2104,7 +2205,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			const softleaf_t entry = softleaf_from_pte(pteval);
 
 			pfn = softleaf_to_pfn(entry);
-			VM_WARN_ON_FOLIO(folio_test_hugetlb(folio), folio);
 		}
 
 		subpage = folio_page(folio, pfn - folio_pfn(folio));
@@ -2112,59 +2212,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 		anon_exclusive = folio_test_anon(folio) &&
 				 PageAnonExclusive(subpage);
 
-		if (folio_test_hugetlb(folio)) {
-			bool anon = folio_test_anon(folio);
-
-			/*
-			 * The try_to_unmap() is only passed a hugetlb folio
-			 * in the case where the hugetlb folio contains a
-			 * poisoned page.
-			 */
-			VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);
-			/*
-			 * huge_pmd_unshare may unmap an entire PMD page.
-			 * There is no way of knowing exactly which PMDs may
-			 * be cached for this mm, so we must flush them all.
-			 * start/end were already adjusted above to cover this
-			 * range.
-			 */
-			flush_cache_range(vma, range.start, range.end);
-
-			/*
-			 * To call huge_pmd_unshare, i_mmap_rwsem must be
-			 * held in write mode.  Caller needs to explicitly
-			 * do this outside rmap routines.
-			 *
-			 * We also must hold hugetlb vma_lock in write mode.
-			 * Lock order dictates acquiring vma_lock BEFORE
-			 * i_mmap_rwsem.  We can only try lock here and fail
-			 * if unsuccessful.
-			 */
-			if (!anon) {
-				struct mmu_gather tlb;
-
-				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
-				if (!hugetlb_vma_trylock_write(vma))
-					goto walk_abort;
-
-				tlb_gather_mmu_vma(&tlb, vma);
-				if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
-					hugetlb_vma_unlock_write(vma);
-					huge_pmd_unshare_flush(&tlb, vma);
-					tlb_finish_mmu(&tlb);
-					/*
-					 * The PMD table was unmapped,
-					 * consequently unmapping the folio.
-					 */
-					goto walk_done;
-				}
-				hugetlb_vma_unlock_write(vma);
-				tlb_finish_mmu(&tlb);
-			}
-			pteval = huge_ptep_clear_flush(vma, address, pvmw.pte);
-			if (pte_dirty(pteval))
-				folio_mark_dirty(folio);
-		} else if (likely(pte_present(pteval))) {
+		if (likely(pte_present(pteval))) {
 			nr_pages = folio_unmap_pte_batch(folio, &pvmw, flags, pteval);
 			end_addr = address + nr_pages * PAGE_SIZE;
 			flush_cache_range(vma, address, end_addr);
@@ -2201,14 +2249,8 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 
 		if (folio_contain_hwpoisoned_page(folio) && (flags & TTU_HWPOISON)) {
 			pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
-			if (folio_test_hugetlb(folio)) {
-				hugetlb_count_sub(folio_nr_pages(folio), mm);
-				set_huge_pte_at(mm, address, pvmw.pte, pteval,
-						hsz);
-			} else {
-				dec_mm_counter(mm, mm_counter(folio));
-				set_pte_at(mm, address, pvmw.pte, pteval);
-			}
+			dec_mm_counter(mm, mm_counter(folio));
+			set_pte_at(mm, address, pvmw.pte, pteval);
 		} else if (likely(pte_present(pteval)) && pte_unused(pteval) &&
 			   !userfaultfd_armed(vma)) {
 			/*
@@ -2341,11 +2383,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			add_mm_counter(mm, mm_counter_file(folio), -nr_pages);
 		}
 discard:
-		if (unlikely(folio_test_hugetlb(folio))) {
-			hugetlb_remove_rmap(folio);
-		} else {
-			folio_remove_rmap_ptes(folio, subpage, nr_pages, vma);
-		}
+		folio_remove_rmap_ptes(folio, subpage, nr_pages, vma);
 		if (vma->vm_flags & VM_LOCKED)
 			mlock_drain_local();
 		folio_put_refs(folio, nr_pages);
@@ -2393,7 +2431,8 @@ static int folio_not_mapped(struct folio *folio)
 void try_to_unmap(struct folio *folio, enum ttu_flags flags)
 {
 	struct rmap_walk_control rwc = {
-		.rmap_one = try_to_unmap_one,
+		.rmap_one = folio_test_hugetlb(folio) ?
+				try_to_unmap_hugetlb_one : try_to_unmap_one,
 		.arg = (void *)flags,
 		.done = folio_not_mapped,
 		.anon_lock = folio_lock_anon_vma_read,
Signature · 1 line
2.34.1
#6 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 8 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

To understand the correctness of the refactoring, the following points are noted:

1. try_to_unmap() is called for hugetlb folios only when they are hwpoisoned.

Ack

Quoted 2 lines

2. A hugetlb VMA cannot be mlocked.

Ack

Quoted 3 lines

3. The pvmw API sets pvmw.pte to the base of the hugetlb folio (pvmw.pmd is NULL).

Ack

Quoted 5 lines

4. We won't ever process a softleaf entry that encodes a hugetlb folio; hugetlb folios are never swapped out, migration entries will be skipped (PVMW_MIGRATION not passed) and device-exclusive does not work for hugetlb.

Ack, we should always find present entries.

Quoted 4 lines

5. uffd-wp bit is lost when converting pvmw.pte to hwpoison softleaf (therefore no need to call pte_install_uffd_wp_if_needed after clearing pvmw.pte)

Are you sure? And if so, can you elaborate why + document?

Quoted 4 lines

6. TTU_HWPOISON is always present; for it to not be present, either folio has to be in swapcache, or mapping_can_writeback() is true (see unmap_poisoned_folio), none of which is true for hugetlb folios.

Agreed. That's one very confusing bit in the existing code.

Quoted 3 lines

7. hugetlb uses separate counters from normal rss counters, therefore update_highwater_rss() need not be called.

Ack.

Quoted 5 lines

While at it:

- Change VM_BUG_* to VM_WARN_*.

Quoted 8 lines

- Do not declare variables which are only used once

- Assert that the subpage derived by the pvmw walk is exactly the head page. This is because try_to_unmap() does not remember the specific subpage which was hwpoisoned, and, since we cannot munmap/mremap across a hugetlb folio partially, the first pte mapping the hugetlb folio (in case of a contpte or contpmd mapped folio) cannot ever point to an intermediate page.

Might want to add a Suggested-by:, so people know directly whom to blame :)

I am very much in favor of decoupling both things cleanly, instead of hacking it together and make the code all big and confusing.

Quoted 11 lines
---
 mm/rmap.c | 203 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 121 insertions(+), 82 deletions(-)
mm/rmap.c +0 -0
index 430c91c8fe2ae..06ab1158d4cd1 100644

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1985

Quoted 8 lines
mm/rmap.c +5 -0
@@ -1978,6 +1978,121 @@ static inline unsigned int folio_unmap_pte_batch(struct folio *folio,
 				     FPB_RESPECT_WRITE | FPB_RESPECT_SOFT_DIRTY);
 }
 
+static bool __try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, struct page_vma_mapped_walk *pvmw,
+		struct mmu_notifier_range *range, enum ttu_flags flags)
+{
+	unsigned long hsz = huge_page_size(hstate_vma(vma));

Can be const.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1986

Quoted 1 line
mm/rmap.c +1 -0
+	unsigned long address = pvmw->address;

Can be const. But do we even need the temporary variable?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1988

Quoted 2 lines
mm/rmap.c +2 -0
+	struct mm_struct *mm = vma->vm_mm;
+	struct page *subpage;

a) subpages do not exist. It's just a folio page.
b) subpages are irrelevant for hugetlb, just drop anything that deals with them.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1991

Quoted 3 lines
mm/rmap.c +3 -0
+	bool ret = true;
+	pte_t pteval;
+

Worth adding a comment here, why we don't need a loop.

/* There is only a single mapping in a VMA. */

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1995

Quoted 4 lines
mm/rmap.c +4 -0
+	if (!page_vma_mapped_walk(pvmw))
+		return true;
+
+	pteval = ptep_get(pvmw->pte);

Should we now properly use huge_ptep_get()?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1997

Quoted 2 lines
mm/rmap.c +2 -0
+	VM_WARN_ON(!pte_present(pteval));
+	subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));

No need for subpages.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1998

Quoted 1 line
mm/rmap.c +1 -0
+	VM_WARN_ON(folio_page(folio, 0) != subpage);
	VM_WARN_ON(pte_pfn(pteval) != folio_pfn(folio));

I assume both can be VM_WARN_ON_ONCE()

Quoted 32 lines
mm/rmap.c +32 -0
+
+	/*
+	 * huge_pmd_unshare may unmap an entire PMD page. There is no way of
+	 * knowing exactly which PMDs may be cached for this mm, so we must
+	 * flush them all. start/end were already adjusted above to cover this
+	 * range.
+	 */
+	flush_cache_range(vma, range->start, range->end);
+
+	/*
+	 * To call huge_pmd_unshare, i_mmap_rwsem must be held in write mode.
+	 * Caller needs to explicitly do this outside rmap routines.
+	 *
+	 * We also must hold hugetlb vma_lock in write mode. Lock order dictates
+	 * acquiring vma_lock BEFORE i_mmap_rwsem. We can only try lock here and
+	 * fail if unsuccessful.
+	 */
+	if (!folio_test_anon(folio)) {
+		struct mmu_gather tlb;
+
+		VM_WARN_ON(!(flags & TTU_RMAP_LOCKED));
+		if (!hugetlb_vma_trylock_write(vma)) {
+			ret = false;
+			goto walk_done;
+		}
+
+		tlb_gather_mmu_vma(&tlb, vma);
+		if (huge_pmd_unshare(&tlb, vma, address, pvmw->pte)) {
+			hugetlb_vma_unlock_write(vma);
+			huge_pmd_unshare_flush(&tlb, vma);
+			tlb_finish_mmu(&tlb);
+			/*

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2040

Quoted 10 lines
mm/rmap.c +10 -0
+			 * The PMD table was unmapped, consequently unmapping
+			 * the folio.
+			 */
+			goto walk_done;
+		}
+		hugetlb_vma_unlock_write(vma);
+		tlb_finish_mmu(&tlb);
+	}
+	pteval = huge_ptep_clear_flush(vma, address, pvmw->pte);
+	if (pte_dirty(pteval))

Should we now use huge_pte_dirty() for consistency?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2043

Quoted 3 lines
mm/rmap.c +3 -0
+		folio_mark_dirty(folio);
+
+	VM_WARN_ON(!(flags & TTU_HWPOISON));

This should be checked right at the very beginning of try_to_unmap_hugetlb_one().

Quoted 14 lines
mm/rmap.c +14 -0
+	pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
+	hugetlb_count_sub(folio_nr_pages(folio), mm);
+	set_huge_pte_at(mm, address, pvmw->pte, pteval, hsz);
+	hugetlb_remove_rmap(folio);
+	folio_put_refs(folio, 1);
+
+walk_done:
+	page_vma_mapped_walk_done(pvmw);
+	return ret;
+}
+
+static bool try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, unsigned long address, void *arg)
+{

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2067

Quoted 10 lines
mm/rmap.c +10 -0
+	DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
+	struct mmu_notifier_range range;
+	enum ttu_flags flags = (enum ttu_flags)(long)arg;
+	bool ret;
+
+	/*
+	 * The try_to_unmap() is only passed a hugetlb folio in the case
+	 * where the hugetlb folio contains a poisoned page.
+	 */
+	VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);

As discussed, I'd prefer to just have folio_test_hwpoison() here.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2074

Quoted 7 lines
mm/rmap.c +7 -0
+
+	/*
+	 * When racing against e.g. zap_pte_range() on another cpu,
+	 * in between its ptep_get_and_clear_full() and folio_remove_rmap_*(),
+	 * try_to_unmap() may return before folio_mapped() has become false,
+	 * if page table locking is skipped: use TTU_SYNC to wait for that.
+	 */

The comment should be updated to mention hugetlb primitives ... but

... it's a good question whether that is required *at all* ...

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2076

Quoted 2 lines
mm/rmap.c +2 -0
+	if (flags & TTU_SYNC)
+		pvmw.flags = PVMW_SYNC;

... and in fact, looking at page_vma_mapped_walk(), PVMW_SYNC is irrelevant.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2080

Quoted 4 lines
mm/rmap.c +4 -0
+
+	/*
+	 * For hugetlb, it could be much worse than THP if we need pud
+	 * invalidation in the case of pmd sharing.

I don't understand that comment. Is it really helpful?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2082

Quoted 2 lines
mm/rmap.c +2 -0
+	 *
+	 * Note that the folio can not be freed in this function as call of

"cannot" ?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2084

Quoted 2 lines
mm/rmap.c +2 -0
+	 * try_to_unmap() must hold a reference on the folio.
+	 */

Do we really need this comment entirely? We don't even look at the folio after unmapping it.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2091

Quoted 7 lines
mm/rmap.c +7 -0
+	range.end = vma_address_end(&pvmw);
+	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
+				address, range.end);
+	adjust_range_if_pmd_sharing_possible(vma, &range.start, &range.end);
+	mmu_notifier_invalidate_range_start(&range);
+	ret = __try_to_unmap_hugetlb_one(folio, vma, &pvmw, &range,
+					 flags);

Is it really worth it moving that stuff to a separate function?

Quoted 4 lines
mm/rmap.c +4 -0
+	mmu_notifier_invalidate_range_end(&range);
+	return ret;
+}
+
Signature · 3 lines
Cheers,

David
reviewer
#7 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

On Tue, Jun 09, 2026 at 04:16:47PM +0200, David Hildenbrand (Arm) wrote:
[...]

Quoted 36 lines
Quoted 9 lines
---
 mm/rmap.c | 203 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 121 insertions(+), 82 deletions(-)
mm/rmap.c +0 -0
index 430c91c8fe2ae..06ab1158d4cd1 100644

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1985

Quoted 8 lines
mm/rmap.c +5 -0
@@ -1978,6 +1978,121 @@ static inline unsigned int folio_unmap_pte_batch(struct folio *folio,
 				     FPB_RESPECT_WRITE | FPB_RESPECT_SOFT_DIRTY);
 }
 
+static bool __try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, struct page_vma_mapped_walk *pvmw,
+		struct mmu_notifier_range *range, enum ttu_flags flags)
+{
+	unsigned long hsz = huge_page_size(hstate_vma(vma));

Can be const.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1986

Quoted 1 line
mm/rmap.c +1 -0
+	unsigned long address = pvmw->address;

Can be const. But do we even need the temporary variable?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1988

Quoted 2 lines
mm/rmap.c +2 -0
+	struct mm_struct *mm = vma->vm_mm;
+	struct page *subpage;

a) subpages do not exist. It's just a folio page.
b) subpages are irrelevant for hugetlb, just drop anything that deals with them.

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1991

Quoted 3 lines
mm/rmap.c +3 -0
+	bool ret = true;
+	pte_t pteval;
+

Worth adding a comment here, why we don't need a loop.

Quoted 8 lines

/* There is only a single mapping in a VMA. */

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1995

Quoted 4 lines
mm/rmap.c +4 -0
+	if (!page_vma_mapped_walk(pvmw))
+		return true;
+
+	pteval = ptep_get(pvmw->pte);

Should we now properly use huge_ptep_get()?

Right. IIUC, on s390, the hugetlb entry stored in the page table is not in the normal PTE layout. set_huge_pte_at() stores the arch huge-entry format there, and huge_ptep_get() is the helper that decodes it back into a normal pte_t.

pte_t huge_ptep_get(struct mm_struct *mm, unsigned long addr, pte_t *ptep)

{
	return __rste_to_pte(pte_val(*ptep));
}
Quoted 4 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1997

Quoted 2 lines
mm/rmap.c +2 -0
+	VM_WARN_ON(!pte_present(pteval));
+	subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));

No need for subpages.

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Cheers, Lance

[...]

#8 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 10 lines
	return __rste_to_pte(pte_val(*ptep));
}
Quoted 4 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1997

Quoted 2 lines
mm/rmap.c +2 -0
+	VM_WARN_ON(!pte_present(pteval));
+	subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));

No need for subpages.

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

Signature · 3 lines
Cheers,

David
reviewer
#9 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 8 lines · David Hildenbrand (Arm)
Quoted 8 lines
	return __rste_to_pte(pte_val(*ptep));
}
Quoted 4 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1997

Quoted 2 lines
mm/rmap.c +2 -0
+	VM_WARN_ON(!pte_present(pteval));
+	subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));

No need for subpages.

Quoted 4 lines
Quoted 2 lines

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

The subpage check can warn, but we still pass that subpage to make_hwpoison_entry(). So the hwpoison marker can end up with the wrong PFN?

+ subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));
+ VM_WARN_ON(folio_page(folio, 0) != subpage);
[...]
+ pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));

#10 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 12 lines · Lance Yang
Quoted 8 lines · David Hildenbrand (Arm)
Quoted 6 lines

    return __rste_to_pte(pte_val(*ptep));
}

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

Quoted 8 lines

The subpage check can warn, but we still pass that subpage to make_hwpoison_entry(). So the hwpoison marker can end up with the wrong PFN?

+    subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio)); +    VM_WARN_ON(folio_page(folio, 0) != subpage);
[...]
+    pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));

My s390x page table knowledge is a bit rusty.

IIUC, it would be a problem if some PTE bits in segment/region entries (pmd/pud/ ...) would pass the

	 pte_pfn(x) -> (pte_val(x) >> PAGE_SHIFT)

check. I don't think this applies, because

While

	#define _SEGMENT_ENTRY_ORIGIN_LARGE ~0xfffffUL

We also have

	#define _SEGMENT_ENTRY_ORIGIN ~0x7ffUL

So these bits are not actually used.

What __rste_to_pte() primarily does is reshuffling present bits etc.

So using any other bits besides the PFN would be problematic I guess.

Am I wrong or isn't the present bit already at a different location? For prot-none hugetlb folios there might be a real issue, as the PTE present bit corresponds to the PMD/PUD read-permission bit.

Oh my :)

So yeah, we should probably fix that ahead of time unless I am missing something? Good that we separate that hugetlb crap out.

Signature · 3 lines
Cheers,

David
reviewer
#11 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 49 lines · David Hildenbrand (Arm)
Quoted 12 lines · Lance Yang
Quoted 8 lines · David Hildenbrand (Arm)
Quoted 6 lines

    return __rste_to_pte(pte_val(*ptep));
}

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

Quoted 8 lines

The subpage check can warn, but we still pass that subpage to make_hwpoison_entry(). So the hwpoison marker can end up with the wrong PFN?

+    subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio)); +    VM_WARN_ON(folio_page(folio, 0) != subpage);
[...]
+    pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));

My s390x page table knowledge is a bit rusty.

IIUC, it would be a problem if some PTE bits in segment/region entries (pmd/pud/ ...) would pass the

	 pte_pfn(x) -> (pte_val(x) >> PAGE_SHIFT)

check. I don't think this applies, because

While

	#define _SEGMENT_ENTRY_ORIGIN_LARGE ~0xfffffUL

We also have

	#define _SEGMENT_ENTRY_ORIGIN ~0x7ffUL

So these bits are not actually used.

What __rste_to_pte() primarily does is reshuffling present bits etc.

So using any other bits besides the PFN would be problematic I guess.

Am I wrong or isn't the present bit already at a different location? For prot-none hugetlb folios there might be a real issue, as the PTE present bit corresponds to the PMD/PUD read-permission bit.

Quoted 4 lines

Oh my :)

So yeah, we should probably fix that ahead of time unless I am missing something? Good that we separate that hugetlb crap out.

Yeah, looks like this was already there before the split. Should this be fixed separately?

author
#12 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 52 lines · Lance Yang
Quoted 49 lines · David Hildenbrand (Arm)
Quoted 12 lines · Lance Yang
Quoted 8 lines · David Hildenbrand (Arm)
Quoted 6 lines

     return __rste_to_pte(pte_val(*ptep));
}

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

Quoted 8 lines

The subpage check can warn, but we still pass that subpage to make_hwpoison_entry(). So the hwpoison marker can end up with the wrong PFN?

+    subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio)); +    VM_WARN_ON(folio_page(folio, 0) != subpage);
[...]
+    pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));

My s390x page table knowledge is a bit rusty.

IIUC, it would be a problem if some PTE bits in segment/region entries (pmd/pud/ ...) would pass the

     pte_pfn(x) -> (pte_val(x) >> PAGE_SHIFT)

check. I don't think this applies, because

While
    #define _SEGMENT_ENTRY_ORIGIN_LARGE ~0xfffffUL

We also have

    #define _SEGMENT_ENTRY_ORIGIN ~0x7ffUL

So these bits are not actually used.

What __rste_to_pte() primarily does is reshuffling present bits etc.

So using any other bits besides the PFN would be problematic I guess.

Am I wrong or isn't the present bit already at a different location? For prot-none hugetlb folios there might be a real issue, as the PTE present bit corresponds to the PMD/PUD read-permission bit.

Quoted 7 lines
Quoted 4 lines

Oh my :)

So yeah, we should probably fix that ahead of time unless I am missing something? Good that we separate that hugetlb crap out.

Yeah, looks like this was already there before the split. Should this be fixed separately?

Same bug is there in try_to_migrate_one(), check_pte(), remove_migration_pte() and prot_none_hugetlb_entry() :)

reviewer
#13 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 57 lines · Dev Jain
Quoted 54 lines · Lance Yang
Quoted 51 lines · David Hildenbrand (Arm)
Quoted 12 lines · Lance Yang
Quoted 8 lines · David Hildenbrand (Arm)
Quoted 6 lines

     return __rste_to_pte(pte_val(*ptep));
}

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

Quoted 8 lines

The subpage check can warn, but we still pass that subpage to make_hwpoison_entry(). So the hwpoison marker can end up with the wrong PFN?

+    subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio)); +    VM_WARN_ON(folio_page(folio, 0) != subpage);
[...]
+    pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));

My s390x page table knowledge is a bit rusty.

IIUC, it would be a problem if some PTE bits in segment/region entries (pmd/pud/ ...) would pass the

     pte_pfn(x) -> (pte_val(x) >> PAGE_SHIFT)

check. I don't think this applies, because

While
    #define _SEGMENT_ENTRY_ORIGIN_LARGE ~0xfffffUL

We also have

    #define _SEGMENT_ENTRY_ORIGIN ~0x7ffUL

So these bits are not actually used.

What __rste_to_pte() primarily does is reshuffling present bits etc.

So using any other bits besides the PFN would be problematic I guess.

Am I wrong or isn't the present bit already at a different location? For prot-none hugetlb folios there might be a real issue, as the PTE present bit corresponds to the PMD/PUD read-permission bit.

Oh my :)

Quoted 8 lines
Quoted 5 lines
Quoted 2 lines

So yeah, we should probably fix that ahead of time unless I am missing something? Good that we separate that hugetlb crap out.

Yeah, looks like this was already there before the split. Should this be fixed separately?

Same bug is there in try_to_migrate_one(), check_pte(), remove_migration_pte() and prot_none_hugetlb_entry() :)

Oh my :)

author
#14 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

In reply to Dev Jain
Quoted 57 lines · Dev Jain
Quoted 54 lines · Lance Yang
Quoted 51 lines · David Hildenbrand (Arm)
Quoted 12 lines · Lance Yang
Quoted 8 lines · David Hildenbrand (Arm)
Quoted 6 lines

     return __rste_to_pte(pte_val(*ptep));
}

So plain ptep_get() can feed raw huge-entry bits into pte_pfn(), and the derived subpage can be wrong.

Good question which impact that might have in practice?

Quoted 8 lines

The subpage check can warn, but we still pass that subpage to make_hwpoison_entry(). So the hwpoison marker can end up with the wrong PFN?

+    subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio)); +    VM_WARN_ON(folio_page(folio, 0) != subpage);
[...]
+    pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));

My s390x page table knowledge is a bit rusty.

IIUC, it would be a problem if some PTE bits in segment/region entries (pmd/pud/ ...) would pass the

     pte_pfn(x) -> (pte_val(x) >> PAGE_SHIFT)

check. I don't think this applies, because

While
    #define _SEGMENT_ENTRY_ORIGIN_LARGE ~0xfffffUL

We also have

    #define _SEGMENT_ENTRY_ORIGIN ~0x7ffUL

So these bits are not actually used.

What __rste_to_pte() primarily does is reshuffling present bits etc.

So using any other bits besides the PFN would be problematic I guess.

Am I wrong or isn't the present bit already at a different location? For prot-none hugetlb folios there might be a real issue, as the PTE present bit corresponds to the PMD/PUD read-permission bit.

Oh my :)

Quoted 9 lines
Quoted 5 lines
Quoted 2 lines

So yeah, we should probably fix that ahead of time unless I am missing something? Good that we separate that hugetlb crap out.

Yeah, looks like this was already there before the split. Should this be fixed separately?

Same bug is there in try_to_migrate_one(), check_pte(), remove_migration_pte() and prot_none_hugetlb_entry() :)

Lemme send a series for them. Main thing is identifying the fixes tag really.

Quoted 2 lines
#15 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 9 lines · Dev Jain
Quoted 6 lines · Dev Jain
Quoted 3 lines · Lance Yang
Quoted 8 lines
Quoted 6 lines
Quoted 2 lines

Yeah, looks like this was already there before the split. Should this be fixed separately?

Same bug is there in try_to_migrate_one(), check_pte(), remove_migration_pte() and prot_none_hugetlb_entry() :)

Lemme send a series for them. Main thing is identifying the fixes tag really.

Thanks!

Signature · 3 lines
Cheers,

David
author
#16 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 13 lines · David Hildenbrand (Arm)
Quoted 12 lines · Dev Jain
Quoted 9 lines · Dev Jain
Quoted 5 lines · Lance Yang

Yeah, looks like this was already there before the split. Should this be fixed separately?

Quoted 7 lines
Quoted 5 lines
Quoted 3 lines

Same bug is there in try_to_migrate_one(), check_pte(), remove_migration_pte() and prot_none_hugetlb_entry() :)

Lemme send a series for them. Main thing is identifying the fixes tag really.

Thanks!

I am confused w.r.t backport. So currently we just have to do this:

- /*

-                * Handle PFN swap PTEs, such as device-exclusive ones, that
-                * actually map pages.
-                */
-               pteval = ptep_get(pvmw.pte);
+               address = pvmw.address;
+               if (folio_test_hugetlb(folio)) {
+                       pteval = huge_ptep_get(mm, address, pvmw.pte);
+               } else {
+                       /*
+                        * Handle PFN swap PTEs, such as device-exclusive ones,
+                        * that actually map pages.
+                        */

+ pteval = ptep_get(pvmw.pte);
+ }

At commit c7ab0d2fdc84, try_to_unmap_one() was converted to use the pvmw API. The code was:

	while (page_vma_mapped_walk(&pvmw)) {
		subpage = page - page_to_pfn(page) + pte_pfn(*pvmw.pte);

Doing a plain dereference. Before this commit, the code was:

pte_t *__page_check_address(struct page *page, struct mm_struct *mm,
unsigned long address, spinlock_t **ptlp, int sync)

{
	pmd_t *pmd;
	pte_t *pte;
	spinlock_t *ptl;

	if (unlikely(PageHuge(page))) {
		/* when pud is not present, pte will be NULL */
		pte = huge_pte_offset(mm, address);
		if (!pte)
			return NULL;

		ptl = huge_pte_lockptr(page_hstate(page), mm, pte);
		goto check;
	}

	pmd = mm_find_pmd(mm, address);
	if (!pmd)
		return NULL;

	pte = pte_offset_map(pmd, address);
	/* Make a quick check before getting the lock */
	if (!sync && !pte_present(*pte)) {
		pte_unmap(pte);
		return NULL;
	}

	ptl = pte_lockptr(mm, pmd);
check:
	spin_lock(ptl);
	if (pte_present(*pte) && page_to_pfn(page) == pte_pfn(*pte)) {
		*ptlp = ptl;
		return pte;
	}
	pte_unmap_unlock(pte, ptl);
	return NULL;
}

This does pte_pfn(*pte), a plain dereference. I am not sure how back I need to go for the backport and do I need to post multiple patches for different stable versions.

And, it is crazy that the bug has stayed for this long and testing didn't find this. Perhaps no one bothered running hugetlb-read-hwpoison on PPC or s390. And for some reason those tests are in a category of "destructive" in run_vmtests.sh, requiring a command line option - and even that option was fixed just now in 3432cbb291aa!

Quoted 1 line
reviewer
#17 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 92 lines · Dev Jain
Quoted 13 lines · David Hildenbrand (Arm)
Quoted 12 lines · Dev Jain
Quoted 9 lines · Dev Jain
Quoted 5 lines · Lance Yang

Yeah, looks like this was already there before the split. Should this be fixed separately?

Quoted 7 lines
Quoted 5 lines
Quoted 3 lines

Same bug is there in try_to_migrate_one(), check_pte(), remove_migration_pte() and prot_none_hugetlb_entry() :)

Lemme send a series for them. Main thing is identifying the fixes tag really.

Thanks!

I am confused w.r.t backport. So currently we just have to do this:

(quoted diff) +10 -5
-               /*
-                * Handle PFN swap PTEs, such as device-exclusive ones, that
-                * actually map pages.
-                */
-               pteval = ptep_get(pvmw.pte);
+               address = pvmw.address;
+               if (folio_test_hugetlb(folio)) {
+                       pteval = huge_ptep_get(mm, address, pvmw.pte);
+               } else {
+                       /*
+                        * Handle PFN swap PTEs, such as device-exclusive ones,
+                        * that actually map pages.
+                        */
+                       pteval = ptep_get(pvmw.pte);
+               }

At commit c7ab0d2fdc84, try_to_unmap_one() was converted to use the pvmw API. The code was:

	while (page_vma_mapped_walk(&pvmw)) {
		subpage = page - page_to_pfn(page) + pte_pfn(*pvmw.pte);

Doing a plain dereference. Before this commit, the code was:

pte_t *__page_check_address(struct page *page, struct mm_struct *mm,
unsigned long address, spinlock_t **ptlp, int sync)

{
	pmd_t *pmd;
	pte_t *pte;
	spinlock_t *ptl;

	if (unlikely(PageHuge(page))) {
		/* when pud is not present, pte will be NULL */
		pte = huge_pte_offset(mm, address);
		if (!pte)
			return NULL;

		ptl = huge_pte_lockptr(page_hstate(page), mm, pte);
		goto check;
	}

	pmd = mm_find_pmd(mm, address);
	if (!pmd)
		return NULL;

	pte = pte_offset_map(pmd, address);
	/* Make a quick check before getting the lock */
	if (!sync && !pte_present(*pte)) {
		pte_unmap(pte);
		return NULL;
	}

	ptl = pte_lockptr(mm, pmd);
check:
	spin_lock(ptl);
	if (pte_present(*pte) && page_to_pfn(page) == pte_pfn(*pte)) {
		*ptlp = ptl;
		return pte;
	}
	pte_unmap_unlock(pte, ptl);
	return NULL;
}
Quoted 3 lines

This does pte_pfn(*pte), a plain dereference. I am not sure how back I need to go for the backport and do I need to post multiple patches for different stable versions.

Yeah ... looks old old old, not just c7ab0d2fdc84.

Stable backport looks kinda messy though. Mainline can probably stay as one clean series, but stable might needs separate review per tree. Going to be a pain ...

Quoted 4 lines

And, it is crazy that the bug has stayed for this long and testing didn't find this. Perhaps no one bothered running hugetlb-read-hwpoison on PPC or s390. And for some reason those tests are in a category of "destructive" in run_vmtests.sh, requiring a command line option - and even that option was fixed just now in 3432cbb291aa!

Sigh ...

author
#18 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

In reply to David Hildenbrand (Arm)
Quoted 29 lines · David Hildenbrand (Arm)
Quoted 8 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

To understand the correctness of the refactoring, the following points are noted:

1. try_to_unmap() is called for hugetlb folios only when they are hwpoisoned.

Ack

Quoted 2 lines

2. A hugetlb VMA cannot be mlocked.

Ack

Quoted 3 lines

3. The pvmw API sets pvmw.pte to the base of the hugetlb folio (pvmw.pmd is NULL).

Ack

Quoted 5 lines

4. We won't ever process a softleaf entry that encodes a hugetlb folio; hugetlb folios are never swapped out, migration entries will be skipped (PVMW_MIGRATION not passed) and device-exclusive does not work for hugetlb.

Quoted 8 lines

Ack, we should always find present entries.

Quoted 4 lines

5. uffd-wp bit is lost when converting pvmw.pte to hwpoison softleaf (therefore no need to call pte_install_uffd_wp_if_needed after clearing pvmw.pte)

Are you sure? And if so, can you elaborate why + document?

If you see the current code, we construct pteval from the subpage, losing any bit state of the previous entry. Also it doesn't make sense anyways to keep uffd-wp for an hwpoisoned entry.

Quoted 22 lines
Quoted 6 lines

6. TTU_HWPOISON is always present; for it to not be present, either folio has to be in swapcache, or mapping_can_writeback() is true (see unmap_poisoned_folio), none of which is true for hugetlb folios.

Agreed. That's one very confusing bit in the existing code.

Quoted 3 lines

7. hugetlb uses separate counters from normal rss counters, therefore update_highwater_rss() need not be called.

Ack.

Quoted 7 lines

While at it:

- Change VM_BUG_* to VM_WARN_*.

- Do not declare variables which are only used once

Quoted 8 lines
Quoted 6 lines

- Assert that the subpage derived by the pvmw walk is exactly the head page. This is because try_to_unmap() does not remember the specific subpage which was hwpoisoned, and, since we cannot munmap/mremap across a hugetlb folio partially, the first pte mapping the hugetlb folio (in case of a contpte or contpmd mapped folio) cannot ever point to an intermediate page.

Might want to add a Suggested-by:, so people know directly whom to blame :)

Sure.

Quoted 18 lines

I am very much in favor of decoupling both things cleanly, instead of hacking it together and make the code all big and confusing.

Quoted 14 lines
---
 mm/rmap.c | 203 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 121 insertions(+), 82 deletions(-)
mm/rmap.c +0 -0
index 430c91c8fe2ae..06ab1158d4cd1 100644
@@ -1978,6 +1978,121 @@ static inline unsigned int folio_unmap_pte_batch(struct folio *folio,
 				     FPB_RESPECT_WRITE | FPB_RESPECT_SOFT_DIRTY);
 }
 
Quoted 7 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1985

Quoted 5 lines
mm/rmap.c +5 -0
+static bool __try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, struct page_vma_mapped_walk *pvmw,
+		struct mmu_notifier_range *range, enum ttu_flags flags)
+{
+	unsigned long hsz = huge_page_size(hstate_vma(vma));

Can be const.

Ok.

Quoted 4 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1986

Quoted 2 lines
mm/rmap.c +1 -0
+	unsigned long address = pvmw->address;

Can be const. But do we even need the temporary variable?

It is being used 3 times in this function, so temporary variable is fine.

Quoted 6 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1988

Quoted 3 lines
mm/rmap.c +2 -0
+	struct mm_struct *mm = vma->vm_mm;
+	struct page *subpage;

a) subpages do not exist. It's just a folio page.
b) subpages are irrelevant for hugetlb, just drop anything that deals with them.

Ok.

Quoted 8 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1991

Quoted 4 lines
mm/rmap.c +3 -0
+	bool ret = true;
+	pte_t pteval;
+

Worth adding a comment here, why we don't need a loop.

/* There is only a single mapping in a VMA. */

Ok.

Quoted 7 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1995

Quoted 5 lines
mm/rmap.c +4 -0
+	if (!page_vma_mapped_walk(pvmw))
+		return true;
+
+	pteval = ptep_get(pvmw->pte);

Should we now properly use huge_ptep_get()?

Sending a series to fix this soon : )

Quoted 5 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1997

Quoted 3 lines
mm/rmap.c +2 -0
+	VM_WARN_ON(!pte_present(pteval));
+	subpage = folio_page(folio, pte_pfn(pteval) - folio_pfn(folio));

No need for subpages.

Ok.

Quoted 7 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 1998

Quoted 2 lines
mm/rmap.c +1 -0
+	VM_WARN_ON(folio_page(folio, 0) != subpage);
	VM_WARN_ON(pte_pfn(pteval) != folio_pfn(folio));

I assume both can be VM_WARN_ON_ONCE()

Ok.

Quoted 37 lines
Quoted 37 lines
mm/rmap.c +36 -0
+
+	/*
+	 * huge_pmd_unshare may unmap an entire PMD page. There is no way of
+	 * knowing exactly which PMDs may be cached for this mm, so we must
+	 * flush them all. start/end were already adjusted above to cover this
+	 * range.
+	 */
+	flush_cache_range(vma, range->start, range->end);
+
+	/*
+	 * To call huge_pmd_unshare, i_mmap_rwsem must be held in write mode.
+	 * Caller needs to explicitly do this outside rmap routines.
+	 *
+	 * We also must hold hugetlb vma_lock in write mode. Lock order dictates
+	 * acquiring vma_lock BEFORE i_mmap_rwsem. We can only try lock here and
+	 * fail if unsuccessful.
+	 */
+	if (!folio_test_anon(folio)) {
+		struct mmu_gather tlb;
+
+		VM_WARN_ON(!(flags & TTU_RMAP_LOCKED));
+		if (!hugetlb_vma_trylock_write(vma)) {
+			ret = false;
+			goto walk_done;
+		}
+
+		tlb_gather_mmu_vma(&tlb, vma);
+		if (huge_pmd_unshare(&tlb, vma, address, pvmw->pte)) {
+			hugetlb_vma_unlock_write(vma);
+			huge_pmd_unshare_flush(&tlb, vma);
+			tlb_finish_mmu(&tlb);
+			/*
+			 * The PMD table was unmapped, consequently unmapping
+			 * the folio.
+			 */
+			goto walk_done;
Quoted 8 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2040

Quoted 6 lines
mm/rmap.c +6 -0
+		}
+		hugetlb_vma_unlock_write(vma);
+		tlb_finish_mmu(&tlb);
+	}
+	pteval = huge_ptep_clear_flush(vma, address, pvmw->pte);
+	if (pte_dirty(pteval))

Should we now use huge_pte_dirty() for consistency?

Ok.

Quoted 6 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2043

Quoted 4 lines
mm/rmap.c +3 -0
+		folio_mark_dirty(folio);
+
+	VM_WARN_ON(!(flags & TTU_HWPOISON));

This should be checked right at the very beginning of try_to_unmap_hugetlb_one().

Ok.

Quoted 19 lines
Quoted 19 lines
mm/rmap.c +18 -0
+	pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
+	hugetlb_count_sub(folio_nr_pages(folio), mm);
+	set_huge_pte_at(mm, address, pvmw->pte, pteval, hsz);
+	hugetlb_remove_rmap(folio);
+	folio_put_refs(folio, 1);
+
+walk_done:
+	page_vma_mapped_walk_done(pvmw);
+	return ret;
+}
+
+static bool try_to_unmap_hugetlb_one(struct folio *folio,
+		struct vm_area_struct *vma, unsigned long address, void *arg)
+{
+	DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
+	struct mmu_notifier_range range;
+	enum ttu_flags flags = (enum ttu_flags)(long)arg;
+	bool ret;
Quoted 8 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2067

Quoted 6 lines
mm/rmap.c +6 -0
+
+	/*
+	 * The try_to_unmap() is only passed a hugetlb folio in the case
+	 * where the hugetlb folio contains a poisoned page.
+	 */
+	VM_WARN_ON_FOLIO(!folio_contain_hwpoisoned_page(folio), folio);

As discussed, I'd prefer to just have folio_test_hwpoison() here.

Ok.

Quoted 11 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2074

Quoted 8 lines
mm/rmap.c +7 -0
+
+	/*
+	 * When racing against e.g. zap_pte_range() on another cpu,
+	 * in between its ptep_get_and_clear_full() and folio_remove_rmap_*(),
+	 * try_to_unmap() may return before folio_mapped() has become false,
+	 * if page table locking is skipped: use TTU_SYNC to wait for that.
+	 */

The comment should be updated to mention hugetlb primitives ... but

Quoted 6 lines

... it's a good question whether that is required *at all* ...

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2076

Quoted 2 lines
mm/rmap.c +2 -0
+	if (flags & TTU_SYNC)
+		pvmw.flags = PVMW_SYNC;

... and in fact, looking at page_vma_mapped_walk(), PVMW_SYNC is irrelevant.

You are right, I'll drop this.

Quoted 13 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2080

Quoted 5 lines
mm/rmap.c +4 -0
+
+	/*
+	 * For hugetlb, it could be much worse than THP if we need pud
+	 * invalidation in the case of pmd sharing.

I don't understand that comment. Is it really helpful?

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2082

Quoted 2 lines
mm/rmap.c +2 -0
+	 *
+	 * Note that the folio can not be freed in this function as call of

"cannot" ?

Quoted 5 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2084

Quoted 2 lines
mm/rmap.c +2 -0
+	 * try_to_unmap() must hold a reference on the folio.
+	 */

Do we really need this comment entirely? We don't even look at the folio after unmapping it.

Yeah I just blindly copied it, looks like historical baggage, I can't understand that comment at all. I'll drop it entirely.

Quoted 10 lines

mm/rmap.c static inline unsigned int folio_unmap_pte_batch(struct folio *folio, · line 2091

Quoted 8 lines
mm/rmap.c +7 -0
+	range.end = vma_address_end(&pvmw);
+	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
+				address, range.end);
+	adjust_range_if_pmd_sharing_possible(vma, &range.start, &range.end);
+	mmu_notifier_invalidate_range_start(&range);
+	ret = __try_to_unmap_hugetlb_one(folio, vma, &pvmw, &range,
+					 flags);

Is it really worth it moving that stuff to a separate function?

I derive pleasure in a code style of:

foo():
lock/start
__foo()
unlock/end

Although, I think this is entirely pointless here since foo() has barely anything apart from __foo(), and that __foo() has 5 function parameters. So I'll collapse into a single function.

Quoted 8 lines
Quoted 6 lines
mm/rmap.c +4 -0
+	mmu_notifier_invalidate_range_end(&range);
+	return ret;
+}
+
#19 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 39 lines · Dev Jain
Quoted 36 lines · David Hildenbrand (Arm)
Quoted 8 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

To understand the correctness of the refactoring, the following points are noted:

1. try_to_unmap() is called for hugetlb folios only when they are hwpoisoned.

Ack

Quoted 2 lines

2. A hugetlb VMA cannot be mlocked.

Ack

Quoted 3 lines

3. The pvmw API sets pvmw.pte to the base of the hugetlb folio (pvmw.pmd is NULL).

Ack

Quoted 5 lines

4. We won't ever process a softleaf entry that encodes a hugetlb folio; hugetlb folios are never swapped out, migration entries will be skipped (PVMW_MIGRATION not passed) and device-exclusive does not work for hugetlb.

Ack, we should always find present entries.

Quoted 4 lines

5. uffd-wp bit is lost when converting pvmw.pte to hwpoison softleaf (therefore no need to call pte_install_uffd_wp_if_needed after clearing pvmw.pte)

Quoted 5 lines
Quoted 1 line

Are you sure? And if so, can you elaborate why + document?

If you see the current code, we construct pteval from the subpage, losing any bit state of the previous entry. Also it doesn't make sense anyways to keep uffd-wp for an hwpoisoned entry.

Ah, yes, makes sense. For some reason I confused hugetlb with hwpoison here ... so yeah, this is good.

Signature · 3 lines
Cheers,

David
#20 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

In reply to Dev Jain
Quoted 2 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

[snip]

mm/rmap.c static int folio_not_mapped(struct folio *folio) · line 2438

Quoted 10 lines
mm/rmap.c +2 -1
@@ -2393,7 +2431,8 @@ static int folio_not_mapped(struct folio *folio)
 void try_to_unmap(struct folio *folio, enum ttu_flags flags)
 {
 	struct rmap_walk_control rwc = {
-		.rmap_one = try_to_unmap_one,
+		.rmap_one = folio_test_hugetlb(folio) ?
+				try_to_unmap_hugetlb_one : try_to_unmap_one,
 		.arg = (void *)flags,
 		.done = folio_not_mapped,
 		.anon_lock = folio_lock_anon_vma_read,

Now that try_to_unmap_hugetlb_one() is split out, should we wrap it in #ifdef CONFIG_HUGETLB_PAGE and a stub function for !HUGETLB case?

I'm working on similar change for try_to_migrate() and had this thought. I think either is fine, but wanted to check the preference.

Thanks,
Shivank

author
#21 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 8 lines · Shivank
Quoted 2 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

[snip]

Quoted 13 lines

mm/rmap.c static int folio_not_mapped(struct folio *folio) · line 2438

Quoted 10 lines
mm/rmap.c +2 -1
@@ -2393,7 +2431,8 @@ static int folio_not_mapped(struct folio *folio)
 void try_to_unmap(struct folio *folio, enum ttu_flags flags)
 {
 	struct rmap_walk_control rwc = {
-		.rmap_one = try_to_unmap_one,
+		.rmap_one = folio_test_hugetlb(folio) ?
+				try_to_unmap_hugetlb_one : try_to_unmap_one,
 		.arg = (void *)flags,
 		.done = folio_not_mapped,
 		.anon_lock = folio_lock_anon_vma_read,

Now that try_to_unmap_hugetlb_one() is split out, should we wrap it in #ifdef CONFIG_HUGETLB_PAGE and a stub function for !HUGETLB case?

I'd rather not, if everything builds correctly?

Quoted 2 lines

I'm working on similar change for try_to_migrate() and had this thought.

Wait, are you doing the batching work for try_to_migrate()? I had that as an obvious follow up to this series so if you are already on it then great!

One other function to batch is page_vma_mkclean_one, but we do not already have the folio there, so have to be careful.

Quoted 4 lines

I think either is fine, but wanted to check the preference.

Thanks,
Shivank

#22 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 29 lines · Dev Jain
Quoted 8 lines · Shivank
Quoted 2 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

[snip]

Quoted 13 lines

mm/rmap.c static int folio_not_mapped(struct folio *folio) · line 2438

Quoted 10 lines
mm/rmap.c +2 -1
@@ -2393,7 +2431,8 @@ static int folio_not_mapped(struct folio *folio)
 void try_to_unmap(struct folio *folio, enum ttu_flags flags)
 {
 	struct rmap_walk_control rwc = {
-		.rmap_one = try_to_unmap_one,
+		.rmap_one = folio_test_hugetlb(folio) ?
+				try_to_unmap_hugetlb_one : try_to_unmap_one,
 		.arg = (void *)flags,
 		.done = folio_not_mapped,
 		.anon_lock = folio_lock_anon_vma_read,

Now that try_to_unmap_hugetlb_one() is split out, should we wrap it in #ifdef CONFIG_HUGETLB_PAGE and a stub function for !HUGETLB case?

I'd rather not, if everything builds correctly?

Quoted 1 line
Quoted 6 lines
Quoted 1 line

I'm working on similar change for try_to_migrate() and had this thought.

Wait, are you doing the batching work for try_to_migrate()? I had that as an obvious follow up to this series so if you are already on it then great!

Yes,
I'm seeing significant throughput boost when combining it with my batch copy offloading series [1] Once batch copying removes the copy bottleneck, the rmap walk becomes the dominant cost for PTE-mapped large folios.

My experiment with move_pages() on two-socket EPYC Zen 3 to move 1GB anon memory from node 0 -> node 1:

  folio   vanilla     DMA (dcbm, 16 chan)
  -----   --------    -------------------
  2MB     10.7 GB/s   64.3  (6.0x) -> no change for PMD mapped
  1MB      7.0 GB/s   14.3  (2.0x) copy offlaod
                      39.0  (5.6x) + rmap batching
  256KB    6.7 GB/s   13.0  (1.9x) copy offload
                      32.3  (4.8x) + rmap batching

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

Quoted 2 lines

One other function to batch is page_vma_mkclean_one, but we do not already have the folio there, so have to be careful.

Thanks,
Shivank

author
#23 lore raw

Re: [PATCH v4 02/12] mm/rmap: Add try_to_unmap_hugetlb_one

Quoted 46 lines · Shivank
Quoted 29 lines · Dev Jain
Quoted 8 lines · Shivank
Quoted 2 lines · Dev Jain

Simplify try_to_unmap_one by separating out the hugetlb parts into try_to_unmap_hugetlb_one.

[snip]

Quoted 13 lines

mm/rmap.c static int folio_not_mapped(struct folio *folio) · line 2438

Quoted 10 lines
mm/rmap.c +2 -1
@@ -2393,7 +2431,8 @@ static int folio_not_mapped(struct folio *folio)
 void try_to_unmap(struct folio *folio, enum ttu_flags flags)
 {
 	struct rmap_walk_control rwc = {
-		.rmap_one = try_to_unmap_one,
+		.rmap_one = folio_test_hugetlb(folio) ?
+				try_to_unmap_hugetlb_one : try_to_unmap_one,
 		.arg = (void *)flags,
 		.done = folio_not_mapped,
 		.anon_lock = folio_lock_anon_vma_read,

Now that try_to_unmap_hugetlb_one() is split out, should we wrap it in #ifdef CONFIG_HUGETLB_PAGE and a stub function for !HUGETLB case?

I'd rather not, if everything builds correctly?

Quoted 1 line
Quoted 6 lines
Quoted 1 line

I'm working on similar change for try_to_migrate() and had this thought.

Wait, are you doing the batching work for try_to_migrate()? I had that as an obvious follow up to this series so if you are already on it then great!

Yes,
I'm seeing significant throughput boost when combining it with my batch copy offloading series [1] Once batch copying removes the copy bottleneck, the rmap walk becomes the dominant cost for PTE-mapped large folios.

My experiment with move_pages() on two-socket EPYC Zen 3 to move 1GB anon memory from node 0 -> node 1:

Quoted 7 lines
  folio   vanilla     DMA (dcbm, 16 chan)
  -----   --------    -------------------
  2MB     10.7 GB/s   64.3  (6.0x) -> no change for PMD mapped
  1MB      7.0 GB/s   14.3  (2.0x) copy offlaod
                      39.0  (5.6x) + rmap batching
  256KB    6.7 GB/s   13.0  (1.9x) copy offload
                      32.3  (4.8x) + rmap batching

Ooh very nice, numbers are awesome!

Quoted 10 lines

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

Quoted 2 lines

One other function to batch is page_vma_mkclean_one, but we do not already have the folio there, so have to be careful.

Thanks,
Shivank

author
#24 lore raw

PATCH v4 3/12 mm/rmap: refactor some code around lazyfree folio unmapping

In reply to Dev Jain

For lazyfree folio unmapping, after clearing the ptes we must abort the operation if the folio got dirtied or it has unexpected references.

Refactor this logic into a function which will return whether we need to abort or not.

If we abort, we restore the ptes and bail out of try_to_unmap_one. Otherwise adjust the rss stats of the mm and jump to a label.

Also rename that label from "discard" to "finish_unmap"; the former is appropriate in the lazyfree context, but the code following the label is executed for other successful unmap code paths too, so 'discard' does not sound correct for them.

---
 mm/rmap.c | 88 +++++++++++++++++++++++++++++++------------------------
 1 file changed, 50 insertions(+), 38 deletions(-)
mm/rmap.c +50 -38
index 06ab1158d4cd1..12bbee57f20da 100644
@@ -2093,6 +2093,52 @@ static bool try_to_unmap_hugetlb_one(struct folio *folio,
 	return ret;
 }
 
+static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
+		struct folio *folio)
+{
+	int ref_count, map_count;
+
+	/*
+	 * Synchronize with gup_pte_range():
+	 * - clear PTE; barrier; read refcount
+	 * - inc refcount; barrier; read PTE
+	 */
+	smp_mb();
+
+	ref_count = folio_ref_count(folio);
+	map_count = folio_mapcount(folio);
+
+	/*
+	 * Order reads for page refcount and dirty flag
+	 * (see comments in __remove_mapping()).
+	 */
+	smp_rmb();
+
+	if (folio_test_dirty(folio) && !(vma->vm_flags & VM_DROPPABLE)) {
+		/*
+		 * redirtied either using the page table or a previously
+		 * obtained GUP reference.
+		 */
+		folio_set_swapbacked(folio);
+		return false;
+	}
+
+	if (ref_count != 1 + map_count) {
+		/*
+		 * Additional reference. Could be a GUP reference or any
+		 * speculative reference. GUP users must mark the folio
+		 * dirty if there was a modification. This folio cannot be
+		 * reclaimed right now either way, so act just like nothing
+		 * happened.
+		 * We'll come back here later and detect if the folio was
+		 * dirtied when the additional reference is gone.
+		 */
+		return false;
+	}
+
+	return true;
+}
+
 /*
  * @arg: enum ttu_flags will be passed to this argument
  */
@@ -2279,47 +2325,13 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 
 			/* MADV_FREE page check */
 			if (!folio_test_swapbacked(folio)) {
-				int ref_count, map_count;
-
-				/*
-				 * Synchronize with gup_pte_range():
-				 * - clear PTE; barrier; read refcount
-				 * - inc refcount; barrier; read PTE
-				 */
-				smp_mb();
-
-				ref_count = folio_ref_count(folio);
-				map_count = folio_mapcount(folio);
-
-				/*
-				 * Order reads for page refcount and dirty flag
-				 * (see comments in __remove_mapping()).
-				 */
-				smp_rmb();
-
-				if (folio_test_dirty(folio) && !(vma->vm_flags & VM_DROPPABLE)) {
-					/*
-					 * redirtied either using the page table or a previously
-					 * obtained GUP reference.
-					 */
-					set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
-					folio_set_swapbacked(folio);
-					goto walk_abort;
-				} else if (ref_count != 1 + map_count) {
-					/*
-					 * Additional reference. Could be a GUP reference or any
-					 * speculative reference. GUP users must mark the folio
-					 * dirty if there was a modification. This folio cannot be
-					 * reclaimed right now either way, so act just like nothing
-					 * happened.
-					 * We'll come back here later and detect if the folio was
-					 * dirtied when the additional reference is gone.
-					 */
+				if (!ttu_lazyfree_folio(vma, folio)) {
 					set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
 					goto walk_abort;
 				}
+
 				add_mm_counter(mm, MM_ANONPAGES, -nr_pages);
-				goto discard;
+				goto finish_unmap;
 			}
 
 			if (folio_dup_swap(folio, subpage) < 0) {
@@ -2382,7 +2394,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			 */
 			add_mm_counter(mm, mm_counter_file(folio), -nr_pages);
 		}
-discard:
+finish_unmap:
 		folio_remove_rmap_ptes(folio, subpage, nr_pages, vma);
 		if (vma->vm_flags & VM_LOCKED)
 			mlock_drain_local();
Signature · 1 line
2.34.1
#25 lore raw

Re: [PATCH v4 03/12] mm/rmap: refactor some code around lazyfree folio unmapping

Quoted 24 lines · Dev Jain

For lazyfree folio unmapping, after clearing the ptes we must abort the operation if the folio got dirtied or it has unexpected references.

Refactor this logic into a function which will return whether we need to abort or not.

If we abort, we restore the ptes and bail out of try_to_unmap_one. Otherwise adjust the rss stats of the mm and jump to a label.

Also rename that label from "discard" to "finish_unmap"; the former is appropriate in the lazyfree context, but the code following the label is executed for other successful unmap code paths too, so 'discard' does not sound correct for them.

---
 mm/rmap.c | 88 +++++++++++++++++++++++++++++++------------------------
 1 file changed, 50 insertions(+), 38 deletions(-)
mm/rmap.c +0 -0
index 06ab1158d4cd1..12bbee57f20da 100644

mm/rmap.c static bool try_to_unmap_hugetlb_one(struct folio *folio, · line 2097

Quoted 5 lines
mm/rmap.c +2 -0
@@ -2093,6 +2093,52 @@ static bool try_to_unmap_hugetlb_one(struct folio *folio,
 	return ret;
 }
 
+static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
+		struct folio *folio)

Nitty, but I wonder whether we want to return an int with 0 vs. -EBUSY instead.

Reading "!ttu_lazyfree_folio" naturally makes me assume that this would be a success path (which it is not).

Apart from that LGTM.

Signature · 3 lines
Cheers,

David
author
#26 lore raw

Re: [PATCH v4 03/12] mm/rmap: refactor some code around lazyfree folio unmapping

Quoted 28 lines · David Hildenbrand (Arm)
Quoted 27 lines · Dev Jain

For lazyfree folio unmapping, after clearing the ptes we must abort the operation if the folio got dirtied or it has unexpected references.

Refactor this logic into a function which will return whether we need to abort or not.

If we abort, we restore the ptes and bail out of try_to_unmap_one. Otherwise adjust the rss stats of the mm and jump to a label.

Also rename that label from "discard" to "finish_unmap"; the former is appropriate in the lazyfree context, but the code following the label is executed for other successful unmap code paths too, so 'discard' does not sound correct for them.

---
 mm/rmap.c | 88 +++++++++++++++++++++++++++++++------------------------
 1 file changed, 50 insertions(+), 38 deletions(-)
mm/rmap.c +0 -0
index 06ab1158d4cd1..12bbee57f20da 100644
@@ -2093,6 +2093,52 @@ static bool try_to_unmap_hugetlb_one(struct folio *folio,
 	return ret;
 }
 
Quoted 7 lines

mm/rmap.c static bool try_to_unmap_hugetlb_one(struct folio *folio, · line 2097

Quoted 2 lines
mm/rmap.c +2 -0
+static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
+		struct folio *folio)

Nitty, but I wonder whether we want to return an int with 0 vs. -EBUSY instead.

Reading "!ttu_lazyfree_folio" naturally makes me assume that this would be a success path (which it is not).

You have a point but if you don't have a strong opinion I'll prefer to keep as is. -EBUSY won't be consumed by the callers anyways : )

Quoted 4 lines

Apart from that LGTM.

#27 lore raw

Re: [PATCH v4 03/12] mm/rmap: refactor some code around lazyfree folio unmapping

Quoted 36 lines · Dev Jain
Quoted 33 lines · David Hildenbrand (Arm)
Quoted 24 lines · Dev Jain

For lazyfree folio unmapping, after clearing the ptes we must abort the operation if the folio got dirtied or it has unexpected references.

Refactor this logic into a function which will return whether we need to abort or not.

If we abort, we restore the ptes and bail out of try_to_unmap_one. Otherwise adjust the rss stats of the mm and jump to a label.

Also rename that label from "discard" to "finish_unmap"; the former is appropriate in the lazyfree context, but the code following the label is executed for other successful unmap code paths too, so 'discard' does not sound correct for them.

---
 mm/rmap.c | 88 +++++++++++++++++++++++++++++++------------------------
 1 file changed, 50 insertions(+), 38 deletions(-)
mm/rmap.c +0 -0
index 06ab1158d4cd1..12bbee57f20da 100644

mm/rmap.c static bool try_to_unmap_hugetlb_one(struct folio *folio, · line 2097

Quoted 5 lines
mm/rmap.c +2 -0
@@ -2093,6 +2093,52 @@ static bool try_to_unmap_hugetlb_one(struct folio *folio,
 	return ret;
 }
 
+static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
+		struct folio *folio)

Nitty, but I wonder whether we want to return an int with 0 vs. -EBUSY instead.

Quoted 5 lines
Quoted 2 lines

Reading "!ttu_lazyfree_folio" naturally makes me assume that this would be a success path (which it is not).

You have a point but if you don't have a strong opinion I'll prefer to keep as is. -EBUSY won't be consumed by the callers anyways : )

Yeah, works for me, the "try_to" part implies what a bool return code means.

Signature · 3 lines
Cheers,

David
author
#28 lore raw

PATCH v4 4/12 mm/memory: Batch set uffd-wp markers during zapping

In reply to Dev Jain

In preparation for the next patch, enable batch setting of uffd-wp ptes.

The code paths passing nr > 1 to zap_install_uffd_wp_if_needed() produce that nr through either folio_pte_batch or swap_pte_batch, therefore batching is correct:

1) all ptes belong to the same type of VMA (anonymous or non-anonymous, wp-armed or non-wp-armed)

2) all ptes being marked with uffd-wp or all being not marked (same is the case with the pte_swp_uffd_wp_any check)

3) uffd_supports_wp_marker() is independent of the function parameters

Note that we will have to use set_pte_at() in a loop instead of set_ptes() since the latter cannot handle present->non-present conversion for nr_pages > 1.

Rename the function to cond_install_uffd_wp_ptes, and convert the documentation to kerneldoc format.

Move the function to memory.c since this has grown too long to be kept in mm_inline.h, while retaining the inline hint.

Rename pte->ptep and pteval->pte.

---
 include/linux/mm.h        |  4 ++
 include/linux/mm_inline.h | 53 -------------------------
 mm/memory.c               | 81 ++++++++++++++++++++++++++++++---------
 mm/rmap.c                 |  2 +-
 4 files changed, 67 insertions(+), 73 deletions(-)
include/linux/mm.h +4 -0
index 31e27ff6a35fa..3169bd6d69f5a 100644
@@ -5216,4 +5216,8 @@ void map_anon_folio_pte_nopf(struct folio *folio, pte_t *pte,
 		struct vm_area_struct *vma, unsigned long addr,
 		bool uffd_wp);
 
+bool cond_install_uffd_wp_ptes(struct vm_area_struct *vma, unsigned long addr,
+		pte_t *ptep, pte_t pte, unsigned long nr_ptes);
+
+
 #endif /* _LINUX_MM_H */
include/linux/mm_inline.h +0 -53

59 lines not on this page · raw message

mm/memory.c +62 -19

97 lines not on this page · raw message

mm/rmap.c +1 -1
index 12bbee57f20da..6a0b43856d6c0 100644
@@ -2288,7 +2288,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 		 * we may want to replace a none pte with a marker pte if
 		 * it's file-backed, so we don't lose the tracking info.
 		 */
-		pte_install_uffd_wp_if_needed(vma, address, pvmw.pte, pteval);
+		cond_install_uffd_wp_ptes(vma, address, pvmw.pte, pteval, 1);
 
 		/* Update high watermark before we lower rss */
 		update_hiwater_rss(mm);
Signature · 1 line
2.34.1
#29 lore raw

Re: [PATCH v4 04/12] mm/memory: Batch set uffd-wp markers during zapping

Quoted 1 line · Dev Jain

In preparation for the next patch, enable batch setting of uffd-wp ptes.

Avoid talking about "next patch". Phrase it as "In preparation for batching...".

Quoted 8 lines

The code paths passing nr > 1 to zap_install_uffd_wp_if_needed() produce that nr through either folio_pte_batch or swap_pte_batch, therefore batching is correct:

1) all ptes belong to the same type of VMA (anonymous or non-anonymous, wp-armed or non-wp-armed)

Quoted 8 lines

2) all ptes being marked with uffd-wp or all being not marked (same is the case with the pte_swp_uffd_wp_any check)

3) uffd_supports_wp_marker() is independent of the function parameters

Note that we will have to use set_pte_at() in a loop instead of set_ptes() since the latter cannot handle present->non-present conversion for nr_pages > 1.

Below you have

mm/memory.c static inline bool zap_drop_markers(struct zap_details *details) · line 1632

Quoted 2 lines
mm/memory.c +2 -0
+	/* The current status of the pte should be "cleared" before calling */
+	WARN_ON_ONCE(!pte_none(ptep_get(ptep)));

Could we easily extend set_ptes() to allow for non-present -> non-present with nr > 1?

Quoted 6 lines

Rename the function to cond_install_uffd_wp_ptes, and convert the documentation to kerneldoc format.

Move the function to memory.c since this has grown too long to be kept in mm_inline.h, while retaining the inline hint.

Well, for functions in the header you really need "static inline", so it's not a hint and I would just drop that comment.

However, the move while modifying a larger function is rather suboptimal for reviewing (more like "spot the difference between these two pictures"). I'd suggest moving it in one patch, and thenb modifying it in a second (this) patch instead. You can perform the rename already while moving I think.

Nothing else jumped at me.

Signature · 3 lines
Cheers,

David
author
#30 lore raw

Re: [PATCH v4 04/12] mm/memory: Batch set uffd-wp markers during zapping

Quoted 22 lines · David Hildenbrand (Arm)
Quoted 1 line · Dev Jain

In preparation for the next patch, enable batch setting of uffd-wp ptes.

Avoid talking about "next patch". Phrase it as "In preparation for batching...".

Quoted 8 lines

The code paths passing nr > 1 to zap_install_uffd_wp_if_needed() produce that nr through either folio_pte_batch or swap_pte_batch, therefore batching is correct:

1) all ptes belong to the same type of VMA (anonymous or non-anonymous, wp-armed or non-wp-armed)

Quoted 8 lines

2) all ptes being marked with uffd-wp or all being not marked (same is the case with the pte_swp_uffd_wp_any check)

3) uffd_supports_wp_marker() is independent of the function parameters

Note that we will have to use set_pte_at() in a loop instead of set_ptes() since the latter cannot handle present->non-present conversion for nr_pages > 1.

Quoted 7 lines

Below you have

mm/memory.c static inline bool zap_drop_markers(struct zap_details *details) · line 1632

Quoted 2 lines
mm/memory.c +2 -0
+	/* The current status of the pte should be "cleared" before calling */
+	WARN_ON_ONCE(!pte_none(ptep_get(ptep)));

Could we easily extend set_ptes() to allow for non-present -> non-present with nr > 1?

We could have a set_nonpresent_ptes() in general, but then for some non-present configs like in this patch, we don't need to change the pfn, while in some configs like setting swp ptes in one go (like I do in later patches) we have an underlying pfn. So I don't see an obvious simplification with a common helper for non-present -> non-present for nr > 1.

Quoted 10 lines
Quoted 7 lines

Rename the function to cond_install_uffd_wp_ptes, and convert the documentation to kerneldoc format.

Move the function to memory.c since this has grown too long to be kept in mm_inline.h, while retaining the inline hint.

Well, for functions in the header you really need "static inline", so it's not a hint and I would just drop that comment.

Ok.

Quoted 5 lines

However, the move while modifying a larger function is rather suboptimal for reviewing (more like "spot the difference between these two pictures"). I'd suggest moving it in one patch, and thenb modifying it in a second (this) patch instead. You can perform the rename already while moving I think.

So I'll move the function, do the renaming of pte and pteval, do the kerneldoc conversion change in one patch. Then batch and rename in the next.

Quoted 4 lines

Nothing else jumped at me.

author
#31 lore raw

Re: [PATCH v4 04/12] mm/memory: Batch set uffd-wp markers during zapping

Quoted 33 lines · Dev Jain
Quoted 22 lines · David Hildenbrand (Arm)
Quoted 1 line · Dev Jain

In preparation for the next patch, enable batch setting of uffd-wp ptes.

Avoid talking about "next patch". Phrase it as "In preparation for batching...".

Quoted 8 lines

The code paths passing nr > 1 to zap_install_uffd_wp_if_needed() produce that nr through either folio_pte_batch or swap_pte_batch, therefore batching is correct:

1) all ptes belong to the same type of VMA (anonymous or non-anonymous, wp-armed or non-wp-armed)

Quoted 8 lines

2) all ptes being marked with uffd-wp or all being not marked (same is the case with the pte_swp_uffd_wp_any check)

3) uffd_supports_wp_marker() is independent of the function parameters

Note that we will have to use set_pte_at() in a loop instead of set_ptes() since the latter cannot handle present->non-present conversion for nr_pages > 1.

Quoted 7 lines

Below you have

mm/memory.c static inline bool zap_drop_markers(struct zap_details *details) · line 1632

Quoted 2 lines
mm/memory.c +2 -0
+	/* The current status of the pte should be "cleared" before calling */
+	WARN_ON_ONCE(!pte_none(ptep_get(ptep)));

Could we easily extend set_ptes() to allow for non-present -> non-present with nr > 1?

Quoted 5 lines

We could have a set_nonpresent_ptes() in general, but then for some non-present configs like in this patch, we don't need to change the pfn, while in some configs like setting swp ptes in one go (like I do in later patches) we have an underlying pfn. So I don't see an obvious simplification with a common helper for non-present -> non-present for nr > 1.

I think a "non-present" entry always means a softleaf? In that case, we can simply branch on softleaf_has_pfn().

Quoted 28 lines
Quoted 10 lines
Quoted 7 lines

Rename the function to cond_install_uffd_wp_ptes, and convert the documentation to kerneldoc format.

Move the function to memory.c since this has grown too long to be kept in mm_inline.h, while retaining the inline hint.

Well, for functions in the header you really need "static inline", so it's not a hint and I would just drop that comment.

Ok.

Quoted 5 lines

However, the move while modifying a larger function is rather suboptimal for reviewing (more like "spot the difference between these two pictures"). I'd suggest moving it in one patch, and thenb modifying it in a second (this) patch instead. You can perform the rename already while moving I think.

So I'll move the function, do the renaming of pte and pteval, do the kerneldoc conversion change in one patch. Then batch and rename in the next.

Quoted 4 lines

Nothing else jumped at me.

#32 lore raw

Re: [PATCH v4 04/12] mm/memory: Batch set uffd-wp markers during zapping

Quoted 16 lines · Dev Jain
Quoted 13 lines · Dev Jain
Quoted 9 lines · David Hildenbrand (Arm)

Avoid talking about "next patch". Phrase it as "In preparation for batching...".

Below you have

Could we easily extend set_ptes() to allow for non-present -> non-present with nr > 1?

Quoted 8 lines
Quoted 5 lines

We could have a set_nonpresent_ptes() in general, but then for some non-present configs like in this patch, we don't need to change the pfn, while in some configs like setting swp ptes in one go (like I do in later patches) we have an underlying pfn. So I don't see an obvious simplification with a common helper for non-present -> non-present for nr > 1.

I think a "non-present" entry always means a softleaf? In that case, we can simply branch on softleaf_has_pfn().

We could, but it might turn the overall function more expensive by throwing in additional runtime checks that the compiler will not really optimize out. Worth playing with.

Signature · 3 lines
Cheers,

David
author
#33 lore raw

PATCH v4 5/12 mm/rmap: batch unmap folios belonging to uffd-wp VMAs

In reply to Dev Jain

Commit a67fe41e214f ("mm: rmap: support batched unmapping for file large folios") extended batched unmapping for file folios. That also required making install_uffd_wp_pte_if_needed() support batching, but that was left out for the time being, and correctness was maintained by stopping batching in case the VMA the folio belongs to is marked uffd-wp.

Now that we have a batched version called cond_install_uffd_wp_ptes, simply call that. folio_unmap_pte_batch() ensures that the original state of the ptes is either all uffd or all non-uffd, so we maintain correctness.

If uffd-wp bit is there, we have the following transitions of ptes after unmapping:

1) anon folio: present -> uffd-wp swap
2) file folio: present -> uffd-wp marker

We must ensure that these ptes are not reprocessed by the while loop - if the batch length is less than the number of pages in the folio, then we must skip over this batch.

The page_vma_mapped_walk API ensures this - check_pte() will return true only if any of [pvmw->pfn, pvmw->pfn + nr_pages) is mapped by the pte. There is no pfn underlying either a uffd-wp swap pte or a uffd-wp marker pte, so check_pte returns false and we keep skipping until we hit a present entry, which is where we want to batch from next.

---
 mm/rmap.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)
mm/rmap.c +1 -4
index 6a0b43856d6c0..ead031361bc61 100644
@@ -1965,9 +1965,6 @@ static inline unsigned int folio_unmap_pte_batch(struct folio *folio,
 	if (pte_unused(pte))
 		return 1;
 
-	if (userfaultfd_wp(vma))
-		return 1;
-
 	/*
 	 * If unmap fails, we need to restore the ptes. To avoid accidentally
 	 * upgrading write permissions for ptes that were not originally
@@ -2288,7 +2285,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 		 * we may want to replace a none pte with a marker pte if
 		 * it's file-backed, so we don't lose the tracking info.
 		 */
-		cond_install_uffd_wp_ptes(vma, address, pvmw.pte, pteval, 1);
+		cond_install_uffd_wp_ptes(vma, address, pvmw.pte, pteval, nr_pages);
 
 		/* Update high watermark before we lower rss */
 		update_hiwater_rss(mm);
Signature · 1 line
2.34.1
author
#34 lore raw

PATCH v4 6/12 mm/swap: rename subpage->page in folio_dup_swap/folio_put_swap

In reply to Dev Jain

Folios have pages, not subpages. Rename 'subpage' parameters to 'page'.

---
 mm/swap.h     |  4 ++--
 mm/swapfile.c | 16 ++++++++--------
 2 files changed, 10 insertions(+), 10 deletions(-)
mm/swap.h +2 -2
index a77016f2423b9..b57547c25ffc9 100644
@@ -206,8 +206,8 @@ extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
  * folio_put_swap(): does the opposite thing of folio_dup_swap().
  */
 int folio_alloc_swap(struct folio *folio);
-int folio_dup_swap(struct folio *folio, struct page *subpage);
-void folio_put_swap(struct folio *folio, struct page *subpage);
+int folio_dup_swap(struct folio *folio, struct page *page);
+void folio_put_swap(struct folio *folio, struct page *page);
 
 /* For internal use */
 extern void __swap_cluster_free_entries(struct swap_info_struct *si,
mm/swapfile.c +8 -8
index 08309c1dafa3b..62f083cff75eb 100644
@@ -1743,7 +1743,7 @@ int folio_alloc_swap(struct folio *folio)
 /**
  * folio_dup_swap() - Increase swap count of swap entries of a folio.
  * @folio: folio with swap entries bounded.
- * @subpage: if not NULL, only increase the swap count of this subpage.
+ * @page: if not NULL, only increase the swap count of this page.
  *
  * Typically called when the folio is unmapped and have its swap entry to
  * take its place: Swap entries allocated to a folio has count == 0 and pinned
@@ -1757,7 +1757,7 @@ int folio_alloc_swap(struct folio *folio)
  * swap_put_entries_direct on its swap entry before this helper returns, or
  * the swap count may underflow.
  */
-int folio_dup_swap(struct folio *folio, struct page *subpage)
+int folio_dup_swap(struct folio *folio, struct page *page)
 {
 	swp_entry_t entry = folio->swap;
 	unsigned long nr_pages = folio_nr_pages(folio);
@@ -1765,8 +1765,8 @@ int folio_dup_swap(struct folio *folio, struct page *subpage)
 	VM_WARN_ON_FOLIO(!folio_test_locked(folio), folio);
 	VM_WARN_ON_FOLIO(!folio_test_swapcache(folio), folio);
 
-	if (subpage) {
-		entry.val += folio_page_idx(folio, subpage);
+	if (page) {
+		entry.val += folio_page_idx(folio, page);
 		nr_pages = 1;
 	}
 
@@ -1777,13 +1777,13 @@ int folio_dup_swap(struct folio *folio, struct page *subpage)
 /**
  * folio_put_swap() - Decrease swap count of swap entries of a folio.
  * @folio: folio with swap entries bounded, must be in swap cache and locked.
- * @subpage: if not NULL, only decrease the swap count of this subpage.
+ * @page: if not NULL, only decrease the swap count of this page.
  *
  * This won't free the swap slots even if swap count drops to zero, they are
  * still pinned by the swap cache. User may call folio_free_swap to free them.
  * Context: Caller must ensure the folio is locked and in the swap cache.
  */
-void folio_put_swap(struct folio *folio, struct page *subpage)
+void folio_put_swap(struct folio *folio, struct page *page)
 {
 	swp_entry_t entry = folio->swap;
 	unsigned long nr_pages = folio_nr_pages(folio);
@@ -1792,8 +1792,8 @@ void folio_put_swap(struct folio *folio, struct page *subpage)
 	VM_WARN_ON_FOLIO(!folio_test_locked(folio), folio);
 	VM_WARN_ON_FOLIO(!folio_test_swapcache(folio), folio);
 
-	if (subpage) {
-		entry.val += folio_page_idx(folio, subpage);
+	if (page) {
+		entry.val += folio_page_idx(folio, page);
 		nr_pages = 1;
 	}
 
Signature · 1 line
2.34.1
maintainer Acked-by on v4 6/12
#35 lore raw

Re: [PATCH v4 06/12] mm/swap: rename subpage->page in folio_dup_swap/folio_put_swap

Quoted 2 lines · Dev Jain

Folios have pages, not subpages. Rename 'subpage' parameters to 'page'.

I would hope that this patch would make Willy really happy ;)

Signature · 3 lines
Cheers,

David
author
#36 lore raw

PATCH v4 7/12 mm/swapfile: Add batched version of folio_dup_swap

In reply to Dev Jain

Add folio_dup_swap_pages to handle a batch of consecutive pages. Note that folio_dup_swap already can handle a subset of this: nr_pages == 1 and nr_pages == folio_nr_pages(folio). Generalize this to any nr_pages.

Currently we have a not-so-nice logic of passing in subpage == NULL if we mean to exercise the logic on the entire folio, and subpage != NULL if we want to exercise the logic on only that subpage. Remove this
indirection: the caller invokes folio_dup_swap_pages() if it wants to operate on a range of pages in the folio (i.e nr_pages may be anything between 1 till folio_nr_pages()), and invokes folio_dup_swap() if it wants to operate on the entire folio.

---
 mm/rmap.c     |  2 +-
 mm/shmem.c    |  2 +-
 mm/swap.h     | 19 +++++++++++++++++--
 mm/swapfile.c | 14 ++++++--------
 4 files changed, 25 insertions(+), 12 deletions(-)
mm/rmap.c +1 -1
index ead031361bc61..3065d384cadf8 100644
@@ -2331,7 +2331,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 				goto finish_unmap;
 			}
 
-			if (folio_dup_swap(folio, subpage) < 0) {
+			if (folio_dup_swap_pages(folio, subpage, 1) < 0) {
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
mm/shmem.c +1 -1
index bab3529af23c5..5e4f521399847 100644
@@ -1698,7 +1698,7 @@ int shmem_writeout(struct folio *folio, struct swap_iocb **plug,
 			spin_unlock(&shmem_swaplist_lock);
 		}
 
-		folio_dup_swap(folio, NULL);
+		folio_dup_swap(folio);
 		shmem_delete_from_page_cache(folio, swp_to_radix_entry(folio->swap));
 
 		BUG_ON(folio_mapped(folio));
mm/swap.h +17 -2
index b57547c25ffc9..36c69b2788c77 100644
@@ -206,7 +206,8 @@ extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
  * folio_put_swap(): does the opposite thing of folio_dup_swap().
  */
 int folio_alloc_swap(struct folio *folio);
-int folio_dup_swap(struct folio *folio, struct page *page);
+int folio_dup_swap_pages(struct folio *folio, struct page *page,
+			 unsigned long nr_pages);
 void folio_put_swap(struct folio *folio, struct page *page);
 
 /* For internal use */
@@ -390,7 +391,8 @@ static inline int folio_alloc_swap(struct folio *folio)
 	return -EINVAL;
 }
 
-static inline int folio_dup_swap(struct folio *folio, struct page *page)
+static inline int folio_dup_swap_pages(struct folio *folio, struct page *page,
+		unsigned long nr_pages)
 {
 	return -EINVAL;
 }
@@ -499,4 +501,17 @@ static inline int non_swapcache_batch(swp_entry_t entry, int max_nr)
 	return 0;
 }
 #endif /* CONFIG_SWAP */
+
+/**
+ * folio_dup_swap() - Increase swap count of all swap entries of a folio.
+ * @folio: folio with swap entries bound.
+ *
+ * See folio_dup_swap_pages() for more information.
+ */
+static inline int folio_dup_swap(struct folio *folio)
+{
+	return folio_dup_swap_pages(folio, folio_page(folio, 0),
+				    folio_nr_pages(folio));
+}
+
 #endif /* _MM_SWAP_H */
mm/swapfile.c +6 -8
index 62f083cff75eb..c91957149ecce 100644
@@ -1741,9 +1741,10 @@ int folio_alloc_swap(struct folio *folio)
 }
 
 /**
- * folio_dup_swap() - Increase swap count of swap entries of a folio.
+ * folio_dup_swap_pages() - Increase swap count of swap entries of a folio.
  * @folio: folio with swap entries bounded.
- * @page: if not NULL, only increase the swap count of this page.
+ * @page: the first page in the folio to increase the swap count for.
+ * @nr_pages: the number of pages in the folio to increase the swap count for.
  *
  * Typically called when the folio is unmapped and have its swap entry to
  * take its place: Swap entries allocated to a folio has count == 0 and pinned
@@ -1757,18 +1758,15 @@ int folio_alloc_swap(struct folio *folio)
  * swap_put_entries_direct on its swap entry before this helper returns, or
  * the swap count may underflow.
  */
-int folio_dup_swap(struct folio *folio, struct page *page)
+int folio_dup_swap_pages(struct folio *folio, struct page *page,
+		unsigned long nr_pages)
 {
 	swp_entry_t entry = folio->swap;
-	unsigned long nr_pages = folio_nr_pages(folio);
 
 	VM_WARN_ON_FOLIO(!folio_test_locked(folio), folio);
 	VM_WARN_ON_FOLIO(!folio_test_swapcache(folio), folio);
 
-	if (page) {
-		entry.val += folio_page_idx(folio, page);
-		nr_pages = 1;
-	}
+	entry.val += folio_page_idx(folio, page);
 
 	return swap_dup_entries_cluster(swap_entry_to_info(entry),
 					swp_offset(entry), nr_pages);
Signature · 1 line
2.34.1
maintainer Acked-by on v4 7/12
#37 lore raw

Re: [PATCH v4 07/12] mm/swapfile: Add batched version of folio_dup_swap

Quoted 12 lines · Dev Jain

Add folio_dup_swap_pages to handle a batch of consecutive pages. Note that folio_dup_swap already can handle a subset of this: nr_pages == 1 and nr_pages == folio_nr_pages(folio). Generalize this to any nr_pages.

Currently we have a not-so-nice logic of passing in subpage == NULL if we mean to exercise the logic on the entire folio, and subpage != NULL if we want to exercise the logic on only that subpage. Remove this
indirection: the caller invokes folio_dup_swap_pages() if it wants to operate on a range of pages in the folio (i.e nr_pages may be anything between 1 till folio_nr_pages()), and invokes folio_dup_swap() if it wants to operate on the entire folio.

Quoted 2 lines

---

I was briefly wondering whether "folio_dup_swap_range()" would be better (a folio range), but *_pages() works for me as well.

Signature · 3 lines
Cheers,

David
#38 lore raw

Re: [PATCH v4 07/12] mm/swapfile: Add batched version of folio_dup_swap

In reply to Dev Jain
Quoted 51 lines · Dev Jain

Add folio_dup_swap_pages to handle a batch of consecutive pages. Note that folio_dup_swap already can handle a subset of this: nr_pages == 1 and nr_pages == folio_nr_pages(folio). Generalize this to any nr_pages.

Currently we have a not-so-nice logic of passing in subpage == NULL if we mean to exercise the logic on the entire folio, and subpage != NULL if we want to exercise the logic on only that subpage. Remove this
indirection: the caller invokes folio_dup_swap_pages() if it wants to operate on a range of pages in the folio (i.e nr_pages may be anything between 1 till folio_nr_pages()), and invokes folio_dup_swap() if it wants to operate on the entire folio.

---
 mm/rmap.c     |  2 +-
 mm/shmem.c    |  2 +-
 mm/swap.h     | 19 +++++++++++++++++--
 mm/swapfile.c | 14 ++++++--------
 4 files changed, 25 insertions(+), 12 deletions(-)
mm/rmap.c +1 -1
index ead031361bc61..3065d384cadf8 100644
@@ -2331,7 +2331,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 				goto finish_unmap;
 			}
 
-			if (folio_dup_swap(folio, subpage) < 0) {
+			if (folio_dup_swap_pages(folio, subpage, 1) < 0) {
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
mm/shmem.c +1 -1
index bab3529af23c5..5e4f521399847 100644
@@ -1698,7 +1698,7 @@ int shmem_writeout(struct folio *folio, struct swap_iocb **plug,
 			spin_unlock(&shmem_swaplist_lock);
 		}
 
-		folio_dup_swap(folio, NULL);
+		folio_dup_swap(folio);
 		shmem_delete_from_page_cache(folio, swp_to_radix_entry(folio->swap));
 
 		BUG_ON(folio_mapped(folio));
mm/swap.h +0 -0
index b57547c25ffc9..36c69b2788c77 100644

mm/swap.h extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp); · line 210

Quoted 6 lines
mm/swap.h +2 -1
@@ -206,7 +206,8 @@ extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
  * folio_put_swap(): does the opposite thing of folio_dup_swap().
  */
 int folio_alloc_swap(struct folio *folio);
-int folio_dup_swap(struct folio *folio, struct page *page);
+int folio_dup_swap_pages(struct folio *folio, struct page *page,
+			 unsigned long nr_pages);

Nit: \tab\tab

Signature · 3 lines
Cheers,

David
author
#39 lore raw

PATCH v4 8/12 mm/swapfile: Add batched version of folio_put_swap

In reply to Dev Jain

Add folio_put_swap_pages to handle a batch of consecutive pages. Note that folio_put_swap already can handle a subset of this: nr_pages == 1 and nr_pages == folio_nr_pages(folio). Generalize this to any nr_pages.

Currently we have a not-so-nice logic of passing in subpage == NULL if we mean to exercise the logic on the entire folio, and subpage != NULL if we want to exercise the logic on only that subpage. Remove this
indirection: the caller invokes folio_put_swap_pages() if it wants to operate on a range of pages in the folio (i.e nr_pages may be anything between 1 till folio_nr_pages()), and invokes folio_put_swap() if it wants to operate on the entire folio.

---
 mm/memory.c   |  6 +++---
 mm/rmap.c     |  4 ++--
 mm/shmem.c    |  6 +++---
 mm/swap.h     | 17 +++++++++++++++--
 mm/swapfile.c | 17 ++++++++---------
 5 files changed, 31 insertions(+), 19 deletions(-)
mm/memory.c +3 -3
index 767c033e95da9..40cd44a7f10a1 100644
@@ -5165,7 +5165,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	if (unlikely(folio != swapcache)) {
 		folio_add_new_anon_rmap(folio, vma, address, RMAP_EXCLUSIVE);
 		folio_add_lru_vma(folio, vma);
-		folio_put_swap(swapcache, NULL);
+		folio_put_swap(swapcache);
 	} else if (!folio_test_anon(folio)) {
 		/*
 		 * We currently only expect !anon folios that are fully
@@ -5174,12 +5174,12 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 		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);
+		folio_put_swap(folio);
 	} 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);
-		folio_put_swap(folio, nr_pages == 1 ? page : NULL);
+		folio_put_swap_pages(folio, page, nr_pages);
 	}
 
 	VM_BUG_ON(!folio_test_anon(folio) ||
mm/rmap.c +2 -2
index 3065d384cadf8..c0e385882f562 100644
@@ -2342,7 +2342,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) {
-				folio_put_swap(folio, subpage);
+				folio_put_swap_pages(folio, subpage, 1);
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
@@ -2350,7 +2350,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)) {
-				folio_put_swap(folio, subpage);
+				folio_put_swap_pages(folio, subpage, 1);
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
mm/shmem.c +3 -3
index 5e4f521399847..bb7e0fc305d87 100644
@@ -1719,7 +1719,7 @@ int shmem_writeout(struct folio *folio, struct swap_iocb **plug,
 		/* Swap entry might be erased by racing shmem_free_swap() */
 		if (!error) {
 			shmem_recalc_inode(inode, 0, -nr_pages);
-			folio_put_swap(folio, NULL);
+			folio_put_swap(folio);
 		}
 
 		/*
@@ -2199,7 +2199,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);
+	folio_put_swap(folio);
 	swap_cache_del_folio(folio);
 	/*
 	 * Don't treat swapin error folio as alloced. Otherwise inode->i_blocks
@@ -2429,7 +2429,7 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
 	if (sgp == SGP_WRITE)
 		folio_mark_accessed(folio);
 
-	folio_put_swap(folio, NULL);
+	folio_put_swap(folio);
 	swap_cache_del_folio(folio);
 	folio_mark_dirty(folio);
 	put_swap_device(si);
mm/swap.h +15 -2
index 36c69b2788c77..48e8deb95a440 100644
@@ -208,7 +208,8 @@ extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
 int folio_alloc_swap(struct folio *folio);
 int folio_dup_swap_pages(struct folio *folio, struct page *page,
 			 unsigned long nr_pages);
-void folio_put_swap(struct folio *folio, struct page *page);
+void folio_put_swap_pages(struct folio *folio, struct page *page,
+			  unsigned long nr_pages);
 
 /* For internal use */
 extern void __swap_cluster_free_entries(struct swap_info_struct *si,
@@ -397,7 +398,8 @@ static inline int folio_dup_swap_pages(struct folio *folio, struct page *page,
 	return -EINVAL;
 }
 
-static inline void folio_put_swap(struct folio *folio, struct page *page)
+static inline void folio_put_swap_pages(struct folio *folio, struct page *page,
+				  unsigned long nr_pages)
 {
 }
 
@@ -514,4 +516,15 @@ static inline int folio_dup_swap(struct folio *folio)
 				    folio_nr_pages(folio));
 }
 
+/**
+ * folio_put_swap() - Decrease swap count of all swap entries of a folio.
+ * @folio: folio with swap entries bound.
+ *
+ * See folio_put_swap_pages() for more information.
+ */
+static inline void folio_put_swap(struct folio *folio)
+{
+	folio_put_swap_pages(folio, folio_page(folio, 0), folio_nr_pages(folio));
+}
+
 #endif /* _MM_SWAP_H */
mm/swapfile.c +8 -9
index c91957149ecce..f1d97989cee31 100644
@@ -1773,27 +1773,25 @@ int folio_dup_swap_pages(struct folio *folio, struct page *page,
 }
 
 /**
- * folio_put_swap() - Decrease swap count of swap entries of a folio.
+ * folio_put_swap_pages() - Decrease swap count of swap entries of a folio.
  * @folio: folio with swap entries bounded, must be in swap cache and locked.
- * @page: if not NULL, only decrease the swap count of this page.
+ * @page: the first page in the folio to decrease the swap count for.
+ * @nr_pages: the number of pages in the folio to decrease the swap count for.
  *
  * This won't free the swap slots even if swap count drops to zero, they are
  * still pinned by the swap cache. User may call folio_free_swap to free them.
  * Context: Caller must ensure the folio is locked and in the swap cache.
  */
-void folio_put_swap(struct folio *folio, struct page *page)
+void folio_put_swap_pages(struct folio *folio, struct page *page,
+			  unsigned long nr_pages)
 {
 	swp_entry_t entry = folio->swap;
-	unsigned long nr_pages = folio_nr_pages(folio);
 	struct swap_info_struct *si = __swap_entry_to_info(entry);
 
 	VM_WARN_ON_FOLIO(!folio_test_locked(folio), folio);
 	VM_WARN_ON_FOLIO(!folio_test_swapcache(folio), folio);
 
-	if (page) {
-		entry.val += folio_page_idx(folio, page);
-		nr_pages = 1;
-	}
+	entry.val += folio_page_idx(folio, page);
 
 	swap_put_entries_cluster(si, swp_offset(entry), nr_pages, false);
 }
@@ -2499,7 +2497,8 @@ static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd,
 		new_pte = pte_mkuffd_wp(new_pte);
 setpte:
 	set_pte_at(vma->vm_mm, addr, pte, new_pte);
-	folio_put_swap(swapcache, folio_file_page(swapcache, swp_offset(entry)));
+	folio_put_swap_pages(swapcache,
+			     folio_file_page(swapcache, swp_offset(entry)), 1);
 out:
 	if (pte)
 		pte_unmap_unlock(pte, ptl);
Signature · 1 line
2.34.1
maintainer Acked-by on v4 8/12
#40 lore raw

Re: [PATCH v4 08/12] mm/swapfile: Add batched version of folio_put_swap

Quoted 10 lines · Dev Jain

Add folio_put_swap_pages to handle a batch of consecutive pages. Note that folio_put_swap already can handle a subset of this: nr_pages == 1 and nr_pages == folio_nr_pages(folio). Generalize this to any nr_pages.

Currently we have a not-so-nice logic of passing in subpage == NULL if we mean to exercise the logic on the entire folio, and subpage != NULL if we want to exercise the logic on only that subpage. Remove this
indirection: the caller invokes folio_put_swap_pages() if it wants to operate on a range of pages in the folio (i.e nr_pages may be anything between 1 till folio_nr_pages()), and invokes folio_put_swap() if it

"between 1 and folio_nr_pages()" ?

Quoted 4 lines

wants to operate on the entire folio.

---

[...]

mm/swap.h extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp); · line 212

Quoted 8 lines

+++ b/mm/swap.h

mm/swap.h +2 -1
@@ -208,7 +208,8 @@ extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
 int folio_alloc_swap(struct folio *folio);
 int folio_dup_swap_pages(struct folio *folio, struct page *page,
 			 unsigned long nr_pages);
-void folio_put_swap(struct folio *folio, struct page *page);
+void folio_put_swap_pages(struct folio *folio, struct page *page,
+			  unsigned long nr_pages);

Nit \tab\tab (same below)

Quoted 7 lines
mm/swap.h +0 -0
 /* For internal use */
 extern void __swap_cluster_free_entries(struct swap_info_struct *si,
@@ -397,7 +398,8 @@ static inline int folio_dup_swap_pages(struct folio *folio, struct page *page,
 	return -EINVAL;
 }
 

[...]

mm/swapfile.c static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd, · line 2504

Quoted 13 lines
mm/swapfile.c +2 -1
 	swap_put_entries_cluster(si, swp_offset(entry), nr_pages, false);
 }
@@ -2499,7 +2497,8 @@ static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd,
 		new_pte = pte_mkuffd_wp(new_pte);
 setpte:
 	set_pte_at(vma->vm_mm, addr, pte, new_pte);
-	folio_put_swap(swapcache, folio_file_page(swapcache, swp_offset(entry)));
+	folio_put_swap_pages(swapcache,
+			     folio_file_page(swapcache, swp_offset(entry)), 1);
 out:
 	if (pte)
 		pte_unmap_unlock(pte, ptl);

We could consider having a simple folio_put_swap_page() helper. But maybe most of these will be converted to batching in the long run either way.

Signature · 3 lines
Cheers,

David
author
#41 lore raw

Re: [PATCH v4 08/12] mm/swapfile: Add batched version of folio_put_swap

Quoted 5 lines · David Hildenbrand (Arm)
Quoted 4 lines · Dev Jain

Add folio_put_swap_pages to handle a batch of consecutive pages. Note that folio_put_swap already can handle a subset of this: nr_pages == 1 and nr_pages == folio_nr_pages(folio). Generalize this to any nr_pages.

Quoted 8 lines
Quoted 6 lines

Currently we have a not-so-nice logic of passing in subpage == NULL if we mean to exercise the logic on the entire folio, and subpage != NULL if we want to exercise the logic on only that subpage. Remove this
indirection: the caller invokes folio_put_swap_pages() if it wants to operate on a range of pages in the folio (i.e nr_pages may be anything between 1 till folio_nr_pages()), and invokes folio_put_swap() if it

"between 1 and folio_nr_pages()" ?

Ok.

Quoted 32 lines
Quoted 5 lines

wants to operate on the entire folio.

---

[...]

mm/swap.h extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp); · line 212

Quoted 8 lines

+++ b/mm/swap.h

mm/swap.h +2 -1
@@ -208,7 +208,8 @@ extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
 int folio_alloc_swap(struct folio *folio);
 int folio_dup_swap_pages(struct folio *folio, struct page *page,
 			 unsigned long nr_pages);
-void folio_put_swap(struct folio *folio, struct page *page);
+void folio_put_swap_pages(struct folio *folio, struct page *page,
+			  unsigned long nr_pages);

Nit \tab\tab (same below)

Quoted 7 lines
mm/swap.h +0 -0
 /* For internal use */
 extern void __swap_cluster_free_entries(struct swap_info_struct *si,
@@ -397,7 +398,8 @@ static inline int folio_dup_swap_pages(struct folio *folio, struct page *page,
 	return -EINVAL;
 }
 

[...]

Quoted 1 line
Quoted 16 lines

mm/swapfile.c static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd, · line 2504

Quoted 12 lines
mm/swapfile.c +2 -1
 	swap_put_entries_cluster(si, swp_offset(entry), nr_pages, false);
 }
@@ -2499,7 +2497,8 @@ static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd,
 		new_pte = pte_mkuffd_wp(new_pte);
 setpte:
 	set_pte_at(vma->vm_mm, addr, pte, new_pte);
-	folio_put_swap(swapcache, folio_file_page(swapcache, swp_offset(entry)));
+	folio_put_swap_pages(swapcache,
+			     folio_file_page(swapcache, swp_offset(entry)), 1);
 out:
 	if (pte)
 		pte_unmap_unlock(pte, ptl);

We could consider having a simple folio_put_swap_page() helper. But maybe most of these will be converted to batching in the long run either way.

Yep.

Quoted 2 lines

Thanks!

Quoted 1 line
author
#42 lore raw

PATCH v4 9/12 mm/rmap: Add batched version of folio_try_share_anon_rmap_pte

In reply to Dev Jain

To enable batched unmapping of anonymous folios, we need to handle the sharing of exclusive pages. Hence, a batched version of folio_try_share_anon_rmap_pte is required.

Currently, the sole purpose of nr_pages in __folio_try_share_anon_rmap is to do some rmap sanity checks. Now, clear the PageAnonExclusive bit on a batch of nr_pages. Refactor the function such that the clearing of the bit can be done at one place without duplication.

Note that __folio_try_share_anon_rmap can receive nr_pages == HPAGE_PMD_NR from the PMD path, but currently we only clear the bit on the head page. Retain this behaviour by setting nr_pages = 1 in case the caller is folio_try_share_anon_rmap_pmd.

While at it, convert nr_pages to unsigned long to future-proof from overflow in case P4D-huge mappings etc get supported down the road. I haven't made such a change in each function receiving nr_pages in try_to_unmap_one - perhaps this can be done incrementally.

---
 include/linux/rmap.h | 52 +++++++++++++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 17 deletions(-)
include/linux/rmap.h +35 -17
index 8dc0871e5f001..64929490a7cfc 100644
@@ -706,17 +706,18 @@ static inline int folio_try_dup_anon_rmap_pmd(struct folio *folio,
 }
 
 static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
-		struct page *page, int nr_pages, enum pgtable_level level)
+		struct page *page, unsigned long nr_pages, enum pgtable_level level)
 {
+	/* device private folios cannot get pinned via GUP. */
+	const bool pinnable = likely(!folio_is_device_private(folio));
+
 	VM_WARN_ON_FOLIO(!folio_test_anon(folio), folio);
 	VM_WARN_ON_FOLIO(!PageAnonExclusive(page), folio);
 	__folio_rmap_sanity_checks(folio, page, nr_pages, level);
 
-	/* device private folios cannot get pinned via GUP. */
-	if (unlikely(folio_is_device_private(folio))) {
-		ClearPageAnonExclusive(page);
-		return 0;
-	}
+	/* We only clear anon-exclusive from head page of PMD folio */
+	if (level == PGTABLE_LEVEL_PMD)
+		nr_pages = 1;
 
 	/*
 	 * We have to make sure that when we clear PageAnonExclusive, that
@@ -760,29 +761,38 @@ static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
 	 * so we use explicit ones here.
 	 */
 
-	/* Paired with the memory barrier in try_grab_folio(). */
-	if (IS_ENABLED(CONFIG_HAVE_GUP_FAST))
-		smp_mb();
+	if (pinnable) {
+		/* Paired with the memory barrier in try_grab_folio(). */
+		if (IS_ENABLED(CONFIG_HAVE_GUP_FAST))
+			smp_mb();
 
-	if (unlikely(folio_maybe_dma_pinned(folio)))
-		return -EBUSY;
-	ClearPageAnonExclusive(page);
+		if (unlikely(folio_maybe_dma_pinned(folio)))
+			return -EBUSY;
+	}
+
+	for (;;) {
+		ClearPageAnonExclusive(page);
+		if (--nr_pages == 0)
+			break;
+		page++;
+	}
 
 	/*
 	 * This is conceptually a smp_wmb() paired with the smp_rmb() in
 	 * gup_must_unshare().
 	 */
-	if (IS_ENABLED(CONFIG_HAVE_GUP_FAST))
+	if (pinnable && IS_ENABLED(CONFIG_HAVE_GUP_FAST))
 		smp_mb__after_atomic();
 	return 0;
 }
 
 /**
- * folio_try_share_anon_rmap_pte - try marking an exclusive anonymous page
- *				   mapped by a PTE possibly shared to prepare
+ * folio_try_share_anon_rmap_ptes - try marking exclusive anonymous pages
+ *				   mapped by PTEs possibly shared to prepare
  *				   for KSM or temporary unmapping
  * @folio:	The folio to share a mapping of
- * @page:	The mapped exclusive page
+ * @page:	The first mapped exclusive page of the batch in the folio
+ * @nr_pages:	The number of pages to share in the folio (batch size)
  *
  * The caller needs to hold the page table lock and has to have the page table
  * entries cleared/invalidated.
@@ -797,11 +807,19 @@ static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
  *
  * Returns 0 if marking the mapped page possibly shared succeeded. Returns
  * -EBUSY otherwise.
+ *
+ * The caller needs to hold the page table lock.
  */
+static inline int folio_try_share_anon_rmap_ptes(struct folio *folio,
+		struct page *page, unsigned long nr_pages)
+{
+	return __folio_try_share_anon_rmap(folio, page, nr_pages, PGTABLE_LEVEL_PTE);
+}
+
 static inline int folio_try_share_anon_rmap_pte(struct folio *folio,
 		struct page *page)
 {
-	return __folio_try_share_anon_rmap(folio, page, 1, PGTABLE_LEVEL_PTE);
+	return folio_try_share_anon_rmap_ptes(folio, page, 1);
 }
 
 /**
Signature · 1 line
2.34.1
#43 lore raw

Re: [PATCH v4 09/12] mm/rmap: Add batched version of folio_try_share_anon_rmap_pte

Quoted 29 lines · Dev Jain

To enable batched unmapping of anonymous folios, we need to handle the sharing of exclusive pages. Hence, a batched version of folio_try_share_anon_rmap_pte is required.

Currently, the sole purpose of nr_pages in __folio_try_share_anon_rmap is to do some rmap sanity checks. Now, clear the PageAnonExclusive bit on a batch of nr_pages. Refactor the function such that the clearing of the bit can be done at one place without duplication.

Note that __folio_try_share_anon_rmap can receive nr_pages == HPAGE_PMD_NR from the PMD path, but currently we only clear the bit on the head page. Retain this behaviour by setting nr_pages = 1 in case the caller is folio_try_share_anon_rmap_pmd.

While at it, convert nr_pages to unsigned long to future-proof from overflow in case P4D-huge mappings etc get supported down the road. I haven't made such a change in each function receiving nr_pages in try_to_unmap_one - perhaps this can be done incrementally.

---
 include/linux/rmap.h | 52 +++++++++++++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 17 deletions(-)
include/linux/rmap.h +0 -0
index 8dc0871e5f001..64929490a7cfc 100644

include/linux/rmap.h static inline int folio_try_dup_anon_rmap_pmd(struct folio *folio, · line 712

Quoted 8 lines
include/linux/rmap.h +3 -1
@@ -706,17 +706,18 @@ static inline int folio_try_dup_anon_rmap_pmd(struct folio *folio,
 }
 
 static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
-		struct page *page, int nr_pages, enum pgtable_level level)
+		struct page *page, unsigned long nr_pages, enum pgtable_level level)
 {
+	/* device private folios cannot get pinned via GUP. */
+	const bool pinnable = likely(!folio_is_device_private(folio));

We don't seem to have a lot of users of likely/unlikely when assigning variables, so I wonder if the compiler really uses that information then.

git grep " = " | grep "likely(" | grep ";$" | wc -l 32

Maybe clearer to just push the "likely" into the actual two condition below.

[...]

Quoted 14 lines
include/linux/rmap.h +4 -3
 /**
- * folio_try_share_anon_rmap_pte - try marking an exclusive anonymous page
- *				   mapped by a PTE possibly shared to prepare
+ * folio_try_share_anon_rmap_ptes - try marking exclusive anonymous pages
+ *				   mapped by PTEs possibly shared to prepare
  *				   for KSM or temporary unmapping
  * @folio:	The folio to share a mapping of
- * @page:	The mapped exclusive page
+ * @page:	The first mapped exclusive page of the batch in the folio
+ * @nr_pages:	The number of pages to share in the folio (batch size)
  *
  * The caller needs to hold the page table lock and has to have the page table
  * entries cleared/invalidated.

include/linux/rmap.h static __always_inline int __folio_try_share_anon_rmap(struct folio *folio, · line 811

Quoted 5 lines
include/linux/rmap.h +2 -0
@@ -797,11 +807,19 @@ static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
  *
  * Returns 0 if marking the mapped page possibly shared succeeded. Returns
  * -EBUSY otherwise.
+ *
+ * The caller needs to hold the page table lock.

Isn't that stated further above?

Quoted 5 lines
include/linux/rmap.h +4 -0
  */
+static inline int folio_try_share_anon_rmap_ptes(struct folio *folio,
+		struct page *page, unsigned long nr_pages)
+{
+	return __folio_try_share_anon_rmap(folio, page, nr_pages, PGTABLE_LEVEL_PTE);

include/linux/rmap.h static __always_inline int __folio_try_share_anon_rmap(struct folio *folio, · line 825

Quoted 10 lines
include/linux/rmap.h +3 -1
+}
+
 static inline int folio_try_share_anon_rmap_pte(struct folio *folio,
 		struct page *page)
 {
-	return __folio_try_share_anon_rmap(folio, page, 1, PGTABLE_LEVEL_PTE);
+	return folio_try_share_anon_rmap_ptes(folio, page, 1);
 }
 
 /**

In general, LGTM.

Signature · 3 lines
Cheers,

David
author
#44 lore raw

Re: [PATCH v4 09/12] mm/rmap: Add batched version of folio_try_share_anon_rmap_pte

Quoted 42 lines · David Hildenbrand (Arm)
Quoted 29 lines · Dev Jain

To enable batched unmapping of anonymous folios, we need to handle the sharing of exclusive pages. Hence, a batched version of folio_try_share_anon_rmap_pte is required.

Currently, the sole purpose of nr_pages in __folio_try_share_anon_rmap is to do some rmap sanity checks. Now, clear the PageAnonExclusive bit on a batch of nr_pages. Refactor the function such that the clearing of the bit can be done at one place without duplication.

Note that __folio_try_share_anon_rmap can receive nr_pages == HPAGE_PMD_NR from the PMD path, but currently we only clear the bit on the head page. Retain this behaviour by setting nr_pages = 1 in case the caller is folio_try_share_anon_rmap_pmd.

While at it, convert nr_pages to unsigned long to future-proof from overflow in case P4D-huge mappings etc get supported down the road. I haven't made such a change in each function receiving nr_pages in try_to_unmap_one - perhaps this can be done incrementally.

---
 include/linux/rmap.h | 52 +++++++++++++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 17 deletions(-)
include/linux/rmap.h +0 -0
index 8dc0871e5f001..64929490a7cfc 100644

include/linux/rmap.h static inline int folio_try_dup_anon_rmap_pmd(struct folio *folio, · line 712

Quoted 8 lines
include/linux/rmap.h +3 -1
@@ -706,17 +706,18 @@ static inline int folio_try_dup_anon_rmap_pmd(struct folio *folio,
 }
 
 static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
-		struct page *page, int nr_pages, enum pgtable_level level)
+		struct page *page, unsigned long nr_pages, enum pgtable_level level)
 {
+	/* device private folios cannot get pinned via GUP. */
+	const bool pinnable = likely(!folio_is_device_private(folio));

We don't seem to have a lot of users of likely/unlikely when assigning variables, so I wonder if the compiler really uses that information then.

Quoted 5 lines

git grep " = " | grep "likely(" | grep ";$" | wc -l 32

Maybe clearer to just push the "likely" into the actual two condition below.

Ok.

Quoted 16 lines

[...]

Quoted 13 lines

/**
- * folio_try_share_anon_rmap_pte - try marking an exclusive anonymous page
- * mapped by a PTE possibly shared to prepare
+ * folio_try_share_anon_rmap_ptes - try marking exclusive anonymous pages
+ * mapped by PTEs possibly shared to prepare
* for KSM or temporary unmapping
* @folio: The folio to share a mapping of
- * @page: The mapped exclusive page
+ * @page: The first mapped exclusive page of the batch in the folio
+ * @nr_pages: The number of pages to share in the folio (batch size)
*
* The caller needs to hold the page table lock and has to have the page table
* entries cleared/invalidated.

Quoted 8 lines

include/linux/rmap.h static __always_inline int __folio_try_share_anon_rmap(struct folio *folio, · line 811

Quoted 6 lines
include/linux/rmap.h +2 -0
@@ -797,11 +807,19 @@ static __always_inline int __folio_try_share_anon_rmap(struct folio *folio,
  *
  * Returns 0 if marking the mapped page possibly shared succeeded. Returns
  * -EBUSY otherwise.
+ *
+ * The caller needs to hold the page table lock.

Isn't that stated further above?

I'll drop it.

Quoted 19 lines
Quoted 6 lines
include/linux/rmap.h +4 -0
  */
+static inline int folio_try_share_anon_rmap_ptes(struct folio *folio,
+		struct page *page, unsigned long nr_pages)
+{
+	return __folio_try_share_anon_rmap(folio, page, nr_pages, PGTABLE_LEVEL_PTE);

include/linux/rmap.h static __always_inline int __folio_try_share_anon_rmap(struct folio *folio, · line 825

Quoted 10 lines
include/linux/rmap.h +3 -1
+}
+
 static inline int folio_try_share_anon_rmap_pte(struct folio *folio,
 		struct page *page)
 {
-	return __folio_try_share_anon_rmap(folio, page, 1, PGTABLE_LEVEL_PTE);
+	return folio_try_share_anon_rmap_ptes(folio, page, 1);
 }
 
 /**

In general, LGTM.

author
#45 lore raw

PATCH v4 10/12 mm/rmap: refactor anon folio unmap in try_to_unmap_one

In reply to Dev Jain

Refactor anonymous folio unmap to ttu_anon_folio.

No functional change intended.

---
 mm/rmap.c | 116 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 68 insertions(+), 48 deletions(-)
mm/rmap.c +68 -48
index c0e385882f562..b1639bad8e27f 100644
@@ -2136,6 +2136,70 @@ static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
 	return true;
 }
 
+static inline void set_swp_pte_at(struct mm_struct *mm, unsigned long address,
+		pte_t *ptep, swp_entry_t entry, pte_t pteval, bool anon_exclusive)
+{
+	pte_t swp_pte = swp_entry_to_pte(entry);
+
+	if (anon_exclusive)
+		swp_pte = pte_swp_mkexclusive(swp_pte);
+
+	if (likely(pte_present(pteval))) {
+		if (pte_soft_dirty(pteval))
+			swp_pte = pte_swp_mksoft_dirty(swp_pte);
+		if (pte_uffd_wp(pteval))
+			swp_pte = pte_swp_mkuffd_wp(swp_pte);
+	} else {
+		/* Device-exclusive entry */
+		if (pte_swp_soft_dirty(pteval))
+			swp_pte = pte_swp_mksoft_dirty(swp_pte);
+		if (pte_swp_uffd_wp(pteval))
+			swp_pte = pte_swp_mkuffd_wp(swp_pte);
+	}
+
+	set_pte_at(mm, address, ptep, swp_pte);
+}
+
+static inline bool ttu_anon_folio(struct vm_area_struct *vma, struct folio *folio,
+		struct page *subpage, unsigned long address, pte_t *ptep,
+		pte_t pteval)
+{
+	bool anon_exclusive = folio_test_anon(folio) && PageAnonExclusive(subpage);
+	swp_entry_t entry = page_swap_entry(subpage);
+	struct mm_struct *mm = vma->vm_mm;
+
+	if (folio_dup_swap_pages(folio, subpage, 1) < 0)
+		return false;
+
+	/*
+	 * arch_unmap_one() is expected to be a NOP on
+	 * architectures where we could have PFN swap PTEs,
+	 * so we'll not check/care.
+	 */
+	if (arch_unmap_one(mm, vma, address, pteval) < 0) {
+		folio_put_swap_pages(folio, subpage, 1);
+		return false;
+	}
+
+	/* See folio_try_share_anon_rmap(): clear PTE first. */
+	if (anon_exclusive && folio_try_share_anon_rmap_pte(folio, subpage)) {
+		folio_put_swap_pages(folio, subpage, 1);
+		return false;
+	}
+
+	if (list_empty(&mm->mmlist)) {
+		spin_lock(&mmlist_lock);
+		if (list_empty(&mm->mmlist))
+			list_add(&mm->mmlist, &init_mm.mmlist);
+		spin_unlock(&mmlist_lock);
+	}
+
+	dec_mm_counter(mm, MM_ANONPAGES);
+	inc_mm_counter(mm, MM_SWAPENTS);
+	set_swp_pte_at(mm, address, ptep, entry, pteval, anon_exclusive);
+	return true;
+}
+
 /*
  * @arg: enum ttu_flags will be passed to this argument
  */
@@ -2144,7 +2208,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 {
 	struct mm_struct *mm = vma->vm_mm;
 	DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
-	bool anon_exclusive, ret = true;
+	bool ret = true;
 	pte_t pteval;
 	struct page *subpage;
 	struct mmu_notifier_range range;
@@ -2252,8 +2316,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 
 		subpage = folio_page(folio, pfn - folio_pfn(folio));
 		address = pvmw.address;
-		anon_exclusive = folio_test_anon(folio) &&
-				 PageAnonExclusive(subpage);
 
 		if (likely(pte_present(pteval))) {
 			nr_pages = folio_unmap_pte_batch(folio, &pvmw, flags, pteval);
@@ -2308,8 +2370,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			 */
 			dec_mm_counter(mm, mm_counter(folio));
 		} else if (folio_test_anon(folio)) {
-			swp_entry_t entry = page_swap_entry(subpage);
-			pte_t swp_pte;
 			/*
 			 * Store the swap location in the pte.
 			 * See handle_pte_fault() ...
@@ -2331,52 +2391,12 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 				goto finish_unmap;
 			}
 
-			if (folio_dup_swap_pages(folio, subpage, 1) < 0) {
-				set_pte_at(mm, address, pvmw.pte, pteval);
-				goto walk_abort;
-			}
-
-			/*
-			 * arch_unmap_one() is expected to be a NOP on
-			 * architectures where we could have PFN swap PTEs,
-			 * so we'll not check/care.
-			 */
-			if (arch_unmap_one(mm, vma, address, pteval) < 0) {
-				folio_put_swap_pages(folio, subpage, 1);
-				set_pte_at(mm, address, pvmw.pte, pteval);
-				goto walk_abort;
-			}
-
-			/* See folio_try_share_anon_rmap(): clear PTE first. */
-			if (anon_exclusive &&
-			    folio_try_share_anon_rmap_pte(folio, subpage)) {
-				folio_put_swap_pages(folio, subpage, 1);
+			if (!ttu_anon_folio(vma, folio, subpage, address,
+						    pvmw.pte, pteval)) {
 				set_pte_at(mm, address, pvmw.pte, pteval);
 				goto walk_abort;
 			}
-			if (list_empty(&mm->mmlist)) {
-				spin_lock(&mmlist_lock);
-				if (list_empty(&mm->mmlist))
-					list_add(&mm->mmlist, &init_mm.mmlist);
-				spin_unlock(&mmlist_lock);
-			}
-			dec_mm_counter(mm, MM_ANONPAGES);
-			inc_mm_counter(mm, MM_SWAPENTS);
-			swp_pte = swp_entry_to_pte(entry);
-			if (anon_exclusive)
-				swp_pte = pte_swp_mkexclusive(swp_pte);
-			if (likely(pte_present(pteval))) {
-				if (pte_soft_dirty(pteval))
-					swp_pte = pte_swp_mksoft_dirty(swp_pte);
-				if (pte_uffd_wp(pteval))
-					swp_pte = pte_swp_mkuffd_wp(swp_pte);
-			} else {
-				if (pte_swp_soft_dirty(pteval))
-					swp_pte = pte_swp_mksoft_dirty(swp_pte);
-				if (pte_swp_uffd_wp(pteval))
-					swp_pte = pte_swp_mkuffd_wp(swp_pte);
-			}
-			set_pte_at(mm, address, pvmw.pte, swp_pte);
+			goto finish_unmap;
 		} else {
 			/*
 			 * This is a locked file-backed folio,
Signature · 1 line
2.34.1
#46 lore raw

Re: [PATCH v4 10/12] mm/rmap: refactor anon folio unmap in try_to_unmap_one

Quoted 1 line · Dev Jain

Refactor anonymous folio unmap to ttu_anon_folio.

Given that the !folio_test_swapbacked() case remains in the caller, should we make it clearer and call it

ttu_anon_swapbacked_folio()

Or alternatively, factor complete anon folio handling out (incl. lazyfree handling)?

Quoted 35 lines

No functional change intended.

---
 mm/rmap.c | 116 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 68 insertions(+), 48 deletions(-)
mm/rmap.c +19 -0
index c0e385882f562..b1639bad8e27f 100644
@@ -2136,6 +2136,70 @@ static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
 	return true;
 }
 
+static inline void set_swp_pte_at(struct mm_struct *mm, unsigned long address,
+		pte_t *ptep, swp_entry_t entry, pte_t pteval, bool anon_exclusive)
+{
+	pte_t swp_pte = swp_entry_to_pte(entry);
+
+	if (anon_exclusive)
+		swp_pte = pte_swp_mkexclusive(swp_pte);
+
+	if (likely(pte_present(pteval))) {
+		if (pte_soft_dirty(pteval))
+			swp_pte = pte_swp_mksoft_dirty(swp_pte);
+		if (pte_uffd_wp(pteval))
+			swp_pte = pte_swp_mkuffd_wp(swp_pte);
+	} else {
+		/* Device-exclusive entry */
+		if (pte_swp_soft_dirty(pteval))
+			swp_pte = pte_swp_mksoft_dirty(swp_pte);
+		if (pte_swp_uffd_wp(pteval))
+			swp_pte = pte_swp_mkuffd_wp(swp_pte);

mm/rmap.c static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma, · line 2167

Quoted 10 lines
mm/rmap.c +10 -0
+	}
+
+	set_pte_at(mm, address, ptep, swp_pte);
+}
+
+static inline bool ttu_anon_folio(struct vm_area_struct *vma, struct folio *folio,
+		struct page *subpage, unsigned long address, pte_t *ptep,
+		pte_t pteval)
+{
+	bool anon_exclusive = folio_test_anon(folio) && PageAnonExclusive(subpage);

"subpage" ;)

Can be const.

[...]

mm/rmap.c static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma, · line 2402

Quoted 3 lines
mm/rmap.c +0 -0
 		} else {
 			/*
 			 * This is a locked file-backed folio,

Happy to see that function shrink a bit ...

Signature · 3 lines
Cheers,

David
author
#47 lore raw

Re: [PATCH v4 10/12] mm/rmap: refactor anon folio unmap in try_to_unmap_one

Quoted 9 lines · David Hildenbrand (Arm)
Quoted 1 line · Dev Jain

Refactor anonymous folio unmap to ttu_anon_folio.

Given that the !folio_test_swapbacked() case remains in the caller, should we make it clearer and call it

ttu_anon_swapbacked_folio()

Or alternatively, factor complete anon folio handling out (incl. lazyfree handling)?

Hmm I'll see if this can be done.

Quoted 40 lines
Quoted 40 lines

No functional change intended.

---
 mm/rmap.c | 116 ++++++++++++++++++++++++++++++++----------------------
 1 file changed, 68 insertions(+), 48 deletions(-)
mm/rmap.c +23 -0
index c0e385882f562..b1639bad8e27f 100644
@@ -2136,6 +2136,70 @@ static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma,
 	return true;
 }
 
+static inline void set_swp_pte_at(struct mm_struct *mm, unsigned long address,
+		pte_t *ptep, swp_entry_t entry, pte_t pteval, bool anon_exclusive)
+{
+	pte_t swp_pte = swp_entry_to_pte(entry);
+
+	if (anon_exclusive)
+		swp_pte = pte_swp_mkexclusive(swp_pte);
+
+	if (likely(pte_present(pteval))) {
+		if (pte_soft_dirty(pteval))
+			swp_pte = pte_swp_mksoft_dirty(swp_pte);
+		if (pte_uffd_wp(pteval))
+			swp_pte = pte_swp_mkuffd_wp(swp_pte);
+	} else {
+		/* Device-exclusive entry */
+		if (pte_swp_soft_dirty(pteval))
+			swp_pte = pte_swp_mksoft_dirty(swp_pte);
+		if (pte_swp_uffd_wp(pteval))
+			swp_pte = pte_swp_mkuffd_wp(swp_pte);
+	}
+
+	set_pte_at(mm, address, ptep, swp_pte);
+}
Quoted 8 lines

mm/rmap.c static inline bool ttu_lazyfree_folio(struct vm_area_struct *vma, · line 2167

Quoted 6 lines
mm/rmap.c +6 -0
+
+static inline bool ttu_anon_folio(struct vm_area_struct *vma, struct folio *folio,
+		struct page *subpage, unsigned long address, pte_t *ptep,
+		pte_t pteval)
+{
+	bool anon_exclusive = folio_test_anon(folio) && PageAnonExclusive(subpage);

"subpage" ;)

I'll change that.

Quoted 2 lines

Can be const.

Okay.

Quoted 9 lines

[...]

mm/rmap.c static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma, · line 2402

Quoted 3 lines
mm/rmap.c +0 -0
 		} else {
 			/*
 			 * This is a locked file-backed folio,

Happy to see that function shrink a bit ...

author
#48 lore raw

PATCH v4 11/12 mm/mprotect: drop 'sub' from page_anon_exclusive_sub_batch

In reply to Dev Jain

Shorten the function name by dropping the "sub-batch" context - the function itself doesn't need this context.

---
 mm/mprotect.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
mm/mprotect.c +2 -2
index 9cbf932b028cf..3357058672016 100644
@@ -143,7 +143,7 @@ static __always_inline void prot_commit_flush_ptes(struct vm_area_struct *vma,
  * !PageAnonExclusive() pages, starting from start_idx. Caller must enforce
  * that the ptes point to consecutive pages of the same anon large folio.
  */
-static __always_inline int page_anon_exclusive_sub_batch(int start_idx, int max_len,
+static __always_inline int page_anon_exclusive_batch(int start_idx, int max_len,
 		struct page *first_page, bool expected_anon_exclusive)
 {
 	int idx;
@@ -179,7 +179,7 @@ static __always_inline void commit_anon_folio_batch(struct vm_area_struct *vma,
 
 	while (nr_ptes) {
 		expected_anon_exclusive = PageAnonExclusive(first_page + sub_batch_idx);
-		len = page_anon_exclusive_sub_batch(sub_batch_idx, nr_ptes,
+		len = page_anon_exclusive_batch(sub_batch_idx, nr_ptes,
 					first_page, expected_anon_exclusive);
 		prot_commit_flush_ptes(vma, addr, ptep, oldpte, ptent, len,
 				       sub_batch_idx, expected_anon_exclusive, tlb);
Signature · 1 line
2.34.1
#49 lore raw

Re: [PATCH v4 11/12] mm/mprotect: drop 'sub' from page_anon_exclusive_sub_batch

Quoted 2 lines · Dev Jain

Shorten the function name by dropping the "sub-batch" context - the function itself doesn't need this context.

What about "sub_batch_idx" ?

This function is rather independent of the rest, so best to send that as a cleanup separately.

Quoted 28 lines
---
 mm/mprotect.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
mm/mprotect.c +2 -2
index 9cbf932b028cf..3357058672016 100644
@@ -143,7 +143,7 @@ static __always_inline void prot_commit_flush_ptes(struct vm_area_struct *vma,
  * !PageAnonExclusive() pages, starting from start_idx. Caller must enforce
  * that the ptes point to consecutive pages of the same anon large folio.
  */
-static __always_inline int page_anon_exclusive_sub_batch(int start_idx, int max_len,
+static __always_inline int page_anon_exclusive_batch(int start_idx, int max_len,
 		struct page *first_page, bool expected_anon_exclusive)
 {
 	int idx;
@@ -179,7 +179,7 @@ static __always_inline void commit_anon_folio_batch(struct vm_area_struct *vma,
 
 	while (nr_ptes) {
 		expected_anon_exclusive = PageAnonExclusive(first_page + sub_batch_idx);
-		len = page_anon_exclusive_sub_batch(sub_batch_idx, nr_ptes,
+		len = page_anon_exclusive_batch(sub_batch_idx, nr_ptes,
 					first_page, expected_anon_exclusive);
 		prot_commit_flush_ptes(vma, addr, ptep, oldpte, ptent, len,
 				       sub_batch_idx, expected_anon_exclusive, tlb);
Signature · 3 lines
Cheers,

David
author
#50 lore raw

Re: [PATCH v4 11/12] mm/mprotect: drop 'sub' from page_anon_exclusive_sub_batch

Quoted 5 lines · David Hildenbrand (Arm)
Quoted 2 lines · Dev Jain

Shorten the function name by dropping the "sub-batch" context - the function itself doesn't need this context.

What about "sub_batch_idx" ?

I'll change that.

Quoted 3 lines

This function is rather independent of the rest, so best to send that as a cleanup separately.

Okay.

Quoted 31 lines
Quoted 29 lines
---
 mm/mprotect.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
mm/mprotect.c +2 -2
index 9cbf932b028cf..3357058672016 100644
@@ -143,7 +143,7 @@ static __always_inline void prot_commit_flush_ptes(struct vm_area_struct *vma,
  * !PageAnonExclusive() pages, starting from start_idx. Caller must enforce
  * that the ptes point to consecutive pages of the same anon large folio.
  */
-static __always_inline int page_anon_exclusive_sub_batch(int start_idx, int max_len,
+static __always_inline int page_anon_exclusive_batch(int start_idx, int max_len,
 		struct page *first_page, bool expected_anon_exclusive)
 {
 	int idx;
@@ -179,7 +179,7 @@ static __always_inline void commit_anon_folio_batch(struct vm_area_struct *vma,
 
 	while (nr_ptes) {
 		expected_anon_exclusive = PageAnonExclusive(first_page + sub_batch_idx);
-		len = page_anon_exclusive_sub_batch(sub_batch_idx, nr_ptes,
+		len = page_anon_exclusive_batch(sub_batch_idx, nr_ptes,
 					first_page, expected_anon_exclusive);
 		prot_commit_flush_ptes(vma, addr, ptep, oldpte, ptent, len,
 				       sub_batch_idx, expected_anon_exclusive, tlb);
author
#51 lore raw

PATCH v4 12/12 mm/rmap: enable batch unmapping of anonymous folios

In reply to Dev Jain

Enable batch clearing of ptes, and batch swap setting of ptes for anon folio unmapping.

Processing all ptes of a large folio in one go helps us batch across atomics (add_mm_counter etc), barriers (in the function __folio_try_share_anon_rmap), repeated calls to page_vma_mapped_walk(), to name a few. In general, batching helps us to execute similar code together, making the execution of the program more memory and CPU friendly.

On arm64-contpte, batching also helps us avoid redundant ptep_get() calls and TLB flushes while breaking the contpte mapping.

The handling of anon-exclusivity is very similar to commit cac1db8c3aad ("mm: optimize mprotect() by PTE batching"). Since folio_unmap_pte_batch() won't look at the bits of the underlying page, we need to process sub-batches of ptes pointing to pages which are same w.r.t exclusivity, and batch set only those ptes to swap ptes in one go. Hence export page_anon_exclusive_batch() to internal.h and reuse it.

arch_unmap_one() is only defined for sparc64; I am not comfortable regarding the nuances between retrieving the pfn from pte_pfn() or from (paddr = pte_val(oldpte) & _PAGE_PADDR_4V).

(And, pte_next_pfn() can't even be called from arch_unmap_one() because that file does not include pgtable.h) So just disable the "sparc64-anon-swapbacked" case for now.

We need to take care of rmap accounting (folio_remove_rmap_ptes) and reference accounting (folio_put_refs) when anon folio unmap succeeds. In case we partially batch the large folio and fail, we need to correctly do the accounting for pages which were successfully unmapped. So, put this accounting code (finish_folio_unmap()) in __ttu_anon_folio() itself, instead of doing some horrible goto jumping at the callsite of ttu_anon_folio().

If the batch length is less than the number of pages in the folio, then we must skip over this batch.

The page_vma_mapped_walk API ensures this - check_pte() will return true only if any of [pvmw->pfn, pvmw->pfn + nr_pages) is mapped by the pte. There is no pfn underlying a swap pte, so check_pte returns false and we keep skipping until we hit a present pte, which is where we want to start unmapping from next.

---
 mm/internal.h | 17 +++++++++
 mm/mprotect.c | 17 ---------
 mm/rmap.c     | 98 +++++++++++++++++++++++++++++++++++++++------------
 3 files changed, 92 insertions(+), 40 deletions(-)
mm/internal.h +17 -0
index 5a2ddcf68e0b6..87a61742d1920 100644
@@ -393,6 +393,23 @@ static inline unsigned int folio_pte_batch_flags(struct folio *folio,
 unsigned int folio_pte_batch(struct folio *folio, pte_t *ptep, pte_t pte,
 		unsigned int max_nr);
 
+/*
+ * Get max length of consecutive ptes pointing to PageAnonExclusive() pages or
+ * !PageAnonExclusive() pages, starting from start_idx. Caller must enforce
+ * that the ptes point to consecutive pages of the same anon large folio.
+ */
+static __always_inline int page_anon_exclusive_batch(int start_idx, int max_len,
+		struct page *first_page, bool expected_anon_exclusive)
+{
+	int idx;
+
+	for (idx = start_idx + 1; idx < start_idx + max_len; ++idx) {
+		if (expected_anon_exclusive != PageAnonExclusive(first_page + idx))
+			break;
+	}
+	return idx - start_idx;
+}
+
 /**
  * pte_move_swp_offset - Move the swap entry offset field of a swap pte
  *	 forward or backward by delta
mm/mprotect.c +0 -17
index 3357058672016..950af1efdd661 100644
@@ -138,23 +138,6 @@ static __always_inline void prot_commit_flush_ptes(struct vm_area_struct *vma,
 		tlb_flush_pte_range(tlb, addr, nr_ptes * PAGE_SIZE);
 }
 
-/*
- * Get max length of consecutive ptes pointing to PageAnonExclusive() pages or
- * !PageAnonExclusive() pages, starting from start_idx. Caller must enforce
- * that the ptes point to consecutive pages of the same anon large folio.
- */
-static __always_inline int page_anon_exclusive_batch(int start_idx, int max_len,
-		struct page *first_page, bool expected_anon_exclusive)
-{
-	int idx;
-
-	for (idx = start_idx + 1; idx < start_idx + max_len; ++idx) {
-		if (expected_anon_exclusive != PageAnonExclusive(first_page + idx))
-			break;
-	}
-	return idx - start_idx;
-}
-
 /*
  * This function is a result of trying our very best to retain the
  * "avoid the write-fault handler" optimization. In can_change_pte_writable(),
mm/rmap.c +75 -23

163 lines not on this page · raw message

Signature · 1 line
2.34.1
author
#52 lore raw
In reply to Dev Jain

Sashiko:

Patch 9:

About anon-exclusivity: At this point, the batched helper is not called with nr_pages > 1. Later, anon-exclusive uniformity is handled.

On PUD folio: that is never passed to folio_try_share_anon_rmap. I was following the pattern followed elsewhere, given that PUD THP is not supported yet. Although there is no harm in changing the check to (level != PGTABLE_LEVEL_PTE).

On hugetlb folio: that is never passed.

Patch 11:

Existing bug: doesn't look like it. uffd-wp is not there for a migration-writable softleaf entry.

Patch 12:

ABout sparc64: Yep I'll change this to #ifdef or something. Apparently is_defined() doesn't work in case the macro doesn't have a value.

About dirty young and uffd-wp bits: we don't care about dirty and young bits since we are converting to swap ptes. uff-wp uniformity is preserved through folio_pte_batch.

About stale nr_pages: The patch initializing nr_pages = 1 at the start of the loop is already in mm-hotfixes.

About O(N): Yes I will update this to retain the nr_pages == folio_nr_pages() early bail out.

#53 lore raw
In reply to Dev Jain
Quoted 46 lines · Dev Jain

Speed up unmapping of anonymous large folios by clearing the ptes, and setting swap ptes, in one go.

The following benchmark (stolen from Barry at [1]) is used to measure the time taken to swapout 256M worth of memory backed by 64K large folios:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>

#define SIZE_MB 256
#define SIZE_BYTES (SIZE_MB * 1024 * 1024)

 int main() {
     void *addr = mmap(NULL, SIZE_BYTES, PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
     if (addr == MAP_FAILED) {
         perror("mmap failed");
         return 1;
     }

     memset(addr, 0, SIZE_BYTES);

     struct timespec start, end;
     clock_gettime(CLOCK_MONOTONIC, &start);

     if (madvise(addr, SIZE_BYTES, MADV_PAGEOUT) != 0) {
         perror("madvise(MADV_PAGEOUT) failed");
         munmap(addr, SIZE_BYTES);
         return 1;
     }

     clock_gettime(CLOCK_MONOTONIC, &end);

     long duration_ns = (end.tv_sec - start.tv_sec) * 1e9 +
                        (end.tv_nsec - start.tv_nsec);
     printf("madvise(MADV_PAGEOUT) took %ld ns (%.3f ms)\n",
            duration_ns, duration_ns / 1e6);

     munmap(addr, SIZE_BYTES);
     return 0;
Quoted 6 lines

}

Performance as measured on a Linux VM on Apple M3 (arm64):

Vanilla - Mean: 37401913 ns, std dev: 12%
Patched - Mean: 17420282 ns, std dev: 11%

-53%

Quoted 7 lines

No regression observed on 4K folios.

Performance as measured on bare metal x86:

Vanilla - mean: 54986286 ns, std dev: 1.5%
Patched - mean: 51930795 ns, std dev: 3%

That's still -5.5% (with a stddev of 3%).

What happens on larger folio sizes? (e.g., 1 MiB)

Signature · 3 lines
Cheers,

David
#54 lore raw
In reply to Dev Jain
Quoted 5 lines · Dev Jain

Speed up unmapping of anonymous large folios by clearing the ptes, and setting swap ptes, in one go.

The following benchmark (stolen from Barry at [1]) is used to measure the time taken to swapout 256M worth of memory backed by 64K large folios:

I suggest sending out patch #1->#3 ahead of times as separate cleanups, so we get more targeted review for them.

Signature · 3 lines
Cheers,

David
author
#55 lore raw
Quoted 9 lines · David Hildenbrand (Arm)
Quoted 5 lines · Dev Jain

Speed up unmapping of anonymous large folios by clearing the ptes, and setting swap ptes, in one go.

The following benchmark (stolen from Barry at [1]) is used to measure the time taken to swapout 256M worth of memory backed by 64K large folios:

I suggest sending out patch #1->#3 ahead of times as separate cleanups, so we get more targeted review for them.

Okay I will do this.

Quoted 1 line