Re: [PATCH v2 7/7] mm/rmap: batch the unmap of large folios in try_to_migrate_one()
From: Miaohe Lin
Date: Tue Aug 18 2026 - 05:01:25 EST
On 2026/8/17 17:14, Lance Yang wrote:
> +Cc Miaohe
>
> On Thu, Aug 13, 2026 at 04:23:18AM +0000, Shivank Garg wrote:
>> try_to_migrate_one() converts present PTEs to migration entries one at a
>> time. For a PTE-mapped large folio, this repeat calls to ptep clear+flush,
>> the migration entry build and set, folio_remove_rmap_pte() and folio_put(),
>> each re-entering page_vma_mapped_walk() once per base page (256 times for
>> 1M folio).
>>
>> Mirror try_to_unmap_one() to introduce folio_migrate_pte_batch() to detect
>> eligible batch for PTEs mapping conseuctive subpages of a large folios,
>> and convert the whole batch in one shot using the batched helpers.
>>
>> A side-effect of this change is trace_set_migration_pte() will record
>> one event per batched run instead of earlier behavior of one per base page.
>>
>> Signed-off-by: Shivank Garg <shivankg@xxxxxxx>
>> ---
>> mm/rmap.c | 115 ++++++++++++++++++++++++++++++++++++++++++++++----------------
>> 1 file changed, 86 insertions(+), 29 deletions(-)
>>
>> diff --git a/mm/rmap.c b/mm/rmap.c
>> index 35752a70f3a0..63b885c0b7ef 100644
>> --- a/mm/rmap.c
>> +++ b/mm/rmap.c
>> @@ -2675,6 +2675,44 @@ static bool try_to_migrate_hugetlb_one(struct folio *folio,
>> return ret;
>> }
>>
>> +static inline unsigned int folio_migrate_pte_batch(struct folio *folio,
>> + struct page_vma_mapped_walk *pvmw, pte_t pte,
>> + struct page *subpage, bool anon_exclusive)
>> +{
>> + unsigned long end_addr, addr = pvmw->address;
>> + struct vm_area_struct *vma = pvmw->vma;
>> + unsigned int max_nr, nr;
>> +
>> +#ifdef __HAVE_ARCH_UNMAP_ONE
>> + /* Cannot batch unmap if arch_unmap_one() is defined. */
>> + return 1;
>> +#endif
>> +
>> + if (!folio_test_large(folio))
>> + return 1;
>> + if (folio_is_zone_device(folio) || folio_test_has_hwpoisoned(folio))
>> + return 1;
>> + if (pte_unused(pte))
>> + return 1;
>> +
>> + /* We may only batch within a single VMA and a single page table. */
>> + end_addr = pmd_addr_end(addr, vma->vm_end);
>> + max_nr = (end_addr - addr) >> PAGE_SHIFT;
>
> Hmm ... can this still batch over a poisoned tail page?
>
> memory_failure() sets PageHWPoison() before taking folio lock, but
> cannot set PG_has_hwpoisoned until it acquires and releases that lock.
>
> So tail page can already be poisoned while folio_test_has_hwpoisoned()
> still returns false ... no?
When memory error hits thp pages, memory_failure() first set PG_has_hwpoisoned and
then tries to split thp pages. And try_to_migrate() will be called to set migration
entries for anon pages. Does folio_migrate_pte_batch() work on this case? If so, the
folio_test_has_hwpoisoned() check above could catch the bad pages?
Or do you worry about the scene that meory error hits a thp while it's under migration?
Thanks both.
.
>
> Starting from a healthy first subpage, folio_migrate_pte_batch() can
> then batch across poisoned tail page. hwpoison only describes first
> subpage, so set_softleaf_ptes() installs a normal migration entry for
> poisoned page instead of an HWPoison entry ...
>
> Should folio_migrate_pte_batch() check PageHWPoison() on every candidate
> subpage and stop before a poisoned one?
>
> Cheers, Lance
>
>
>> + /*
>> + * If unmap fails, we need to restore the ptes. To avoid accidentally
>> + * upgrading write permissions for ptes that were not originally writable,
>> + * and to avoid losing the soft-dirty bit, use the appropriate FPB flags.
>> + */
>> + nr = folio_pte_batch_flags(folio, vma, pvmw->pte, &pte, max_nr,
>> + FPB_RESPECT_WRITE | FPB_RESPECT_SOFT_DIRTY);
>> +
>> + /* Limit possible batch count to a uniform PageAnonExclusive value */
>> + if (folio_test_anon(folio))
>> + nr = page_anon_exclusive_batch(0, nr, subpage, anon_exclusive);
>> +
>> + return nr;
>> +}
>> +
>> /*
>> * @arg: enum ttu_flags will be passed to this argument.
>> *
>> @@ -2686,12 +2724,12 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> {
>> struct mm_struct *mm = vma->vm_mm;
>> DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
>> - bool anon_exclusive, writable, ret = true;
>> + bool anon_exclusive, hwpoison, writable, ret = true;
>> pte_t pteval;
>> struct page *subpage;
>> struct mmu_notifier_range range;
>> enum ttu_flags flags = (enum ttu_flags)(long)arg;
>> - unsigned long pfn;
>> + unsigned long pfn, end_addr, nr_pages;
>>
>> /*
>> * When racing against e.g. zap_pte_range() on another cpu,
>> @@ -2744,11 +2782,8 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> VM_BUG_ON_FOLIO(folio_test_hugetlb(folio) ||
>> !folio_test_pmd_mappable(folio), folio);
>>
>> - if (set_pmd_migration_entry(&pvmw, subpage)) {
>> - ret = false;
>> - page_vma_mapped_walk_done(&pvmw);
>> - break;
>> - }
>> + if (set_pmd_migration_entry(&pvmw, subpage))
>> + goto walk_abort;
>> continue;
>> #endif
>> }
>> @@ -2773,10 +2808,25 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> subpage = folio_page(folio, pfn - folio_pfn(folio));
>> anon_exclusive = folio_test_anon(folio) &&
>> PageAnonExclusive(subpage);
>> + /*
>> + * memory_failure() can set PageHWPoison concurrently without holding
>> + * the folio lock. Snapshot the flag here to decide whether to batch
>> + * PTEs or install hwpoison entry.
>> + */
>> + hwpoison = PageHWPoison(subpage);
>>
>> + nr_pages = 1;
>> if (likely(pte_present(pteval))) {
>> - flush_cache_page(vma, address, pfn);
>> - /* Nuke the page table entry. */
>> + if (!hwpoison)
>> + nr_pages = folio_migrate_pte_batch(folio, &pvmw,
>> + pteval, subpage,
>> + anon_exclusive);
>> +
>> + end_addr = address + nr_pages * PAGE_SIZE;
>> + flush_cache_range(vma, address, end_addr);
>> +
>> + /* Nuke the page table entries. */
>> + pteval = get_and_clear_ptes(mm, address, pvmw.pte, nr_pages);
>> if (should_defer_flush(mm, flags)) {
>> /*
>> * We clear the PTE but do not flush so potentially
>> @@ -2786,11 +2836,9 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> * transition on a cached TLB entry is written through
>> * and traps if the PTE is unmapped.
>> */
>> - pteval = ptep_get_and_clear(mm, address, pvmw.pte);
>> -
>> - set_tlb_ubc_flush_pending(mm, pteval, address, address + PAGE_SIZE);
>> + set_tlb_ubc_flush_pending(mm, pteval, address, end_addr);
>> } else {
>> - pteval = ptep_clear_flush(vma, address, pvmw.pte);
>> + flush_tlb_range(vma, address, end_addr);
>> }
>> if (pte_dirty(pteval))
>> folio_mark_dirty(folio);
>> @@ -2809,7 +2857,8 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> /* Update high watermark before we lower rss */
>> update_hiwater_rss(mm);
>>
>> - if (PageHWPoison(subpage)) {
>> + if (hwpoison) {
>> + VM_WARN_ON_ONCE(nr_pages != 1);
>> VM_WARN_ON_FOLIO(folio_is_device_private(folio), folio);
>>
>> pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
>> @@ -2837,19 +2886,15 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> * so we'll not check/care.
>> */
>> if (arch_unmap_one(mm, vma, address, pteval) < 0) {
>> - set_pte_at(mm, address, pvmw.pte, pteval);
>> - ret = false;
>> - page_vma_mapped_walk_done(&pvmw);
>> - break;
>> + set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
>> + goto walk_abort;
>> }
>>
>> - /* See folio_try_share_anon_rmap_pte(): clear PTE first. */
>> + /* See folio_try_share_anon_rmap_ptes(): clear PTE first. */
>> if (anon_exclusive &&
>> - folio_try_share_anon_rmap_pte(folio, subpage)) {
>> - set_pte_at(mm, address, pvmw.pte, pteval);
>> - ret = false;
>> - page_vma_mapped_walk_done(&pvmw);
>> - break;
>> + folio_try_share_anon_rmap_ptes(folio, subpage, nr_pages)) {
>> + set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
>> + goto walk_abort;
>> }
>>
>> /*
>> @@ -2859,19 +2904,31 @@ static bool try_to_migrate_one(struct folio *folio, struct vm_area_struct *vma,
>> */
>> swp_pte = make_migration_pte(subpage, pteval,
>> writable, anon_exclusive);
>> - set_pte_at(mm, address, pvmw.pte, swp_pte);
>> trace_set_migration_pte(address, pte_val(swp_pte),
>> folio_order(folio));
>> +
>> + /* Set nr_pages migration entries, advancing the PFN. */
>> + set_softleaf_ptes(mm, address, pvmw.pte, swp_pte, nr_pages);
>> /*
>> * No need to invalidate here it will synchronize on
>> * against the special swap migration pte.
>> */
>> }
>>
>> - folio_remove_rmap_pte(folio, subpage, vma);
>> - if (vma->vm_flags & VM_LOCKED)
>> - mlock_drain_local();
>> - folio_put(folio);
>> + finish_folio_unmap(vma, folio, subpage, nr_pages);
>> +
>> + /*
>> + * If we batched the entire folio, there is nothing left to
>> + * walk; stop right here.
>> + */
>> + if (nr_pages == folio_nr_pages(folio))
>> + goto walk_done;
>> + continue;
>> +walk_abort:
>> + ret = false;
>> +walk_done:
>> + page_vma_mapped_walk_done(&pvmw);
>> + break;
>> }
>>
>> mmu_notifier_invalidate_range_end(&range);
>>
>> --
>> 2.43.0
>>
>>
> .
>