Skip to contentSkip to conversation

mm/hugetlb: fixes for PMD table sharing (incl. using mmu_gather)

PATCH Applied David Hildenbrand (Red Hat) 9 participants 32 messages Dec 5, 2025 – Dec 18, 2025

Subsystems (7)

v1 of 3 Applied 4/4 Latest is v3 →

lore
Share & tools
lore

David Hildenbrand (Red Hat)

author maintainer
#0 lore raw

cover mm/hugetlb: fixes for PMD table sharing (incl. using mmu_gather)

One functional fix, one performance regression fix, and two related comment fixes.

I cleaned up my prototype I recently shared [1] for the performance fix, deferring most of the cleanups I had in the prototype to a later point. While doing that I identified the other things.

The goal of this patch set is to be backported to stable trees "fairly" easily. At least patch #1 and #4.

Patch #1 fixes hugetlb_pmd_shared() not detecting any sharing Patch #2 + #3 are simple comment fixes that patch #4 interacts with. Patch #4 is a fix for the reported performance regression due to excessive IPI broadcasts during fork()+exit().

The last patch is all about TLB flushes, IPIs and mmu_gather.
Read: complicated

I added as much comments + description that I possibly could, and I am hoping for review from Jann.

There are plenty of cleanups in the future to be had + one reasonable optimization on x86. But that's all out of scope for this series.

Compile tested on plenty of architectures.

Runtime tested, with a focus on fixing the performance regression using the original reproducer [2] on x86.

I'm still busy with more testing (making sure that my TLB flushing changes are good), but sending this out already so people can test and review while I am soon heading for LPC.

[1] https://lore.kernel.org/all/[email protected]/
[2] https://lore.kernel.org/all/[email protected]/

Cc Will Deacon <[email protected]>
Cc "Aneesh Kumar K.V" <[email protected]>
Cc Andrew Morton <[email protected]>
Cc Nick Piggin <[email protected]>
Cc Peter Zijlstra <[email protected]>
Cc Arnd Bergmann <[email protected]>
Cc Muchun Song <[email protected]>
Cc Oscar Salvador <[email protected]>
Cc "Liam R. Howlett" <[email protected]>
Cc Lorenzo Stoakes <[email protected]>
Cc Vlastimil Babka <[email protected]>
Cc Jann Horn <[email protected]>
Cc Pedro Falcato <[email protected]>
Cc Rik van Riel <[email protected]>
Cc Harry Yoo <[email protected]>
Cc Uschakow, Stanislav" <[email protected]>
Cc Laurence Oberman <[email protected]>
Cc Prakash Sangappa <[email protected]>
Cc Nadav Amit <[email protected]>

David Hildenbrand (Red Hat) (4):
mm/hugetlb: fix hugetlb_pmd_shared()
mm/hugetlb: fix two comments related to huge_pmd_unshare() mm/rmap: fix two comments related to huge_pmd_unshare() mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables
using mmu_gather

 include/asm-generic/tlb.h |  69 +++++++++++++++++++-
 include/linux/hugetlb.h   |  21 ++++---
 mm/hugetlb.c              | 129 ++++++++++++++++++++------------------
 mm/mmu_gather.c           |   6 ++
 mm/mprotect.c             |   2 +-
 mm/rmap.c                 |  45 +++++++------
 6 files changed, 178 insertions(+), 94 deletions(-)
Signature · 1 line
2.52.0
author maintainer
#1 lore raw

PATCH 1/4 mm/hugetlb: fix hugetlb_pmd_shared()

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>
---
 include/linux/hugetlb.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/hugetlb.h +1 -1
index 019a1c5281e4e..03c8725efa289 100644
@@ -1326,7 +1326,7 @@ static inline __init void hugetlb_cma_reserve(int order)
 #ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
 static inline bool hugetlb_pmd_shared(pte_t *pte)
 {
-	return page_count(virt_to_page(pte)) > 1;
+	return ptdesc_pmd_is_shared(virt_to_ptdesc(pte));
 }
 #else
 static inline bool hugetlb_pmd_shared(pte_t *pte)
Signature · 1 line
2.52.0
reviewer Reviewed-by on v1 1/4
#2 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

Quoted 10 lines · David Hildenbrand (Red Hat)

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared

count")

Signature · 1 line
All Rights Reversed.
Reviewed-by on v1 1/4
#3 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

In reply to David Hildenbrand (Red Hat)

From: Lance Yang <[email protected]>

Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

---

Good catch! Feel free to add:

Quoted 16 lines
 include/linux/hugetlb.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/hugetlb.h +1 -1
index 019a1c5281e4e..03c8725efa289 100644
@@ -1326,7 +1326,7 @@ static inline __init void hugetlb_cma_reserve(int order)
 #ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
 static inline bool hugetlb_pmd_shared(pte_t *pte)
 {
-	return page_count(virt_to_page(pte)) > 1;
+	return ptdesc_pmd_is_shared(virt_to_ptdesc(pte));
 }
 #else
 static inline bool hugetlb_pmd_shared(pte_t *pte)
reviewer Reviewed-by
#4 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

Quoted 27 lines · Lance Yang

From: Lance Yang <[email protected]>

Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

---

Quoted 3 lines

Good catch! Feel free to add:

Actually:

Tested-by on v1 1/4
#5 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

In reply to David Hildenbrand (Red Hat)

From: Lance Yang <[email protected]>

Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

---

Tested on x86 with two independent processes sharing a 1GiB hugetlbfs file (aligned a 1GiB boundary).

Before the fix, even though PMD sharing worked (pt_share_count=1), hugetlb_pmd_shared() returned false because page_count() was still 1, causing smaps to report it as "Private" and pagemap to set it PM_MMAP_EXCLUSIVE incorrectly :(

After the fix, hugetlb_pmd_shared() correctly detects the sharing, smaps reports it as "Shared", and PM_MMAP_EXCLUSIVE is cleared ;)

Cheers!

Quoted 16 lines
 include/linux/hugetlb.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/hugetlb.h +1 -1
index 019a1c5281e4e..03c8725efa289 100644
@@ -1326,7 +1326,7 @@ static inline __init void hugetlb_cma_reserve(int order)
 #ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
 static inline bool hugetlb_pmd_shared(pte_t *pte)
 {
-	return page_count(virt_to_page(pte)) > 1;
+	return ptdesc_pmd_is_shared(virt_to_ptdesc(pte));
 }
 #else
 static inline bool hugetlb_pmd_shared(pte_t *pte)
author maintainer
#6 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

Quoted 35 lines · Lance Yang

From: Lance Yang <[email protected]>

Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

---

Tested on x86 with two independent processes sharing a 1GiB hugetlbfs file (aligned a 1GiB boundary).

Before the fix, even though PMD sharing worked (pt_share_count=1), hugetlb_pmd_shared() returned false because page_count() was still 1, causing smaps to report it as "Private" and pagemap to set it PM_MMAP_EXCLUSIVE incorrectly :(

Quoted 4 lines

After the fix, hugetlb_pmd_shared() correctly detects the sharing, smaps reports it as "Shared", and PM_MMAP_EXCLUSIVE is cleared ;)

Thanks a lot Lance for the testing and thanks to everybody for the review!

Signature · 3 lines
Cheers

David
#7 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

In reply to Lance Yang
Quoted 30 lines · Lance Yang

From: Lance Yang <[email protected]>

Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

---

Tested on x86 with two independent processes sharing a 1GiB hugetlbfs file (aligned a 1GiB boundary).

Quoted 7 lines

Before the fix, even though PMD sharing worked (pt_share_count=1), hugetlb_pmd_shared() returned false because page_count() was still 1, causing smaps to report it as "Private" and pagemap to set it PM_MMAP_EXCLUSIVE incorrectly :(

After the fix, hugetlb_pmd_shared() correctly detects the sharing, smaps reports it as "Shared", and PM_MMAP_EXCLUSIVE is cleared ;)

Yikes yikes yikes...

I wonder what else might be broken in this stuff :/

Quoted 21 lines

Cheers!

Quoted 16 lines
 include/linux/hugetlb.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/hugetlb.h +1 -1
index 019a1c5281e4e..03c8725efa289 100644
@@ -1326,7 +1326,7 @@ static inline __init void hugetlb_cma_reserve(int order)
 #ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
 static inline bool hugetlb_pmd_shared(pte_t *pte)
 {
-	return page_count(virt_to_page(pte)) > 1;
+	return ptdesc_pmd_is_shared(virt_to_ptdesc(pte));
 }
 #else
 static inline bool hugetlb_pmd_shared(pte_t *pte)
Reviewed-by on v1 1/4
#8 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

In reply to David Hildenbrand (Red Hat)
Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

---

Oops, didn't notice there's still missing conversion!

Looks good to me,

Signature · 2 lines
Cheers,
Harry / Hyeonggon
Reviewed-by on v1 1/4
#9 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

In reply to David Hildenbrand (Red Hat)
Quoted 9 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Quoted 5 lines

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Yikes this seems pretty serious!!

How did we not pick up on this before...

Quoted 7 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

Esp. given Lance's testing... LGTM so:

Quoted 20 lines
---
 include/linux/hugetlb.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/hugetlb.h +1 -2
index 019a1c5281e4e..03c8725efa289 100644
@@ -1326,7 +1326,7 @@ static inline __init void hugetlb_cma_reserve(int order)
 #ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
 static inline bool hugetlb_pmd_shared(pte_t *pte)
 {
-	return page_count(virt_to_page(pte)) > 1;
+	return ptdesc_pmd_is_shared(virt_to_ptdesc(pte));
 }
 #else
 static inline bool hugetlb_pmd_shared(pte_t *pte)
--

2.52.0

maintainer Acked-by on v1 1/4
#10 lore raw

Re: [PATCH v1 1/4] mm/hugetlb: fix hugetlb_pmd_shared()

In reply to David Hildenbrand (Red Hat)
Quoted 15 lines · David Hildenbrand (Red Hat)

We switched from (wrongly) using the page count to an independent shared count. Now, shared page tables have a refcount of 1 (excluding speculative references) and instead use ptdesc->pt_share_count to identify sharing.

We didn't convert hugetlb_pmd_shared(), so right now, we would never detect a shared PMD table as such, because sharing/unsharing no longer touches the refcount of a PMD table.

Page migration, like mbind() or migrate_pages() would allow for migrating folios mapped into such shared PMD tables, even though the folios are not exclusive. In smaps we would account them as "private" although they are "shared", and we would be wrongly setting the PM_MMAP_EXCLUSIVE in the pagemap interface.

Quoted 6 lines

Fix it by properly using ptdesc_pmd_is_shared() in hugetlb_pmd_shared().

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

Good catch David,

Quoted 21 lines
---
 include/linux/hugetlb.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/hugetlb.h +1 -1
index 019a1c5281e4e..03c8725efa289 100644
@@ -1326,7 +1326,7 @@ static inline __init void hugetlb_cma_reserve(int order)
 #ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
 static inline bool hugetlb_pmd_shared(pte_t *pte)
 {
-	return page_count(virt_to_page(pte)) > 1;
+	return ptdesc_pmd_is_shared(virt_to_ptdesc(pte));
 }
 #else
 static inline bool hugetlb_pmd_shared(pte_t *pte)
Signature · 1 line
2.52.0
Signature · 2 lines
Oscar Salvador
SUSE Labs
author maintainer
#11 lore raw

PATCH 2/4 mm/hugetlb: fix two comments related to huge_pmd_unshare()

In reply to David Hildenbrand (Red Hat)

Ever since we stopped using the page count to detect shared PMD page tables, these comments are outdated.

The only reason we have to flush the TLB early is because once we drop the i_mmap_rwsem, the previously shared page table could get freed (to then get reallocated and used for other purpose). So we really have to flush the TLB before that could happen.

So let's simplify the comments a bit.

The "If we unshared PMDs, the TLB flush was not recorded in mmu_gather." part introduced as in commit a4a118f2eead ("hugetlbfs: flush TLBs correctly after huge_pmd_unshare") was confusing: sure it is recorded in the mmu_gather, otherwise tlb_flush_mmu_tlbonly() wouldn't do anything. So let's drop that comment while at it as well.

We'll centralize these comments in a single helper as we rework the code next.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>
---
 mm/hugetlb.c | 24 ++++++++----------------
 1 file changed, 8 insertions(+), 16 deletions(-)
mm/hugetlb.c +8 -16
index 51273baec9e5d..3c77cdef12a32 100644
@@ -5304,17 +5304,10 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	tlb_end_vma(tlb, vma);
 
 	/*
-	 * If we unshared PMDs, the TLB flush was not recorded in mmu_gather. We
-	 * could defer the flush until now, since by holding i_mmap_rwsem we
-	 * guaranteed that the last reference would not be dropped. But we must
-	 * do the flushing before we return, as otherwise i_mmap_rwsem will be
-	 * dropped and the last reference to the shared PMDs page might be
-	 * dropped as well.
-	 *
-	 * In theory we could defer the freeing of the PMD pages as well, but
-	 * huge_pmd_unshare() relies on the exact page_count for the PMD page to
-	 * detect sharing, so we cannot defer the release of the page either.
-	 * Instead, do flush now.
+	 * There is nothing protecting a previously-shared page table that we
+	 * unshared through huge_pmd_unshare() from getting freed after we
+	 * release i_mmap_rwsem, so flush the TLB now. If huge_pmd_unshare()
+	 * succeeded, flush the range corresponding to the pud.
 	 */
 	if (force_flush)
 		tlb_flush_mmu_tlbonly(tlb);
@@ -6536,11 +6529,10 @@ long hugetlb_change_protection(struct vm_area_struct *vma,
 		cond_resched();
 	}
 	/*
-	 * Must flush TLB before releasing i_mmap_rwsem: x86's huge_pmd_unshare
-	 * may have cleared our pud entry and done put_page on the page table:
-	 * once we release i_mmap_rwsem, another task can do the final put_page
-	 * and that page table be reused and filled with junk.  If we actually
-	 * did unshare a page of pmds, flush the range corresponding to the pud.
+	 * There is nothing protecting a previously-shared page table that we
+	 * unshared through huge_pmd_unshare() from getting freed after we
+	 * release i_mmap_rwsem, so flush the TLB now. If huge_pmd_unshare()
+	 * succeeded, flush the range corresponding to the pud.
 	 */
 	if (shared_pmd)
 		flush_hugetlb_tlb_range(vma, range.start, range.end);
Signature · 1 line
2.52.0
Reviewed-by on v1 2/4
#13 lore raw

Re: [PATCH v1 2/4] mm/hugetlb: fix two comments related to huge_pmd_unshare()

In reply to David Hildenbrand (Red Hat)
Quoted 16 lines · David Hildenbrand (Red Hat)

Ever since we stopped using the page count to detect shared PMD page tables, these comments are outdated.

The only reason we have to flush the TLB early is because once we drop the i_mmap_rwsem, the previously shared page table could get freed (to then get reallocated and used for other purpose). So we really have to flush the TLB before that could happen.

So let's simplify the comments a bit.

The "If we unshared PMDs, the TLB flush was not recorded in mmu_gather." part introduced as in commit a4a118f2eead ("hugetlbfs: flush TLBs correctly after huge_pmd_unshare") was confusing: sure it is recorded in the mmu_gather, otherwise tlb_flush_mmu_tlbonly() wouldn't do anything. So let's drop that comment while at it as well.

Quoted 6 lines

We'll centralize these comments in a single helper as we rework the code next.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

LGTM, so:

