synchronously scan and reclaim empty user PTE pages
Subsystems (8)
v2 cover synchronously scan and reclaim empty user PTE pages
Changes in v2:
- fix [PATCH v1 1/7] (Jann Horn)
- reset force_flush and force_break to false in [PATCH v1 2/7] (Jann Horn)
- introduce zap_nonpresent_ptes() and do_zap_pte_range()
- check pte_none() instead of can_reclaim_pt after the processing of PTEs (remove [PATCH v1 3/7] and [PATCH v1 4/7])
- reorder patches
- rebase onto the next-20241031
Changes in v1:
- replace [RFC PATCH 1/7] with a separate serise (already merge into mm-unstable):
https://lore.kernel.org/lkml/[email protected]/
(suggested by David Hildenbrand)
- squash [RFC PATCH 2/7] into [RFC PATCH 4/7]
(suggested by David Hildenbrand)
- change to scan and reclaim empty user PTE pages in zap_pte_range() (suggested by David Hildenbrand)
- sent a separate RFC patch to track the tlb flushing issue, and remove that part form this series ([RFC PATCH 3/7] and [RFC PATCH 6/7]).
- add [PATCH v1 1/7] into this series
- drop RFC tag
- rebase onto the next-20241011
Changes in RFC v2:
- fix compilation errors in [RFC PATCH 5/7] and [RFC PATCH 7/7] reproted by kernel test robot
- use pte_offset_map_nolock() + pmd_same() instead of check_pmd_still_valid() in retract_page_tables() (in [RFC PATCH 4/7])
- rebase onto the next-20240805
Hi all,
Previously, we tried to use a completely asynchronous method to reclaim empty user PTE pages [1]. After discussing with David Hildenbrand, we decided to implement synchronous reclaimation in the case of madvise(MADV_DONTNEED) as the first step.
So this series aims to synchronously free the empty PTE pages in madvise(MADV_DONTNEED) case. We will detect and free empty PTE pages in zap_pte_range(), and will add zap_details.reclaim_pt to exclude cases other than madvise(MADV_DONTNEED).
In zap_pte_range(), mmu_gather is used to perform batch tlb flushing and page freeing operations. Therefore, if we want to free the empty PTE page in this path, the most natural way is to add it to mmu_gather as well. Now, if CONFIG_MMU_GATHER_RCU_TABLE_FREE is selected, mmu_gather will free page table pages by semi RCU:
- batch table freeing: asynchronous free by RCU
- single table freeing: IPI + synchronous free
But this is not enough to free the empty PTE page table pages in paths other that munmap and exit_mmap path, because IPI cannot be synchronized with rcu_read_lock() in pte_offset_map{_lock}(). So we should let single table also be freed by RCU like batch table freeing.
As a first step, we supported this feature on x86_64 and selectd the newly introduced CONFIG_ARCH_SUPPORTS_PT_RECLAIM.
For other cases such as madvise(MADV_FREE), consider scanning and freeing empty PTE pages asynchronously in the future.
This series is based on next-20241031 (which contains the series [2]).
Note: issues related to TLB flushing are not new to this series and are tracked
in the separate RFC patch [3]. And more context please refer to this thread [4].
Comments and suggestions are welcome!
Thanks,
Qi
[1]. https://lore.kernel.org/lkml/[email protected]/
[2]. https://lore.kernel.org/lkml/[email protected]/
[3]. https://lore.kernel.org/lkml/[email protected]/
[4]. https://lore.kernel.org/lkml/[email protected]/
Qi Zheng (7):
mm: khugepaged: retract_page_tables() use pte_offset_map_rw_nolock()
mm: introduce zap_nonpresent_ptes()
mm: introduce do_zap_pte_range()
mm: make zap_pte_range() handle full within-PMD range
mm: pgtable: try to reclaim empty PTE page in madvise(MADV_DONTNEED)
x86: mm: free page table pages by RCU instead of semi RCU
x86: select ARCH_SUPPORTS_PT_RECLAIM if X86_64
arch/x86/Kconfig | 1 + arch/x86/include/asm/tlb.h | 19 ++++ arch/x86/kernel/paravirt.c | 7 ++ arch/x86/mm/pgtable.c | 10 +- include/linux/mm.h | 1 + mm/Kconfig | 15 +++ mm/Makefile | 1 + mm/internal.h | 23 ++++ mm/khugepaged.c | 17 ++- mm/madvise.c | 4 +- mm/memory.c | 228 ++++++++++++++++++++++++------------- mm/mmu_gather.c | 9 +- mm/pt_reclaim.c | 68 +++++++++++ 13 files changed, 319 insertions(+), 84 deletions(-) create mode 100644 mm/pt_reclaim.c
Signature · 1 line
2.20.1
PATCH v2 1/7 mm: khugepaged: retract_page_tables() use pte_offset_map_rw_nolock()
In retract_page_tables(), we may modify the pmd entry after acquiring the pml and ptl, so we should also check whether the pmd entry is stable. Using pte_offset_map_rw_nolock() + pmd_same() to do it, and then we can also remove the calling of the pte_lockptr().
--- mm/khugepaged.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-)
mm/khugepaged.c +16 -1
@@ -1721,6 +1721,7 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)spinlock_t *pml;spinlock_t *ptl;bool skipped_uffd = false; +pte_t *pte;/* * Check vma->anon_vma to exclude MAP_PRIVATE mappings that
@@ -1756,11 +1757,25 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) addr, addr + HPAGE_PMD_SIZE);mmu_notifier_invalidate_range_start (&range); + pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pgt_pmd, &ptl); +if (!pte) { +mmu_notifier_invalidate_range_end (&range); +continue ; + } + pml =pmd_lock (mm, pmd); - ptl =pte_lockptr (mm, pmd);if (ptl != pml)spin_lock_nested (ptl,SINGLE_DEPTH_NESTING ); +if (unlikely (!pmd_same (pgt_pmd,pmdp_get_lockless (pmd)))) { +pte_unmap_unlock (pte, ptl); +if (ptl != pml) +spin_unlock (pml); +mmu_notifier_invalidate_range_end (&range); +continue ; + } +pte_unmap (pte); +/* * Huge page lock is still held, so normally the page table * must remain empty; and we have already skipped anon_vma
Signature · 1 line
2.20.1
Jann Horn
reviewerRe: [PATCH v2 1/7] mm: khugepaged: retract_page_tables() use pte_offset_map_rw_nolock()
Quoted 2 lines · Qi Zheng
In
retract_page_tables(), we may modify the pmd entry after acquiring the pml and ptl, so we should also check whether the pmd entry is stable.
Why does taking the PMD lock not guarantee that the PMD entry is stable?
Quoted 21 lines
Using
pte_offset_map_rw_nolock()+pmd_same()to do it, and then we can also remove the calling of thepte_lockptr().Signed-off-by Qi Zheng <[email protected]>--- mm/khugepaged.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-)mm/khugepaged.c +1
-0@@ -1721,6 +1721,7 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)spinlock_t *pml;spinlock_t *ptl;bool skipped_uffd = false; +pte_t *pte;/* * Check vma->anon_vma to exclude MAP_PRIVATE mappings that
mm/khugepaged.c static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) · line 1766
Quoted 10 lines
mm/khugepaged.c +6
-0@@ -1756,11 +1757,25 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) addr, addr + HPAGE_PMD_SIZE);mmu_notifier_invalidate_range_start (&range); + pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pgt_pmd, &ptl); +if (!pte) { +mmu_notifier_invalidate_range_end (&range); +continue ; + } + pml =pmd_lock (mm, pmd);
I don't understand why you're mapping the page table before locking the PMD. Doesn't that just mean we need more error checking afterwards?
Quoted 19 lines
mm/khugepaged.c +9
-2- ptl =pte_lockptr (mm, pmd);if (ptl != pml)spin_lock_nested (ptl,SINGLE_DEPTH_NESTING ); +if (unlikely (!pmd_same (pgt_pmd,pmdp_get_lockless (pmd)))) { +pte_unmap_unlock (pte, ptl); +if (ptl != pml) +spin_unlock (pml); +mmu_notifier_invalidate_range_end (&range); +continue ; + } +pte_unmap (pte); +/* * Huge page lock is still held, so normally the page table * must remain empty; and we have already skipped anon_vma -- 2.20.1
Re: [PATCH v2 1/7] mm: khugepaged: retract_page_tables() use pte_offset_map_rw_nolock()
Hi Jann,
Quoted 5 lines · Jann Horn
Quoted 2 lines · Qi Zheng
In
retract_page_tables(), we may modify the pmd entry after acquiring the pml and ptl, so we should also check whether the pmd entry is stable.Why does taking the PMD lock not guarantee that the PMD entry is stable?
Because the pmd entry may have changed before taking the pmd lock, so we need to recheck it after taking the pmd or pte lock.
Quoted 25 lines
Quoted 25 lines
Using
pte_offset_map_rw_nolock()+pmd_same()to do it, and then we can also remove the calling of thepte_lockptr().Signed-off-by Qi Zheng <[email protected]>--- mm/khugepaged.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-)mm/khugepaged.c +1
-0@@ -1721,6 +1721,7 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)spinlock_t *pml;spinlock_t *ptl;bool skipped_uffd = false; +pte_t *pte;/* * Check vma->anon_vma to exclude MAP_PRIVATE mappings that @@ -1756,11 +1757,25 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) addr, addr + HPAGE_PMD_SIZE);mmu_notifier_invalidate_range_start (&range);
Quoted 11 lines
mm/khugepaged.c static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) · line 1766
Quoted 7 lines
mm/khugepaged.c +6
-0+ pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pgt_pmd, &ptl); +if (!pte) { +mmu_notifier_invalidate_range_end (&range); +continue ; + } + pml =pmd_lock (mm, pmd);I don't understand why you're mapping the page table before locking the PMD. Doesn't that just mean we need more error checking afterwards?
The main purpose is to obtain the pmdval. If we don't use pte_offset_map_rw_nolock, we should pay attention to recheck pmd entry before pte_lockptr(), like this:
pmdval = pmdp_get_lockless(pmd);pmd_lock
recheck pmdvalpte_lockptr(mm, pmd)
Otherwise, it may cause the system to crash. Consider the following situation:
CPU 0 CPU 1
zap_pte_range
--> clear pmd entry
free pte page (by RCU)
retract_page_tables
--> pmd_lock
pte_lockptr(mm, pmd) <-- BOOM!!So maybe calling pte_offset_map_rw_nolock() is more convenient.
Thanks,
Qi
Quoted 21 lines
Quoted 21 lines
mm/khugepaged.c +9
-2- ptl =pte_lockptr (mm, pmd);if (ptl != pml)spin_lock_nested (ptl,SINGLE_DEPTH_NESTING ); +if (unlikely (!pmd_same (pgt_pmd,pmdp_get_lockless (pmd)))) { +pte_unmap_unlock (pte, ptl); +if (ptl != pml) +spin_unlock (pml); +mmu_notifier_invalidate_range_end (&range); +continue ; + } +pte_unmap (pte); +/* * Huge page lock is still held, so normally the page table * must remain empty; and we have already skipped anon_vma -- 2.20.1
Jann Horn
reviewerRe: [PATCH v2 1/7] mm: khugepaged: retract_page_tables() use pte_offset_map_rw_nolock()
Quoted 9 lines · Qi Zheng
Quoted 5 lines · Jann Horn
Quoted 2 lines · Qi Zheng
In
retract_page_tables(), we may modify the pmd entry after acquiring the pml and ptl, so we should also check whether the pmd entry is stable.Why does taking the PMD lock not guarantee that the PMD entry is stable?
Because the pmd entry may have changed before taking the pmd lock, so we need to recheck it after taking the pmd or pte lock.
You mean it could have changed from the value we obtained from find_pmd_or_thp_or_none(mm, addr, &pmd)? I don't think that matters though.
Quoted 52 lines
Quoted 24 lines
Quoted 24 lines
Using
pte_offset_map_rw_nolock()+pmd_same()to do it, and then we can also remove the calling of thepte_lockptr().Signed-off-by Qi Zheng <[email protected]>--- mm/khugepaged.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-)mm/khugepaged.c +1
-0@@ -1721,6 +1721,7 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)spinlock_t *pml;spinlock_t *ptl;bool skipped_uffd = false; +pte_t *pte;/* * Check vma->anon_vma to exclude MAP_PRIVATE mappings that @@ -1756,11 +1757,25 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) addr, addr + HPAGE_PMD_SIZE);mmu_notifier_invalidate_range_start (&range);Quoted 11 lines
mm/khugepaged.c static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) · line 1766
Quoted 7 lines
mm/khugepaged.c +6
-0+ pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pgt_pmd, &ptl); +if (!pte) { +mmu_notifier_invalidate_range_end (&range); +continue ; + } + pml =pmd_lock (mm, pmd);I don't understand why you're mapping the page table before locking the PMD. Doesn't that just mean we need more error checking afterwards?
The main purpose is to obtain the pmdval. If we don't use
pte_offset_map_rw_nolock, we should pay attention to recheck pmd entry beforepte_lockptr(), like this:pmdval =
pmdp_get_lockless(pmd);pmd_lock
recheck pmdvalpte_lockptr(mm, pmd)Otherwise, it may cause the system to crash. Consider the following situation:
CPU 0 CPU 1
zap_pte_range
--> clear pmd entry
Quoted 7 lines
free pte page (by RCU) retract_page_tables --> pmd_lock pte_lockptr(mm, pmd) <-- BOOM!!So maybe calling
pte_offset_map_rw_nolock()is more convenient.
How about refactoring find_pmd_or_thp_or_none() like this, by moving the checks of the PMD entry value into a separate helper:
-static int find_pmd_or_thp_or_none(struct mm_struct *mm,
- unsigned long address,
- pmd_t **pmd)
+static int check_pmd_state(pmd_t *pmd)
{
- pmd_t pmde;
+ pmd_t pmde = pmdp_get_lockless(*pmd);
- *pmd = mm_find_pmd(mm, address);
- if (!*pmd)
- return SCAN_PMD_NULL;
-
- pmde = pmdp_get_lockless(*pmd);
if (pmd_none(pmde))
return SCAN_PMD_NONE;
if (!pmd_present(pmde))
return SCAN_PMD_NULL;
if (pmd_trans_huge(pmde))
return SCAN_PMD_MAPPED;
if (pmd_devmap(pmde))
return SCAN_PMD_NULL;
if (pmd_bad(pmde))
return SCAN_PMD_NULL;
return SCAN_SUCCEED;}
+static int find_pmd_or_thp_or_none(struct mm_struct *mm,
+ unsigned long address,
+ pmd_t **pmd)
+{
+
+ *pmd = mm_find_pmd(mm, address);
+ if (!*pmd)
+ return SCAN_PMD_NULL;
+ return check_pmd_state(*pmd);
+}
+
And simplifying retract_page_tables() a little bit like this:
i_mmap_lock_read(mapping);
vma_interval_tree_foreach(vma, &mapping->i_mmap, pgoff, pgoff) {
struct mmu_notifier_range range;
struct mm_struct *mm;
unsigned long addr;
pmd_t *pmd, pgt_pmd;
spinlock_t *pml;
spinlock_t *ptl;- bool skipped_uffd = false;
+ bool success = false;
/*
* Check vma->anon_vma to exclude MAP_PRIVATE mappings that
* got written to. These VMAs are likely not worth removing
* page tables from, as PMD-mapping is likely to be split later.
*/
if (READ_ONCE(vma->anon_vma))
continue;
addr = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);@@ -1763,34 +1767,34 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)
/*
* Huge page lock is still held, so normally the page table
* must remain empty; and we have already skipped anon_vma
* and userfaultfd_wp() vmas. But since the mmap_lock is not
* held, it is still possible for a racing userfaultfd_ioctl()
* to have inserted ptes or markers. Now that we hold ptlock,
* repeating the anon_vma check protects from one category,
* and repeating the userfaultfd_wp() check from another. */
- if (unlikely(vma->anon_vma || userfaultfd_wp(vma))) {
- skipped_uffd = true;
- } else {
+ if (likely(!vma->anon_vma && !userfaultfd_wp(vma))) {
pgt_pmd = pmdp_collapse_flush(vma, addr, pmd);
pmdp_get_lockless_sync();
+ success = true;
}
if (ptl != pml)
spin_unlock(ptl);
+drop_pml:
spin_unlock(pml);
mmu_notifier_invalidate_range_end(&range);- if (!skipped_uffd) {
+ if (success) {
mm_dec_nr_ptes(mm);
page_table_check_pte_clear_range(mm, addr, pgt_pmd);
pte_free_defer(mm, pmd_pgtable(pgt_pmd));
}
}
i_mmap_unlock_read(mapping);And then instead of your patch, I think you can just do this?
@@ -1754,20 +1754,22 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)
*/
if (userfaultfd_wp(vma))
continue;
/* PTEs were notified when unmapped; but now for the PMD? */
mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
addr, addr + HPAGE_PMD_SIZE);
mmu_notifier_invalidate_range_start(&range);
pml = pmd_lock(mm, pmd);+ if (check_pmd_state(mm, addr, pmd) != SCAN_SUCCEED)
+ goto drop_pml;
ptl = pte_lockptr(mm, pmd);
if (ptl != pml)
spin_lock_nested(ptl, SINGLE_DEPTH_NESTING);
/*
* Huge page lock is still held, so normally the page table
* must remain empty; and we have already skipped anon_vma
* and userfaultfd_wp() vmas. But since the mmap_lock is not
* held, it is still possible for a racing userfaultfd_ioctl()
* to have inserted ptes or markers. Now that we hold ptlock,
Re: [PATCH v2 1/7] mm: khugepaged: retract_page_tables() use pte_offset_map_rw_nolock()
Quoted 180 lines · Jann Horn
Quoted 9 lines · Qi Zheng
Quoted 5 lines · Jann Horn
Quoted 2 lines · Qi Zheng
In
retract_page_tables(), we may modify the pmd entry after acquiring the pml and ptl, so we should also check whether the pmd entry is stable.Why does taking the PMD lock not guarantee that the PMD entry is stable?
Because the pmd entry may have changed before taking the pmd lock, so we need to recheck it after taking the pmd or pte lock.
You mean it could have changed from the value we obtained from
find_pmd_or_thp_or_none(mm, addr, &pmd)? I don't think that matters though.Quoted 52 lines
Quoted 24 lines
Quoted 24 lines
Using
pte_offset_map_rw_nolock()+pmd_same()to do it, and then we can also remove the calling of thepte_lockptr().Signed-off-by Qi Zheng <[email protected]>--- mm/khugepaged.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-)mm/khugepaged.c +1
-0@@ -1721,6 +1721,7 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)spinlock_t *pml;spinlock_t *ptl;bool skipped_uffd = false; +pte_t *pte;/* * Check vma->anon_vma to exclude MAP_PRIVATE mappings that @@ -1756,11 +1757,25 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) addr, addr + HPAGE_PMD_SIZE);mmu_notifier_invalidate_range_start (&range);Quoted 11 lines
mm/khugepaged.c static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) · line 1766
Quoted 7 lines
mm/khugepaged.c +6
-0+ pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pgt_pmd, &ptl); +if (!pte) { +mmu_notifier_invalidate_range_end (&range); +continue ; + } + pml =pmd_lock (mm, pmd);I don't understand why you're mapping the page table before locking the PMD. Doesn't that just mean we need more error checking afterwards?
The main purpose is to obtain the pmdval. If we don't use
pte_offset_map_rw_nolock, we should pay attention to recheck pmd entry beforepte_lockptr(), like this:pmdval =
pmdp_get_lockless(pmd);pmd_lock
recheck pmdvalpte_lockptr(mm, pmd)Otherwise, it may cause the system to crash. Consider the following situation:
CPU 0 CPU 1
zap_pte_range
--> clear pmd entryQuoted 7 lines
free pte page (by RCU) retract_page_tables --> pmd_lock pte_lockptr(mm, pmd) <-- BOOM!!So maybe calling
pte_offset_map_rw_nolock()is more convenient.How about refactoring
find_pmd_or_thp_or_none()like this, by moving the checks of the PMD entry value into a separate helper:(quoted diff) +13
-9-static int find_pmd_or_thp_or_none(struct mm_struct *mm, - unsigned long address, - pmd_t **pmd) +static int check_pmd_state(pmd_t *pmd) { - pmd_t pmde; + pmd_t pmde = pmdp_get_lockless(*pmd); - *pmd = mm_find_pmd(mm, address); - if (!*pmd) - return SCAN_PMD_NULL; - - pmde = pmdp_get_lockless(*pmd); if (pmd_none(pmde)) return SCAN_PMD_NONE; if (!pmd_present(pmde)) return SCAN_PMD_NULL; if (pmd_trans_huge(pmde)) return SCAN_PMD_MAPPED; if (pmd_devmap(pmde)) return SCAN_PMD_NULL; if (pmd_bad(pmde)) return SCAN_PMD_NULL; return SCAN_SUCCEED; } +static int find_pmd_or_thp_or_none(struct mm_struct *mm, + unsigned long address, + pmd_t **pmd) +{ + + *pmd = mm_find_pmd(mm, address); + if (!*pmd) + return SCAN_PMD_NULL; + return check_pmd_state(*pmd); +} +And simplifying
retract_page_tables()a little bit like this:i_mmap_lock_read(mapping); vma_interval_tree_foreach(vma, &mapping->i_mmap, pgoff, pgoff) { struct mmu_notifier_range range; struct mm_struct *mm; unsigned long addr; pmd_t *pmd, pgt_pmd; spinlock_t *pml; spinlock_t *ptl;(quoted diff) +5
-5- bool skipped_uffd = false; + bool success = false; /* * Check vma->anon_vma to exclude MAP_PRIVATE mappings that * got written to. These VMAs are likely not worth removing * page tables from, as PMD-mapping is likely to be split later. */ if (READ_ONCE(vma->anon_vma)) continue; addr = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);@@ -1763,34 +1767,34 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) /* * Huge page lock is still held, so normally the page table * must remain empty; and we have already skipped anon_vma * and userfaultfd_wp() vmas. But since the mmap_lock is not * held, it is still possible for a racing userfaultfd_ioctl() * to have inserted ptes or markers. Now that we hold ptlock, * repeating the anon_vma check protects from one category, * and repeating the userfaultfd_wp() check from another. */ - if (unlikely(vma->anon_vma || userfaultfd_wp(vma))) { - skipped_uffd = true; - } else { + if (likely(!vma->anon_vma && !userfaultfd_wp(vma))) { pgt_pmd = pmdp_collapse_flush(vma, addr, pmd); pmdp_get_lockless_sync(); + success = true; } if (ptl != pml) spin_unlock(ptl); +drop_pml: spin_unlock(pml); mmu_notifier_invalidate_range_end(&range); - if (!skipped_uffd) { + if (success) { mm_dec_nr_ptes(mm); page_table_check_pte_clear_range(mm, addr, pgt_pmd); pte_free_defer(mm, pmd_pgtable(pgt_pmd)); } } i_mmap_unlock_read(mapping);
Quoted 1 line
And then instead of your patch, I think you can just do this?
Ah, this does look much better! Will change to this in the next version.
Thanks!
Quoted 26 lines
(quoted diff) +2
-0@@ -1754,20 +1754,22 @@ static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff) */ if (userfaultfd_wp(vma)) continue; /* PTEs were notified when unmapped; but now for the PMD? */ mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm, addr, addr + HPAGE_PMD_SIZE); mmu_notifier_invalidate_range_start(&range); pml = pmd_lock(mm, pmd); + if (check_pmd_state(mm, addr, pmd) != SCAN_SUCCEED) + goto drop_pml; ptl = pte_lockptr(mm, pmd); if (ptl != pml) spin_lock_nested(ptl, SINGLE_DEPTH_NESTING); /* * Huge page lock is still held, so normally the page table * must remain empty; and we have already skipped anon_vma * and userfaultfd_wp() vmas. But since the mmap_lock is not * held, it is still possible for a racing userfaultfd_ioctl()* to have inserted ptes or markers. Now that we hold ptlock,
PATCH v2 2/7 mm: introduce zap_nonpresent_ptes()
In reply to Qi ZhengSimilar to zap_present_ptes(), let's introduce zap_nonpresent_ptes() to handle non-present ptes, which can improve code readability.
No functional change.
--- mm/memory.c | 136 ++++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 63 deletions(-)
mm/memory.c +73 -63
@@ -1587,6 +1587,76 @@ static inline int zap_present_ptes(struct mmu_gather *tlb,return 1 ; } +static inline int zap_nonpresent_ptes (struct mmu_gather *tlb, +struct vm_area_struct *vma,pte_t *pte,pte_t ptent, +unsigned int max_nr,unsigned long addr, +struct zap_details *details,int *rss) +{ +swp_entry_t entry; +int nr =1 ; + + entry =pte_to_swp_entry (ptent); +if (is_device_private_entry (entry) || +is_device_exclusive_entry (entry)) { +struct page *page =pfn_swap_entry_to_page (entry); +struct folio *folio =page_folio (page); + +if (unlikely (!should_zap_folio (details, folio))) +return 1 ; +/* +* Both device private/exclusive mappings should only +* work with anonymous page so far, so we don't need to +* consider uffd-wp bit when zap. For more information, +* see zap_install_uffd_wp_if_needed(). +*/ +WARN_ON_ONCE (!vma_is_anonymous (vma)); + rss[mm_counter (folio)]--; +if (is_device_private_entry (entry)) +folio_remove_rmap_pte (folio, page, vma); +folio_put (folio); + }else if (!non_swap_entry (entry)) { +/* Genuine swap entries, hence a private anon pages */ +if (!should_zap_cows (details)) +return 1 ; + + nr =swap_pte_batch (pte, max_nr, ptent); + rss[MM_SWAPENTS ] -= nr; +free_swap_and_cache_nr (entry, nr); + }else if (is_migration_entry (entry)) { +struct folio *folio =pfn_swap_entry_folio (entry); + +if (!should_zap_folio (details, folio)) +return 1 ; + rss[mm_counter (folio)]--; + }else if (pte_marker_entry_uffd_wp (entry)) { +/* +* For anon: always drop the marker; for file: only +* drop the marker if explicitly requested. +*/ +if (!vma_is_anonymous (vma) && !zap_drop_markers (details)) +return 1 ; + }else if (is_guard_swp_entry (entry)) { +/* +* Ordinary zapping should not remove guard PTE +* markers. Only do so if we should remove PTE markers +* in general. +*/ +if (!zap_drop_markers (details)) +return 1 ; + }else if (is_hwpoison_entry (entry) ||is_poisoned_swp_entry (entry)) { +if (!should_zap_cows (details)) +return 1 ; + }else { +/* We should have covered all the swap entry types */ +pr_alert ("unrecognized swap entry 0x%lx\n" , entry.val); +WARN_ON_ONCE (1 ); + } +clear_not_present_full_ptes (vma->vm_mm, addr, pte, nr, tlb->fullmm); +zap_install_uffd_wp_if_needed (vma, addr, pte, nr, details, ptent); + +return nr; +} + static unsigned long zap_pte_range(struct mmu_gather *tlb, struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr, unsigned long end,
@@ -1598,7 +1668,6 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,spinlock_t *ptl;pte_t *start_pte;pte_t *pte; -swp_entry_t entry;int nr;tlb_change_page_size (tlb,PAGE_SIZE );
@@ -1611,8 +1680,6 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,arch_enter_lazy_mmu_mode (); do { pte_t ptent = ptep_get(pte); - struct folio *folio; - struct page *page; int max_nr; nr = 1;
@@ -1622,8 +1689,8 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,if (need_resched ())break ; + max_nr = (end - addr) /PAGE_SIZE ;if (pte_present (ptent)) { - max_nr = (end - addr) /PAGE_SIZE ; nr =zap_present_ptes (tlb, vma, pte, ptent, max_nr, addr, details, rss, &force_flush, &force_break);
@@ -1631,67 +1698,10 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb, addr += nr *PAGE_SIZE ;break ; } -continue ; - } - - entry =pte_to_swp_entry (ptent); -if (is_device_private_entry (entry) || -is_device_exclusive_entry (entry)) { - page =pfn_swap_entry_to_page (entry); - folio =page_folio (page); -if (unlikely (!should_zap_folio (details, folio))) -continue ; -/* -* Both device private/exclusive mappings should only -* work with anonymous page so far, so we don't need to -* consider uffd-wp bit when zap. For more information, -* see zap_install_uffd_wp_if_needed(). -*/ -WARN_ON_ONCE (!vma_is_anonymous (vma)); - rss[mm_counter (folio)]--; -if (is_device_private_entry (entry)) -folio_remove_rmap_pte (folio, page, vma); -folio_put (folio); - }else if (!non_swap_entry (entry)) { - max_nr = (end - addr) /PAGE_SIZE ; - nr =swap_pte_batch (pte, max_nr, ptent); -/* Genuine swap entries, hence a private anon pages */ -if (!should_zap_cows (details)) -continue ; - rss[MM_SWAPENTS ] -= nr; -free_swap_and_cache_nr (entry, nr); - }else if (is_migration_entry (entry)) { - folio =pfn_swap_entry_folio (entry); -if (!should_zap_folio (details, folio)) -continue ; - rss[mm_counter (folio)]--; - }else if (pte_marker_entry_uffd_wp (entry)) { -/* -* For anon: always drop the marker; for file: only -* drop the marker if explicitly requested. -*/ -if (!vma_is_anonymous (vma) && - !zap_drop_markers (details)) -continue ; - }else if (is_guard_swp_entry (entry)) { -/* -* Ordinary zapping should not remove guard PTE -* markers. Only do so if we should remove PTE markers -* in general. -*/ -if (!zap_drop_markers (details)) -continue ; - }else if (is_hwpoison_entry (entry) || -is_poisoned_swp_entry (entry)) { -if (!should_zap_cows (details)) -continue ; }else { -/* We should have covered all the swap entry types */ -pr_alert ("unrecognized swap entry 0x%lx\n" , entry.val); -WARN_ON_ONCE (1 ); + nr =zap_nonpresent_ptes (tlb, vma, pte, ptent, max_nr, + addr, details, rss); } -clear_not_present_full_ptes (mm, addr, pte, nr, tlb->fullmm); -zap_install_uffd_wp_if_needed (vma, addr, pte, nr, details, ptent); }while (pte += nr, addr +=PAGE_SIZE * nr, addr != end);add_mm_rss_vec (mm, rss);
Signature · 1 line
2.20.1
Re: [PATCH v2 2/7] mm: introduce zap_nonpresent_ptes()
Quoted 6 lines · Qi Zheng
Similar to
zap_present_ptes(), let's introducezap_nonpresent_ptes()to handle non-present ptes, which can improve code readability.No functional change.
Signed-off-by Qi Zheng <[email protected]>
This is a nice cleanup.
Re: [PATCH v2 2/7] mm: introduce zap_nonpresent_ptes()
In reply to Qi ZhengQuoted 6 lines · Qi Zheng
Similar to
zap_present_ptes(), let's introducezap_nonpresent_ptes()to handle non-present ptes, which can improve code readability.No functional change.
Signed-off-by Qi Zheng <[email protected]>
Signature · 3 lines
Cheers, David / dhildenb
PATCH v2 3/7 mm: introduce do_zap_pte_range()
In reply to Qi ZhengThis commit introduces do_zap_pte_range() to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.
No functional change.
--- mm/memory.c | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-)
mm/memory.c +26 -19
@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, +struct vm_area_struct *vma,pte_t *pte, +unsigned long addr,unsigned long end, +struct zap_details *details,int *rss, +bool *force_flush,bool *force_break) +{ +pte_t ptent =ptep_get (pte); +int max_nr = (end - addr) /PAGE_SIZE ; + +if (pte_none (ptent)) +return 1 ; + +if (pte_present (ptent)) +return zap_present_ptes (tlb, vma, pte, ptent, max_nr, + addr, details, rss, force_flush, + force_break); + +return zap_nonpresent_ptes (tlb, vma, pte, ptent, max_nr, addr, + details, rss); +} + static unsigned long zap_pte_range(struct mmu_gather *tlb, struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr, unsigned long end,
@@ -1679,28 +1700,14 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,flush_tlb_batched_pending (mm);arch_enter_lazy_mmu_mode ();do { -pte_t ptent =ptep_get (pte); -int max_nr; - - nr =1 ; -if (pte_none (ptent)) -continue ; -if (need_resched ())break ; - max_nr = (end - addr) /PAGE_SIZE ; -if (pte_present (ptent)) { - nr =zap_present_ptes (tlb, vma, pte, ptent, max_nr, - addr, details, rss, &force_flush, - &force_break); -if (unlikely (force_break)) { - addr += nr *PAGE_SIZE ; -break ; - } - }else { - nr =zap_nonpresent_ptes (tlb, vma, pte, ptent, max_nr, - addr, details, rss); + nr =do_zap_pte_range (tlb, vma, pte, addr, end, details, rss, + &force_flush, &force_break); +if (unlikely (force_break)) { + addr += nr *PAGE_SIZE ; +break ; } }while (pte += nr, addr +=PAGE_SIZE * nr, addr != end);
Signature · 1 line
2.20.1
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
Quoted 7 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
In reply to Qi ZhengQuoted 20 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>--- mm/memory.c | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-)mm/memory.c +1
-0@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,return nr; } +static inline int do_zap_pte_range(struct mmu_gather *tlb,
mm/memory.c static inline int zap_nonpresent_ptes(struct mmu_gather *tlb, · line 1670
Quoted 10 lines
mm/memory.c +10
-0+ struct vm_area_struct *vma, pte_t *pte, + unsigned long addr, unsigned long end, + struct zap_details *details, int *rss, + bool *force_flush, bool *force_break) +{ + pte_t ptent = ptep_get(pte); +int max_nr = (end - addr) /PAGE_SIZE ; + +if (pte_none (ptent)) +return 1 ;
Maybe we should just skip all applicable pte_none() here directly.
Signature · 3 lines
Cheers, David / dhildenb
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
Quoted 25 lines · David Hildenbrand
Quoted 24 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>---
mm/memory.c| 45 ++++++++++++++++++++++++++-------------------1 file changed, 26 insertions(+), 19 deletions(-)mm/memory.c +5
-0@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(structmmu_gather *tlb, return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, + struct vm_area_struct *vma, pte_t *pte, + unsigned long addr, unsigned long end, + struct zap_details *details, int *rss, + bool *force_flush, bool *force_break)
Quoted 8 lines
mm/memory.c static inline int zap_nonpresent_ptes(struct · line 1670
Quoted 6 lines
mm/memory.c +6
-0+{ + pte_t ptent = ptep_get(pte); + int max_nr = (end - addr) / PAGE_SIZE; + + if (pte_none(ptent)) + return 1;Maybe we should just skip all applicable
pte_none()here directly.
Do you mean we should keep pte_none() case in zap_pte_range()? Like below:
mm/memory.c +9 -8
@@ -1666,9 +1666,6 @@ static inline int do_zap_pte_range(struct mmu_gather *tlb,pte_t ptent =ptep_get (pte);int max_nr = (end - addr) /PAGE_SIZE ; -if (pte_none (ptent)) -return 1 ; -if (pte_present (ptent)) return zap_present_ptes(tlb, vma, pte, ptent, max_nr, addr, details, rss, force_flush,
@@ -1704,11 +1701,15 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,if (need_resched ())break ; - nr =do_zap_pte_range (tlb, vma, pte, addr, end, details, rss, - &force_flush, &force_break); -if (unlikely (force_break)) { - addr += nr *PAGE_SIZE ; -break ; +if (pte_none (ptep_get (pte))) { + nr =1 ; + }else { + nr =do_zap_pte_range (tlb, vma, pte, addr, end, details, + rss, &force_flush, &force_break); +if (unlikely (force_break)) { + addr += nr *PAGE_SIZE ; +break ; + } } }while (pte += nr, addr +=PAGE_SIZE * nr, addr != end);
This avoids repeated checks for pte_none() later. Both are fine for me, will change to this in the next version.
Quoted 2 lines
Thanks!
Quoted 1 line
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
Quoted 37 lines · Qi Zheng
Quoted 25 lines · David Hildenbrand
Quoted 24 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>--- mm/memory.c | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-)mm/memory.c +5
-0@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(structmmu_gather *tlb, return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, + struct vm_area_struct *vma, pte_t *pte, + unsigned long addr, unsigned long end, + struct zap_details *details, int *rss, + bool *force_flush, bool *force_break)Quoted 8 lines
mm/memory.c static inline int zap_nonpresent_ptes(struct · line 1670
Quoted 6 lines
mm/memory.c +6
-0+{ + pte_t ptent = ptep_get(pte); + int max_nr = (end - addr) / PAGE_SIZE; + + if (pte_none(ptent)) + return 1;Maybe we should just skip all applicable
pte_none()here directly.
Quoted 3 lines
Do you mean we should keep
pte_none()case inzap_pte_range()? Like below:
No rather an addon patch that will simply skip over all consecutive pte_none, like:
if (pte_none(ptent)) {
int nr;
for (nr = 1; nr < max_nr; nr++) {
ptent = ptep_get(pte + nr);
if (pte_none(ptent))
continue;
}
max_nr -= nr;
if (!max_nr)
return nr;
addr += nr * PAGE_SIZE;
pte += nr;
}Assuming that it's likely more common to have larger pte_none() holes that single ones, optimizing out the need_resched()+force_break+incremental pte/addr increments etc.
Signature · 3 lines
Cheers, David / dhildenb
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
Quoted 54 lines · David Hildenbrand
Quoted 37 lines · Qi Zheng
Quoted 25 lines · David Hildenbrand
Quoted 24 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>---
mm/memory.c| 45 ++++++++++++++++++++++++++-------------------1 file changed, 26 insertions(+), 19 deletions(-)mm/memory.c +5
-0@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(structmmu_gather *tlb, return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, + struct vm_area_struct *vma, pte_t *pte, + unsigned long addr, unsigned long end, + struct zap_details *details, int *rss, + bool *force_flush, bool *force_break)Quoted 8 lines
mm/memory.c static inline int zap_nonpresent_ptes(struct · line 1670
Quoted 6 lines
mm/memory.c +6
-0+{ + pte_t ptent = ptep_get(pte); + int max_nr = (end - addr) / PAGE_SIZE; + + if (pte_none(ptent)) + return 1;Maybe we should just skip all applicable
pte_none()here directly.Quoted 3 lines
Do you mean we should keep
pte_none()case inzap_pte_range()? Like below:No rather an addon patch that will simply skip over all consecutive
pte_none, like:if (
pte_none(ptent)) {
int nr;for (nr = 1; nr <
max_nr; nr++) {
ptent =ptep_get(pte + nr);
if (pte_none(ptent))
continue;
}
Quoted 8 lines
max_nr -= nr;
if (!max_nr)
return nr;
addr += nr *PAGE_SIZE;
pte += nr;
}Assuming that it's likely more common to have larger
pte_none()holes that single ones, optimizing out the need_resched()+force_break+incremental pte/addr increments etc.
Ah, got it. And I agree with you, will change to it in the next version.
Thanks!
Quoted 1 line
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
In reply to David HildenbrandQuoted 54 lines · David Hildenbrand
Quoted 37 lines · Qi Zheng
Quoted 25 lines · David Hildenbrand
Quoted 24 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>---
mm/memory.c| 45 ++++++++++++++++++++++++++-------------------1 file changed, 26 insertions(+), 19 deletions(-)mm/memory.c +5
-0@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(structmmu_gather *tlb, return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, + struct vm_area_struct *vma, pte_t *pte, + unsigned long addr, unsigned long end, + struct zap_details *details, int *rss, + bool *force_flush, bool *force_break)Quoted 8 lines
mm/memory.c static inline int zap_nonpresent_ptes(struct · line 1670
Quoted 6 lines
mm/memory.c +6
-0+{ + pte_t ptent = ptep_get(pte); + int max_nr = (end - addr) / PAGE_SIZE; + + if (pte_none(ptent)) + return 1;Maybe we should just skip all applicable
pte_none()here directly.Quoted 3 lines
Do you mean we should keep
pte_none()case inzap_pte_range()? Like below:No rather an addon patch that will simply skip over all consecutive
pte_none, like:if (
pte_none(ptent)) {
int nr;for (nr = 1; nr <
max_nr; nr++) {
ptent =ptep_get(pte + nr);
if (pte_none(ptent))
continue;
}
Quoted 6 lines
max_nr -= nr;
if (!max_nr)
return nr;
addr += nr *PAGE_SIZE;
pte += nr;
}
I tend to hand over the pte/addr increments here to the loop outside do_zap_pte_range(), like this:
mm/memory.c +35 -19
@@ -1657,6 +1657,36 @@ static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, +struct vm_area_struct *vma,pte_t *pte, +unsigned long addr,unsigned long end, +struct zap_details *details,int *rss, +bool *force_flush,bool *force_break) +{ +pte_t ptent =ptep_get (pte); +int max_nr = (end - addr) /PAGE_SIZE ; + +if (pte_none (ptent)) { +int nr =1 ; + +for (; nr < max_nr; nr++) { + ptent =ptep_get (pte + nr); +if (!pte_none (ptent)) +break ; + } + +return nr; + } + +if (pte_present (ptent)) +return zap_present_ptes (tlb, vma, pte, ptent, max_nr, + addr, details, rss, force_flush, + force_break); + +return zap_nonpresent_ptes (tlb, vma, pte, ptent, max_nr, addr, + details, rss); +} + static unsigned long zap_pte_range(struct mmu_gather *tlb, struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr, unsigned long end,
@@ -1679,28 +1709,14 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,flush_tlb_batched_pending (mm);arch_enter_lazy_mmu_mode ();do { -pte_t ptent =ptep_get (pte); -int max_nr; - - nr =1 ; -if (pte_none (ptent)) -continue ; -if (need_resched ())break ; - max_nr = (end - addr) /PAGE_SIZE ; -if (pte_present (ptent)) { - nr =zap_present_ptes (tlb, vma, pte, ptent, max_nr, - addr, details, rss, &force_flush, - &force_break); -if (unlikely (force_break)) { - addr += nr *PAGE_SIZE ; -break ; - } - }else { - nr =zap_nonpresent_ptes (tlb, vma, pte, ptent, max_nr, - addr, details, rss); + nr =do_zap_pte_range (tlb, vma, pte, addr, end, details, + rss, &force_flush, &force_break); +if (unlikely (force_break)) { + addr += nr *PAGE_SIZE ; +break ; } }while (pte += nr, addr +=PAGE_SIZE * nr, addr != end);
Quoted 3 lines
Assuming that it's likely more common to have larger
pte_none()holes that single ones, optimizing out the need_resched()+force_break+incremental pte/addr increments etc.
Re: [PATCH v2 3/7] mm: introduce do_zap_pte_range()
Quoted 64 lines · Qi Zheng
Quoted 54 lines · David Hildenbrand
Quoted 37 lines · Qi Zheng
Quoted 25 lines · David Hildenbrand
Quoted 24 lines · Qi Zheng
This commit introduces
do_zap_pte_range()to actually zap the PTEs, which will help improve code readability and facilitate secondary checking of the processed PTEs in the future.No functional change.
Signed-off-by Qi Zheng <[email protected]>---
mm/memory.c| 45 ++++++++++++++++++++++++++-------------------1 file changed, 26 insertions(+), 19 deletions(-)mm/memory.c +5
-0@@ -1657,6 +1657,27 @@ static inline int zap_nonpresent_ptes(structmmu_gather *tlb, return nr; } +static inline int do_zap_pte_range (struct mmu_gather *tlb, + struct vm_area_struct *vma, pte_t *pte, + unsigned long addr, unsigned long end, + struct zap_details *details, int *rss, + bool *force_flush, bool *force_break)Quoted 8 lines
mm/memory.c static inline int zap_nonpresent_ptes(struct · line 1670
Quoted 6 lines
mm/memory.c +6
-0+{ + pte_t ptent = ptep_get(pte); + int max_nr = (end - addr) / PAGE_SIZE; + + if (pte_none(ptent)) + return 1;Maybe we should just skip all applicable
pte_none()here directly.Quoted 3 lines
Do you mean we should keep
pte_none()case inzap_pte_range()? Like below:No rather an addon patch that will simply skip over all consecutive
pte_none, like:if (
pte_none(ptent)) {
int nr;for (nr = 1; nr <
max_nr; nr++) {
ptent =ptep_get(pte + nr);
if (pte_none(ptent))
continue;
}Quoted 6 lines
max_nr -= nr;
if (!max_nr)
return nr;
addr += nr *PAGE_SIZE;
pte += nr;
}
Quoted 3 lines
I tend to hand over the pte/addr increments here to the loop outside
do_zap_pte_range(), like this:
Finally, I choose to introduce skip_none_ptes() to do this:
mm/memory.c +30 -4
@@ -1657,6 +1657,28 @@ static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,return nr; } +static inline int skip_none_ptes (pte_t *pte,unsigned long addr, +unsigned long end) +{ +pte_t ptent =ptep_get (pte); +int max_nr; +int nr; + +if (!pte_none (ptent)) +return 0 ; + + max_nr = (end - addr) /PAGE_SIZE ; + nr =1 ; + +for (; nr < max_nr; nr++) { + ptent =ptep_get (pte + nr); +if (!pte_none (ptent)) +break ; + } + +return nr; +} + static unsigned long zap_pte_range(struct mmu_gather *tlb, struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr, unsigned long end,
@@ -1682,13 +1704,17 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,pte_t ptent =ptep_get (pte);int max_nr; - nr =1 ; -if (pte_none (ptent)) -continue ; -if (need_resched ())break ; + nr =skip_none_ptes (pte, addr, end); +if (nr) { + addr +=PAGE_SIZE * nr; +if (addr == end) +break ; + pte += nr; + } + max_nr = (end - addr) /PAGE_SIZE ;if (pte_present (ptent)) { nr = zap_present_ptes(tlb, vma, pte, ptent, max_nr,
Thanks!
PATCH v2 4/7 mm: make zap_pte_range() handle full within-PMD range
In reply to Qi ZhengIn preparation for reclaiming empty PTE pages, this commit first makes zap_pte_range() to handle the full within-PMD range, so that we can more easily detect and free PTE pages in this function in subsequent commits.
--- mm/memory.c | 8 ++++++++ 1 file changed, 8 insertions(+)
mm/memory.c +8 -0
@@ -1691,6 +1691,7 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,pte_t *pte;int nr; +retry:tlb_change_page_size (tlb,PAGE_SIZE );init_rss_vec (rss); start_pte = pte =pte_offset_map_lock (mm, pmd, addr, &ptl);
@@ -1730,6 +1731,13 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,if (force_flush)tlb_flush_mmu (tlb); +if (addr != end) { +cond_resched (); + force_flush = false; + force_break = false; + goto retry; + } +return addr; }
Signature · 1 line
2.20.1
Re: [PATCH v2 4/7] mm: make zap_pte_range() handle full within-PMD range
Quoted 5 lines · Qi Zheng
In preparation for reclaiming empty PTE pages, this commit first makes
zap_pte_range()to handle the full within-PMD range, so that we can more easily detect and free PTE pages in this function in subsequent commits.Signed-off-by Qi Zheng <[email protected]>
PATCH v2 5/7 mm: pgtable: try to reclaim empty PTE page in madvise(MADV_DONTNEED)
In reply to Qi ZhengNow in order to pursue high performance, applications mostly use some high-performance user-mode memory allocators, such as jemalloc or tcmalloc. These memory allocators use madvise(MADV_DONTNEED or MADV_FREE) to release physical memory, but neither MADV_DONTNEED nor MADV_FREE will release page table memory, which may cause huge page table memory usage.
The following are a memory usage snapshot of one process which actually happened on our server:
VIRT: 55t
RES: 590g
VmPTE: 110gIn this case, most of the page table entries are empty. For such a PTE page where all entries are empty, we can actually free it back to the system for others to use.
As a first step, this commit aims to synchronously free the empty PTE pages in madvise(MADV_DONTNEED) case. We will detect and free empty PTE pages in zap_pte_range(), and will add zap_details.reclaim_pt to exclude cases other than madvise(MADV_DONTNEED).
Once an empty PTE is detected, we first try to hold the pmd lock within the pte lock. If successful, we clear the pmd entry directly (fast path). Otherwise, we wait until the pte lock is released, then re-hold the pmd and pte locks and loop PTRS_PER_PTE times to check pte_none() to re-detect whether the PTE page is empty and free it (slow path).
For other cases such as madvise(MADV_FREE), consider scanning and freeing empty PTE pages asynchronously in the future.
The following code snippet can show the effect of optimization:
mmap 50G
while (1) {
for (; i < 1024 * 25; i++) {
touch 2M memory
madvise MADV_DONTNEED 2M
}
}As we can see, the memory usage of VmPTE is reduced:
before after
VIRT 50.0 GB 50.0 GB
RES 3.1 MB 3.1 MB
VmPTE 102640 KB 240 KB
--- include/linux/mm.h | 1 + mm/Kconfig | 15 ++++++++++ mm/Makefile | 1 + mm/internal.h | 23 ++++++++++++++++ mm/madvise.c | 4 ++- mm/memory.c | 45 +++++++++++++++++++++++++++++- mm/pt_reclaim.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 mm/pt_reclaim.c
include/linux/mm.h +1 -0
@@ -2319,6 +2319,7 @@ extern void pagefault_out_of_memory(void);struct zap_details {struct folio *single_folio;/* Locked folio to be unmapped */ bool even_cows;/* Zap COWed private pages too? */ +bool reclaim_pt;/* Need reclaim page tables? */ zap_flags_t zap_flags;/* Extra flags for zapping */ };
mm/Kconfig +15 -0
@@ -1301,6 +1301,21 @@ config ARCH_HAS_USER_SHADOW_STACK The architecture has hardware support for userspace shadow call stacks (eg, x86 CET, arm64 GCS or RISC-V Zicfiss). +config ARCH_SUPPORTS_PT_RECLAIM + def_bool n + +config PT_RECLAIM + bool "reclaim empty user page table pages" + default y + depends on ARCH_SUPPORTS_PT_RECLAIM && MMU && SMP + select MMU_GATHER_RCU_TABLE_FREE + help + Try to reclaim empty user page table pages in paths other that munmap + and exit_mmap path. + + Note: now only empty user PTE page table pages will be reclaimed. + + source "mm/damon/Kconfig" endmenu
mm/Makefile +1 -0
@@ -145,3 +145,4 @@ obj-$(CONFIG_GENERIC_IOREMAP) += ioremap.o obj-$(CONFIG_SHRINKER_DEBUG) += shrinker_debug.o obj-$(CONFIG_EXECMEM) += execmem.o obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o +obj-$(CONFIG_PT_RECLAIM) += pt_reclaim.o
mm/internal.h +23 -0
@@ -1508,4 +1508,27 @@ int walk_page_range_mm(struct mm_struct *mm, unsigned long start, unsigned long end, const struct mm_walk_ops *ops, void *private); +#ifdef CONFIG_PT_RECLAIM +bool try_get_and_clear_pmd (struct mm_struct *mm,pmd_t *pmd,pmd_t *pmdval); +void free_pte (struct mm_struct *mm,unsigned long addr,struct mmu_gather *tlb, +pmd_t pmdval); +void try_to_free_pte (struct mm_struct *mm,pmd_t *pmd,unsigned long addr, +struct mmu_gather *tlb); +#else +static inline bool try_get_and_clear_pmd (struct mm_struct *mm,pmd_t *pmd, +pmd_t *pmdval) +{ +return false; +} +static inline void free_pte (struct mm_struct *mm,unsigned long addr, +struct mmu_gather *tlb,pmd_t pmdval) +{ +} +static inline void try_to_free_pte (struct mm_struct *mm,pmd_t *pmd, +unsigned long addr,struct mmu_gather *tlb) +{ +} +#endif /* CONFIG_PT_RECLAIM */ + +#endif /* __MM_INTERNAL_H */
mm/madvise.c +3 -1
@@ -851,7 +851,9 @@ static int madvise_free_single_vma(struct vm_area_struct *vma,static long madvise_dontneed_single_vma (struct vm_area_struct *vma,unsigned long start,unsigned long end) { -zap_page_range_single (vma, start, end - start,NULL ); +struct zap_details details = {.reclaim_pt = true,}; + +zap_page_range_single (vma, start, end - start, &details);return 0 ; }
mm/memory.c +44 -1
83 lines not on this page · raw message
mm/pt_reclaim.c new file +68 -0
68 lines not on this page · raw message
Signature · 1 line
2.20.1
Jann Horn
reviewerRe: [PATCH v2 5/7] mm: pgtable: try to reclaim empty PTE page in madvise(MADV_DONTNEED)
Quoted 10 lines · Qi Zheng
As a first step, this commit aims to synchronously free the empty PTE pages in
madvise(MADV_DONTNEED)case. We will detect and free empty PTE pages inzap_pte_range(), and will add zap_details.reclaim_pt to exclude cases other thanmadvise(MADV_DONTNEED).Once an empty PTE is detected, we first try to hold the pmd lock within the pte lock. If successful, we clear the pmd entry directly (fast path). Otherwise, we wait until the pte lock is released, then re-hold the pmd and pte locks and loop
PTRS_PER_PTEtimes to checkpte_none()to re-detect whether the PTE page is empty and free it (slow path).
How does this interact with move_pages_pte()? I am looking at your series applied on top of next-20241106, and it looks to me like move_pages_pte() uses pte_offset_map_rw_nolock() and assumes that the PMD entry can't change. You can clearly see this assumption at the WARN_ON_ONCE(pmd_none(*dst_pmd)). And if we race the wrong way, I think for example move_present_pte() can end up moving a present PTE into a page table that has already been scheduled for RCU freeing.
Quoted 32 lines
Signed-off-by Qi Zheng <[email protected]>--- include/linux/mm.h | 1 + mm/Kconfig | 15 ++++++++++ mm/Makefile | 1 + mm/internal.h | 23 ++++++++++++++++ mm/madvise.c | 4 ++- mm/memory.c | 45 +++++++++++++++++++++++++++++- mm/pt_reclaim.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 mm/pt_reclaim.cinclude/linux/mm.h +1
-0@@ -2319,6 +2319,7 @@ extern void pagefault_out_of_memory(void);struct zap_details {struct folio *single_folio;/* Locked folio to be unmapped */ bool even_cows;/* Zap COWed private pages too? */ +bool reclaim_pt;/* Need reclaim page tables? */ zap_flags_t zap_flags;/* Extra flags for zapping */ };mm/Kconfig +0
-0@@ -1301,6 +1301,21 @@ config ARCH_HAS_USER_SHADOW_STACK The architecture has hardware support for userspace shadow call stacks (eg, x86 CET, arm64 GCS or RISC-V Zicfiss).
mm/Kconfig config ARCH_HAS_USER_SHADOW_STACK · line 1313
Quoted 10 lines
mm/Kconfig +10
-0+config ARCH_SUPPORTS_PT_RECLAIM + def_bool n + +config PT_RECLAIM + bool "reclaim empty user page table pages" + default y + depends on ARCH_SUPPORTS_PT_RECLAIM && MMU && SMP + select MMU_GATHER_RCU_TABLE_FREE + help + Try to reclaim empty user page table pages in paths other that munmap
nit: s/other that/other than/
Quoted 3 lines
mm/Kconfig +3
-0+ and exit_mmap path. + + Note: now only empty user PTE page table pages will be reclaimed.
[...]
Quoted 20 lines
mm/madvise.c +3
-1@@ -851,7 +851,9 @@ static int madvise_free_single_vma(struct vm_area_struct *vma,static long madvise_dontneed_single_vma (struct vm_area_struct *vma,unsigned long start,unsigned long end) { -zap_page_range_single (vma, start, end - start,NULL ); +struct zap_details details = {.reclaim_pt = true,}; + +zap_page_range_single (vma, start, end - start, &details);return 0 ; }mm/memory.c +0
-0
mm/memory.c copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma) · line 1442
Quoted 8 lines
mm/memory.c +1
-1@@ -1436,7 +1436,7 @@ copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma)static inline bool should_zap_cows (struct zap_details *details) {/* By default, zap all pages */ -if (!details) +if (!details || details->reclaim_pt)return true;/* Or, we zap COWed pages only if the caller wants to */
This looks hacky - when we have a "details" object, its ->even_cows member is supposed to indicate whether COW pages should be zapped. So please instead set .even_cows=true in madvise_dontneed_single_vma().
mm/memory.c static inline int do_zap_pte_range(struct mmu_gather *tlb, · line 1689
Quoted 13 lines
mm/memory.c +9
-0@@ -1678,6 +1678,30 @@ static inline int do_zap_pte_range(struct mmu_gather *tlb, details, rss); } +static inline int count_pte_none (pte_t *pte,int nr) +{ +int none_nr =0 ; + +/* +* If PTE_MARKER_UFFD_WP is enabled, the uffd-wp PTEs may be +* re-installed, so we need to check pte_none() one by one. +* Otherwise, checking a single PTE in a batch is sufficient. +*/
Please also add a comment noting that count_pte_none() relies on this invariant on top of do_zap_pte_range() or somewhere like that.
Quoted 98 lines
mm/memory.c +34
-0+#ifdef CONFIG_PTE_MARKER_UFFD_WP +for (;;) { +if (pte_none (ptep_get (pte))) + none_nr++; +if (--nr ==0 ) +break ; + pte++; + } +#else +if (pte_none (ptep_get (pte))) + none_nr = nr; +#endif +return none_nr; +} + static unsigned long zap_pte_range(struct mmu_gather *tlb, struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr, unsigned long end,@@ -1689,8 +1713,16 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,spinlock_t *ptl;pte_t *start_pte;pte_t *pte; +pmd_t pmdval; +bool can_reclaim_pt = false; +bool direct_reclaim = false; +unsigned long start = addr; +int none_nr =0 ;int nr; +if (details && details->reclaim_pt && (end - start >=PMD_SIZE )) + can_reclaim_pt = true; + retry:tlb_change_page_size (tlb,PAGE_SIZE );init_rss_vec (rss);@@ -1706,12 +1738,16 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb, nr =do_zap_pte_range (tlb, vma, pte, addr, end, details, rss, &force_flush, &force_break); + none_nr +=count_pte_none (pte, nr);if (unlikely (force_break)) { addr += nr *PAGE_SIZE ;break ; } }while (pte += nr, addr +=PAGE_SIZE * nr, addr != end); +if (addr == end && can_reclaim_pt && (none_nr ==PTRS_PER_PTE )) + direct_reclaim =try_get_and_clear_pmd (mm, pmd, &pmdval); +add_mm_rss_vec (mm, rss);arch_leave_lazy_mmu_mode ();@@ -1738,6 +1774,13 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb, goto retry; } +if (can_reclaim_pt) { +if (direct_reclaim) +free_pte (mm, start, tlb, pmdval); +else +try_to_free_pte (mm, pmd, start, tlb); + } +return addr; }mm/pt_reclaim.c new file +26
-0@@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <linux/hugetlb.h> +#include <asm-generic/tlb.h> +#include <asm/pgalloc.h> + +#include "internal.h" + +bool try_get_and_clear_pmd (struct mm_struct *mm,pmd_t *pmd,pmd_t *pmdval) +{ +spinlock_t *pml =pmd_lockptr (mm, pmd); + +if (!spin_trylock (pml)) +return false; + + *pmdval =pmdp_get_lockless (pmd); +pmd_clear (pmd); +spin_unlock (pml); + +return true; +} + +void free_pte (struct mm_struct *mm,unsigned long addr,struct mmu_gather *tlb, +pmd_t pmdval) +{ +pte_free_tlb (tlb,pmd_pgtable (pmdval), addr); +mm_dec_nr_ptes (mm);
mm/pt_reclaim.c line 36
Quoted 10 lines
mm/pt_reclaim.c new file +10
-0+} + +void try_to_free_pte (struct mm_struct *mm,pmd_t *pmd,unsigned long addr, +struct mmu_gather *tlb) +{ +pmd_t pmdval; +spinlock_t *pml, *ptl; +pte_t *start_pte, *pte; +int i; +
If you swap the following two operations around (first pmd_lock(), then pte_offset_map_rw_nolock(), then take the PTE lock):
mm/pt_reclaim.c line 44
Quoted 8 lines
mm/pt_reclaim.c new file +8
-0+ start_pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pmdval, &ptl); +if (!start_pte) +return ; + + pml =pmd_lock (mm, pmd); +if (ptl != pml) +spin_lock_nested (ptl,SINGLE_DEPTH_NESTING ); +
Then I think this check can go away:
Quoted 27 lines
mm/pt_reclaim.c new file +24
-1+if (unlikely (!pmd_same (pmdval,pmdp_get_lockless (pmd)))) + goto out_ptl; + +/* Check if it is empty PTE page */ +for (i =0 , pte = start_pte; i <PTRS_PER_PTE ; i++, pte++) { +if (!pte_none (ptep_get (pte))) + goto out_ptl; + } +pte_unmap (start_pte); + +pmd_clear (pmd); + +if (ptl != pml) +spin_unlock (ptl); +spin_unlock (pml); + +free_pte (mm, addr, tlb, pmdval); + +return ; +out_ptl: +pte_unmap_unlock (start_pte, ptl); +if (pml != ptl) +spin_unlock (pml); +} --2.20.1
Re: [PATCH v2 5/7] mm: pgtable: try to reclaim empty PTE page in madvise(MADV_DONTNEED)
Hi Jann,
Quoted 12 lines · Jann Horn
Quoted 10 lines · Qi Zheng
As a first step, this commit aims to synchronously free the empty PTE pages in
madvise(MADV_DONTNEED)case. We will detect and free empty PTE pages inzap_pte_range(), and will add zap_details.reclaim_pt to exclude cases other thanmadvise(MADV_DONTNEED).Once an empty PTE is detected, we first try to hold the pmd lock within the pte lock. If successful, we clear the pmd entry directly (fast path). Otherwise, we wait until the pte lock is released, then re-hold the pmd and pte locks and loop
PTRS_PER_PTEtimes to checkpte_none()to re-detect whether the PTE page is empty and free it (slow path).
Quoted 5 lines
How does this interact with
move_pages_pte()? I am looking at your series applied on top of next-20241106, and it looks to me likemove_pages_pte()usespte_offset_map_rw_nolock()and assumes that the PMD entry can't change. You can clearly see this assumption at the WARN_ON_ONCE(pmd_none(*dst_pmd)). And if we race the wrong way, I
In move_pages_pte(), the following conditions may indeed be triggered:
/* Sanity checks before the operation */
if (WARN_ON_ONCE(pmd_none(*dst_pmd)) || WARN_ON_ONCE(pmd_none(*src_pmd)) ||
WARN_ON_ONCE(pmd_trans_huge(*dst_pmd)) ||
WARN_ON_ONCE(pmd_trans_huge(*src_pmd))) {
err = -EINVAL;
goto out;
}But maybe we can just remove the WARN_ON_ONCE(), because...
Quoted 2 lines
think for example
move_present_pte()can end up moving a present PTE into a page table that has already been scheduled for RCU freeing.
...this situation is impossible to happen. Before performing move operation, the pte_same() check will be performed after holding the pte lock, which can ensure that the PTE page is stable:
CPU 0 CPU 1
zap_pte_range
orig_src_pte = ptep_get(src_pte);
pmd_lockpte_lock
check if all PTEs are pte_none()
--> clear pmd entry
unlock pte
unlock pmd
src_pte_lock pte_same(orig_src_pte, ptep_get(src_pte)) --> return false and will skip the move op
Quoted 33 lines
Quoted 33 lines
Signed-off-by Qi Zheng <[email protected]>--- include/linux/mm.h | 1 + mm/Kconfig | 15 ++++++++++ mm/Makefile | 1 + mm/internal.h | 23 ++++++++++++++++ mm/madvise.c | 4 ++- mm/memory.c | 45 +++++++++++++++++++++++++++++- mm/pt_reclaim.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 2 deletions(-)create mode 100644
mm/pt_reclaim.cinclude/linux/mm.h +1
-0@@ -2319,6 +2319,7 @@ extern void pagefault_out_of_memory(void);struct zap_details {struct folio *single_folio;/* Locked folio to be unmapped */ bool even_cows;/* Zap COWed private pages too? */ +bool reclaim_pt;/* Need reclaim page tables? */ zap_flags_t zap_flags;/* Extra flags for zapping */ };mm/Kconfig +0
-0@@ -1301,6 +1301,21 @@ config ARCH_HAS_USER_SHADOW_STACK The architecture has hardware support for userspace shadow call stacks (eg, x86 CET, arm64 GCS or RISC-V Zicfiss).
Quoted 12 lines
mm/Kconfig config ARCH_HAS_USER_SHADOW_STACK · line 1313
Quoted 10 lines
mm/Kconfig +10
-0+config ARCH_SUPPORTS_PT_RECLAIM + def_bool n + +config PT_RECLAIM + bool "reclaim empty user page table pages" + default y + depends on ARCH_SUPPORTS_PT_RECLAIM && MMU && SMP + select MMU_GATHER_RCU_TABLE_FREE + help + Try to reclaim empty user page table pages in paths other that munmapnit: s/other that/other than/
will fix.
Quoted 32 lines
Quoted 4 lines
mm/Kconfig +3
-0+ and exit_mmap path. + + Note: now only empty user PTE page table pages will be reclaimed.[...]
Quoted 27 lines
mm/madvise.c +3
-1@@ -851,7 +851,9 @@ static int madvise_free_single_vma(struct vm_area_struct *vma,static long madvise_dontneed_single_vma (struct vm_area_struct *vma,unsigned long start,unsigned long end) { -zap_page_range_single (vma, start, end - start,NULL ); +struct zap_details details = {.reclaim_pt = true,}; + +zap_page_range_single (vma, start, end - start, &details);return 0 ; }mm/memory.c +1
-1@@ -1436,7 +1436,7 @@ copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma)static inline bool should_zap_cows (struct zap_details *details) {/* By default, zap all pages */ -if (!details) +if (!details || details->reclaim_pt)return true;
Quoted 5 lines
mm/memory.c copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma) · line 1442
Quoted 1 line
mm/memory.c +0
-0/* Or, we zap COWed pages only if the caller wants to */ This looks hacky - when we have a "details" object, its ->even_cows member is supposed to indicate whether COW pages should be zapped. So please instead set .even_cows=true in
madvise_dontneed_single_vma().
But the details->reclaim_pt should continue to be set, right? Because we need to use .reclaim_pt to indicate whether the empty PTE page should be reclaimed.
Quoted 5 lines
Quoted 5 lines
mm/memory.c +0
-0@@ -1678,6 +1678,30 @@ static inline int do_zap_pte_range(struct mmu_gather *tlb, details, rss); }
Quoted 12 lines
mm/memory.c static inline int do_zap_pte_range(struct mmu_gather *tlb, · line 1689
Quoted 9 lines
mm/memory.c +9
-0+static inline int count_pte_none (pte_t *pte,int nr) +{ +int none_nr =0 ; + +/* +* If PTE_MARKER_UFFD_WP is enabled, the uffd-wp PTEs may be +* re-installed, so we need to check pte_none() one by one. +* Otherwise, checking a single PTE in a batch is sufficient. +*/ Please also add a comment noting that
count_pte_none()relies on this invariant on top ofdo_zap_pte_range()or somewhere like that.
OK, will add a comment above do_zap_pte_range() to explain this special case.
Quoted 104 lines
Quoted 104 lines
mm/memory.c +34
-0+#ifdef CONFIG_PTE_MARKER_UFFD_WP +for (;;) { +if (pte_none (ptep_get (pte))) + none_nr++; +if (--nr ==0 ) +break ; + pte++; + } +#else +if (pte_none (ptep_get (pte))) + none_nr = nr; +#endif +return none_nr; +} + static unsigned long zap_pte_range(struct mmu_gather *tlb, struct vm_area_struct *vma, pmd_t *pmd, unsigned long addr, unsigned long end,@@ -1689,8 +1713,16 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,spinlock_t *ptl;pte_t *start_pte;pte_t *pte; +pmd_t pmdval; +bool can_reclaim_pt = false; +bool direct_reclaim = false; +unsigned long start = addr; +int none_nr =0 ;int nr; +if (details && details->reclaim_pt && (end - start >=PMD_SIZE )) + can_reclaim_pt = true; + retry:tlb_change_page_size (tlb,PAGE_SIZE );init_rss_vec (rss);@@ -1706,12 +1738,16 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb, nr =do_zap_pte_range (tlb, vma, pte, addr, end, details, rss, &force_flush, &force_break); + none_nr +=count_pte_none (pte, nr);if (unlikely (force_break)) { addr += nr *PAGE_SIZE ;break ; } }while (pte += nr, addr +=PAGE_SIZE * nr, addr != end); +if (addr == end && can_reclaim_pt && (none_nr ==PTRS_PER_PTE )) + direct_reclaim =try_get_and_clear_pmd (mm, pmd, &pmdval); +add_mm_rss_vec (mm, rss);arch_leave_lazy_mmu_mode ();@@ -1738,6 +1774,13 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb, goto retry; } +if (can_reclaim_pt) { +if (direct_reclaim) +free_pte (mm, start, tlb, pmdval); +else +try_to_free_pte (mm, pmd, start, tlb); + } +return addr; }mm/pt_reclaim.c new file +31
-0@@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <linux/hugetlb.h> +#include <asm-generic/tlb.h> +#include <asm/pgalloc.h> + +#include "internal.h" + +bool try_get_and_clear_pmd (struct mm_struct *mm,pmd_t *pmd,pmd_t *pmdval) +{ +spinlock_t *pml =pmd_lockptr (mm, pmd); + +if (!spin_trylock (pml)) +return false; + + *pmdval =pmdp_get_lockless (pmd); +pmd_clear (pmd); +spin_unlock (pml); + +return true; +} + +void free_pte (struct mm_struct *mm,unsigned long addr,struct mmu_gather *tlb, +pmd_t pmdval) +{ +pte_free_tlb (tlb,pmd_pgtable (pmdval), addr); +mm_dec_nr_ptes (mm); +} + +void try_to_free_pte (struct mm_struct *mm,pmd_t *pmd,unsigned long addr, +struct mmu_gather *tlb) +{
Quoted 8 lines
mm/pt_reclaim.c line 36
Quoted 5 lines
mm/pt_reclaim.c new file +5
-0+pmd_t pmdval; +spinlock_t *pml, *ptl; +pte_t *start_pte, *pte; +int i; +If you swap the following two operations around (first
pmd_lock(), thenpte_offset_map_rw_nolock(), then take the PTE lock):
Indeed, will change to it.
Thanks,
Qi
Quoted 39 lines
mm/pt_reclaim.c line 44
Quoted 9 lines
mm/pt_reclaim.c new file +8
-0+ start_pte =pte_offset_map_rw_nolock (mm, pmd, addr, &pmdval, &ptl); +if (!start_pte) +return ; + + pml =pmd_lock (mm, pmd); +if (ptl != pml) +spin_lock_nested (ptl,SINGLE_DEPTH_NESTING ); +Then I think this check can go away:
Quoted 27 lines
mm/pt_reclaim.c new file +24
-1+if (unlikely (!pmd_same (pmdval,pmdp_get_lockless (pmd)))) + goto out_ptl; + +/* Check if it is empty PTE page */ +for (i =0 , pte = start_pte; i <PTRS_PER_PTE ; i++, pte++) { +if (!pte_none (ptep_get (pte))) + goto out_ptl; + } +pte_unmap (start_pte); + +pmd_clear (pmd); + +if (ptl != pml) +spin_unlock (ptl); +spin_unlock (pml); + +free_pte (mm, addr, tlb, pmdval); + +return ; +out_ptl: +pte_unmap_unlock (start_pte, ptl); +if (pml != ptl) +spin_unlock (pml); +} --2.20.1
Jann Horn
reviewerRe: [PATCH v2 5/7] mm: pgtable: try to reclaim empty PTE page in madvise(MADV_DONTNEED)
Quoted 51 lines · Qi Zheng
Quoted 12 lines · Jann Horn
Quoted 10 lines · Qi Zheng
As a first step, this commit aims to synchronously free the empty PTE pages in
madvise(MADV_DONTNEED)case. We will detect and free empty PTE pages inzap_pte_range(), and will add zap_details.reclaim_pt to exclude cases other thanmadvise(MADV_DONTNEED).Once an empty PTE is detected, we first try to hold the pmd lock within the pte lock. If successful, we clear the pmd entry directly (fast path). Otherwise, we wait until the pte lock is released, then re-hold the pmd and pte locks and loop
PTRS_PER_PTEtimes to checkpte_none()to re-detect whether the PTE page is empty and free it (slow path).Quoted 5 lines
How does this interact with
move_pages_pte()? I am looking at your series applied on top of next-20241106, and it looks to me likemove_pages_pte()usespte_offset_map_rw_nolock()and assumes that the PMD entry can't change. You can clearly see this assumption at the WARN_ON_ONCE(pmd_none(*dst_pmd)). And if we race the wrong way, IIn
move_pages_pte(), the following conditions may indeed be triggered:/* Sanity checks before the operation */ if (WARN_ON_ONCE(pmd_none(*dst_pmd)) || WARN_ON_ONCE(pmd_none(*src_pmd)) || WARN_ON_ONCE(pmd_trans_huge(*dst_pmd)) || WARN_ON_ONCE(pmd_trans_huge(*src_pmd))) { err = -EINVAL; goto out; }But maybe we can just remove the
WARN_ON_ONCE(), because...Quoted 2 lines
think for example
move_present_pte()can end up moving a present PTE into a page table that has already been scheduled for RCU freeing....this situation is impossible to happen. Before performing move operation, the
pte_same()check will be performed after holding the pte lock, which can ensure that the PTE page is stable:CPU 0 CPU 1
zap_pte_rangeorig_src_pte = ptep_get(src_pte);
pmd_lockpte_lock
check if all PTEs arepte_none()
--> clear pmd entry
unlock pte
unlock pmd
Quoted 3 lines
src_pte_lock
pte_same(orig_src_pte, ptep_get(src_pte))
--> return false and will skip the move opYes, that works for the source PTE. But what about the destination?
Operations on the destination PTE in move_pages_pte() are, when moving a normal present source PTE pointing to an order-0 page, and assuming that the optimistic folio_trylock(src_folio) and anon_vma_trylock_write(src_anon_vma) succeed:
dst_pte = pte_offset_map_rw_nolock(mm, dst_pmd, dst_addr, &dummy_pmdval, &dst_ptl)
[check that dst_pte is non-NULL]
some racy WARN_ON_ONCE() checks spin_lock(dst_ptl);orig_dst_pte = ptep_get(dst_pte); spin_unlock(dst_ptl);
[bail if orig_dst_pte isn't none] double_pt_lock(dst_ptl, src_ptl)
[check pte_same(ptep_get(dst_pte), orig_dst_pte)]
and then we're past the point of no return. Note that there is a pte_same() check against orig_dst_pte, but pte_none(orig_dst_pte) is intentionally pte_none(), so the pte_same() check does not guarantee that the destination page table is still linked in.
Quoted 18 lines
Quoted 12 lines
Quoted 12 lines
mm/memory.c +1
-1@@ -1436,7 +1436,7 @@ copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma)static inline bool should_zap_cows (struct zap_details *details) {/* By default, zap all pages */ -if (!details) +if (!details || details->reclaim_pt)return true;Quoted 5 lines
mm/memory.c copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma) · line 1442
Quoted 1 line
mm/memory.c +0
-0/* Or, we zap COWed pages only if the caller wants to */ This looks hacky - when we have a "details" object, its ->even_cows member is supposed to indicate whether COW pages should be zapped. So please instead set .even_cows=true in
madvise_dontneed_single_vma().
Quoted 3 lines
But the
details->reclaim_ptshould continue to be set, right? Because we need to use .reclaim_pt to indicate whether the empty PTE page should be reclaimed.
Yeah, you should set both.
Re: [PATCH v2 5/7] mm: pgtable: try to reclaim empty PTE page in madvise(MADV_DONTNEED)
Quoted 74 lines · Jann Horn
Quoted 51 lines · Qi Zheng
Quoted 12 lines · Jann Horn
Quoted 10 lines · Qi Zheng
As a first step, this commit aims to synchronously free the empty PTE pages in
madvise(MADV_DONTNEED)case. We will detect and free empty PTE pages inzap_pte_range(), and will add zap_details.reclaim_pt to exclude cases other thanmadvise(MADV_DONTNEED).Once an empty PTE is detected, we first try to hold the pmd lock within the pte lock. If successful, we clear the pmd entry directly (fast path). Otherwise, we wait until the pte lock is released, then re-hold the pmd and pte locks and loop
PTRS_PER_PTEtimes to checkpte_none()to re-detect whether the PTE page is empty and free it (slow path).Quoted 5 lines
How does this interact with
move_pages_pte()? I am looking at your series applied on top of next-20241106, and it looks to me likemove_pages_pte()usespte_offset_map_rw_nolock()and assumes that the PMD entry can't change. You can clearly see this assumption at the WARN_ON_ONCE(pmd_none(*dst_pmd)). And if we race the wrong way, IIn
move_pages_pte(), the following conditions may indeed be triggered:/* Sanity checks before the operation */ if (WARN_ON_ONCE(pmd_none(*dst_pmd)) || WARN_ON_ONCE(pmd_none(*src_pmd)) || WARN_ON_ONCE(pmd_trans_huge(*dst_pmd)) || WARN_ON_ONCE(pmd_trans_huge(*src_pmd))) { err = -EINVAL; goto out; }But maybe we can just remove the
WARN_ON_ONCE(), because...Quoted 2 lines
think for example
move_present_pte()can end up moving a present PTE into a page table that has already been scheduled for RCU freeing....this situation is impossible to happen. Before performing move operation, the
pte_same()check will be performed after holding the pte lock, which can ensure that the PTE page is stable:CPU 0 CPU 1
zap_pte_rangeorig_src_pte = ptep_get(src_pte);
pmd_lockpte_lock
check if all PTEs arepte_none()
--> clear pmd entry
unlock pte
unlock pmdQuoted 3 lines
src_pte_lock pte_same(orig_src_pte, ptep_get(src_pte)) --> return false and will skip the move opYes, that works for the source PTE. But what about the destination?
Operations on the destination PTE in
move_pages_pte()are, when moving a normal present source PTE pointing to an order-0 page, and assuming that the optimisticfolio_trylock(src_folio)andanon_vma_trylock_write(src_anon_vma)succeed:
dst_pte=pte_offset_map_rw_nolock(mm, dst_pmd, dst_addr, &dummy_pmdval, &dst_ptl)
[check thatdst_pteis non-NULL]
some racyWARN_ON_ONCE()checksspin_lock(dst_ptl);orig_dst_pte=ptep_get(dst_pte);spin_unlock(dst_ptl);
[bail iforig_dst_pteisn't none]double_pt_lock(dst_ptl, src_ptl)
[checkpte_same(ptep_get(dst_pte), orig_dst_pte)]
Quoted 4 lines
and then we're past the point of no return. Note that there is a
pte_same()check againstorig_dst_pte, butpte_none(orig_dst_pte)is intentionallypte_none(), so thepte_same()check does not guarantee that the destination page table is still linked in.
OK, now I got what you mean. This is indeed a problem. In this case, it is still necessary to recheck pmd_same() to ensure the stability of dst_pte page. Will fix it.
Quoted 19 lines
Quoted 19 lines
Quoted 18 lines
Quoted 6 lines
mm/memory.c +0
-0mm/memory.c copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma) · line 1442
Quoted 8 lines
mm/memory.c +1
-1@@ -1436,7 +1436,7 @@ copy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma)static inline bool should_zap_cows (struct zap_details *details) {/* By default, zap all pages */ -if (!details) +if (!details || details->reclaim_pt)return true;/* Or, we zap COWed pages only if the caller wants to */ This looks hacky - when we have a "details" object, its ->even_cows member is supposed to indicate whether COW pages should be zapped. So please instead set .even_cows=true in
madvise_dontneed_single_vma().
Quoted 5 lines
Quoted 3 lines
But the
details->reclaim_ptshould continue to be set, right? Because we need to use .reclaim_pt to indicate whether the empty PTE page should be reclaimed.Yeah, you should set both.
OK.
Thanks!
PATCH v2 6/7 x86: mm: free page table pages by RCU instead of semi RCU
In reply to Qi ZhengNow, if CONFIG_MMU_GATHER_RCU_TABLE_FREE is selected, the page table pages will be freed by semi RCU, that is:
- batch table freeing: asynchronous free by RCU
- single table freeing: IPI + synchronous free
In this way, the page table can be lockless traversed by disabling IRQ in paths such as fast GUP. But this is not enough to free the empty PTE page table pages in paths other that munmap and exit_mmap path, because IPI cannot be synchronized with rcu_read_lock() in pte_offset_map{_lock}().
In preparation for supporting empty PTE page table pages reclaimation, let single table also be freed by RCU like batch table freeing. Then we can also use pte_offset_map() etc to prevent PTE page from being freed.
Like pte_free_defer(), we can also safely use ptdesc->pt_rcu_head to free the page table pages:
- The pt_rcu_head is unioned with pt_list and pmd_huge_pte.
- For pt_list, it is used to manage the PGD page in x86. Fortunately tlb_remove_table() will not be used for free PGD pages, so it is safe to use pt_rcu_head.
- For pmd_huge_pte, we will do zap_deposited_table() before freeing the PMD page, so it is also safe.
--- arch/x86/include/asm/tlb.h | 19 +++++++++++++++++++ arch/x86/kernel/paravirt.c | 7 +++++++ arch/x86/mm/pgtable.c | 10 +++++++++- mm/mmu_gather.c | 9 ++++++++- 4 files changed, 43 insertions(+), 2 deletions(-)
arch/x86/include/asm/tlb.h +19 -0
@@ -34,4 +34,23 @@ static inline void __tlb_remove_table(void *table)free_page_and_swap_cache (table); } +#ifdef CONFIG_PT_RECLAIM +static inline void __tlb_remove_table_one_rcu (struct rcu_head *head) +{ +struct page *page; + + page = container_of(head, struct page, rcu_head); +free_page_and_swap_cache (page); +} + +static inline void __tlb_remove_table_one (void *table) +{ +struct page *page; + + page = table; +call_rcu (&page->rcu_head, __tlb_remove_table_one_rcu); +} +#define __tlb_remove_table_one __tlb_remove_table_one +#endif /* CONFIG_PT_RECLAIM */ +#endif /* _ASM_X86_TLB_H */
arch/x86/kernel/paravirt.c +7 -0
@@ -59,10 +59,17 @@ void __init native_pv_lock_init(void)static_branch_enable (&virt_spin_lock_key); } +#ifndef CONFIG_PT_RECLAIM static void native_tlb_remove_table (struct mmu_gather *tlb,void *table) {tlb_remove_page (tlb, table); } +#else +static void native_tlb_remove_table (struct mmu_gather *tlb,void *table) +{ +tlb_remove_table (tlb, table); +} +#endif struct static_key paravirt_steal_enabled;struct static_key paravirt_steal_rq_enabled;
arch/x86/mm/pgtable.c +9 -1
@@ -19,12 +19,20 @@ EXPORT_SYMBOL(physical_mask);#endif #ifndef CONFIG_PARAVIRT +#ifndef CONFIG_PT_RECLAIM static inline void paravirt_tlb_remove_table (struct mmu_gather *tlb,void *table) {tlb_remove_page (tlb, table); } -#endif +#else +static inline +void paravirt_tlb_remove_table (struct mmu_gather *tlb,void *table) +{ +tlb_remove_table (tlb, table); +} +#endif /* !CONFIG_PT_RECLAIM */ +#endif /* !CONFIG_PARAVIRT */ gfp_t __userpte_alloc_gfp =GFP_PGTABLE_USER |PGTABLE_HIGHMEM ;
mm/mmu_gather.c +8 -1
@@ -311,10 +311,17 @@ static inline void tlb_table_invalidate(struct mmu_gather *tlb) } } +#ifndef __tlb_remove_table_one +static inline void __tlb_remove_table_one (void *table) +{ +__tlb_remove_table (table); +} +#endif +static void tlb_remove_table_one (void *table) {tlb_remove_table_sync_one (); -__tlb_remove_table (table); +__tlb_remove_table_one (table); }static void tlb_table_flush (struct mmu_gather *tlb)
Signature · 1 line
2.20.1
Jann Horn
reviewerRe: [PATCH v2 6/7] x86: mm: free page table pages by RCU instead of semi RCU
+x86 MM maintainers - [email protected] was already cc'ed, but I don't know if that is enough for them to see it, and I haven't seen them comment on this series yet; I think you need an ack from them for this change.
Quoted 6 lines · Qi Zheng
Now, if
CONFIG_MMU_GATHER_RCU_TABLE_FREEis selected, the page table pages will be freed by semi RCU, that is:- batch table freeing: asynchronous free by RCU
- single table freeing: IPI + synchronous free
Quoted 8 lines
In this way, the page table can be lockless traversed by disabling IRQ in paths such as fast GUP. But this is not enough to free the empty PTE page table pages in paths other that munmap and
exit_mmappath, because IPI cannot be synchronized withrcu_read_lock()in pte_offset_map{_lock}().In preparation for supporting empty PTE page table pages reclaimation, let single table also be freed by RCU like batch table freeing. Then we can also use
pte_offset_map()etc to prevent PTE page from being freed.
I applied your series locally and followed the page table freeing path that the reclaim feature would use on x86-64. Looks like it goes like this with the series applied:
free_pte
pte_free_tlb
__pte_free_tlb
___pte_free_tlb
paravirt_tlb_remove_table
tlb_remove_table [!CONFIG_PARAVIRT, Xen PV, Hyper-V, KVM]
[no-free-memory slowpath:]
tlb_table_invalidate
tlb_remove_table_one
tlb_remove_table_sync_one [does IPI for GUP-fast] __tlb_remove_table_one [frees via RCU]
[fastpath:]
tlb_table_flush
tlb_remove_table_free [frees via RCU]
native_tlb_remove_table [CONFIG_PARAVIRT on native]
tlb_remove_table [see above]
Basically, the only remaining case in which paravirt_tlb_remove_table() does not use tlb_remove_table() with RCU delay is !CONFIG_PARAVIRT && !CONFIG_PT_RECLAIM. Given that CONFIG_PT_RECLAIM is defined as "default y" when supported, I guess that means X86's direct page table freeing path will almost never be used? If it stays that way and the X86 folks don't see a performance impact from using RCU to free tables on munmap() / process exit, I guess we might want to get rid of the direct page table freeing path on x86 at some point to simplify things...
(That simplification might also help prepare for Intel Remote Action Request, if that is a thing people want...)
Quoted 11 lines
Like
pte_free_defer(), we can also safely useptdesc->pt_rcu_headto free the page table pages:- The
pt_rcu_headis unioned withpt_listandpmd_huge_pte.- For
pt_list, it is used to manage the PGD page in x86. Fortunatelytlb_remove_table()will not be used for free PGD pages, so it is safe to usept_rcu_head.- For
pmd_huge_pte, we will dozap_deposited_table()before freeing the PMD page, so it is also safe.
Please also update the comments on "struct ptdesc" accordingly.
Quoted 14 lines
Signed-off-by Qi Zheng <[email protected]>--- arch/x86/include/asm/tlb.h | 19 +++++++++++++++++++ arch/x86/kernel/paravirt.c | 7 +++++++ arch/x86/mm/pgtable.c | 10 +++++++++- mm/mmu_gather.c | 9 ++++++++- 4 files changed, 43 insertions(+), 2 deletions(-)arch/x86/include/asm/tlb.h +0
-0@@ -34,4 +34,23 @@ static inline void __tlb_remove_table(void *table)free_page_and_swap_cache (table);
arch/x86/include/asm/tlb.h static inline void __tlb_remove_table(void *table) · line 44
Quoted 10 lines
arch/x86/include/asm/tlb.h +8
-0} +#ifdef CONFIG_PT_RECLAIM +static inline void __tlb_remove_table_one_rcu (struct rcu_head *head) +{ +struct page *page; + + page = container_of(head, struct page, rcu_head); +free_page_and_swap_cache (page); +}
Why free_page_and_swap_cache()? Page tables shouldn't have swap cache, so I think something like put_page() would do the job.
Quoted 85 lines
arch/x86/include/asm/tlb.h +10
-0+static inline void __tlb_remove_table_one (void *table) +{ +struct page *page; + + page = table; +call_rcu (&page->rcu_head, __tlb_remove_table_one_rcu); +} +#define __tlb_remove_table_one __tlb_remove_table_one +#endif /* CONFIG_PT_RECLAIM */ +#endif /* _ASM_X86_TLB_H */ arch/x86/kernel/paravirt.c +7
-0@@ -59,10 +59,17 @@ void __init native_pv_lock_init(void)static_branch_enable (&virt_spin_lock_key); } +#ifndef CONFIG_PT_RECLAIM static void native_tlb_remove_table (struct mmu_gather *tlb,void *table) {tlb_remove_page (tlb, table); } +#else +static void native_tlb_remove_table (struct mmu_gather *tlb,void *table) +{ +tlb_remove_table (tlb, table); +} +#endif struct static_key paravirt_steal_enabled;struct static_key paravirt_steal_rq_enabled;arch/x86/mm/pgtable.c +9
-1@@ -19,12 +19,20 @@ EXPORT_SYMBOL(physical_mask);#endif #ifndef CONFIG_PARAVIRT +#ifndef CONFIG_PT_RECLAIM static inline void paravirt_tlb_remove_table (struct mmu_gather *tlb,void *table) {tlb_remove_page (tlb, table); } -#endif +#else +static inline +void paravirt_tlb_remove_table (struct mmu_gather *tlb,void *table) +{ +tlb_remove_table (tlb, table); +} +#endif /* !CONFIG_PT_RECLAIM */ +#endif /* !CONFIG_PARAVIRT */ gfp_t __userpte_alloc_gfp =GFP_PGTABLE_USER |PGTABLE_HIGHMEM ;mm/mmu_gather.c +8
-2@@ -311,10 +311,17 @@ static inline void tlb_table_invalidate(struct mmu_gather *tlb) } } +#ifndef __tlb_remove_table_one +static inline void __tlb_remove_table_one (void *table) +{ +__tlb_remove_table (table); +} +#endif +static void tlb_remove_table_one (void *table) {tlb_remove_table_sync_one (); -__tlb_remove_table (table); +__tlb_remove_table_one (table); }static void tlb_table_flush (struct mmu_gather *tlb) --2.20.1
Re: [PATCH v2 6/7] x86: mm: free page table pages by RCU instead of semi RCU
Hi Jann,
Quoted 4 lines · Jann Horn
+x86 MM maintainers - [email protected] was already cc'ed, but I don't know if that is enough for them to see it, and I haven't seen them comment on this series yet; I think you need an ack from them for this change.
Yes, thanks to Jann for cc-ing x86 MM maintainers, and look forward to their feedback!
Quoted 13 lines · Qi Zheng
Quoted 11 lines
Now, if
CONFIG_MMU_GATHER_RCU_TABLE_FREEis selected, the page table pages will be freed by semi RCU, that is:- batch table freeing: asynchronous free by RCU
- single table freeing: IPI + synchronous freeIn this way, the page table can be lockless traversed by disabling IRQ in paths such as fast GUP. But this is not enough to free the empty PTE page table pages in paths other that munmap and
exit_mmappath, because IPI cannot be synchronized withrcu_read_lock()in pte_offset_map{_lock}().
Quoted 7 lines
Quoted 3 lines
In preparation for supporting empty PTE page table pages reclaimation, let single table also be freed by RCU like batch table freeing. Then we can also use
pte_offset_map()etc to prevent PTE page from being freed.I applied your series locally and followed the page table freeing path that the reclaim feature would use on x86-64. Looks like it goes like this with the series applied:
Yes.
Quoted 11 lines
free_pte
pte_free_tlb
__pte_free_tlb
___pte_free_tlb
paravirt_tlb_remove_table
tlb_remove_table[!CONFIG_PARAVIRT, Xen PV, Hyper-V, KVM]
[no-free-memory slowpath:]
tlb_table_invalidate
tlb_remove_table_one
tlb_remove_table_sync_one[does IPI for GUP-fast]
^
It seems that this step can be ommitted when CONFIG_PT_RECLAIM is enabled, because GUP-fast will disable IRQ, which can also serve as the RCU critical section.
Quoted 7 lines
__tlb_remove_table_one[frees via RCU]
[fastpath:]
tlb_table_flush
tlb_remove_table_free[frees via RCU]
native_tlb_remove_table[CONFIG_PARAVIRTon native]
tlb_remove_table[see above]
Quoted 9 lines
Basically, the only remaining case in which
paravirt_tlb_remove_table()does not usetlb_remove_table()with RCU delay is !CONFIG_PARAVIRT && !CONFIG_PT_RECLAIM. Given thatCONFIG_PT_RECLAIMis defined as "default y" when supported, I guess that means X86's direct page table freeing path will almost never be used? If it stays that way and the X86 folks don't see a performance impact from using RCU to free tables onmunmap()/ process exit, I guess we might want to get rid of the direct page table freeing path on x86 at some point to simplify things...
In theory, using RCU to asynchronously free PTE pages should make munmap() / process exit path faster. I can try to grab some data.
Quoted 3 lines
(That simplification might also help prepare for Intel Remote Action Request, if that is a thing people want...)
If so, even better!
Quoted 6 lines
Quoted 6 lines
Like
pte_free_defer(), we can also safely useptdesc->pt_rcu_headto free the page table pages:- The
pt_rcu_headis unioned withpt_listandpmd_huge_pte.
Quoted 8 lines
Quoted 6 lines
- For
pt_list, it is used to manage the PGD page in x86. Fortunatelytlb_remove_table()will not be used for free PGD pages, so it is safe to usept_rcu_head.- For
pmd_huge_pte, we will dozap_deposited_table()before freeing the PMD page, so it is also safe.Please also update the comments on "struct ptdesc" accordingly.
OK, will do.
Quoted 17 lines
Quoted 17 lines
Signed-off-by Qi Zheng <[email protected]>--- arch/x86/include/asm/tlb.h | 19 +++++++++++++++++++ arch/x86/kernel/paravirt.c | 7 +++++++ arch/x86/mm/pgtable.c | 10 +++++++++- mm/mmu_gather.c | 9 ++++++++- 4 files changed, 43 insertions(+), 2 deletions(-)arch/x86/include/asm/tlb.h +0
-0@@ -34,4 +34,23 @@ static inline void __tlb_remove_table(void *table)free_page_and_swap_cache (table); }
Quoted 11 lines
arch/x86/include/asm/tlb.h static inline void __tlb_remove_table(void *table) · line 44
Quoted 8 lines
arch/x86/include/asm/tlb.h +8
-0+#ifdef CONFIG_PT_RECLAIM +static inline void __tlb_remove_table_one_rcu (struct rcu_head *head) +{ +struct page *page; + + page = container_of(head, struct page, rcu_head); +free_page_and_swap_cache (page); +}Why
free_page_and_swap_cache()? Page tables shouldn't have swap cache, so I think something likeput_page()would do the job.
Ah, I just did the same thing as __tlb_remove_table(). But I also have the same doubt as you, why does __tlb_remove_table() need to call free_page_and_swap_cache() instead of put_page().
Thanks,
Qi
Quoted 86 lines
Quoted 86 lines
arch/x86/include/asm/tlb.h +10
-0+static inline void __tlb_remove_table_one (void *table) +{ +struct page *page; + + page = table; +call_rcu (&page->rcu_head, __tlb_remove_table_one_rcu); +} +#define __tlb_remove_table_one __tlb_remove_table_one +#endif /* CONFIG_PT_RECLAIM */ +#endif /* _ASM_X86_TLB_H */ arch/x86/kernel/paravirt.c +7
-0@@ -59,10 +59,17 @@ void __init native_pv_lock_init(void)static_branch_enable (&virt_spin_lock_key); } +#ifndef CONFIG_PT_RECLAIM static void native_tlb_remove_table (struct mmu_gather *tlb,void *table) {tlb_remove_page (tlb, table); } +#else +static void native_tlb_remove_table (struct mmu_gather *tlb,void *table) +{ +tlb_remove_table (tlb, table); +} +#endif struct static_key paravirt_steal_enabled;struct static_key paravirt_steal_rq_enabled;arch/x86/mm/pgtable.c +9
-1@@ -19,12 +19,20 @@ EXPORT_SYMBOL(physical_mask);#endif #ifndef CONFIG_PARAVIRT +#ifndef CONFIG_PT_RECLAIM static inline void paravirt_tlb_remove_table (struct mmu_gather *tlb,void *table) {tlb_remove_page (tlb, table); } -#endif +#else +static inline +void paravirt_tlb_remove_table (struct mmu_gather *tlb,void *table) +{ +tlb_remove_table (tlb, table); +} +#endif /* !CONFIG_PT_RECLAIM */ +#endif /* !CONFIG_PARAVIRT */ gfp_t __userpte_alloc_gfp =GFP_PGTABLE_USER |PGTABLE_HIGHMEM ;mm/mmu_gather.c +8
-2@@ -311,10 +311,17 @@ static inline void tlb_table_invalidate(struct mmu_gather *tlb) } } +#ifndef __tlb_remove_table_one +static inline void __tlb_remove_table_one (void *table) +{ +__tlb_remove_table (table); +} +#endif +static void tlb_remove_table_one (void *table) {tlb_remove_table_sync_one (); -__tlb_remove_table (table); +__tlb_remove_table_one (table); }static void tlb_table_flush (struct mmu_gather *tlb) --2.20.1
Jann Horn
reviewerRe: [PATCH v2 6/7] x86: mm: free page table pages by RCU instead of semi RCU
Quoted 13 lines · Qi Zheng
Quoted 11 lines · Jann Horn
On Thu, Oct 31, 2024 at 9:14 AM Qi Zheng <[email protected]> wrote:
free_pte
pte_free_tlb
__pte_free_tlb
___pte_free_tlb
paravirt_tlb_remove_table
tlb_remove_table[!CONFIG_PARAVIRT, Xen PV, Hyper-V, KVM]
[no-free-memory slowpath:]
tlb_table_invalidate
tlb_remove_table_one
tlb_remove_table_sync_one[does IPI for GUP-fast]
Quoted 5 lines
^
It seems that this step can be ommitted whenCONFIG_PT_RECLAIMis enabled, because GUP-fast will disable IRQ, which can also serve as the RCU critical section.
Yeah, I think so too.
Quoted 12 lines
Quoted 11 lines
arch/x86/include/asm/tlb.h static inline void __tlb_remove_table(void *table) · line 44
Quoted 8 lines
arch/x86/include/asm/tlb.h +8
-0+#ifdef CONFIG_PT_RECLAIM +static inline void __tlb_remove_table_one_rcu (struct rcu_head *head) +{ +struct page *page; + + page = container_of(head, struct page, rcu_head); +free_page_and_swap_cache (page); +}Why
free_page_and_swap_cache()? Page tables shouldn't have swap cache, so I think something likeput_page()would do the job.
Quoted 3 lines
Ah, I just did the same thing as
__tlb_remove_table(). But I also have the same doubt as you, why does__tlb_remove_table()need to callfree_page_and_swap_cache()instead ofput_page().
I think commit 9e52fc2b50de3a1c08b44f94c610fbe998c0031a probably just copy-pasted it from a more generic page freeing path...
Re: [PATCH v2 6/7] x86: mm: free page table pages by RCU instead of semi RCU
Quoted 14 lines · Jann Horn
Quoted 13 lines · Qi Zheng
Quoted 11 lines · Jann Horn
On Thu, Oct 31, 2024 at 9:14 AM Qi Zheng <[email protected]> wrote:
free_pte
pte_free_tlb
__pte_free_tlb
___pte_free_tlb
paravirt_tlb_remove_table
tlb_remove_table[!CONFIG_PARAVIRT, Xen PV, Hyper-V, KVM]
[no-free-memory slowpath:]
tlb_table_invalidate
tlb_remove_table_one
tlb_remove_table_sync_one[does IPI for GUP-fast]
Quoted 7 lines
Quoted 5 lines
^
It seems that this step can be ommitted whenCONFIG_PT_RECLAIMis enabled, because GUP-fast will disable IRQ, which can also serve as the RCU critical section.Yeah, I think so too.
Will remove this step in the next version.
Quoted 13 lines
Quoted 13 lines
Quoted 12 lines
arch/x86/include/asm/tlb.h static inline void __tlb_remove_table(void *table) · line 44
Quoted 9 lines
arch/x86/include/asm/tlb.h +8
-0+#ifdef CONFIG_PT_RECLAIM +static inline void __tlb_remove_table_one_rcu (struct rcu_head *head) +{ +struct page *page; + + page = container_of(head, struct page, rcu_head); +free_page_and_swap_cache (page); +}Why
free_page_and_swap_cache()? Page tables shouldn't have swap cache, so I think something likeput_page()would do the job.
Quoted 6 lines
Quoted 3 lines
Ah, I just did the same thing as
__tlb_remove_table(). But I also have the same doubt as you, why does__tlb_remove_table()need to callfree_page_and_swap_cache()instead ofput_page().I think commit
9e52fc2b50de3a1c08b44f94c610fbe998c0031aprobably just copy-pasted it from a more generic page freeing path...
I guess so. Will use put_page() instead of free_page_and_swap_cache() in the next version. But for __tlb_remove_table(), I prefer to send a separate patch to modify.
Thanks!
Re: [PATCH v2 6/7] x86: mm: free page table pages by RCU instead of semi RCU
In reply to Qi ZhengQuoted 69 lines · Qi Zheng
Hi Jann,
Quoted 4 lines · Jann Horn
+x86 MM maintainers - [email protected] was already cc'ed, but I don't know if that is enough for them to see it, and I haven't seen them comment on this series yet; I think you need an ack from them for this change.
Yes, thanks to Jann for cc-ing x86 MM maintainers, and look forward to their feedback!
Quoted 13 lines · Qi Zheng
Quoted 11 lines
Now, if
CONFIG_MMU_GATHER_RCU_TABLE_FREEis selected, the page table pages will be freed by semi RCU, that is:- batch table freeing: asynchronous free by RCU
- single table freeing: IPI + synchronous freeIn this way, the page table can be lockless traversed by disabling IRQ in paths such as fast GUP. But this is not enough to free the empty PTE page table pages in paths other that munmap and
exit_mmappath, because IPI cannot be synchronized withrcu_read_lock()in pte_offset_map{_lock}().Quoted 7 lines
Quoted 3 lines
In preparation for supporting empty PTE page table pages reclaimation, let single table also be freed by RCU like batch table freeing. Then we can also use
pte_offset_map()etc to prevent PTE page from being freed.I applied your series locally and followed the page table freeing path that the reclaim feature would use on x86-64. Looks like it goes like this with the series applied:
Yes.
Quoted 11 lines
free_pte
pte_free_tlb
__pte_free_tlb
___pte_free_tlb
paravirt_tlb_remove_table
tlb_remove_table[!CONFIG_PARAVIRT, Xen PV, Hyper-V, KVM] [no-free-memory slowpath:]tlb_table_invalidate
tlb_remove_table_onetlb_remove_table_sync_one[does IPI for GUP-fast]^
It seems that this step can be ommitted whenCONFIG_PT_RECLAIMis enabled, because GUP-fast will disable IRQ, which can also serve as the RCU critical section.Quoted 7 lines
__tlb_remove_table_one[frees via RCU] [fastpath:]
tlb_table_flush
tlb_remove_table_free[frees via RCU]native_tlb_remove_table[CONFIG_PARAVIRTon native]tlb_remove_table[see above]Quoted 9 lines
Basically, the only remaining case in which
paravirt_tlb_remove_table()does not usetlb_remove_table()with RCU delay is !CONFIG_PARAVIRT && !CONFIG_PT_RECLAIM. Given thatCONFIG_PT_RECLAIMis defined as "default y" when supported, I guess that means X86's direct page table freeing path will almost never be used? If it stays that way and the X86 folks don't see a performance impact from using RCU to free tables onmunmap()/ process exit, I guess we might want to get rid of the direct page table freeing path on x86 at some point to simplify things...
Quoted 3 lines
In theory, using RCU to asynchronously free PTE pages should make
munmap()/ process exit path faster. I can try to grab some data.
I ran 'stress-ng --mmap 1 --mmap-bytes 1G', and grabbed the data with bpftrace like this:
bpftrace -e 'tracepoint:syscalls:sys_enter_munmap /comm == "stress-ng"/{@start[tid] = nsecs;} tracepoint:syscalls:sys_exit_munmap /@start[tid]/ { @ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); } interval:s:1 {exit();}'
The results are as follows:
without patch:
@ns[stress-ng]:
[1K, 2K) 99566 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2K, 4K) 77756 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
[4K, 8K) 32545 |@@@@@@@@@@@@@@@@
|
[8K, 16K) 442 |
|
[16K, 32K) 69 |
|
[32K, 64K) 1 |
|
[64K, 128K) 1 |
|
[128K, 256K) 14 |
|
[256K, 512K) 14 |
|
[512K, 1M) 68 |
|with patch:
@ns[stress-ng]:
[512, 1K) 69 |
|
[1K, 2K) 53921 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2K, 4K) 47088 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
[4K, 8K) 20583 |@@@@@@@@@@@@@@@@@@@
|
[8K, 16K) 659 |
|
[16K, 32K) 93 |
|
[32K, 64K) 24 |
|
[64K, 128K) 14 |
|
[128K, 256K) 6 |
|
[256K, 512K) 10 |
|
[512K, 1M) 29 |
|It doesn't seem to have much effect on munmap.
Thanks,
Qi
PATCH v2 7/7 x86: select ARCH_SUPPORTS_PT_RECLAIM if X86_64
In reply to Qi ZhengNow, x86 has fully supported the CONFIG_PT_RECLAIM feature, and reclaiming PTE pages is profitable only on 64-bit systems, so select ARCH_SUPPORTS_PT_RECLAIM if X86_64.
--- arch/x86/Kconfig | 1 + 1 file changed, 1 insertion(+)
arch/x86/Kconfig +1 -0
@@ -320,6 +320,7 @@ config X86 select FUNCTION_ALIGNMENT_4B imply IMA_SECURE_AND_OR_TRUSTED_BOOT if EFI select HAVE_DYNAMIC_FTRACE_NO_PATCHABLE + select ARCH_SUPPORTS_PT_RECLAIM if X86_64 config INSTRUCTION_DECODER def_bool y
Signature · 1 line
2.20.1