Quoted 12 lines
---
 mm/hugetlb.c | 24 ++++++++----------------
 1 file changed, 8 insertions(+), 16 deletions(-)
mm/hugetlb.c +0 -0
index 51273baec9e5d..3c77cdef12a32 100644
@@ -5304,17 +5304,10 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	tlb_end_vma(tlb, vma);
 
 	/*

mm/hugetlb.c void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma, · line 5316

Quoted 10 lines
mm/hugetlb.c +0 -10
-	 * If we unshared PMDs, the TLB flush was not recorded in mmu_gather. We
-	 * could defer the flush until now, since by holding i_mmap_rwsem we
-	 * guaranteed that the last reference would not be dropped. But we must
-	 * do the flushing before we return, as otherwise i_mmap_rwsem will be
-	 * dropped and the last reference to the shared PMDs page might be
-	 * dropped as well.
-	 *
-	 * In theory we could defer the freeing of the PMD pages as well, but
-	 * huge_pmd_unshare() relies on the exact page_count for the PMD page to
-	 * detect sharing, so we cannot defer the release of the page either.

Was it this comment that led you to question the page_count issue? :)

Quoted 27 lines
mm/hugetlb.c +8 -7
-	 * Instead, do flush now.
+	 * There is nothing protecting a previously-shared page table that we
+	 * unshared through huge_pmd_unshare() from getting freed after we
+	 * release i_mmap_rwsem, so flush the TLB now. If huge_pmd_unshare()
+	 * succeeded, flush the range corresponding to the pud.
 	 */
 	if (force_flush)
 		tlb_flush_mmu_tlbonly(tlb);
@@ -6536,11 +6529,10 @@ long hugetlb_change_protection(struct vm_area_struct *vma,
 		cond_resched();
 	}
 	/*
-	 * Must flush TLB before releasing i_mmap_rwsem: x86's huge_pmd_unshare
-	 * may have cleared our pud entry and done put_page on the page table:
-	 * once we release i_mmap_rwsem, another task can do the final put_page
-	 * and that page table be reused and filled with junk.  If we actually
-	 * did unshare a page of pmds, flush the range corresponding to the pud.
+	 * There is nothing protecting a previously-shared page table that we
+	 * unshared through huge_pmd_unshare() from getting freed after we
+	 * release i_mmap_rwsem, so flush the TLB now. If huge_pmd_unshare()
+	 * succeeded, flush the range corresponding to the pud.
 	 */
 	if (shared_pmd)
 		flush_hugetlb_tlb_range(vma, range.start, range.end);
--

2.52.0

author maintainer
#14 lore raw

Re: [PATCH v1 2/4] mm/hugetlb: fix two comments related to huge_pmd_unshare()

Quoted 20 lines · Lorenzo Stoakes
Quoted 19 lines · David Hildenbrand (Red Hat)

Ever since we stopped using the page count to detect shared PMD page tables, these comments are outdated.

The only reason we have to flush the TLB early is because once we drop the i_mmap_rwsem, the previously shared page table could get freed (to then get reallocated and used for other purpose). So we really have to flush the TLB before that could happen.

So let's simplify the comments a bit.

The "If we unshared PMDs, the TLB flush was not recorded in mmu_gather." part introduced as in commit a4a118f2eead ("hugetlbfs: flush TLBs correctly after huge_pmd_unshare") was confusing: sure it is recorded in the mmu_gather, otherwise tlb_flush_mmu_tlbonly() wouldn't do anything. So let's drop that comment while at it as well.

We'll centralize these comments in a single helper as we rework the code next.

Quoted 7 lines
Quoted 3 lines
Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

LGTM, so:

Thanks!

Quoted 12 lines
Quoted 12 lines
---
  mm/hugetlb.c | 24 ++++++++----------------
  1 file changed, 8 insertions(+), 16 deletions(-)
mm/hugetlb.c +0 -0
index 51273baec9e5d..3c77cdef12a32 100644
@@ -5304,17 +5304,10 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
  	tlb_end_vma(tlb, vma);
 
Quoted 13 lines

mm/hugetlb.c void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma, · line 5316

Quoted 11 lines
mm/hugetlb.c +0 -10
  	/*
-	 * If we unshared PMDs, the TLB flush was not recorded in mmu_gather. We
-	 * could defer the flush until now, since by holding i_mmap_rwsem we
-	 * guaranteed that the last reference would not be dropped. But we must
-	 * do the flushing before we return, as otherwise i_mmap_rwsem will be
-	 * dropped and the last reference to the shared PMDs page might be
-	 * dropped as well.
-	 *
-	 * In theory we could defer the freeing of the PMD pages as well, but
-	 * huge_pmd_unshare() relies on the exact page_count for the PMD page to
-	 * detect sharing, so we cannot defer the release of the page either.

Was it this comment that led you to question the page_count issue? :)

Heh, no, I know about the changed handling already. I stumbled over the page_count() remaining usage while working on some cleanups I previously had as part of this series :)

Signature · 3 lines
Cheers

David
#15 lore raw

Re: [PATCH v1 2/4] mm/hugetlb: fix two comments related to huge_pmd_unshare()

Quoted 57 lines · David Hildenbrand (Red Hat)
Quoted 20 lines · Lorenzo Stoakes
Quoted 19 lines · David Hildenbrand (Red Hat)

Ever since we stopped using the page count to detect shared PMD page tables, these comments are outdated.

The only reason we have to flush the TLB early is because once we drop the i_mmap_rwsem, the previously shared page table could get freed (to then get reallocated and used for other purpose). So we really have to flush the TLB before that could happen.

So let's simplify the comments a bit.

The "If we unshared PMDs, the TLB flush was not recorded in mmu_gather." part introduced as in commit a4a118f2eead ("hugetlbfs: flush TLBs correctly after huge_pmd_unshare") was confusing: sure it is recorded in the mmu_gather, otherwise tlb_flush_mmu_tlbonly() wouldn't do anything. So let's drop that comment while at it as well.

We'll centralize these comments in a single helper as we rework the code next.

Quoted 7 lines
Quoted 3 lines
Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

LGTM, so:

Thanks!

Quoted 12 lines
Quoted 12 lines
---
  mm/hugetlb.c | 24 ++++++++----------------
  1 file changed, 8 insertions(+), 16 deletions(-)
mm/hugetlb.c +0 -0
index 51273baec9e5d..3c77cdef12a32 100644
@@ -5304,17 +5304,10 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
  	tlb_end_vma(tlb, vma);
 
Quoted 13 lines

mm/hugetlb.c void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma, · line 5316

Quoted 11 lines
mm/hugetlb.c +0 -10
  	/*
-	 * If we unshared PMDs, the TLB flush was not recorded in mmu_gather. We
-	 * could defer the flush until now, since by holding i_mmap_rwsem we
-	 * guaranteed that the last reference would not be dropped. But we must
-	 * do the flushing before we return, as otherwise i_mmap_rwsem will be
-	 * dropped and the last reference to the shared PMDs page might be
-	 * dropped as well.
-	 *
-	 * In theory we could defer the freeing of the PMD pages as well, but
-	 * huge_pmd_unshare() relies on the exact page_count for the PMD page to
-	 * detect sharing, so we cannot defer the release of the page either.

Was it this comment that led you to question the page_count issue? :)

Quoted 3 lines

Heh, no, I know about the changed handling already. I stumbled over the page_count() remaining usage while working on some cleanups I previously had as part of this series :)

Ah :)

Quoted 5 lines

--
Cheers

David

maintainer Acked-by on v1 2/4
#16 lore raw

Re: [PATCH v1 2/4] mm/hugetlb: fix two comments related to huge_pmd_unshare()

In reply to David Hildenbrand (Red Hat)
Quoted 16 lines · David Hildenbrand (Red Hat)

Ever since we stopped using the page count to detect shared PMD page tables, these comments are outdated.

The only reason we have to flush the TLB early is because once we drop the i_mmap_rwsem, the previously shared page table could get freed (to then get reallocated and used for other purpose). So we really have to flush the TLB before that could happen.

So let's simplify the comments a bit.

The "If we unshared PMDs, the TLB flush was not recorded in mmu_gather." part introduced as in commit a4a118f2eead ("hugetlbfs: flush TLBs correctly after huge_pmd_unshare") was confusing: sure it is recorded in the mmu_gather, otherwise tlb_flush_mmu_tlbonly() wouldn't do anything. So let's drop that comment while at it as well.

Quoted 6 lines

We'll centralize these comments in a single helper as we rework the code next.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>
Signature · 2 lines
Oscar Salvador
SUSE Labs
author maintainer
#17 lore raw

PATCH 3/4 mm/rmap: fix two comments related to huge_pmd_unshare()

In reply to David Hildenbrand (Red Hat)

PMD page table unsharing no longer touches the refcount of a PMD page table. Also, it is not about dropping the refcount of a "PMD page" but the "PMD page table".

Let's just simplify by saying that the PMD page table was unmapped, consequently also unmapping the folio that was mapped into this page.

This code should be deduplicated in the future.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>
---
 mm/rmap.c | 20 ++++----------------
 1 file changed, 4 insertions(+), 16 deletions(-)
mm/rmap.c +4 -16
index f955f02d570ed..748f48727a162 100644
@@ -2016,14 +2016,8 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 					flush_tlb_range(vma,
 						range.start, range.end);
 					/*
-					 * The ref count of the PMD page was
-					 * dropped which is part of the way map
-					 * counting is done for shared PMDs.
-					 * Return 'true' here.  When there is
-					 * no other sharing, huge_pmd_unshare
-					 * returns false and we will unmap the
-					 * actual page and drop map count
-					 * to zero.
+					 * The PMD table was unmapped,
+					 * consequently unmapping the folio.
 					 */
 					goto walk_done;
 				}
@@ -2416,14 +2410,8 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
 						range.start, range.end);
 
 					/*
-					 * The ref count of the PMD page was
-					 * dropped which is part of the way map
-					 * counting is done for shared PMDs.
-					 * Return 'true' here.  When there is
-					 * no other sharing, huge_pmd_unshare
-					 * returns false and we will unmap the
-					 * actual page and drop map count
-					 * to zero.
+					 * The PMD table was unmapped,
+					 * consequently unmapping the folio.
 					 */
 					page_vma_mapped_walk_done(&pvmw);
 					break;
Signature · 1 line
2.52.0
reviewer Reviewed-by on v1 3/4
#18 lore raw

Re: [PATCH v1 3/4] mm/rmap: fix two comments related to huge_pmd_unshare()

Quoted 8 lines · David Hildenbrand (Red Hat)

PMD page table unsharing no longer touches the refcount of a PMD page table. Also, it is not about dropping the refcount of a "PMD page" but
the "PMD page table".

Let's just simplify by saying that the PMD page table was unmapped, consequently also unmapping the folio that was mapped into this page.

Quoted 7 lines

This code should be deduplicated in the future.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared

count")

Signature · 1 line
All Rights Reversed.
Reviewed-by on v1 3/4
#19 lore raw

Re: [PATCH v1 3/4] mm/rmap: fix two comments related to huge_pmd_unshare()

In reply to David Hildenbrand (Red Hat)
Quoted 4 lines · David Hildenbrand (Red Hat)

PMD page table unsharing no longer touches the refcount of a PMD page table. Also, it is not about dropping the refcount of a "PMD page" but the "PMD page table".

Quoted 8 lines

Let's just simplify by saying that the PMD page table was unmapped, consequently also unmapping the folio that was mapped into this page.

This code should be deduplicated in the future.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>

LGTM, so:

Quoted 45 lines
---
 mm/rmap.c | 20 ++++----------------
 1 file changed, 4 insertions(+), 16 deletions(-)
mm/rmap.c +4 -17
index f955f02d570ed..748f48727a162 100644
@@ -2016,14 +2016,8 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 					flush_tlb_range(vma,
 						range.start, range.end);
 					/*
-					 * The ref count of the PMD page was
-					 * dropped which is part of the way map
-					 * counting is done for shared PMDs.
-					 * Return 'true' here.  When there is
-					 * no other sharing, huge_pmd_unshare
-					 * returns false and we will unmap the
-					 * actual page and drop map count
-					 * to zero.
+					 * The PMD table was unmapped,
+					 * consequently unmapping the folio.
 					 */
 					goto walk_done;
 				}
@@ -2416,14 +2410,8 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
 						range.start, range.end);
 
 					/*
-					 * The ref count of the PMD page was
-					 * dropped which is part of the way map
-					 * counting is done for shared PMDs.
-					 * Return 'true' here.  When there is
-					 * no other sharing, huge_pmd_unshare
-					 * returns false and we will unmap the
-					 * actual page and drop map count
-					 * to zero.
+					 * The PMD table was unmapped,
+					 * consequently unmapping the folio.
 					 */
 					page_vma_mapped_walk_done(&pvmw);
 					break;
--

2.52.0

maintainer Acked-by on v1 3/4
#20 lore raw

Re: [PATCH v1 3/4] mm/rmap: fix two comments related to huge_pmd_unshare()

In reply to David Hildenbrand (Red Hat)
Quoted 4 lines · David Hildenbrand (Red Hat)

PMD page table unsharing no longer touches the refcount of a PMD page table. Also, it is not about dropping the refcount of a "PMD page" but the "PMD page table".

Quoted 8 lines

Let's just simplify by saying that the PMD page table was unmapped, consequently also unmapping the folio that was mapped into this page.

This code should be deduplicated in the future.

Fixes 59d9094df3d7 ("mm: hugetlb: independent PMD page table shared count")
Cc Liu Shixin <[email protected]>
Signature · 2 lines
Oscar Salvador
SUSE Labs
author maintainer
#21 lore raw

PATCH 4/4 mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

In reply to David Hildenbrand (Red Hat)

As reported, ever since commit 1013af4f585f ("mm/hugetlb: fix huge_pmd_unshare() vs GUP-fast race") we can end up in some situations where we perform so many IPI broadcasts when unsharing hugetlb PMD page tables that it severely regresses some workloads.

In particular, when we fork()+exit(), or when we munmap() a large area backed by many shared PMD tables, we perform one IPI broadcast per unshared PMD table.

There are two optimizations to be had:

(1) When we process (unshare) multiple such PMD tables, such as during exit(), it is sufficient to send a single IPI broadcast (as long as we respect locking rules) instead of one per PMD table.

Locking prevents that any of these PMD tables could get reuse before we drop the lock.

(2) When we are not the last sharer (> 2 users including us), there is no need to send the IPI broadcast. The shared PMD tables cannot become exclusive (fully unshared) before an IPI will be broadcasted by the last sharer.

Concurrent GUP-fast could walk into a PMD table just before we unshared it. It could then succeed in grabbing a page from the shared page table even after munmap() etc succeeded (and supressed an IPI). But there is not difference compared to GUP-fast just sleeping for a while after grabbing the page and re-enabling IRQs.

Most importantly, GUP-fast will never walk into page tables that are no-longer shared, because the last sharer will issue an IPI broadcast.

(if ever required, checking whether the PUD changed in GUP-fast
after grabbing the page like we do in the PTE case could handle this)

So let's rework PMD sharing TLB flushing + IPI sync to use the mmu_gather infrastructure so we can implement these optimizations and demystify the code at least a bit. Extend the mmu_gather infrastructure to be able to deal with our special hugetlb PMD table sharing implementation.

We'll consolidate the handling for (full) unsharing of PMD tables in tlb_unshare_pmd_ptdesc() and tlb_flush_unshared_tables(), and track in "struct mmu_gather" whether we had (full) unsharing of PMD tables.

Because locking is very special (concurrent unsharing+reuse must be prevented), we disallow deferring flushing to tlb_finish_mmu() and instead require an explicit earlier call to tlb_flush_unshared_tables().

From hugetlb code, we call huge_pmd_unshare_flush() where we make sure that the expected lock protecting us from concurrent unsharing+reuse is still held.

Check with a VM_WARN_ON_ONCE() in tlb_finish_mmu() that tlb_flush_unshared_tables() was properly called earlier.

Document it all properly.

Notes about tlb_remove_table_sync_one() interaction with unsharing:

There are two fairly tricky things:

(1) tlb_remove_table_sync_one() is a NOP on architectures without CONFIG_MMU_GATHER_RCU_TABLE_FREE.

Here, the assumption is that the previous TLB flush would send an IPI to all relevant CPUs. Careful: some architectures like x86 only send IPIs to all relevant CPUs when tlb->freed_tables is set.

The relevant architectures should be selecting MMU_GATHER_RCU_TABLE_FREE, but x86 might not do that in stable kernels and it might have been problematic before this patch.

Also, the arch flushing behavior (independent of IPIs) is different when tlb->freed_tables is set. Do we have to enlighten them to also take care of tlb->unshared_tables? So far we didn't care, so hopefully we are fine. Of course, we could be setting tlb->freed_tables as well, but that might then unnecessarily flush too much, because the semantics of tlb->freed_tables are a bit fuzzy.

This patch changes nothing in this regard.

(2) tlb_remove_table_sync_one() is not a NOP on architectures with CONFIG_MMU_GATHER_RCU_TABLE_FREE that actually don't need a sync.

Take x86 as an example: in the common case (!pv, !X86_FEATURE_INVLPGB) we still issue IPIs during TLB flushes and don't actually need the second tlb_remove_table_sync_one().

This optimized can be implemented on top of this, by checking e.g., in tlb_remove_table_sync_one() whether we really need IPIs. But as described in (1), it really must honor tlb->freed_tables then to send IPIs to all relevant CPUs.

Further note that the ptdesc_pmd_pts_dec() in huge_pmd_share() is not a concern, as we are holding the i_mmap_lock the whole time, preventing concurrent unsharing. That ptdesc_pmd_pts_dec() usage will be removed separately as a cleanup later.

There are plenty more cleanups to be had, but they have to wait until this is fixed.

Fixes 1013af4f585f ("mm/hugetlb: fix huge_pmd_unshare() vs GUP-fast race")
---
 include/asm-generic/tlb.h |  69 +++++++++++++++++++++-
 include/linux/hugetlb.h   |  19 +++---
 mm/hugetlb.c              | 121 ++++++++++++++++++++++----------------
 mm/mmu_gather.c           |   6 ++
 mm/mprotect.c             |   2 +-
 mm/rmap.c                 |  25 +++++---
 6 files changed, 173 insertions(+), 69 deletions(-)
include/asm-generic/tlb.h +68 -1
index 1fff717cae510..324a21f53b644 100644
@@ -364,6 +364,17 @@ struct mmu_gather {
 	unsigned int		vma_huge : 1;
 	unsigned int		vma_pfn  : 1;
 
+	/*
+	 * Did we unshare (unmap) any shared page tables?
+	 */
+	unsigned int		unshared_tables : 1;
+
+	/*
+	 * Did we unshare any page tables such that they are now exclusive
+	 * and could get reused+modified by the new owner?
+	 */
+	unsigned int		fully_unshared_tables : 1;
+
 	unsigned int		batch_count;
 
 #ifndef CONFIG_MMU_GATHER_NO_GATHER
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 	tlb->cleared_pmds = 0;
 	tlb->cleared_puds = 0;
 	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;
 	/*
 	 * Do not reset mmu_gather::vma_* fields here, we do not
 	 * call into tlb_start_vma() again to set them if there is an
@@ -484,7 +496,7 @@ static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
 	 * these bits.
 	 */
 	if (!(tlb->freed_tables || tlb->cleared_ptes || tlb->cleared_pmds ||
-	      tlb->cleared_puds || tlb->cleared_p4ds))
+	      tlb->cleared_puds || tlb->cleared_p4ds || tlb->unshared_tables))
 		return;
 
 	tlb_flush(tlb);
@@ -773,6 +785,61 @@ static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd)
 }
 #endif
 
+#ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
+static inline void tlb_unshare_pmd_ptdesc(struct mmu_gather *tlb, struct ptdesc *pt,
+					  unsigned long addr)
+{
+	/*
+	 * The caller must make sure that concurrent unsharing + exclusive
+	 * reuse is impossible until tlb_flush_unshared_tables() was called.
+	 */
+	VM_WARN_ON_ONCE(!ptdesc_pmd_is_shared(pt));
+	ptdesc_pmd_pts_dec(pt);
+
+	/* Clearing a PUD pointing at a PMD table with PMD leaves. */
+	tlb_flush_pmd_range(tlb, addr & PUD_MASK, PUD_SIZE);
+
+	/*
+	 * If the page table is now exclusively owned, we fully unshared
+	 * a page table.
+	 */
+	if (!ptdesc_pmd_is_shared(pt))
+		tlb->fully_unshared_tables = true;
+	tlb->unshared_tables = true;
+}
+
+static inline void tlb_flush_unshared_tables(struct mmu_gather *tlb)
+{
+	/*
+	 * As soon as the caller drops locks to allow for reuse of
+	 * previously-shared tables, these tables could get modified and
+	 * even reused outside of hugetlb context. So flush the TLB now.
+	 *
+	 * Note that we cannot defer the flush to a later point even if we are
+	 * not the last sharer of the page table.
+	 */
+	if (tlb->unshared_tables)
+		tlb_flush_mmu_tlbonly(tlb);
+
+	/*
+	 * Similarly, we must make sure that concurrent GUP-fast will not
+	 * walk previously-shared page tables that are getting modified+reused
+	 * elsewhere. So broadcast an IPI to wait for any concurrent GUP-fast.
+	 *
+	 * We only perform this when we are the last sharer of a page table,
+	 * as the IPI will reach all CPUs: any GUP-fast.
+	 *
+	 * Note that on configs where tlb_remove_table_sync_one() is a NOP,
+	 * the expectation is that the tlb_flush_mmu_tlbonly() would have issued
+	 * required IPIs already for us.
+	 */
+	if (tlb->fully_unshared_tables) {
+		tlb_remove_table_sync_one();
+		tlb->fully_unshared_tables = false;
+	}
+}
+#endif /* CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING */
+
 #endif /* CONFIG_MMU */
 
 #endif /* _ASM_GENERIC__TLB_H */
include/linux/hugetlb.h +12 -7
index 03c8725efa289..63b248c6bfd47 100644
@@ -240,8 +240,9 @@ pte_t *huge_pte_alloc(struct mm_struct *mm, struct vm_area_struct *vma,
 pte_t *huge_pte_offset(struct mm_struct *mm,
 		       unsigned long addr, unsigned long sz);
 unsigned long hugetlb_mask_last_page(struct hstate *h);
-int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
-				unsigned long addr, pte_t *ptep);
+int huge_pmd_unshare(struct mmu_gather *tlb, struct vm_area_struct *vma,
+		unsigned long addr, pte_t *ptep);
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma);
 void adjust_range_if_pmd_sharing_possible(struct vm_area_struct *vma,
 				unsigned long *start, unsigned long *end);
 
@@ -271,7 +272,7 @@ void hugetlb_vma_unlock_write(struct vm_area_struct *vma);
 int hugetlb_vma_trylock_write(struct vm_area_struct *vma);
 void hugetlb_vma_assert_locked(struct vm_area_struct *vma);
 void hugetlb_vma_lock_release(struct kref *kref);
-long hugetlb_change_protection(struct vm_area_struct *vma,
+long hugetlb_change_protection(struct mmu_gather *tlb, struct vm_area_struct *vma,
 		unsigned long address, unsigned long end, pgprot_t newprot,
 		unsigned long cp_flags);
 void hugetlb_unshare_all_pmds(struct vm_area_struct *vma);
@@ -300,13 +301,17 @@ static inline struct address_space *hugetlb_folio_mapping_lock_write(
 	return NULL;
 }
 
-static inline int huge_pmd_unshare(struct mm_struct *mm,
-					struct vm_area_struct *vma,
-					unsigned long addr, pte_t *ptep)
+static inline int huge_pmd_unshare(struct mmu_gather *tlb,
+		struct vm_area_struct *vma, unsigned long addr, pte_t *ptep)
 {
 	return 0;
 }
 
+static inline void huge_pmd_unshare_flush(struct mmu_gather *tlb,
+		struct vm_area_struct *vma)
+{
+}
+
 static inline void adjust_range_if_pmd_sharing_possible(
 				struct vm_area_struct *vma,
 				unsigned long *start, unsigned long *end)
@@ -432,7 +437,7 @@ static inline void move_hugetlb_state(struct folio *old_folio,
 {
 }
 
-static inline long hugetlb_change_protection(
+static inline long hugetlb_change_protection(struct mmu_gather *tlb,
 			struct vm_area_struct *vma, unsigned long address,
 			unsigned long end, pgprot_t newprot,
 			unsigned long cp_flags)
mm/hugetlb.c +69 -52

259 lines not on this page · raw message

mm/mmu_gather.c +6 -0
index 247e3f9db6c7a..822a790127375 100644
@@ -468,6 +468,12 @@ void tlb_gather_mmu_fullmm(struct mmu_gather *tlb, struct mm_struct *mm)
  */
 void tlb_finish_mmu(struct mmu_gather *tlb)
 {
+	/*
+	 * We expect an earlier huge_pmd_unshare_flush() call to sort this out,
+	 * due to complicated locking requirements with page table unsharing.
+	 */
+	VM_WARN_ON_ONCE(tlb->fully_unshared_tables);
+
 	/*
 	 * If there are parallel threads are doing PTE changes on same range
 	 * under non-exclusive lock (e.g., mmap_lock read-side) but defer TLB
mm/mprotect.c +1 -1
index 283889e4f1cec..5c330e817129e 100644
@@ -652,7 +652,7 @@ long change_protection(struct mmu_gather *tlb,
 #endif
 
 	if (is_vm_hugetlb_page(vma))
-		pages = hugetlb_change_protection(vma, start, end, newprot,
+		pages = hugetlb_change_protection(tlb, vma, start, end, newprot,
 						  cp_flags);
 	else
 		pages = change_protection_range(tlb, vma, start, end, newprot,
mm/rmap.c +17 -8
index 748f48727a162..d6799afe11147 100644
@@ -76,7 +76,7 @@
 #include <linux/mm_inline.h>
 #include <linux/oom.h>
 
-#include <asm/tlbflush.h>
+#include <asm/tlb.h>
 
 #define CREATE_TRACE_POINTS
 #include <trace/events/migrate.h>
@@ -2008,13 +2008,17 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			 * if unsuccessful.
 			 */
 			if (!anon) {
+				struct mmu_gather tlb;
+
 				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
 				if (!hugetlb_vma_trylock_write(vma))
 					goto walk_abort;
-				if (huge_pmd_unshare(mm, vma, address, pvmw.pte)) {
+
+				tlb_gather_mmu(&tlb, mm);
+				if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
 					hugetlb_vma_unlock_write(vma);
-					flush_tlb_range(vma,
-						range.start, range.end);
+					huge_pmd_unshare_flush(&tlb, vma);
+					tlb_finish_mmu(&tlb);
 					/*
 					 * The PMD table was unmapped,
 					 * consequently unmapping the folio.
@@ -2022,6 +2026,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 					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))
@@ -2398,17 +2403,20 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
 			 * fail if unsuccessful.
 			 */
 			if (!anon) {
+				struct mmu_gather tlb;
+
 				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
 				if (!hugetlb_vma_trylock_write(vma)) {
 					page_vma_mapped_walk_done(&pvmw);
 					ret = false;
 					break;
 				}
-				if (huge_pmd_unshare(mm, vma, address, pvmw.pte)) {
-					hugetlb_vma_unlock_write(vma);
-					flush_tlb_range(vma,
-						range.start, range.end);
 
+				tlb_gather_mmu(&tlb, mm);
+				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.
@@ -2417,6 +2425,7 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
 					break;
 				}
 				hugetlb_vma_unlock_write(vma);
+				tlb_finish_mmu(&tlb);
 			}
 			/* Nuke the hugetlb page table entry */
 			pteval = huge_ptep_clear_flush(vma, address, pvmw.pte);
Signature · 1 line
2.52.0
#22 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

include/asm-generic/tlb.h static inline void __tlb_reset_range(struct mmu_gather *tlb) · line 417

Quoted 11 lines · David Hildenbrand (Red Hat)
include/asm-generic/tlb.h +1 -0
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 	tlb->cleared_pmds = 0;
 	tlb->cleared_puds = 0;
 	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;
 	/*
 	 * Do not reset mmu_gather::vma_* fields here, we do not
 	 * call into tlb_start_vma() again to set them if there is an

I understand you don’t want to initialize fully_unshared_tables here, but tlb_gather_mmu() needs to happen somewhere. So you probably want it to take place in tlb_gather_mmu(), no?

#23 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 19 lines · Nadav Amit
Quoted 6 lines · David Hildenbrand (Red Hat)
(quoted diff) +0 -0
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 
include/asm-generic/tlb.h +1 -0
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 	tlb->cleared_pmds = 0;
 	tlb->cleared_puds = 0;
 	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;
 	/*
 	 * Do not reset mmu_gather::vma_* fields here, we do not
 	 * call into tlb_start_vma() again to set them if there is an
Quoted 3 lines

I understand you don’t want to initialize fully_unshared_tables here, but tlb_gather_mmu() needs to happen somewhere. So you probably want it to take place in tlb_gather_mmu(), no?

To clarify my messed up response: the code needs to initialize fully_unshared_tables somewhere during tlb_gather_mmu() invocation.

author maintainer
#24 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 19 lines · Nadav Amit
Quoted 19 lines · Nadav Amit
Quoted 6 lines · David Hildenbrand (Red Hat)
(quoted diff) +0 -0
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 
include/asm-generic/tlb.h +1 -0
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 	tlb->cleared_pmds = 0;
 	tlb->cleared_puds = 0;
 	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;
 	/*
 	 * Do not reset mmu_gather::vma_* fields here, we do not
 	 * call into tlb_start_vma() again to set them if there is an
Quoted 6 lines
Quoted 3 lines

I understand you don’t want to initialize fully_unshared_tables here, but tlb_gather_mmu() needs to happen somewhere. So you probably want it to take place in tlb_gather_mmu(), no?

To clarify my messed up response: the code needs to initialize fully_unshared_tables somewhere during tlb_gather_mmu() invocation.

Good point, __tlb_gather_mmu() needs to initialize it explicitly!

Thanks a lot for the review, appreciated!

Signature · 3 lines
Cheers

David
#25 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

In reply to David Hildenbrand (Red Hat)
Quoted 9 lines · David Hildenbrand (Red Hat)

As reported, ever since commit 1013af4f585f ("mm/hugetlb: fix huge_pmd_unshare() vs GUP-fast race") we can end up in some situations where we perform so many IPI broadcasts when unsharing hugetlb PMD page tables that it severely regresses some workloads.

In particular, when we fork()+exit(), or when we munmap() a large area backed by many shared PMD tables, we perform one IPI broadcast per unshared PMD table.

Quoted 5 lines

There are two optimizations to be had:

(1) When we process (unshare) multiple such PMD tables, such as during exit(), it is sufficient to send a single IPI broadcast (as long as we respect locking rules) instead of one per PMD table.

Yes :)

Quoted 8 lines

Locking prevents that any of these PMD tables could get reuse before we drop the lock.

(2) When we are not the last sharer (> 2 users including us), there is no need to send the IPI broadcast. The shared PMD tables cannot become exclusive (fully unshared) before an IPI will be broadcasted by the last sharer.

Smart this makes sense.

Quoted 60 lines

Concurrent GUP-fast could walk into a PMD table just before we unshared it. It could then succeed in grabbing a page from the shared page table even after munmap() etc succeeded (and supressed an IPI). But there is not difference compared to GUP-fast just sleeping for a while after grabbing the page and re-enabling IRQs.

Most importantly, GUP-fast will never walk into page tables that are no-longer shared, because the last sharer will issue an IPI broadcast.

(if ever required, checking whether the PUD changed in GUP-fast
after grabbing the page like we do in the PTE case could handle this)

So let's rework PMD sharing TLB flushing + IPI sync to use the mmu_gather infrastructure so we can implement these optimizations and demystify the code at least a bit. Extend the mmu_gather infrastructure to be able to deal with our special hugetlb PMD table sharing implementation.

We'll consolidate the handling for (full) unsharing of PMD tables in tlb_unshare_pmd_ptdesc() and tlb_flush_unshared_tables(), and track in "struct mmu_gather" whether we had (full) unsharing of PMD tables.

Because locking is very special (concurrent unsharing+reuse must be prevented), we disallow deferring flushing to tlb_finish_mmu() and instead require an explicit earlier call to tlb_flush_unshared_tables().

From hugetlb code, we call huge_pmd_unshare_flush() where we make sure that the expected lock protecting us from concurrent unsharing+reuse is still held.

Check with a VM_WARN_ON_ONCE() in tlb_finish_mmu() that tlb_flush_unshared_tables() was properly called earlier.

Document it all properly.

Notes about tlb_remove_table_sync_one() interaction with unsharing:

There are two fairly tricky things:

(1) tlb_remove_table_sync_one() is a NOP on architectures without CONFIG_MMU_GATHER_RCU_TABLE_FREE.

Here, the assumption is that the previous TLB flush would send an IPI to all relevant CPUs. Careful: some architectures like x86 only send IPIs to all relevant CPUs when tlb->freed_tables is set.

The relevant architectures should be selecting MMU_GATHER_RCU_TABLE_FREE, but x86 might not do that in stable kernels and it might have been problematic before this patch.

Also, the arch flushing behavior (independent of IPIs) is different when tlb->freed_tables is set. Do we have to enlighten them to also take care of tlb->unshared_tables? So far we didn't care, so hopefully we are fine. Of course, we could be setting tlb->freed_tables as well, but that might then unnecessarily flush too much, because the semantics of tlb->freed_tables are a bit fuzzy.

Quoted 1 line

This patch changes nothing in this regard.

Ugh at the 'special snowflaking' of hugetlb yet again...

Quoted 7 lines

(2) tlb_remove_table_sync_one() is not a NOP on architectures with CONFIG_MMU_GATHER_RCU_TABLE_FREE that actually don't need a sync.

Take x86 as an example: in the common case (!pv, !X86_FEATURE_INVLPGB) we still issue IPIs during TLB flushes and don't actually need the second tlb_remove_table_sync_one().

Hmm wasn't aware that x86 would still IPI even with CONFIG_MMU_GATHER_RCU_TABLE_FREE??

But then we'd have to set tlb->freed_tables and as per your above point maybe overkill...hm one for another time then I guess! :)

Quoted 6 lines

This optimized can be implemented on top of this, by checking e.g., in tlb_remove_table_sync_one() whether we really need IPIs. But as described in (1), it really must honor tlb->freed_tables then to send IPIs to all relevant CPUs.

Quoted 7 lines

Further note that the ptdesc_pmd_pts_dec() in huge_pmd_share() is not a concern, as we are holding the i_mmap_lock the whole time, preventing concurrent unsharing. That ptdesc_pmd_pts_dec() usage will be removed separately as a cleanup later.

There are plenty more cleanups to be had, but they have to wait until this is fixed.

Yes!

Quoted 6 lines
Fixes 1013af4f585f ("mm/hugetlb: fix huge_pmd_unshare() vs GUP-fast race")

Overall the approach looks reasonable, I raised various thoughts, questions, concerns below.

I know you're at LPC right now so we'll get back to this either when you're back or you get bored/can't sleep due to caffeine intake ;)

Cheers, Lorenzo

Quoted 14 lines
---
 include/asm-generic/tlb.h |  69 +++++++++++++++++++++-
 include/linux/hugetlb.h   |  19 +++---
 mm/hugetlb.c              | 121 ++++++++++++++++++++++----------------
 mm/mmu_gather.c           |   6 ++
 mm/mprotect.c             |   2 +-
 mm/rmap.c                 |  25 +++++---
 6 files changed, 173 insertions(+), 69 deletions(-)
include/asm-generic/tlb.h +0 -0
index 1fff717cae510..324a21f53b644 100644

include/asm-generic/tlb.h struct mmu_gather { · line 368

Quoted 5 lines
include/asm-generic/tlb.h +2 -0
@@ -364,6 +364,17 @@ struct mmu_gather {
 	unsigned int		vma_huge : 1;
 	unsigned int		vma_pfn  : 1;
 
+	/*
+	 * Did we unshare (unmap) any shared page tables?

Given mshare is incoming, maybe worth clarifying and being explicit about hugetlb in both comment and name?

include/asm-generic/tlb.h struct mmu_gather { · line 376

Quoted 8 lines
include/asm-generic/tlb.h +8 -0
+	 */
+	unsigned int		unshared_tables : 1;
+
+	/*
+	 * Did we unshare any page tables such that they are now exclusive
+	 * and could get reused+modified by the new owner?
+	 */
+	unsigned int		fully_unshared_tables : 1;

Does fully_unshared_tables rely on unshared_tables also being set?

include/asm-generic/tlb.h static inline void __tlb_reset_range(struct mmu_gather *tlb) · line 414

Quoted 9 lines
include/asm-generic/tlb.h +2 -0
+
 	unsigned int		batch_count;
 
 #ifndef CONFIG_MMU_GATHER_NO_GATHER
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
 	tlb->cleared_pmds = 0;
 	tlb->cleared_puds = 0;
 	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;

As Nadav points out, should also initialise fully_unshared_tables.

include/asm-generic/tlb.h static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb) · line 499

Quoted 9 lines
include/asm-generic/tlb.h +1 -1
 	/*
 	 * Do not reset mmu_gather::vma_* fields here, we do not
 	 * call into tlb_start_vma() again to set them if there is an
@@ -484,7 +496,7 @@ static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
 	 * these bits.
 	 */
 	if (!(tlb->freed_tables || tlb->cleared_ptes || tlb->cleared_pmds ||
-	      tlb->cleared_puds || tlb->cleared_p4ds))
+	      tlb->cleared_puds || tlb->cleared_p4ds || tlb->unshared_tables))

What about fully_unshared_tables? I guess though unshared_tables implies fully_unshared_tables.

Quoted 10 lines
include/asm-generic/tlb.h +3 -0
 		return;
 
 	tlb_flush(tlb);
@@ -773,6 +785,61 @@ static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd)
 }
 #endif
 
+#ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
+static inline void tlb_unshare_pmd_ptdesc(struct mmu_gather *tlb, struct ptdesc *pt,
+					  unsigned long addr)

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 800

Quoted 10 lines
include/asm-generic/tlb.h +10 -0
+{
+	/*
+	 * The caller must make sure that concurrent unsharing + exclusive
+	 * reuse is impossible until tlb_flush_unshared_tables() was called.
+	 */
+	VM_WARN_ON_ONCE(!ptdesc_pmd_is_shared(pt));
+	ptdesc_pmd_pts_dec(pt);
+
+	/* Clearing a PUD pointing at a PMD table with PMD leaves. */
+	tlb_flush_pmd_range(tlb, addr & PUD_MASK, PUD_SIZE);

OK I guess before we were always flushing for each page, but now we are accumulating the flushes here.

Quoted 6 lines
include/asm-generic/tlb.h +6 -0
+
+	/*
+	 * If the page table is now exclusively owned, we fully unshared
+	 * a page table.
+	 */
+	if (!ptdesc_pmd_is_shared(pt))

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 816

Quoted 10 lines
include/asm-generic/tlb.h +10 -0
+		tlb->fully_unshared_tables = true;
+	tlb->unshared_tables = true;
+}
+
+static inline void tlb_flush_unshared_tables(struct mmu_gather *tlb)
+{
+	/*
+	 * As soon as the caller drops locks to allow for reuse of
+	 * previously-shared tables, these tables could get modified and
+	 * even reused outside of hugetlb context. So flush the TLB now.

Hmm but you're doing this in both the case of unshare and fully unsharing, so is this the right place to make this comment?

Surely here this is about flushing TLBs for the unsharer only as it no longer uses it?

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 820

Quoted 4 lines
include/asm-generic/tlb.h +4 -0
+	 *
+	 * Note that we cannot defer the flush to a later point even if we are
+	 * not the last sharer of the page table.
+	 */

Not hugely clear, some double negative here. Maybe worth saying something like:

'Even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer who no longer has access to this memory'

Or something? Assuming this is accurate :)

Quoted 5 lines
include/asm-generic/tlb.h +5 -0
+	if (tlb->unshared_tables)
+		tlb_flush_mmu_tlbonly(tlb);
+
+	/*
+	 * Similarly, we must make sure that concurrent GUP-fast will not

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 835

Quoted 10 lines
include/asm-generic/tlb.h +10 -0
+	 * walk previously-shared page tables that are getting modified+reused
+	 * elsewhere. So broadcast an IPI to wait for any concurrent GUP-fast.
+	 *
+	 * We only perform this when we are the last sharer of a page table,
+	 * as the IPI will reach all CPUs: any GUP-fast.
+	 *
+	 * Note that on configs where tlb_remove_table_sync_one() is a NOP,
+	 * the expectation is that the tlb_flush_mmu_tlbonly() would have issued
+	 * required IPIs already for us.
+	 */

Nice, great comment!

Quoted 82 lines
include/asm-generic/tlb.h +7 -0
+	if (tlb->fully_unshared_tables) {
+		tlb_remove_table_sync_one();
+		tlb->fully_unshared_tables = false;
+	}
+}
+#endif /* CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING */
+
 #endif /* CONFIG_MMU */
 
 #endif /* _ASM_GENERIC__TLB_H */
include/linux/hugetlb.h +12 -7
index 03c8725efa289..63b248c6bfd47 100644
@@ -240,8 +240,9 @@ pte_t *huge_pte_alloc(struct mm_struct *mm, struct vm_area_struct *vma,
 pte_t *huge_pte_offset(struct mm_struct *mm,
 		       unsigned long addr, unsigned long sz);
 unsigned long hugetlb_mask_last_page(struct hstate *h);
-int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
-				unsigned long addr, pte_t *ptep);
+int huge_pmd_unshare(struct mmu_gather *tlb, struct vm_area_struct *vma,
+		unsigned long addr, pte_t *ptep);
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma);
 void adjust_range_if_pmd_sharing_possible(struct vm_area_struct *vma,
 				unsigned long *start, unsigned long *end);
 
@@ -271,7 +272,7 @@ void hugetlb_vma_unlock_write(struct vm_area_struct *vma);
 int hugetlb_vma_trylock_write(struct vm_area_struct *vma);
 void hugetlb_vma_assert_locked(struct vm_area_struct *vma);
 void hugetlb_vma_lock_release(struct kref *kref);
-long hugetlb_change_protection(struct vm_area_struct *vma,
+long hugetlb_change_protection(struct mmu_gather *tlb, struct vm_area_struct *vma,
 		unsigned long address, unsigned long end, pgprot_t newprot,
 		unsigned long cp_flags);
 void hugetlb_unshare_all_pmds(struct vm_area_struct *vma);
@@ -300,13 +301,17 @@ static inline struct address_space *hugetlb_folio_mapping_lock_write(
 	return NULL;
 }
 
-static inline int huge_pmd_unshare(struct mm_struct *mm,
-					struct vm_area_struct *vma,
-					unsigned long addr, pte_t *ptep)
+static inline int huge_pmd_unshare(struct mmu_gather *tlb,
+		struct vm_area_struct *vma, unsigned long addr, pte_t *ptep)
 {
 	return 0;
 }
 
+static inline void huge_pmd_unshare_flush(struct mmu_gather *tlb,
+		struct vm_area_struct *vma)
+{
+}
+
 static inline void adjust_range_if_pmd_sharing_possible(
 				struct vm_area_struct *vma,
 				unsigned long *start, unsigned long *end)
@@ -432,7 +437,7 @@ static inline void move_hugetlb_state(struct folio *old_folio,
 {
 }
 
-static inline long hugetlb_change_protection(
+static inline long hugetlb_change_protection(struct mmu_gather *tlb,
 			struct vm_area_struct *vma, unsigned long address,
 			unsigned long end, pgprot_t newprot,
 			unsigned long cp_flags)
mm/hugetlb.c +2 -1
index 3c77cdef12a32..3db94693a06fc 100644
@@ -5096,8 +5096,9 @@ int move_hugetlb_page_tables(struct vm_area_struct *vma,
 	unsigned long last_addr_mask;
 	pte_t *src_pte, *dst_pte;
 	struct mmu_notifier_range range;
-	bool shared_pmd = false;
+	struct mmu_gather tlb;
 
+	tlb_gather_mmu(&tlb, vma->vm_mm);
 	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm, old_addr,
 				old_end);
 	adjust_range_if_pmd_sharing_possible(vma, &range.start, &range.end);
@@ -5122,12 +5123,12 @@ int move_hugetlb_page_tables(struct vm_area_struct *vma,
 		if (huge_pte_none(huge_ptep_get(mm, old_addr, src_pte)))

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5131

Quoted 10 lines
mm/hugetlb.c +2 -2
 			continue;
 
-		if (huge_pmd_unshare(mm, vma, old_addr, src_pte)) {
-			shared_pmd = true;
+		if (huge_pmd_unshare(&tlb, vma, old_addr, src_pte)) {
 			old_addr |= last_addr_mask;
 			new_addr |= last_addr_mask;
 			continue;
 		}
+		tlb_remove_huge_tlb_entry(h, &tlb, src_pte, old_addr);

OK I guess we need to add these to cases where we remove previous entries because before we weren't accumulating TLB state except in __unmap_hugepage_range()?

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5140

Quoted 12 lines
mm/hugetlb.c +1 -4
 		dst_pte = huge_pte_alloc(mm, new_vma, new_addr, sz);
 		if (!dst_pte)
@@ -5136,13 +5137,13 @@ int move_hugetlb_page_tables(struct vm_area_struct *vma,
 		move_huge_pte(vma, old_addr, new_addr, src_pte, dst_pte, sz);
 	}
 
-	if (shared_pmd)
-		flush_hugetlb_tlb_range(vma, range.start, range.end);
-	else
-		flush_hugetlb_tlb_range(vma, old_end - len, old_end);
+	tlb_flush_mmu_tlbonly(&tlb);

Maybe:

	if (!tlb->unshared_tables)
		tlb_flush_mmu_tlbonly(&tlb);

To avoid doing that twice? Not sure if so important as will be noop second time though.

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5141

Quoted 1 line
mm/hugetlb.c +1 -0
+	huge_pmd_unshare_flush(&tlb, vma);

OK I guess the semantics are

huge_pmd_unshare() for everything that needs unsharing, accumulating tlb state...

huge_pmd_unshare_flush() to, err, flush :) followed by tlb_finish_mmu() obv, and with i_mmap lock held...

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5146

Quoted 5 lines
mm/hugetlb.c +2 -0
+
 	mmu_notifier_invalidate_range_end(&range);
 	i_mmap_unlock_write(mapping);
 	hugetlb_vma_unlock_write(vma);
+	tlb_finish_mmu(&tlb);

Does it matter that the hugetlb VMA lock is gone when we invoke tlb_finish_mmu()? I'm guessing not.

Kinda wish we could wrap these start/end states, it's fiddly to know that you have to:

- call huge_pmd_unshare_flush()
- release i_mmap lock
- unlock vma hugetlb (ugh god don't even get me started on how that's implemented :)
- call tlb_finish_mmu()

Obv. this kind of thing can be part of future cleanups

Quoted 164 lines
mm/hugetlb.c +37 -41
 	return len + old_addr - old_end;
 }
@@ -5161,7 +5162,6 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	unsigned long sz = huge_page_size(h);
 	bool adjust_reservation;
 	unsigned long last_addr_mask;
-	bool force_flush = false;
 
 	WARN_ON(!is_vm_hugetlb_page(vma));
 	BUG_ON(start & ~huge_page_mask(h));
@@ -5184,10 +5184,8 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
 		}
 
 		ptl = huge_pte_lock(h, mm, ptep);
-		if (huge_pmd_unshare(mm, vma, address, ptep)) {
+		if (huge_pmd_unshare(tlb, vma, address, ptep)) {
 			spin_unlock(ptl);
-			tlb_flush_pmd_range(tlb, address & PUD_MASK, PUD_SIZE);
-			force_flush = true;
 			address |= last_addr_mask;
 			continue;
 		}
@@ -5303,14 +5301,7 @@ void __unmap_hugepage_range(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	}
 	tlb_end_vma(tlb, vma);
 
-	/*
-	 * There is nothing protecting a previously-shared page table that we
-	 * unshared through huge_pmd_unshare() from getting freed after we
-	 * release i_mmap_rwsem, so flush the TLB now. If huge_pmd_unshare()
-	 * succeeded, flush the range corresponding to the pud.
-	 */
-	if (force_flush)
-		tlb_flush_mmu_tlbonly(tlb);
+	huge_pmd_unshare_flush(tlb, vma);
 }
 
 void __hugetlb_zap_begin(struct vm_area_struct *vma,
@@ -6399,7 +6390,7 @@ int hugetlb_mfill_atomic_pte(pte_t *dst_pte,
 }
 #endif /* CONFIG_USERFAULTFD */
 
-long hugetlb_change_protection(struct vm_area_struct *vma,
+long hugetlb_change_protection(struct mmu_gather *tlb, struct vm_area_struct *vma,
 		unsigned long address, unsigned long end,
 		pgprot_t newprot, unsigned long cp_flags)
 {
@@ -6409,7 +6400,6 @@ long hugetlb_change_protection(struct vm_area_struct *vma,
 	pte_t pte;
 	struct hstate *h = hstate_vma(vma);
 	long pages = 0, psize = huge_page_size(h);
-	bool shared_pmd = false;
 	struct mmu_notifier_range range;
 	unsigned long last_addr_mask;
 	bool uffd_wp = cp_flags & MM_CP_UFFD_WP;
@@ -6452,7 +6442,7 @@ long hugetlb_change_protection(struct vm_area_struct *vma,
 			}
 		}
 		ptl = huge_pte_lock(h, mm, ptep);
-		if (huge_pmd_unshare(mm, vma, address, ptep)) {
+		if (huge_pmd_unshare(tlb, vma, address, ptep)) {
 			/*
 			 * When uffd-wp is enabled on the vma, unshare
 			 * shouldn't happen at all.  Warn about it if it
@@ -6461,7 +6451,6 @@ long hugetlb_change_protection(struct vm_area_struct *vma,
 			WARN_ON_ONCE(uffd_wp || uffd_wp_resolve);
 			pages++;
 			spin_unlock(ptl);
-			shared_pmd = true;
 			address |= last_addr_mask;
 			continue;
 		}
@@ -6522,22 +6511,16 @@ long hugetlb_change_protection(struct vm_area_struct *vma,
 				pte = huge_pte_clear_uffd_wp(pte);
 			huge_ptep_modify_prot_commit(vma, address, ptep, old_pte, pte);
 			pages++;
+			tlb_remove_huge_tlb_entry(h, tlb, ptep, address);
 		}
 
 next:
 		spin_unlock(ptl);
 		cond_resched();
 	}
-	/*
-	 * There is nothing protecting a previously-shared page table that we
-	 * unshared through huge_pmd_unshare() from getting freed after we
-	 * release i_mmap_rwsem, so flush the TLB now. If huge_pmd_unshare()
-	 * succeeded, flush the range corresponding to the pud.
-	 */
-	if (shared_pmd)
-		flush_hugetlb_tlb_range(vma, range.start, range.end);
-	else
-		flush_hugetlb_tlb_range(vma, start, end);
+
+	tlb_flush_mmu_tlbonly(tlb);
+	huge_pmd_unshare_flush(tlb, vma);
 	/*
 	 * No need to call mmu_notifier_arch_invalidate_secondary_tlbs() we are
 	 * downgrading page table protection not changing it to point to a new
@@ -6904,18 +6887,27 @@ pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
 	return pte;
 }
 
-/*
- * unmap huge page backed by shared pte.
+/**
+ * huge_pmd_unshare - Unmap a pmd table if it is shared by multiple users
+ * @tlb: the current mmu_gather.
+ * @vma: the vma covering the pmd table.
+ * @addr: the address we are trying to unshare.
+ * @ptep: pointer into the (pmd) page table.
+ *
+ * Called with the page table lock held, the i_mmap_rwsem held in write mode
+ * and the hugetlb vma lock held in write mode.
  *
- * Called with page table lock held.
+ * Note: The caller must call huge_pmd_unshare_flush() before dropping the
+ * i_mmap_rwsem.
  *
- * returns: 1 successfully unmapped a shared pte page
- *	    0 the underlying pte page is not shared, or it is the last user
+ * Returns: 1 if it was a shared PMD table and it got unmapped, or 0 if it
+ *	    was not a shared PMD table.
  */
-int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
-					unsigned long addr, pte_t *ptep)
+int huge_pmd_unshare(struct mmu_gather *tlb, struct vm_area_struct *vma,
+		unsigned long addr, pte_t *ptep)
 {
 	unsigned long sz = huge_page_size(hstate_vma(vma));
+	struct mm_struct *mm = vma->vm_mm;
 	pgd_t *pgd = pgd_offset(mm, addr);
 	p4d_t *p4d = p4d_offset(pgd, addr);
 	pud_t *pud = pud_offset(p4d, addr);
@@ -6927,18 +6919,36 @@ int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
 	i_mmap_assert_write_locked(vma->vm_file->f_mapping);
 	hugetlb_vma_assert_locked(vma);
 	pud_clear(pud);
-	/*
-	 * Once our caller drops the rmap lock, some other process might be
-	 * using this page table as a normal, non-hugetlb page table.
-	 * Wait for pending gup_fast() in other threads to finish before letting
-	 * that happen.
-	 */
-	tlb_remove_table_sync_one();
-	ptdesc_pmd_pts_dec(virt_to_ptdesc(ptep));
+
+	tlb_unshare_pmd_ptdesc(tlb, virt_to_ptdesc(ptep), addr);
+
 	mm_dec_nr_pmds(mm);
 	return 1;
 }
 
+/*
+ * huge_pmd_unshare_flush - Complete a sequence of huge_pmd_unshare() calls
+ * @tlb: the current mmu_gather.
+ * @vma: the vma covering the pmd table.
+ *
+ * Perform necessary TLB flushes or IPI broadcasts to synchronize PMD table
+ * unsharing with concurrent page table walkers (TLB, GUP-fast, etc.).
+ *
+ * This function must be called after a sequence of huge_pmd_unshare()
+ * calls while still holding the i_mmap_rwsem.

mm/hugetlb.c int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma, · line 6948

Quoted 10 lines
mm/hugetlb.c +10 -0
+ */
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma)
+{
+	/*
+	 * We must synchronize page table unsharing such that nobody will
+	 * try reusing a previously-shared page table while it might still
+	 * be in use by previous sharers (TLB, GUP_fast).
+	 */
+	i_mmap_assert_write_locked(vma->vm_file->f_mapping);
+

Extreme nit: inconsistent newline here compared to elsewhere :)

Not even sure why I'm making this point tbh

Quoted 55 lines
mm/hugetlb.c +13 -4
+	tlb_flush_unshared_tables(tlb);
+}
+
 #else /* !CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING */
 
 pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
@@ -6947,12 +6957,16 @@ pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
 	return NULL;
 }
 
-int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
-				unsigned long addr, pte_t *ptep)
+int huge_pmd_unshare(struct mmu_gather *tlb, struct vm_area_struct *vma,
+		unsigned long addr, pte_t *ptep)
 {
 	return 0;
 }
 
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma)
+{
+}
+
 void adjust_range_if_pmd_sharing_possible(struct vm_area_struct *vma,
 				unsigned long *start, unsigned long *end)
 {
@@ -7219,6 +7233,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
 	unsigned long sz = huge_page_size(h);
 	struct mm_struct *mm = vma->vm_mm;
 	struct mmu_notifier_range range;
+	struct mmu_gather tlb;
 	unsigned long address;
 	spinlock_t *ptl;
 	pte_t *ptep;
@@ -7229,6 +7244,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
 	if (start >= end)
 		return;
 
+	tlb_gather_mmu(&tlb, mm);
 	flush_cache_range(vma, start, end);
 	/*
 	 * No need to call adjust_range_if_pmd_sharing_possible(), because
@@ -7248,10 +7264,10 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
 		if (!ptep)
 			continue;
 		ptl = huge_pte_lock(h, mm, ptep);
-		huge_pmd_unshare(mm, vma, address, ptep);
+		huge_pmd_unshare(&tlb, vma, address, ptep);
 		spin_unlock(ptl);
 	}
-	flush_hugetlb_tlb_range(vma, start, end);
+	huge_pmd_unshare_flush(&tlb, vma);
 	if (take_locks) {
 		i_mmap_unlock_write(vma->vm_file->f_mapping);
 		hugetlb_vma_unlock_write(vma);

mm/hugetlb.c static void hugetlb_unshare_pmds(struct vm_area_struct *vma, · line 7280

Quoted 4 lines
mm/hugetlb.c +1 -0
@@ -7261,6 +7277,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
 	 * Documentation/mm/mmu_notifier.rst.
 	 */
 	mmu_notifier_invalidate_range_end(&range);
+	tlb_finish_mmu(&tlb);

Hmm, does it matter that if !take_locks, the i_mmap lock and hugetlb vma locks will still be held when tlb_finish_mmu() is invoked here? I'm guessing it has no bearing but just to be sure :)

Quoted 54 lines
mm/hugetlb.c +0 -0
 }
 
 /*
mm/mmu_gather.c +6 -0
index 247e3f9db6c7a..822a790127375 100644
@@ -468,6 +468,12 @@ void tlb_gather_mmu_fullmm(struct mmu_gather *tlb, struct mm_struct *mm)
  */
 void tlb_finish_mmu(struct mmu_gather *tlb)
 {
+	/*
+	 * We expect an earlier huge_pmd_unshare_flush() call to sort this out,
+	 * due to complicated locking requirements with page table unsharing.
+	 */
+	VM_WARN_ON_ONCE(tlb->fully_unshared_tables);
+
 	/*
 	 * If there are parallel threads are doing PTE changes on same range
 	 * under non-exclusive lock (e.g., mmap_lock read-side) but defer TLB
mm/mprotect.c +1 -1
index 283889e4f1cec..5c330e817129e 100644
@@ -652,7 +652,7 @@ long change_protection(struct mmu_gather *tlb,
 #endif
 
 	if (is_vm_hugetlb_page(vma))
-		pages = hugetlb_change_protection(vma, start, end, newprot,
+		pages = hugetlb_change_protection(tlb, vma, start, end, newprot,
 						  cp_flags);
 	else
 		pages = change_protection_range(tlb, vma, start, end, newprot,
mm/rmap.c +3 -1
index 748f48727a162..d6799afe11147 100644
@@ -76,7 +76,7 @@
 #include <linux/mm_inline.h>
 #include <linux/oom.h>
 
-#include <asm/tlbflush.h>
+#include <asm/tlb.h>
 
 #define CREATE_TRACE_POINTS
 #include <trace/events/migrate.h>
@@ -2008,13 +2008,17 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 			 * if unsuccessful.
 			 */
 			if (!anon) {
+				struct mmu_gather tlb;
+
 				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
 				if (!hugetlb_vma_trylock_write(vma))

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

Quoted 10 lines
mm/rmap.c +5 -3
 					goto walk_abort;
-				if (huge_pmd_unshare(mm, vma, address, pvmw.pte)) {
+
+				tlb_gather_mmu(&tlb, mm);
+				if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
 					hugetlb_vma_unlock_write(vma);
-					flush_tlb_range(vma,
-						range.start, range.end);
+					huge_pmd_unshare_flush(&tlb, vma);
+					tlb_finish_mmu(&tlb);

Not sure if it matters, but elsewhere you order the locks as:

- huge_pmd_unshare_flush()
- release i_mmap lock
- unlock vma hugetlb
- call tlb_finish_mmu()

But here it's:

- unlock vma hugetlb
- huge_pmd_unshare_flush()
- call tlb_finish_mmu()
- (later) release i_mmap lock

Does that matter in terms of lock inversions etc.?

Quoted 23 lines
mm/rmap.c +3 -0
 					/*
 					 * The PMD table was unmapped,
 					 * consequently unmapping the folio.
@@ -2022,6 +2026,7 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 					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))
@@ -2398,17 +2403,20 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
 			 * fail if unsuccessful.
 			 */
 			if (!anon) {
+				struct mmu_gather tlb;
+
 				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
 				if (!hugetlb_vma_trylock_write(vma)) {
 					page_vma_mapped_walk_done(&pvmw);
 					ret = false;
 					break;
 				}

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

Quoted 10 lines
mm/rmap.c +5 -4
-				if (huge_pmd_unshare(mm, vma, address, pvmw.pte)) {
-					hugetlb_vma_unlock_write(vma);
-					flush_tlb_range(vma,
-						range.start, range.end);
 
+				tlb_gather_mmu(&tlb, mm);
+				if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
+					hugetlb_vma_unlock_write(vma);
+					huge_pmd_unshare_flush(&tlb, vma);
+					tlb_finish_mmu(&tlb);

Again this ordering is different from elsewhere, as per above. Not sure if an issue?

Quoted 14 lines
mm/rmap.c +1 -1
 					/*
 					 * The PMD table was unmapped,
 					 * consequently unmapping the folio.
@@ -2417,6 +2425,7 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
 					break;
 				}
 				hugetlb_vma_unlock_write(vma);
+				tlb_finish_mmu(&tlb);
 			}
 			/* Nuke the hugetlb page table entry */
 			pteval = huge_ptep_clear_flush(vma, address, pvmw.pte);
--

2.52.0

author maintainer
#26 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 7 lines
Quoted 6 lines

(2) tlb_remove_table_sync_one() is not a NOP on architectures with CONFIG_MMU_GATHER_RCU_TABLE_FREE that actually don't need a sync.

Take x86 as an example: in the common case (!pv, !X86_FEATURE_INVLPGB) we still issue IPIs during TLB flushes and don't actually need the second tlb_remove_table_sync_one().

Quoted 5 lines

Hmm wasn't aware that x86 would still IPI even with CONFIG_MMU_GATHER_RCU_TABLE_FREE??

But then we'd have to set tlb->freed_tables and as per your above point maybe overkill...hm one for another time then I guess! :)

I have a prototype patch to handle that, Lance wants to look into polishing it up :)

[...]

Quoted 10 lines

include/asm-generic/tlb.h struct mmu_gather { · line 368

Quoted 7 lines

+++ b/include/asm-generic/tlb.h

include/asm-generic/tlb.h +2 -0
@@ -364,6 +364,17 @@ struct mmu_gather {
  	unsigned int		vma_huge : 1;
  	unsigned int		vma_pfn  : 1;
 
+	/*
+	 * Did we unshare (unmap) any shared page tables?

Given mshare is incoming, maybe worth clarifying and being explicit about hugetlb in both comment and name?

I think we should instead call out mshare eplicitly, once we know what that would need here.

In general, I don't think we'll really use the "unshare" termonology a lot with mshare, as it's not really something transparent. In the mshare world it's simply an unmap in the mshare-owner MM. (mshare_detach similarly is rather a unmap operation)

Long story short: I'll lean towards keeping this here as it is and not creating even longer names.

Quoted 11 lines

include/asm-generic/tlb.h struct mmu_gather { · line 376

Quoted 9 lines
include/asm-generic/tlb.h +8 -0
+	 */
+	unsigned int		unshared_tables : 1;
+
+	/*
+	 * Did we unshare any page tables such that they are now exclusive
+	 * and could get reused+modified by the new owner?
+	 */
+	unsigned int		fully_unshared_tables : 1;

Does fully_unshared_tables rely on unshared_tables also being set?

See below.

Quoted 4 lines
Quoted 4 lines
include/asm-generic/tlb.h +1 -0
+
  	unsigned int		batch_count;
 
Quoted 8 lines

include/asm-generic/tlb.h static inline void __tlb_reset_range(struct mmu_gather *tlb) · line 414

Quoted 6 lines
include/asm-generic/tlb.h +1 -0
  #ifndef CONFIG_MMU_GATHER_NO_GATHER
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
  	tlb->cleared_pmds = 0;
  	tlb->cleared_puds = 0;
  	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;

As Nadav points out, should also initialise fully_unshared_tables.

Right, but on an earlier init path, not on the range reset path here.

Quoted 13 lines

include/asm-generic/tlb.h static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb) · line 499

Quoted 10 lines
include/asm-generic/tlb.h +1 -1
  	/*
  	 * Do not reset mmu_gather::vma_* fields here, we do not
  	 * call into tlb_start_vma() again to set them if there is an
@@ -484,7 +496,7 @@ static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
  	 * these bits.
  	 */
  	if (!(tlb->freed_tables || tlb->cleared_ptes || tlb->cleared_pmds ||
-	      tlb->cleared_puds || tlb->cleared_p4ds))
+	      tlb->cleared_puds || tlb->cleared_p4ds || tlb->unshared_tables))

What about fully_unshared_tables? I guess though unshared_tables implies fully_unshared_tables.

fully_unshared_tables is only for triggering IPIs and consequently not about flushing TLBs.

The TLB part is taken care of by unshared_tables, and we will always set unshared_tables when unsharing any page tables (incl. fully unshared ones).

Quoted 8 lines
Quoted 8 lines
include/asm-generic/tlb.h +0 -0
  		return;
 
  	tlb_flush(tlb);
@@ -773,6 +785,61 @@ static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd)
  }
  #endif
 
Quoted 16 lines

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 800

Quoted 13 lines
include/asm-generic/tlb.h +13 -0
+#ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
+static inline void tlb_unshare_pmd_ptdesc(struct mmu_gather *tlb, struct ptdesc *pt,
+					  unsigned long addr)
+{
+	/*
+	 * The caller must make sure that concurrent unsharing + exclusive
+	 * reuse is impossible until tlb_flush_unshared_tables() was called.
+	 */
+	VM_WARN_ON_ONCE(!ptdesc_pmd_is_shared(pt));
+	ptdesc_pmd_pts_dec(pt);
+
+	/* Clearing a PUD pointing at a PMD table with PMD leaves. */
+	tlb_flush_pmd_range(tlb, addr & PUD_MASK, PUD_SIZE);

OK I guess before we were always flushing for each page, but now we are accumulating the flushes here.

Yes.

Quoted 12 lines
Quoted 12 lines
include/asm-generic/tlb.h +11 -0
+
+	/*
+	 * If the page table is now exclusively owned, we fully unshared
+	 * a page table.
+	 */
+	if (!ptdesc_pmd_is_shared(pt))
+		tlb->fully_unshared_tables = true;
+	tlb->unshared_tables = true;
+}
+
+static inline void tlb_flush_unshared_tables(struct mmu_gather *tlb)
Quoted 8 lines

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 816

Quoted 5 lines
include/asm-generic/tlb.h +5 -0
+{
+	/*
+	 * As soon as the caller drops locks to allow for reuse of
+	 * previously-shared tables, these tables could get modified and
+	 * even reused outside of hugetlb context. So flush the TLB now.

Hmm but you're doing this in both the case of unshare and fully unsharing, so is this the right place to make this comment?

That's why I start the comment below with "Similarly", to make it clear that the comments build up on each other.

But I'm afraid I might not be getting your point fully here :/

Quoted 11 lines

Surely here this is about flushing TLBs for the unsharer only as it no longer uses it?

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 820

Quoted 4 lines
include/asm-generic/tlb.h +4 -0
+	 *
+	 * Note that we cannot defer the flush to a later point even if we are
+	 * not the last sharer of the page table.
+	 */

Not hugely clear, some double negative here. Maybe worth saying something like:

Quoted 4 lines

'Even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer who no longer has access to this memory'

Or something? Assuming this is accurate :)

I'll adjust it to "Not that even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer now.".

[...]

Quoted 5 lines

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5131

Quoted 1 line
mm/hugetlb.c +1 -0
+		tlb_remove_huge_tlb_entry(h, &tlb, src_pte, old_addr);

OK I guess we need to add these to cases where we remove previous entries because before we weren't accumulating TLB state except in __unmap_hugepage_range()?

Exactly.

Quoted 16 lines

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5140

Quoted 13 lines
mm/hugetlb.c +1 -4
  		dst_pte = huge_pte_alloc(mm, new_vma, new_addr, sz);
  		if (!dst_pte)
@@ -5136,13 +5137,13 @@ int move_hugetlb_page_tables(struct vm_area_struct *vma,
  		move_huge_pte(vma, old_addr, new_addr, src_pte, dst_pte, sz);
  	}
 
-	if (shared_pmd)
-		flush_hugetlb_tlb_range(vma, range.start, range.end);
-	else
-		flush_hugetlb_tlb_range(vma, old_end - len, old_end);
+	tlb_flush_mmu_tlbonly(&tlb);

Maybe:

Quoted 5 lines
	if (!tlb->unshared_tables)
		tlb_flush_mmu_tlbonly(&tlb);

To avoid doing that twice? Not sure if so important as will be noop second time though.

No, see the existing code on the !shared path: we flush even if we didn't unshare anything, and I am not changing these semantics.

The huge_pmd_unshare_flush() will skip the second tlb_flush_mmu_tlbonly().

Quoted 9 lines

Review int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5141

Quoted 2 lines
(quoted diff) +1 -0
+	huge_pmd_unshare_flush(&tlb, vma);

OK I guess the semantics are

huge_pmd_unshare() for everything that needs unsharing, accumulating tlb state...

huge_pmd_unshare_flush() to, err, flush :) followed by tlb_finish_mmu() obv, and with i_mmap lock held...

The tlb_finish_mmu() can be defered, as it's mostly for safety checks in the hugetlb usage here. For pure unsharing, huge_pmd_unshare_flush() will do any flushing early.

Quoted 9 lines

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5146

Quoted 6 lines
mm/hugetlb.c +2 -0
+
  	mmu_notifier_invalidate_range_end(&range);
  	i_mmap_unlock_write(mapping);
  	hugetlb_vma_unlock_write(vma);
+	tlb_finish_mmu(&tlb);

Does it matter that the hugetlb VMA lock is gone when we invoke tlb_finish_mmu()? I'm guessing not.

It shouldn't, as it's mostly safety-checks only for our use case here.

Quoted 10 lines

Kinda wish we could wrap these start/end states, it's fiddly to know that you have to:

- call huge_pmd_unshare_flush()
- release i_mmap lock
- unlock vma hugetlb (ugh god don't even get me started on how that's implemented :)
- call tlb_finish_mmu()

Obv. this kind of thing can be part of future cleanups

Yes, not messing with there here :)

[...]

Quoted 23 lines
Quoted 10 lines
mm/hugetlb.c +10 -0
+/*
+ * huge_pmd_unshare_flush - Complete a sequence of huge_pmd_unshare() calls
+ * @tlb: the current mmu_gather.
+ * @vma: the vma covering the pmd table.
+ *
+ * Perform necessary TLB flushes or IPI broadcasts to synchronize PMD table
+ * unsharing with concurrent page table walkers (TLB, GUP-fast, etc.).
+ *
+ * This function must be called after a sequence of huge_pmd_unshare()
+ * calls while still holding the i_mmap_rwsem.

mm/hugetlb.c int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma, · line 6948

Quoted 10 lines
mm/hugetlb.c +10 -0
+ */
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma)
+{
+	/*
+	 * We must synchronize page table unsharing such that nobody will
+	 * try reusing a previously-shared page table while it might still
+	 * be in use by previous sharers (TLB, GUP_fast).
+	 */
+	i_mmap_assert_write_locked(vma->vm_file->f_mapping);
+

Extreme nit: inconsistent newline here compared to elsewhere :)

Quoted 1 line

Not even sure why I'm making this point tbh

Inconsistent with what exactly? I prefered separating the safety-check+comment that explains why we check for the lock from the actual logic that carries out the actual logic.

Quoted 56 lines
Quoted 56 lines
mm/hugetlb.c +13 -4
+	tlb_flush_unshared_tables(tlb);
+}
+
  #else /* !CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING */
 
  pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
@@ -6947,12 +6957,16 @@ pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
  	return NULL;
  }
 
-int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
-				unsigned long addr, pte_t *ptep)
+int huge_pmd_unshare(struct mmu_gather *tlb, struct vm_area_struct *vma,
+		unsigned long addr, pte_t *ptep)
  {
  	return 0;
  }
 
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma)
+{
+}
+
  void adjust_range_if_pmd_sharing_possible(struct vm_area_struct *vma,
  				unsigned long *start, unsigned long *end)
  {
@@ -7219,6 +7233,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
  	unsigned long sz = huge_page_size(h);
  	struct mm_struct *mm = vma->vm_mm;
  	struct mmu_notifier_range range;
+	struct mmu_gather tlb;
  	unsigned long address;
  	spinlock_t *ptl;
  	pte_t *ptep;
@@ -7229,6 +7244,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
  	if (start >= end)
  		return;
 
+	tlb_gather_mmu(&tlb, mm);
  	flush_cache_range(vma, start, end);
  	/*
  	 * No need to call adjust_range_if_pmd_sharing_possible(), because
@@ -7248,10 +7264,10 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
  		if (!ptep)
  			continue;
  		ptl = huge_pte_lock(h, mm, ptep);
-		huge_pmd_unshare(mm, vma, address, ptep);
+		huge_pmd_unshare(&tlb, vma, address, ptep);
  		spin_unlock(ptl);
  	}
-	flush_hugetlb_tlb_range(vma, start, end);
+	huge_pmd_unshare_flush(&tlb, vma);
  	if (take_locks) {
  		i_mmap_unlock_write(vma->vm_file->f_mapping);
  		hugetlb_vma_unlock_write(vma);
@@ -7261,6 +7277,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
Quoted 8 lines

mm/hugetlb.c static void hugetlb_unshare_pmds(struct vm_area_struct *vma, · line 7280

Quoted 4 lines
mm/hugetlb.c +1 -0
  	 * Documentation/mm/mmu_notifier.rst.
  	 */
  	mmu_notifier_invalidate_range_end(&range);
+	tlb_finish_mmu(&tlb);

Hmm, does it matter that if !take_locks, the i_mmap lock and hugetlb vma locks will still be held when tlb_finish_mmu() is invoked here? I'm guessing it has no bearing but just to be sure :)

See above regarding safety checks.

[...]

Quoted 28 lines
Quoted 10 lines
mm/rmap.c +2 -0
  #define CREATE_TRACE_POINTS
  #include <trace/events/migrate.h>
@@ -2008,13 +2008,17 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
  			 * if unsuccessful.
  			 */
  			if (!anon) {
+				struct mmu_gather tlb;
+
  				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
  				if (!hugetlb_vma_trylock_write(vma))

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

Quoted 10 lines
mm/rmap.c +5 -3
  					goto walk_abort;
-				if (huge_pmd_unshare(mm, vma, address, pvmw.pte)) {
+
+				tlb_gather_mmu(&tlb, mm);
+				if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
  					hugetlb_vma_unlock_write(vma);
-					flush_tlb_range(vma,
-						range.start, range.end);
+					huge_pmd_unshare_flush(&tlb, vma);
+					tlb_finish_mmu(&tlb);

Not sure if it matters, but elsewhere you order the locks as:

- huge_pmd_unshare_flush()
- release i_mmap lock
- unlock vma hugetlb
- call tlb_finish_mmu()

Quoted 9 lines

But here it's:

- unlock vma hugetlb
- huge_pmd_unshare_flush()
- call tlb_finish_mmu()
- (later) release i_mmap lock

Does that matter in terms of lock inversions etc.?

I had a cleanup patch to change some of that, but I decided to keep it has is for this series: flush the tlb and issue the IPI while we still hold the page table lock.

(no idea what the hugetlb vma locking even does here, and how it interacts with the existing TLB flush -- not touching that confusing bit :) )

Thanks for the review!

Signature · 3 lines
Cheers

David
#27 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 7 lines · David Hildenbrand (Red Hat)
Quoted 7 lines
Quoted 6 lines

(2) tlb_remove_table_sync_one() is not a NOP on architectures with CONFIG_MMU_GATHER_RCU_TABLE_FREE that actually don't need a sync.

Take x86 as an example: in the common case (!pv, !X86_FEATURE_INVLPGB) we still issue IPIs during TLB flushes and don't actually need the second tlb_remove_table_sync_one().

Quoted 8 lines
Quoted 5 lines

Hmm wasn't aware that x86 would still IPI even with CONFIG_MMU_GATHER_RCU_TABLE_FREE??

But then we'd have to set tlb->freed_tables and as per your above point maybe overkill...hm one for another time then I guess! :)

I have a prototype patch to handle that, Lance wants to look into polishing it up :)

OK cool.

Quoted 8 lines

[...]

Quoted 5 lines
Quoted 5 lines

+++ b/include/asm-generic/tlb.h

include/asm-generic/tlb.h +0 -0
@@ -364,6 +364,17 @@ struct mmu_gather {
  	unsigned int		vma_huge : 1;
  	unsigned int		vma_pfn  : 1;
 
Quoted 8 lines
Quoted 5 lines

include/asm-generic/tlb.h struct mmu_gather { · line 368

Quoted 2 lines
include/asm-generic/tlb.h +2 -0
+	/*
+	 * Did we unshare (unmap) any shared page tables?

Given mshare is incoming, maybe worth clarifying and being explicit about hugetlb in both comment and name?

I think we should instead call out mshare eplicitly, once we know what that would need here.

Right, fair enough.

Quoted 5 lines

In general, I don't think we'll really use the "unshare" termonology a lot with mshare, as it's not really something transparent. In the mshare world it's simply an unmap in the mshare-owner MM. (mshare_detach similarly is rather a unmap operation)

Right yeah. By having an owner mm we keep things simpler than they'd otherwise be.

Quoted 3 lines

Long story short: I'll lean towards keeping this here as it is and not creating even longer names.

Ack.

Quoted 28 lines
Quoted 12 lines

include/asm-generic/tlb.h struct mmu_gather { · line 376

Quoted 10 lines
include/asm-generic/tlb.h +8 -0
+	 */
+	unsigned int		unshared_tables : 1;
+
+	/*
+	 * Did we unshare any page tables such that they are now exclusive
+	 * and could get reused+modified by the new owner?
+	 */
+	unsigned int		fully_unshared_tables : 1;

Does fully_unshared_tables rely on unshared_tables also being set?

See below.

Quoted 4 lines
Quoted 4 lines
include/asm-generic/tlb.h +1 -0
+
  	unsigned int		batch_count;
 
Quoted 8 lines

include/asm-generic/tlb.h static inline void __tlb_reset_range(struct mmu_gather *tlb) · line 414

Quoted 6 lines
include/asm-generic/tlb.h +1 -0
  #ifndef CONFIG_MMU_GATHER_NO_GATHER
@@ -400,6 +411,7 @@ static inline void __tlb_reset_range(struct mmu_gather *tlb)
  	tlb->cleared_pmds = 0;
  	tlb->cleared_puds = 0;
  	tlb->cleared_p4ds = 0;
+	tlb->unshared_tables = 0;

As Nadav points out, should also initialise fully_unshared_tables.

Quoted 1 line

Right, but on an earlier init path, not on the range reset path here.

Shouldn't we reset it also?

I mean __tlb_reset_range() is also called by __tlb_gather_mmu() (invoked by tlb_gather_mmu[_fullmm]()).

Quoted 15 lines
Quoted 14 lines

include/asm-generic/tlb.h static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb) · line 499

Quoted 11 lines
include/asm-generic/tlb.h +1 -1
  	/*
  	 * Do not reset mmu_gather::vma_* fields here, we do not
  	 * call into tlb_start_vma() again to set them if there is an
@@ -484,7 +496,7 @@ static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
  	 * these bits.
  	 */
  	if (!(tlb->freed_tables || tlb->cleared_ptes || tlb->cleared_pmds ||
-	      tlb->cleared_puds || tlb->cleared_p4ds))
+	      tlb->cleared_puds || tlb->cleared_p4ds || tlb->unshared_tables))

What about fully_unshared_tables? I guess though unshared_tables implies fully_unshared_tables.

Quoted 5 lines

fully_unshared_tables is only for triggering IPIs and consequently not about flushing TLBs.

The TLB part is taken care of by unshared_tables, and we will always set unshared_tables when unsharing any page tables (incl. fully unshared ones).

OK, so is there ever a situation where fully_unshared_tables would be set without unshared_tables? Presumably not.

Quoted 50 lines
Quoted 10 lines
Quoted 10 lines
include/asm-generic/tlb.h +0 -0
  		return;
 
  	tlb_flush(tlb);
@@ -773,6 +785,61 @@ static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd)
  }
  #endif
 
Quoted 16 lines

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 800

Quoted 13 lines
include/asm-generic/tlb.h +13 -0
+#ifdef CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING
+static inline void tlb_unshare_pmd_ptdesc(struct mmu_gather *tlb, struct ptdesc *pt,
+					  unsigned long addr)
+{
+	/*
+	 * The caller must make sure that concurrent unsharing + exclusive
+	 * reuse is impossible until tlb_flush_unshared_tables() was called.
+	 */
+	VM_WARN_ON_ONCE(!ptdesc_pmd_is_shared(pt));
+	ptdesc_pmd_pts_dec(pt);
+
+	/* Clearing a PUD pointing at a PMD table with PMD leaves. */
+	tlb_flush_pmd_range(tlb, addr & PUD_MASK, PUD_SIZE);

OK I guess before we were always flushing for each page, but now we are accumulating the flushes here.

Yes.

Quoted 12 lines
Quoted 12 lines
include/asm-generic/tlb.h +11 -0
+
+	/*
+	 * If the page table is now exclusively owned, we fully unshared
+	 * a page table.
+	 */
+	if (!ptdesc_pmd_is_shared(pt))
+		tlb->fully_unshared_tables = true;
+	tlb->unshared_tables = true;
+}
+
+static inline void tlb_flush_unshared_tables(struct mmu_gather *tlb)
Quoted 8 lines

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 816

Quoted 5 lines
include/asm-generic/tlb.h +5 -0
+{
+	/*
+	 * As soon as the caller drops locks to allow for reuse of
+	 * previously-shared tables, these tables could get modified and
+	 * even reused outside of hugetlb context. So flush the TLB now.

Hmm but you're doing this in both the case of unshare and fully unsharing, so is this the right place to make this comment?

Quoted 4 lines

That's why I start the comment below with "Similarly", to make it clear that the comments build up on each other.

But I'm afraid I might not be getting your point fully here :/

what I mean is, if we are not at the point of the table being fully unshared, nobody else can come in and reuse it right? Because we're still using it, just dropped a ref + flushed tlb?

Isn't really the correct comment here that ranges that previously mapped the shared pages might no longer, so we must clear the TLB? I may be missing something :)

Or maybe the right thing is 'we must always flush the TLB because <blahdyblah>, and if we are fully unsharing tables we must avoid reuse of previously-shared tables when the caller drops the locks' or something?

Quoted 12 lines
Quoted 12 lines

Surely here this is about flushing TLBs for the unsharer only as it no longer uses it?

include/asm-generic/tlb.h static inline bool huge_pmd_needs_flush(pmd_t oldpmd, pmd_t newpmd) · line 820

Quoted 4 lines
include/asm-generic/tlb.h +4 -0
+	 *
+	 * Note that we cannot defer the flush to a later point even if we are
+	 * not the last sharer of the page table.
+	 */

Not hugely clear, some double negative here. Maybe worth saying something like:

Quoted 7 lines
Quoted 4 lines

'Even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer who no longer has access to this memory'

Or something? Assuming this is accurate :)

I'll adjust it to "Not that even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer now.".

I guess you mean Note or that's even more confusing :P

Quoted 30 lines

[...]

Quoted 5 lines

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5131

Quoted 1 line
mm/hugetlb.c +1 -0
+		tlb_remove_huge_tlb_entry(h, &tlb, src_pte, old_addr);

OK I guess we need to add these to cases where we remove previous entries because before we weren't accumulating TLB state except in __unmap_hugepage_range()?

Exactly.

Quoted 19 lines

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5140

Quoted 13 lines

  		dst_pte = huge_pte_alloc(mm, new_vma, new_addr, sz);
  		if (!dst_pte)
mm/hugetlb.c +1 -4
@@ -5136,13 +5137,13 @@ int move_hugetlb_page_tables(struct vm_area_struct *vma,
  		move_huge_pte(vma, old_addr, new_addr, src_pte, dst_pte, sz);
  	}
 
-	if (shared_pmd)
-		flush_hugetlb_tlb_range(vma, range.start, range.end);
-	else
-		flush_hugetlb_tlb_range(vma, old_end - len, old_end);
+	tlb_flush_mmu_tlbonly(&tlb);

Maybe:

	if (!tlb->unshared_tables)
		tlb_flush_mmu_tlbonly(&tlb);
Quoted 7 lines
Quoted 2 lines

To avoid doing that twice? Not sure if so important as will be noop second time though.

No, see the existing code on the !shared path: we flush even if we didn't unshare anything, and I am not changing these semantics.

The huge_pmd_unshare_flush() will skip the second tlb_flush_mmu_tlbonly().

Yup ok not a big deal

Quoted 8 lines
Quoted 8 lines

Review int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5141

Quoted 3 lines
(quoted diff) +1 -0
+	huge_pmd_unshare_flush(&tlb, vma);

OK I guess the semantics are

huge_pmd_unshare() for everything that needs unsharing, accumulating tlb state...

Quoted 7 lines
Quoted 2 lines

huge_pmd_unshare_flush() to, err, flush :) followed by tlb_finish_mmu() obv, and with i_mmap lock held...

The tlb_finish_mmu() can be defered, as it's mostly for safety checks in the hugetlb usage here. For pure unsharing, huge_pmd_unshare_flush() will do any flushing early.

Right.

Quoted 11 lines
Quoted 10 lines

mm/hugetlb.c int move_hugetlb_page_tables(struct vm_area_struct *vma, · line 5146

Quoted 7 lines
mm/hugetlb.c +2 -0
+
  	mmu_notifier_invalidate_range_end(&range);
  	i_mmap_unlock_write(mapping);
  	hugetlb_vma_unlock_write(vma);
+	tlb_finish_mmu(&tlb);

Does it matter that the hugetlb VMA lock is gone when we invoke tlb_finish_mmu()? I'm guessing not.

Quoted 1 line

It shouldn't, as it's mostly safety-checks only for our use case here.

OK.

Quoted 5 lines
Quoted 5 lines

Kinda wish we could wrap these start/end states, it's fiddly to know that you have to:

Quoted 8 lines
Quoted 6 lines

- call huge_pmd_unshare_flush()
- release i_mmap lock
- unlock vma hugetlb (ugh god don't even get me started on how that's implemented :)
- call tlb_finish_mmu()

Obv. this kind of thing can be part of future cleanups

Yes, not messing with there here :)

Fine.

Quoted 26 lines

[...]

Quoted 23 lines
Quoted 10 lines
mm/hugetlb.c +10 -0
+/*
+ * huge_pmd_unshare_flush - Complete a sequence of huge_pmd_unshare() calls
+ * @tlb: the current mmu_gather.
+ * @vma: the vma covering the pmd table.
+ *
+ * Perform necessary TLB flushes or IPI broadcasts to synchronize PMD table
+ * unsharing with concurrent page table walkers (TLB, GUP-fast, etc.).
+ *
+ * This function must be called after a sequence of huge_pmd_unshare()
+ * calls while still holding the i_mmap_rwsem.

mm/hugetlb.c int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma, · line 6948

Quoted 10 lines
mm/hugetlb.c +10 -0
+ */
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma)
+{
+	/*
+	 * We must synchronize page table unsharing such that nobody will
+	 * try reusing a previously-shared page table while it might still
+	 * be in use by previous sharers (TLB, GUP_fast).
+	 */
+	i_mmap_assert_write_locked(vma->vm_file->f_mapping);
+

Extreme nit: inconsistent newline here compared to elsewhere :)

Quoted 5 lines
Quoted 1 line

Not even sure why I'm making this point tbh

Inconsistent with what exactly? I prefered separating the safety-check+comment that explains why we check for the lock from the actual logic that carries out the actual logic.

As I said, I'm not sure why I made the comment :P not a big deal.

Quoted 66 lines
Quoted 57 lines
Quoted 57 lines
mm/hugetlb.c +13 -4
+	tlb_flush_unshared_tables(tlb);
+}
+
  #else /* !CONFIG_HUGETLB_PMD_PAGE_TABLE_SHARING */
 
  pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
@@ -6947,12 +6957,16 @@ pte_t *huge_pmd_share(struct mm_struct *mm, struct vm_area_struct *vma,
  	return NULL;
  }
 
-int huge_pmd_unshare(struct mm_struct *mm, struct vm_area_struct *vma,
-				unsigned long addr, pte_t *ptep)
+int huge_pmd_unshare(struct mmu_gather *tlb, struct vm_area_struct *vma,
+		unsigned long addr, pte_t *ptep)
  {
  	return 0;
  }
 
+void huge_pmd_unshare_flush(struct mmu_gather *tlb, struct vm_area_struct *vma)
+{
+}
+
  void adjust_range_if_pmd_sharing_possible(struct vm_area_struct *vma,
  				unsigned long *start, unsigned long *end)
  {
@@ -7219,6 +7233,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
  	unsigned long sz = huge_page_size(h);
  	struct mm_struct *mm = vma->vm_mm;
  	struct mmu_notifier_range range;
+	struct mmu_gather tlb;
  	unsigned long address;
  	spinlock_t *ptl;
  	pte_t *ptep;
@@ -7229,6 +7244,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
  	if (start >= end)
  		return;
 
+	tlb_gather_mmu(&tlb, mm);
  	flush_cache_range(vma, start, end);
  	/*
  	 * No need to call adjust_range_if_pmd_sharing_possible(), because
@@ -7248,10 +7264,10 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
  		if (!ptep)
  			continue;
  		ptl = huge_pte_lock(h, mm, ptep);
-		huge_pmd_unshare(mm, vma, address, ptep);
+		huge_pmd_unshare(&tlb, vma, address, ptep);
  		spin_unlock(ptl);
  	}
-	flush_hugetlb_tlb_range(vma, start, end);
+	huge_pmd_unshare_flush(&tlb, vma);
  	if (take_locks) {
  		i_mmap_unlock_write(vma->vm_file->f_mapping);
  		hugetlb_vma_unlock_write(vma);
@@ -7261,6 +7277,7 @@ static void hugetlb_unshare_pmds(struct vm_area_struct *vma,
Quoted 8 lines

mm/hugetlb.c static void hugetlb_unshare_pmds(struct vm_area_struct *vma, · line 7280

Quoted 4 lines
mm/hugetlb.c +1 -0
  	 * Documentation/mm/mmu_notifier.rst.
  	 */
  	mmu_notifier_invalidate_range_end(&range);
+	tlb_finish_mmu(&tlb);

Hmm, does it matter that if !take_locks, the i_mmap lock and hugetlb vma locks will still be held when tlb_finish_mmu() is invoked here? I'm guessing it has no bearing but just to be sure :)

Quoted 1 line

See above regarding safety checks.

OK yeah, I didn't think there'd be any issues holding those locks here.

Quoted 38 lines

[...]

Quoted 35 lines
Quoted 10 lines
  #define CREATE_TRACE_POINTS
  #include <trace/events/migrate.h>
mm/rmap.c +2 -0
@@ -2008,13 +2008,17 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
  			 * if unsuccessful.
  			 */
  			if (!anon) {
+				struct mmu_gather tlb;
+
  				VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
  				if (!hugetlb_vma_trylock_write(vma))

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

Quoted 10 lines
mm/rmap.c +5 -3
  					goto walk_abort;
-				if (huge_pmd_unshare(mm, vma, address, pvmw.pte)) {
+
+				tlb_gather_mmu(&tlb, mm);
+				if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
  					hugetlb_vma_unlock_write(vma);
-					flush_tlb_range(vma,
-						range.start, range.end);
+					huge_pmd_unshare_flush(&tlb, vma);
+					tlb_finish_mmu(&tlb);

Not sure if it matters, but elsewhere you order the locks as:

- huge_pmd_unshare_flush()
- release i_mmap lock
- unlock vma hugetlb
- call tlb_finish_mmu()

But here it's:

- unlock vma hugetlb
- huge_pmd_unshare_flush()
- call tlb_finish_mmu()
- (later) release i_mmap lock

Quoted 6 lines
Quoted 2 lines

Does that matter in terms of lock inversions etc.?

I had a cleanup patch to change some of that, but I decided to keep it has is for this series: flush the tlb and issue the IPI while we still hold the page table lock.

I guess my concern is lock inversion, but not sure there's really any chance of that here.

Lockdep will help point it out if so :)

Quoted 5 lines

(no idea what the hugetlb vma locking even does here, and how it interacts with the existing TLB flush -- not touching that confusing bit :) )

Thanks for the review!

No problem, thank you for the series!

Quoted 5 lines

--
Cheers

David

Cheers, Lorenzo

author maintainer
#28 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 11 lines
Quoted 5 lines
Quoted 3 lines

As Nadav points out, should also initialise fully_unshared_tables.

Right, but on an earlier init path, not on the range reset path here.

Shouldn't we reset it also?

I mean __tlb_reset_range() is also called by __tlb_gather_mmu() (invoked by tlb_gather_mmu[_fullmm]()).

__tlb_reset_range() is all about flushing the TLB.

In v2 I have:

mm/mmu_gather.c +1 -0
index 247e3f9db6c7a..030a162a263ba 100644
@@ -426,6 +426,7 @@ static void __tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,
 #endif
        tlb->vma_pfn = 0;
 
+       tlb->fully_unshared_tables = 0;
        __tlb_reset_range(tlb);
        inc_tlb_flush_pending(tlb->mm);
 }
Quoted 15 lines
Quoted 15 lines
Quoted 14 lines

Review static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb) · line 499

Quoted 11 lines

/*
* Do not reset mmu_gather::vma_* fields here, we do not
* call into tlb_start_vma() again to set them if there is an

(quoted diff) +1 -1
@@ -484,7 +496,7 @@ static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
   	 * these bits.
   	 */
   	if (!(tlb->freed_tables || tlb->cleared_ptes || tlb->cleared_pmds ||
-	      tlb->cleared_puds || tlb->cleared_p4ds))
+	      tlb->cleared_puds || tlb->cleared_p4ds || tlb->unshared_tables))

What about fully_unshared_tables? I guess though unshared_tables implies fully_unshared_tables.

Quoted 8 lines
Quoted 5 lines

fully_unshared_tables is only for triggering IPIs and consequently not about flushing TLBs.

The TLB part is taken care of by unshared_tables, and we will always set unshared_tables when unsharing any page tables (incl. fully unshared ones).

OK, so is there ever a situation where fully_unshared_tables would be set without unshared_tables? Presumably not.

tlb_unshare_pmd_ptdesc() will always set "unshared_tables" but only conditionally sets "fully_unshared_tables".

"unshared_tables" might get handled by a prior TLB flush,
leaving only fully_unshared_tables set to perform the IPI in tlb_flush_unshared_tables().

So the important part is that whenever we unshare, we set "unshared_tables".

[...]

Quoted 9 lines
Quoted 9 lines
Quoted 8 lines

Review static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)

Quoted 5 lines
(quoted diff) +5 -0
+{
+	/*
+	 * As soon as the caller drops locks to allow for reuse of
+	 * previously-shared tables, these tables could get modified and
+	 * even reused outside of hugetlb context. So flush the TLB now.

Hmm but you're doing this in both the case of unshare and fully unsharing, so is this the right place to make this comment?

Quoted 8 lines
Quoted 4 lines

That's why I start the comment below with "Similarly", to make it clear that the comments build up on each other.

But I'm afraid I might not be getting your point fully here :/

what I mean is, if we are not at the point of the table being fully unshared, nobody else can come in and reuse it right? Because we're still using it, just dropped a ref + flushed tlb?

After we drop the lock, someone else could fully unshare it. And that other (MM) would not be able to flush the TLB for us -- in contrast to the IPI that would affect all CPUs.

Quoted 4 lines

Isn't really the correct comment here that ranges that previously mapped the shared pages might no longer, so we must clear the TLB? I may be missing something :)

There are cases where we defer flushing the TLB until we dropped all (exclusive) locks. In particular, MADV_DONTNEED does that in some cases, essentially deferring the flush to the tlb_finish_mmu().

free_pgtables() will also defer the flush, performing the TLB flush during tlb_finish_mmu(), before

The point is (as I tried to make clear in the comment), for unsharing we have no control whenn the page table gets freed after we drop the lock.

So we must flush the TLB now and cannot defer it like we do in the other cases.

Quoted 4 lines

Or maybe the right thing is 'we must always flush the TLB because <blahdyblah>, and if we are fully unsharing tables we must avoid reuse of previously-shared tables when the caller drops the locks' or something?

I hope the description above made it clearer why I spell out that the TLB must be flushed now.

Quoted 16 lines
Quoted 16 lines
Quoted 16 lines

Surely here this is about flushing TLBs for the unsharer only as it no longer uses it?

Review

Quoted 4 lines
(quoted diff) +4 -0
+	 *
+	 * Note that we cannot defer the flush to a later point even if we are
+	 * not the last sharer of the page table.
+	 */

Not hugely clear, some double negative here. Maybe worth saying something like:

'Even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer who no longer has access to this memory'

Quoted 6 lines
Quoted 4 lines
Quoted 1 line

Or something? Assuming this is accurate :)

I'll adjust it to "Not that even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer now.".

I guess you mean Note or that's even more confusing :P

:) Yeah, I did that in v2.

Signature · 3 lines
Cheers

David
#29 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 8 lines · David Hildenbrand (Red Hat)
Quoted 8 lines
Quoted 5 lines
Quoted 3 lines

As Nadav points out, should also initialise fully_unshared_tables.

Right, but on an earlier init path, not on the range reset path here.

Shouldn't we reset it also?

Quoted 7 lines
Quoted 3 lines

I mean __tlb_reset_range() is also called by __tlb_gather_mmu() (invoked by tlb_gather_mmu[_fullmm]()).

__tlb_reset_range() is all about flushing the TLB.

In v2 I have:

(Was waiting for reply here before looking there of course :)

mm/mmu_gather.c static void __tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm, · line 432

Quoted 13 lines
mm/mmu_gather.c +1 -0
index 247e3f9db6c7a..030a162a263ba 100644
@@ -426,6 +426,7 @@ static void __tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,
 #endif
        tlb->vma_pfn = 0;
+       tlb->fully_unshared_tables = 0;
        __tlb_reset_range(tlb);
        inc_tlb_flush_pending(tlb->mm);
 }
 

OK so I guess since this isn't tied to flushing the TLB, but rather only to the IPI we're good.

Quoted 19 lines
Quoted 19 lines
Quoted 19 lines
Quoted 15 lines

Review static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb) · line 499

Quoted 12 lines

/*
* Do not reset mmu_gather::vma_* fields here, we do not
* call into tlb_start_vma() again to set them if there is an

(quoted diff) +1 -1
@@ -484,7 +496,7 @@ static inline void tlb_flush_mmu_tlbonly(struct mmu_gather *tlb)
   	 * these bits.
   	 */
   	if (!(tlb->freed_tables || tlb->cleared_ptes || tlb->cleared_pmds ||
-	      tlb->cleared_puds || tlb->cleared_p4ds))
+	      tlb->cleared_puds || tlb->cleared_p4ds || tlb->unshared_tables))

What about fully_unshared_tables? I guess though unshared_tables implies fully_unshared_tables.

fully_unshared_tables is only for triggering IPIs and consequently not about flushing TLBs.

Quoted 8 lines
Quoted 5 lines
Quoted 2 lines

The TLB part is taken care of by unshared_tables, and we will always set unshared_tables when unsharing any page tables (incl. fully unshared ones).

OK, so is there ever a situation where fully_unshared_tables would be set without unshared_tables? Presumably not.

tlb_unshare_pmd_ptdesc() will always set "unshared_tables" but only conditionally sets "fully_unshared_tables".

Of course.

Quoted 3 lines

"unshared_tables" might get handled by a prior TLB flush,
leaving only fully_unshared_tables set to perform the IPI in tlb_flush_unshared_tables().

I see, right, so we can in fact have one set without the other.

But this logic pertains to the TLB flush so we're ok.

Quoted 2 lines

So the important part is that whenever we unshare, we set "unshared_tables".

Yes.

Quoted 17 lines

[...]

Quoted 14 lines
Quoted 13 lines
Quoted 8 lines

Review

Quoted 5 lines
(quoted diff) +5 -0
+{
+	/*
+	 * As soon as the caller drops locks to allow for reuse of
+	 * previously-shared tables, these tables could get modified and
+	 * even reused outside of hugetlb context. So flush the TLB now.

Hmm but you're doing this in both the case of unshare and fully unsharing, so is this the right place to make this comment?

That's why I start the comment below with "Similarly", to make it clear that the comments build up on each other.

But I'm afraid I might not be getting your point fully here :/

Quoted 7 lines
Quoted 3 lines

what I mean is, if we are not at the point of the table being fully unshared, nobody else can come in and reuse it right? Because we're still using it, just dropped a ref + flushed tlb?

After we drop the lock, someone else could fully unshare it. And that other (MM) would not be able to flush the TLB for us -- in contrast to the IPI that would affect all CPUs.

Ah so it's about TLB flushing for _us_ whereas the other user who fully unshares will flush for _them_ which may not affect the CPUs we're using.

Yeah what fun this is :)

Quoted 10 lines
Quoted 5 lines

Isn't really the correct comment here that ranges that previously mapped the shared pages might no longer, so we must clear the TLB? I may be missing something :)

There are cases where we defer flushing the TLB until we dropped all (exclusive) locks. In particular, MADV_DONTNEED does that in some cases, essentially deferring the flush to the tlb_finish_mmu().

Quoted 7 lines

free_pgtables() will also defer the flush, performing the TLB flush during tlb_finish_mmu(), before

The point is (as I tried to make clear in the comment), for unsharing we have no control whenn the page table gets freed after we drop the lock.

So we must flush the TLB now and cannot defer it like we do in the other cases.

Yeah I guess because of the above - that is - other users may unshare for their CPUs but not unshare for ours?

Quoted 8 lines
Quoted 5 lines

Or maybe the right thing is 'we must always flush the TLB because <blahdyblah>, and if we are fully unsharing tables we must avoid reuse of previously-shared tables when the caller drops the locks' or something?

I hope the description above made it clearer why I spell out that the TLB must be flushed now.

Yeah I think so thanks :)

Quoted 17 lines
Quoted 17 lines
Quoted 17 lines
Quoted 17 lines

Surely here this is about flushing TLBs for the unsharer only as it no longer uses it?

Review

Quoted 4 lines
(quoted diff) +4 -0
+	 *
+	 * Note that we cannot defer the flush to a later point even if we are
+	 * not the last sharer of the page table.
+	 */

Not hugely clear, some double negative here. Maybe worth saying something like:

'Even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer who no longer has access to this memory'

Quoted 8 lines
Quoted 6 lines
Quoted 4 lines
Quoted 1 line

Or something? Assuming this is accurate :)

I'll adjust it to "Not that even if we are not fully unsharing a PMD table, we must flush the TLB for the unsharer now.".

I guess you mean Note or that's even more confusing :P

:) Yeah, I did that in v2.

Thanks :)

Quoted 5 lines

--
Cheers

David

Cheers, Lorenzo

author maintainer
#30 lore raw

Re: [PATCH v1 4/4] mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 12 lines
Quoted 12 lines
Quoted 4 lines

Isn't really the correct comment here that ranges that previously mapped the shared pages might no longer, so we must clear the TLB? I may be missing something :)

There are cases where we defer flushing the TLB until we dropped all (exclusive) locks. In particular, MADV_DONTNEED does that in some cases, essentially deferring the flush to the tlb_finish_mmu().

free_pgtables() will also defer the flush, performing the TLB flush during tlb_finish_mmu(), before

Quoted 7 lines
Quoted 4 lines

The point is (as I tried to make clear in the comment), for unsharing we have no control whenn the page table gets freed after we drop the lock.

So we must flush the TLB now and cannot defer it like we do in the other cases.

Yeah I guess because of the above - that is - other users may unshare for their CPUs but not unshare for ours?

Yes :)

It's all very complicated, therefore I decided to rather add more comments describing what we depend on.

Signature · 3 lines
Cheers

David
Tested-by on the v1 series
#31 lore raw
In reply to David Hildenbrand (Red Hat)
Quoted 72 lines · David Hildenbrand (Red Hat)

One functional fix, one performance regression fix, and two related comment fixes.

I cleaned up my prototype I recently shared [1] for the performance fix,
deferring most of the cleanups I had in the prototype to a later point.
While doing that I identified the other things.

The goal of this patch set is to be backported to stable trees "fairly"
easily. At least patch #1 and #4.

Patch #1 fixes hugetlb_pmd_shared() not detecting any sharing Patch #2 + #3 are simple comment fixes that patch #4 interacts with. Patch #4 is a fix for the reported performance regression due to excessive
IPI broadcasts during fork()+exit().

The last patch is all about TLB flushes, IPIs and mmu_gather.
Read: complicated

I added as much comments + description that I possibly could, and I am
hoping for review from Jann.

There are plenty of cleanups in the future to be had + one reasonable optimization on x86. But that's all out of scope for this series.

Compile tested on plenty of architectures.

Runtime tested, with a focus on fixing the performance regression using
the original reproducer [2] on x86.

I'm still busy with more testing (making sure that my TLB flushing changes
are good), but sending this out already so people can test and review while I am soon heading for LPC.

[1]
https://lore.kernel.org/all/[email protected]/
[2]
https://lore.kernel.org/all/[email protected]/

Cc Will Deacon <[email protected]>
Cc "Aneesh Kumar K.V" <[email protected]>
Cc Andrew Morton <[email protected]>
Cc Nick Piggin <[email protected]>
Cc Peter Zijlstra <[email protected]>
Cc Arnd Bergmann <[email protected]>
Cc Muchun Song <[email protected]>
Cc Oscar Salvador <[email protected]>
Cc "Liam R. Howlett" <[email protected]>
Cc Lorenzo Stoakes <[email protected]>
Cc Vlastimil Babka <[email protected]>
Cc Jann Horn <[email protected]>
Cc Pedro Falcato <[email protected]>
Cc Rik van Riel <[email protected]>
Cc Harry Yoo <[email protected]>
Cc Uschakow, Stanislav" <[email protected]>
Cc Laurence Oberman <[email protected]>
Cc Prakash Sangappa <[email protected]>
Cc Nadav Amit <[email protected]>

David Hildenbrand (Red Hat) (4):
  mm/hugetlb: fix hugetlb_pmd_shared()
  mm/hugetlb: fix two comments related to huge_pmd_unshare() mm/rmap: fix two comments related to huge_pmd_unshare() mm/hugetlb: fix excessive IPI broadcasts when unsharing PMD tables using mmu_gather

Quoted 9 lines

 include/asm-generic/tlb.h |  69 +++++++++++++++++++- include/linux/hugetlb.h   |  21 ++++---
 mm/hugetlb.c              | 129 ++++++++++++++++++++---------------- --
 mm/mmu_gather.c           |   6 ++
 mm/mprotect.c             |   2 +-
 mm/rmap.c                 |  45 +++++++------

 6 files changed, 178 insertions(+), 94 deletions(-)

For the Series passed generic testing with a focus on the CVE regression and looks good.