* [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask()
@ 2026-08-01 3:15 Rik van Riel
2026-08-01 3:15 ` [PATCH 1/5] mm/gup: convert follow_page_mask() to return a long Rik van Riel
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Rik van Riel @ 2026-08-01 3:15 UTC (permalink / raw)
To: linux-kernel, Andrew Morton, David Hildenbrand
Cc: kernel-team, Rik van Riel, Jason Gunthorpe, John Hubbard,
Peter Xu, linux-mm, Lorenzo Stoakes
follow_page_mask() walks the page tables one page at a time, even when
the caller asked for a whole run of contiguous pages. Every page of a
large folio re-enters the pmd/pud/pte walk and re-takes the page table
lock.
This series changes follow_page_mask() to return a page count and a
new @end argument bounding how many pages remain, instead of a single
struct page, so a walker can hand back more than one page per call.
Patch 1 converts the follow_page_mask()/follow_p4d_mask()/
follow_pud_mask()/follow_pmd_mask()/follow_page_pte() call chain to
return a long instead of a struct page pointer or ERR_PTR(), with no
functional change: every path still handles exactly one page.
Patch 2 is pure code motion, splitting the "commit to a resolved page"
tail of follow_page_pte() into its own follow_page_pte_commit(), no
functional change.
Patch 3 adds gup_fill_pages(), a small helper that fills pages[] and
flushes caches for a run of subpages, and converts the three existing
per-page call sites to use it with nr == 1, no functional change.
Patch 4 has follow_huge_pud()/follow_huge_pmd() report the huge page's
real subpage count instead of a separate *page_mask output, and
retires *page_mask and __get_user_pages()'s dead second
try_grab_folio() call and subpage loop.
It also defers gup_fill_pages() past the pud/pmd unlock, so a 1 GB
PUD-mapped folio doesn't hold that lock for a full array fill and
cache flush.
Patch 5 adds follow_pte_batch() and has follow_page_pte() call it once
per contiguous same-folio run instead of once per page, so a
PTE-mapped mTHP no longer restarts the walk and re-takes the PTE lock
per subpage.
This is the only patch that changes the number of page table walks or
lock acquisitions.
Benchmarked with mm/gup_test.c (PIN_LONGTERM_BENCHMARK, pin_user_pages
+ FOLL_LONGTERM, 256 MB region, median of 16 runs, folio formation
verified via the per-size anon_fault_alloc counter):
before after
4 kB base pages 2721 us 1198 us (2.3x)
64 kB mTHP 2929 us 201 us (14.6x)
2 MB THP 73 us 69 us (flat)
The 4 kB result comes entirely from patch 5 merging two separate
try_grab_folio() calls and lock acquisitions into one; folio size and
PTE batching play no part in it.
64 kB mTHP adds the walk-restart avoidance on top. 2 MB THP is
unaffected, since follow_huge_pmd() already handled it in one call.
Patch 4's lock-hold-time change is a scalability argument, not a
measured one -- it is not visible in this single-threaded benchmark.
Suggested-by: David Hildenbrand <david@kernel.org>
Rik van Riel (5):
mm/gup: convert follow_page_mask() to return a long
mm/gup: split follow_page_pte_commit() out of follow_page_pte()
mm/gup: add gup_fill_pages() and use it
mm/gup: return a huge page's full count from follow_page_mask()
mm/gup: walk multiple PTEs per follow_page_pte() call
mm/gup.c | 532 +++++++++++++++++++++++++++++++++----------------------
1 file changed, 322 insertions(+), 210 deletions(-)
base-commit: fc02acf6ac0c
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/5] mm/gup: convert follow_page_mask() to return a long
2026-08-01 3:15 [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask() Rik van Riel
@ 2026-08-01 3:15 ` Rik van Riel
2026-08-01 3:15 ` [PATCH 2/5] mm/gup: split follow_page_pte_commit() out of follow_page_pte() Rik van Riel
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Rik van Riel @ 2026-08-01 3:15 UTC (permalink / raw)
To: linux-kernel, Andrew Morton, David Hildenbrand
Cc: kernel-team, Rik van Riel, Jason Gunthorpe, John Hubbard,
Peter Xu, linux-mm, Lorenzo Stoakes
follow_page_mask() and its helpers return a struct page pointer: NULL,
ERR_PTR(), or the page found. Change the return type to long instead:
0, a negative errno, or 1 with the page stored in a new pages[0] slot.
This lets the return value carry a page count in a later change,
rather than only ever a single struct page pointer.
follow_huge_pud() and follow_huge_pmd() now store the found page and
flush its caches themselves; __get_user_pages() reads pages[i] back
to still expand a large folio's remaining subpages itself. The
vsyscall gate area, which bypasses follow_page_mask(), fills its own
slot the same way.
*page_mask and __get_user_pages()'s handling of a large folio's
remaining subpages are untouched, and mm/gup_test.c
(PIN_LONGTERM_BENCHMARK) shows no measurable difference for 4 kB,
64 kB mTHP, or 2 MB THP.
No functional changes intended.
Suggested-by: David Hildenbrand <david@kernel.org>
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
mm/gup.c | 269 ++++++++++++++++++++++++++++++-------------------------
1 file changed, 148 insertions(+), 121 deletions(-)
diff --git a/mm/gup.c b/mm/gup.c
index 0692119b7904..09c64ef2f57c 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -608,15 +608,15 @@ static inline bool can_follow_write_common(struct page *page,
return page && PageAnon(page) && PageAnonExclusive(page);
}
-static struct page *no_page_table(struct vm_area_struct *vma,
- unsigned int flags, unsigned long address)
+static long no_page_table(struct vm_area_struct *vma,
+ unsigned int flags, unsigned long address)
{
if (!(flags & FOLL_DUMP))
- return NULL;
+ return 0;
/*
* When core dumping, we don't want to allocate unnecessary pages or
- * page tables. Return error instead of NULL to skip handle_mm_fault,
+ * page tables. Return error instead of 0 to skip handle_mm_fault,
* then get_dump_page() will return NULL to leave a hole in the dump.
* But we can only make this optimization where a hole would surely
* be zero-filled if handle_mm_fault() actually did handle it.
@@ -625,12 +625,12 @@ static struct page *no_page_table(struct vm_area_struct *vma,
struct hstate *h = hstate_vma(vma);
if (!hugetlbfs_pagecache_present(h, vma, address))
- return ERR_PTR(-EFAULT);
+ return -EFAULT;
} else if ((vma_is_anonymous(vma) || !vma->vm_ops->fault)) {
- return ERR_PTR(-EFAULT);
+ return -EFAULT;
}
- return NULL;
+ return 0;
}
#ifdef CONFIG_PGTABLE_HAS_HUGE_LEAVES
@@ -646,9 +646,10 @@ static inline bool can_follow_write_pud(pud_t pud, struct page *page,
return can_follow_write_common(page, vma, flags);
}
-static struct page *follow_huge_pud(struct vm_area_struct *vma,
- unsigned long addr, pud_t *pudp,
- int flags, unsigned long *page_mask)
+static long follow_huge_pud(struct vm_area_struct *vma,
+ unsigned long addr, pud_t *pudp,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
struct mm_struct *mm = vma->vm_mm;
struct page *page;
@@ -659,25 +660,31 @@ static struct page *follow_huge_pud(struct vm_area_struct *vma,
assert_spin_locked(pud_lockptr(mm, pudp));
if (!pud_present(pud))
- return NULL;
+ return 0;
if ((flags & FOLL_WRITE) &&
!can_follow_write_pud(pud, pfn_to_page(pfn), vma, flags))
- return NULL;
+ return 0;
pfn += (addr & ~PUD_MASK) >> PAGE_SHIFT;
page = pfn_to_page(pfn);
if (!pud_write(pud) && gup_must_unshare(vma, flags, page))
- return ERR_PTR(-EMLINK);
+ return -EMLINK;
ret = try_grab_folio(page_folio(page), 1, flags);
if (ret)
- page = ERR_PTR(ret);
- else
- *page_mask = HPAGE_PUD_NR - 1;
+ return ret;
+
+ *page_mask = HPAGE_PUD_NR - 1;
+
+ if (pages) {
+ pages[0] = page;
+ flush_anon_page(vma, page, addr);
+ flush_dcache_page(page);
+ }
- return page;
+ return 1;
}
/* FOLL_FORCE can write to even unwritable PMDs in COW mappings. */
@@ -698,10 +705,10 @@ static inline bool can_follow_write_pmd(pmd_t pmd, struct page *page,
return !userfaultfd_huge_pmd_wp(vma, pmd);
}
-static struct page *follow_huge_pmd(struct vm_area_struct *vma,
- unsigned long addr, pmd_t *pmd,
- unsigned int flags,
- unsigned long *page_mask)
+static long follow_huge_pmd(struct vm_area_struct *vma,
+ unsigned long addr, pmd_t *pmd,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
struct mm_struct *mm = vma->vm_mm;
pmd_t pmdval = *pmd;
@@ -713,24 +720,24 @@ static struct page *follow_huge_pmd(struct vm_area_struct *vma,
page = pmd_page(pmdval);
if ((flags & FOLL_WRITE) &&
!can_follow_write_pmd(pmdval, page, vma, flags))
- return NULL;
+ return 0;
/* Avoid dumping huge zero page */
if ((flags & FOLL_DUMP) && is_huge_zero_pmd(pmdval))
- return ERR_PTR(-EFAULT);
+ return -EFAULT;
if (pmd_protnone(*pmd) && !gup_can_follow_protnone(vma, flags))
- return NULL;
+ return 0;
if (!pmd_write(pmdval) && gup_must_unshare(vma, flags, page))
- return ERR_PTR(-EMLINK);
+ return -EMLINK;
VM_WARN_ON_ONCE_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
!PageAnonExclusive(page), page);
ret = try_grab_folio(page_folio(page), 1, flags);
if (ret)
- return ERR_PTR(ret);
+ return ret;
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
if (pmd_trans_huge(pmdval) && (flags & FOLL_TOUCH))
@@ -740,23 +747,30 @@ static struct page *follow_huge_pmd(struct vm_area_struct *vma,
page += (addr & ~HPAGE_PMD_MASK) >> PAGE_SHIFT;
*page_mask = HPAGE_PMD_NR - 1;
- return page;
+ if (pages) {
+ pages[0] = page;
+ flush_anon_page(vma, page, addr);
+ flush_dcache_page(page);
+ }
+
+ return 1;
}
#else /* CONFIG_PGTABLE_HAS_HUGE_LEAVES */
-static struct page *follow_huge_pud(struct vm_area_struct *vma,
- unsigned long addr, pud_t *pudp,
- int flags, unsigned long *page_mask)
+static long follow_huge_pud(struct vm_area_struct *vma,
+ unsigned long addr, pud_t *pudp,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
- return NULL;
+ return 0;
}
-static struct page *follow_huge_pmd(struct vm_area_struct *vma,
- unsigned long addr, pmd_t *pmd,
- unsigned int flags,
- unsigned long *page_mask)
+static long follow_huge_pmd(struct vm_area_struct *vma,
+ unsigned long addr, pmd_t *pmd,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
- return NULL;
+ return 0;
}
#endif /* CONFIG_PGTABLE_HAS_HUGE_LEAVES */
@@ -799,15 +813,16 @@ static inline bool can_follow_write_pte(pte_t pte, struct page *page,
return !userfaultfd_pte_wp(vma, pte);
}
-static struct page *follow_page_pte(struct vm_area_struct *vma,
- unsigned long address, pmd_t *pmd, unsigned int flags)
+static long follow_page_pte(struct vm_area_struct *vma,
+ unsigned long address, pmd_t *pmd, unsigned int flags,
+ struct page **pages)
{
struct mm_struct *mm = vma->vm_mm;
struct folio *folio;
struct page *page;
spinlock_t *ptl;
pte_t *ptep, pte;
- int ret;
+ long ret;
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!ptep)
@@ -825,14 +840,14 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
*/
if ((flags & FOLL_WRITE) &&
!can_follow_write_pte(pte, page, vma, flags)) {
- page = NULL;
+ ret = 0;
goto out;
}
if (unlikely(!page)) {
if (flags & FOLL_DUMP) {
/* Avoid special (like zero) pages in core dumps */
- page = ERR_PTR(-EFAULT);
+ ret = -EFAULT;
goto out;
}
@@ -840,14 +855,13 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
page = pte_page(pte);
} else {
ret = follow_pfn_pte(vma, address, ptep, flags);
- page = ERR_PTR(ret);
goto out;
}
}
folio = page_folio(page);
if (!pte_write(pte) && gup_must_unshare(vma, flags, page)) {
- page = ERR_PTR(-EMLINK);
+ ret = -EMLINK;
goto out;
}
@@ -856,10 +870,8 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
/* try_grab_folio() does nothing unless FOLL_GET or FOLL_PIN is set. */
ret = try_grab_folio(folio, 1, flags);
- if (unlikely(ret)) {
- page = ERR_PTR(ret);
+ if (unlikely(ret))
goto out;
- }
/*
* We need to make the page accessible if and only if we are going
@@ -869,8 +881,7 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
if (flags & FOLL_PIN) {
ret = arch_make_folio_accessible(folio);
if (ret) {
- unpin_user_page(page);
- page = ERR_PTR(ret);
+ gup_put_folio(folio, 1, flags);
goto out;
}
}
@@ -885,24 +896,31 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
*/
folio_mark_accessed(folio);
}
+
+ if (pages) {
+ pages[0] = page;
+ flush_anon_page(vma, page, address);
+ flush_dcache_page(page);
+ }
+ ret = 1;
out:
pte_unmap_unlock(ptep, ptl);
- return page;
+ return ret;
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
- return NULL;
+ return 0;
return no_page_table(vma, flags, address);
}
-static struct page *follow_pmd_mask(struct vm_area_struct *vma,
- unsigned long address, pud_t *pudp,
- unsigned int flags,
- unsigned long *page_mask)
+static long follow_pmd_mask(struct vm_area_struct *vma,
+ unsigned long address, pud_t *pudp,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
pmd_t *pmd, pmdval;
spinlock_t *ptl;
- struct page *page;
+ long ret;
struct mm_struct *mm = vma->vm_mm;
pmd = pmd_offset(pudp, address);
@@ -912,7 +930,7 @@ static struct page *follow_pmd_mask(struct vm_area_struct *vma,
if (!pmd_present(pmdval))
return no_page_table(vma, flags, address);
if (likely(!pmd_leaf(pmdval)))
- return follow_page_pte(vma, address, pmd, flags);
+ return follow_page_pte(vma, address, pmd, flags, pages);
if (pmd_protnone(pmdval) && !gup_can_follow_protnone(vma, flags))
return no_page_table(vma, flags, address);
@@ -925,28 +943,28 @@ static struct page *follow_pmd_mask(struct vm_area_struct *vma,
}
if (unlikely(!pmd_leaf(pmdval))) {
spin_unlock(ptl);
- return follow_page_pte(vma, address, pmd, flags);
+ return follow_page_pte(vma, address, pmd, flags, pages);
}
if (pmd_trans_huge(pmdval) && (flags & FOLL_SPLIT_PMD)) {
spin_unlock(ptl);
split_huge_pmd(vma, pmd, address);
/* If pmd was left empty, stuff a page table in there quickly */
- return pte_alloc(mm, pmd) ? ERR_PTR(-ENOMEM) :
- follow_page_pte(vma, address, pmd, flags);
+ return pte_alloc(mm, pmd) ? -ENOMEM :
+ follow_page_pte(vma, address, pmd, flags, pages);
}
- page = follow_huge_pmd(vma, address, pmd, flags, page_mask);
+ ret = follow_huge_pmd(vma, address, pmd, flags, page_mask, pages);
spin_unlock(ptl);
- return page;
+ return ret;
}
-static struct page *follow_pud_mask(struct vm_area_struct *vma,
- unsigned long address, p4d_t *p4dp,
- unsigned int flags,
- unsigned long *page_mask)
+static long follow_pud_mask(struct vm_area_struct *vma,
+ unsigned long address, p4d_t *p4dp,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
pud_t *pudp, pud;
spinlock_t *ptl;
- struct page *page;
+ long ret;
struct mm_struct *mm = vma->vm_mm;
pudp = pud_offset(p4dp, address);
@@ -955,22 +973,22 @@ static struct page *follow_pud_mask(struct vm_area_struct *vma,
return no_page_table(vma, flags, address);
if (pud_leaf(pud)) {
ptl = pud_lock(mm, pudp);
- page = follow_huge_pud(vma, address, pudp, flags, page_mask);
+ ret = follow_huge_pud(vma, address, pudp, flags, page_mask, pages);
spin_unlock(ptl);
- if (page)
- return page;
+ if (ret)
+ return ret;
return no_page_table(vma, flags, address);
}
if (unlikely(pud_bad(pud)))
return no_page_table(vma, flags, address);
- return follow_pmd_mask(vma, address, pudp, flags, page_mask);
+ return follow_pmd_mask(vma, address, pudp, flags, page_mask, pages);
}
-static struct page *follow_p4d_mask(struct vm_area_struct *vma,
- unsigned long address, pgd_t *pgdp,
- unsigned int flags,
- unsigned long *page_mask)
+static long follow_p4d_mask(struct vm_area_struct *vma,
+ unsigned long address, pgd_t *pgdp,
+ unsigned int flags, unsigned long *page_mask,
+ struct page **pages)
{
p4d_t *p4dp, p4d;
@@ -981,7 +999,7 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,
if (!p4d_present(p4d) || p4d_bad(p4d))
return no_page_table(vma, flags, address);
- return follow_pud_mask(vma, address, p4dp, flags, page_mask);
+ return follow_pud_mask(vma, address, p4dp, flags, page_mask, pages);
}
/**
@@ -990,6 +1008,9 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,
* @address: virtual address to look up
* @flags: flags modifying lookup behaviour
* @page_mask: a pointer to output page_mask
+ * @pages: array to receive the page found, refcounted per @flags, or NULL
+ * to walk the page tables (e.g. to fault pages in) without
+ * collecting or refcounting them
*
* @flags can have FOLL_ flags set, defined in <linux/mm.h>
*
@@ -1000,17 +1021,17 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,
*
* On output, @page_mask is set according to the size of the page.
*
- * Return: the mapped (struct page *), %NULL if no mapping exists, or
- * an error pointer if there is a mapping to something not represented
- * by a page descriptor (see also vm_normal_page()).
+ * Return: 1 with @pages[0] filled in if a page was found, 0 if no mapping
+ * exists at @address, or a negative errno for a mapping to something not
+ * represented by a page descriptor (see also vm_normal_page()).
*/
-static struct page *follow_page_mask(struct vm_area_struct *vma,
- unsigned long address, unsigned int flags,
- unsigned long *page_mask)
+static long follow_page_mask(struct vm_area_struct *vma,
+ unsigned long address, unsigned int flags,
+ unsigned long *page_mask, struct page **pages)
{
pgd_t *pgd;
struct mm_struct *mm = vma->vm_mm;
- struct page *page;
+ long ret;
vma_pgtable_walk_begin(vma);
@@ -1018,13 +1039,13 @@ static struct page *follow_page_mask(struct vm_area_struct *vma,
pgd = pgd_offset(mm, address);
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
- page = no_page_table(vma, flags, address);
+ ret = no_page_table(vma, flags, address);
else
- page = follow_p4d_mask(vma, address, pgd, flags, page_mask);
+ ret = follow_p4d_mask(vma, address, pgd, flags, page_mask, pages);
vma_pgtable_walk_end(vma);
- return page;
+ return ret;
}
static int get_gate_page(struct mm_struct *mm, unsigned long address,
@@ -1374,6 +1395,7 @@ static long __get_user_pages(struct mm_struct *mm,
do {
struct page *page;
unsigned int page_increm;
+ long nr;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
@@ -1400,8 +1422,15 @@ static long __get_user_pages(struct mm_struct *mm,
pages ? &page : NULL);
if (ret)
goto out;
- page_mask = 0;
- goto next_page;
+ if (pages) {
+ pages[i] = page;
+ flush_anon_page(vma, page, start);
+ flush_dcache_page(page);
+ }
+ i++;
+ start += PAGE_SIZE;
+ nr_pages--;
+ continue;
}
if (!vma) {
@@ -1423,10 +1452,11 @@ static long __get_user_pages(struct mm_struct *mm,
}
cond_resched();
- page = follow_page_mask(vma, start, gup_flags, &page_mask);
- if (!page || PTR_ERR(page) == -EMLINK) {
+ nr = follow_page_mask(vma, start, gup_flags, &page_mask,
+ pages ? &pages[i] : NULL);
+ if (!nr || nr == -EMLINK) {
ret = faultin_page(vma, start, gup_flags,
- PTR_ERR(page) == -EMLINK, locked);
+ nr == -EMLINK, locked);
switch (ret) {
case 0:
goto retry;
@@ -1440,7 +1470,7 @@ static long __get_user_pages(struct mm_struct *mm,
goto out;
}
BUG();
- } else if (PTR_ERR(page) == -EEXIST) {
+ } else if (nr == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page. If the caller expects **pages to be
@@ -1448,53 +1478,50 @@ static long __get_user_pages(struct mm_struct *mm,
* for this page.
*/
if (pages) {
- ret = PTR_ERR(page);
+ ret = nr;
goto out;
}
- } else if (IS_ERR(page)) {
- ret = PTR_ERR(page);
+ } else if (nr < 0) {
+ ret = nr;
goto out;
}
-next_page:
+
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
- if (pages) {
+ /*
+ * This must be a large folio (and doesn't need to
+ * be the whole folio; it can be part of it), do
+ * the refcount work for all the subpages too.
+ *
+ * NOTE: here the page may not be the head page
+ * e.g. when start addr is not thp-size aligned.
+ * try_grab_folio() should have taken care of tail
+ * pages.
+ */
+ if (pages && page_increm > 1) {
struct page *subpage;
unsigned int j;
+ struct folio *folio = page_folio(pages[i]);
/*
- * This must be a large folio (and doesn't need to
- * be the whole folio; it can be part of it), do
- * the refcount work for all the subpages too.
- *
- * NOTE: here the page may not be the head page
- * e.g. when start addr is not thp-size aligned.
- * try_grab_folio() should have taken care of tail
- * pages.
+ * Since we already hold refcount on the
+ * large folio, this should never fail.
*/
- if (page_increm > 1) {
- struct folio *folio = page_folio(page);
-
+ if (try_grab_folio(folio, page_increm - 1,
+ gup_flags)) {
/*
- * Since we already hold refcount on the
- * large folio, this should never fail.
+ * Release the 1st page ref if the
+ * folio is problematic, fail hard.
*/
- if (try_grab_folio(folio, page_increm - 1,
- gup_flags)) {
- /*
- * Release the 1st page ref if the
- * folio is problematic, fail hard.
- */
- gup_put_folio(folio, 1, gup_flags);
- ret = -EFAULT;
- goto out;
- }
+ gup_put_folio(folio, 1, gup_flags);
+ ret = -EFAULT;
+ goto out;
}
- for (j = 0; j < page_increm; j++) {
- subpage = page + j;
+ for (j = 1; j < page_increm; j++) {
+ subpage = pages[i] + j;
pages[i + j] = subpage;
flush_anon_page(vma, subpage, start + j * PAGE_SIZE);
flush_dcache_page(subpage);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 2/5] mm/gup: split follow_page_pte_commit() out of follow_page_pte()
2026-08-01 3:15 [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask() Rik van Riel
2026-08-01 3:15 ` [PATCH 1/5] mm/gup: convert follow_page_mask() to return a long Rik van Riel
@ 2026-08-01 3:15 ` Rik van Riel
2026-08-01 3:15 ` [PATCH 3/5] mm/gup: add gup_fill_pages() and use it Rik van Riel
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Rik van Riel @ 2026-08-01 3:15 UTC (permalink / raw)
To: linux-kernel, Andrew Morton, David Hildenbrand
Cc: kernel-team, Rik van Riel, Jason Gunthorpe, John Hubbard,
Peter Xu, linux-mm, Lorenzo Stoakes
follow_page_pte() does two things once it has resolved a present PTE
to a page: run the per-PTE safety checks (write-fault, unshare), then
commit to that page: grab a ref, fault it in if pinning, mark it
dirty/accessed, and hand it back to the caller.
Split the second part into its own follow_page_pte_commit(), unchanged
except for taking its inputs as parameters instead of local variables.
A later change teaches it to commit more than one page at a time.
No functional changes intended.
Suggested-by: David Hildenbrand <david@kernel.org>
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
mm/gup.c | 86 ++++++++++++++++++++++++++++++++++----------------------
1 file changed, 53 insertions(+), 33 deletions(-)
diff --git a/mm/gup.c b/mm/gup.c
index 09c64ef2f57c..053da43760a2 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -813,6 +813,56 @@ static inline bool can_follow_write_pte(pte_t pte, struct page *page,
return !userfaultfd_pte_wp(vma, pte);
}
+/*
+ * The caller has already run every per-PTE safety check (present,
+ * write-fault, gup_must_unshare()) on the PTE, so this only does the
+ * per-folio work: the refcount grab, the FOLL_PIN accessibility fault-in,
+ * dirty/accessed marking, and the array fill with the cache flush.
+ */
+static long follow_page_pte_commit(struct vm_area_struct *vma,
+ unsigned long address, struct folio *folio, struct page *page,
+ pte_t pte, unsigned int flags, struct page **pages)
+{
+ long ret;
+
+ /* try_grab_folio() does nothing unless FOLL_GET or FOLL_PIN is set. */
+ ret = try_grab_folio(folio, 1, flags);
+ if (unlikely(ret))
+ return ret;
+
+ /*
+ * We need to make the page accessible if and only if we are going
+ * to access its content (the FOLL_PIN case). Please see
+ * Documentation/core-api/pin_user_pages.rst for details.
+ */
+ if (flags & FOLL_PIN) {
+ ret = arch_make_folio_accessible(folio);
+ if (ret) {
+ gup_put_folio(folio, 1, flags);
+ return ret;
+ }
+ }
+ if (flags & FOLL_TOUCH) {
+ if ((flags & FOLL_WRITE) &&
+ !pte_dirty(pte) && !folio_test_dirty(folio))
+ folio_mark_dirty(folio);
+ /*
+ * pte_mkyoung() would be more correct here, but atomic care
+ * is needed to avoid losing the dirty bit: it is easier to use
+ * folio_mark_accessed().
+ */
+ folio_mark_accessed(folio);
+ }
+
+ if (pages) {
+ pages[0] = page;
+ flush_anon_page(vma, page, address);
+ flush_dcache_page(page);
+ }
+
+ return 0;
+}
+
static long follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags,
struct page **pages)
@@ -868,40 +918,10 @@ static long follow_page_pte(struct vm_area_struct *vma,
VM_WARN_ON_ONCE_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
!PageAnonExclusive(page), page);
- /* try_grab_folio() does nothing unless FOLL_GET or FOLL_PIN is set. */
- ret = try_grab_folio(folio, 1, flags);
- if (unlikely(ret))
+ ret = follow_page_pte_commit(vma, address, folio, page, pte, flags,
+ pages);
+ if (ret)
goto out;
-
- /*
- * We need to make the page accessible if and only if we are going
- * to access its content (the FOLL_PIN case). Please see
- * Documentation/core-api/pin_user_pages.rst for details.
- */
- if (flags & FOLL_PIN) {
- ret = arch_make_folio_accessible(folio);
- if (ret) {
- gup_put_folio(folio, 1, flags);
- goto out;
- }
- }
- if (flags & FOLL_TOUCH) {
- if ((flags & FOLL_WRITE) &&
- !pte_dirty(pte) && !folio_test_dirty(folio))
- folio_mark_dirty(folio);
- /*
- * pte_mkyoung() would be more correct here, but atomic care
- * is needed to avoid losing the dirty bit: it is easier to use
- * folio_mark_accessed().
- */
- folio_mark_accessed(folio);
- }
-
- if (pages) {
- pages[0] = page;
- flush_anon_page(vma, page, address);
- flush_dcache_page(page);
- }
ret = 1;
out:
pte_unmap_unlock(ptep, ptl);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 3/5] mm/gup: add gup_fill_pages() and use it
2026-08-01 3:15 [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask() Rik van Riel
2026-08-01 3:15 ` [PATCH 1/5] mm/gup: convert follow_page_mask() to return a long Rik van Riel
2026-08-01 3:15 ` [PATCH 2/5] mm/gup: split follow_page_pte_commit() out of follow_page_pte() Rik van Riel
@ 2026-08-01 3:15 ` Rik van Riel
2026-08-01 3:15 ` [PATCH 4/5] mm/gup: return a huge page's full count from follow_page_mask() Rik van Riel
2026-08-01 3:15 ` [PATCH 5/5] mm/gup: walk multiple PTEs per follow_page_pte() call Rik van Riel
4 siblings, 0 replies; 6+ messages in thread
From: Rik van Riel @ 2026-08-01 3:15 UTC (permalink / raw)
To: linux-kernel, Andrew Morton, David Hildenbrand
Cc: kernel-team, Rik van Riel, Jason Gunthorpe, John Hubbard,
Peter Xu, linux-mm, Lorenzo Stoakes
follow_huge_pud(), follow_huge_pmd(), and follow_page_pte_commit() each
store the page they found into pages[0] and flush its caches with the
same three lines.
Add a gup_fill_pages() helper for that, and call it from all three
instead of repeating the loop.
No functional changes intended.
Suggested-by: David Hildenbrand <david@kernel.org>
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
mm/gup.c | 35 ++++++++++++++++++++---------------
1 file changed, 20 insertions(+), 15 deletions(-)
diff --git a/mm/gup.c b/mm/gup.c
index 053da43760a2..b6d508a44ced 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -633,6 +633,23 @@ static long no_page_table(struct vm_area_struct *vma,
return 0;
}
+static void gup_fill_pages(struct vm_area_struct *vma, unsigned long address,
+ struct page *page, unsigned long nr, struct page **pages)
+{
+ unsigned long i;
+
+ if (!pages)
+ return;
+
+ for (i = 0; i < nr; i++) {
+ struct page *subpage = page + i;
+
+ pages[i] = subpage;
+ flush_anon_page(vma, subpage, address + i * PAGE_SIZE);
+ flush_dcache_page(subpage);
+ }
+}
+
#ifdef CONFIG_PGTABLE_HAS_HUGE_LEAVES
/* FOLL_FORCE can write to even unwritable PUDs in COW mappings. */
static inline bool can_follow_write_pud(pud_t pud, struct page *page,
@@ -678,11 +695,7 @@ static long follow_huge_pud(struct vm_area_struct *vma,
*page_mask = HPAGE_PUD_NR - 1;
- if (pages) {
- pages[0] = page;
- flush_anon_page(vma, page, addr);
- flush_dcache_page(page);
- }
+ gup_fill_pages(vma, addr, page, 1, pages);
return 1;
}
@@ -747,11 +760,7 @@ static long follow_huge_pmd(struct vm_area_struct *vma,
page += (addr & ~HPAGE_PMD_MASK) >> PAGE_SHIFT;
*page_mask = HPAGE_PMD_NR - 1;
- if (pages) {
- pages[0] = page;
- flush_anon_page(vma, page, addr);
- flush_dcache_page(page);
- }
+ gup_fill_pages(vma, addr, page, 1, pages);
return 1;
}
@@ -854,11 +863,7 @@ static long follow_page_pte_commit(struct vm_area_struct *vma,
folio_mark_accessed(folio);
}
- if (pages) {
- pages[0] = page;
- flush_anon_page(vma, page, address);
- flush_dcache_page(page);
- }
+ gup_fill_pages(vma, address, page, 1, pages);
return 0;
}
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 4/5] mm/gup: return a huge page's full count from follow_page_mask()
2026-08-01 3:15 [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask() Rik van Riel
` (2 preceding siblings ...)
2026-08-01 3:15 ` [PATCH 3/5] mm/gup: add gup_fill_pages() and use it Rik van Riel
@ 2026-08-01 3:15 ` Rik van Riel
2026-08-01 3:15 ` [PATCH 5/5] mm/gup: walk multiple PTEs per follow_page_pte() call Rik van Riel
4 siblings, 0 replies; 6+ messages in thread
From: Rik van Riel @ 2026-08-01 3:15 UTC (permalink / raw)
To: linux-kernel, Andrew Morton, David Hildenbrand
Cc: kernel-team, Rik van Riel, Jason Gunthorpe, John Hubbard,
Peter Xu, linux-mm, Lorenzo Stoakes
follow_huge_pud()/follow_huge_pmd() already know the huge page's full
size but report it via a separate *page_mask output; __get_user_pages()
does a second try_grab_folio() call and subpage loop for everything
past the first page.
Add an @end argument, have the huge paths report their count as the
return value instead, clamped to the huge page's size and @end, and
fill every subpage via gup_fill_pages().
The huge paths grab refs under the pud/pmd lock, then store the first
page in pages[0] and return; follow_pud_mask()/follow_pmd_mask() fill
the rest of the array and flush caches after releasing the lock. The
pages are already pinned by then, so this is safe.
This keeps the lock held for a fixed amount of work regardless of
folio size: a 1 GB PUD-mapped folio can be up to HPAGE_PUD_NR pages,
too long to flush while blocking every other user of that lock.
This merges two try_grab_folio() calls into one, and returns whatever
error try_grab_folio() gives instead of forcing -EFAULT on the second
call's failure.
*page_mask and __get_user_pages()'s second-grab/subpage loop are now
dead; remove them. The old silent page_increm clamp becomes a
VM_WARN_ON_ONCE, since @end already bounds the count and refs/pages[]
are already committed by the time the caller sees it -- truncating
here would leak references, not just waste a comparison.
follow_page_pte() is unaffected, still returning at most 1 page.
__get_user_pages()'s -EEXIST handling also needs an explicit nr = 1 for
the pages == NULL case now: it used to get that for free from
page_mask staying 0 for a PFN-special PTE, but nr is -EEXIST there
without it, which would grow nr_pages instead of shrinking it.
Verified with mm/gup_test.c (PIN_LONGTERM_BENCHMARK): no measurable
change for 4 kB, 64 kB mTHP, or 2 MB THP -- these paths don't reach
follow_huge_pud()/follow_huge_pmd(), or the merged call is too small
to measure. This single-threaded benchmark can't show a lock-hold-time
change; the fix above is by inspection, not measurement.
Suggested-by: David Hildenbrand <david@kernel.org>
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
mm/gup.c | 177 ++++++++++++++++++++++++-------------------------------
1 file changed, 78 insertions(+), 99 deletions(-)
diff --git a/mm/gup.c b/mm/gup.c
index b6d508a44ced..ccbef9476ff6 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -664,14 +664,14 @@ static inline bool can_follow_write_pud(pud_t pud, struct page *page,
}
static long follow_huge_pud(struct vm_area_struct *vma,
- unsigned long addr, pud_t *pudp,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long addr, unsigned long end, pud_t *pudp,
+ unsigned int flags, struct page **pages)
{
struct mm_struct *mm = vma->vm_mm;
struct page *page;
pud_t pud = *pudp;
unsigned long pfn = pud_pfn(pud);
+ unsigned long off, nr;
int ret;
assert_spin_locked(pud_lockptr(mm, pudp));
@@ -683,21 +683,23 @@ static long follow_huge_pud(struct vm_area_struct *vma,
!can_follow_write_pud(pud, pfn_to_page(pfn), vma, flags))
return 0;
- pfn += (addr & ~PUD_MASK) >> PAGE_SHIFT;
+ off = PFN_DOWN(addr & ~PUD_MASK);
+ pfn += off;
page = pfn_to_page(pfn);
if (!pud_write(pud) && gup_must_unshare(vma, flags, page))
return -EMLINK;
- ret = try_grab_folio(page_folio(page), 1, flags);
+ nr = min(HPAGE_PUD_NR - off, PFN_DOWN(end - addr));
+
+ ret = try_grab_folio(page_folio(page), nr, flags);
if (ret)
return ret;
- *page_mask = HPAGE_PUD_NR - 1;
-
- gup_fill_pages(vma, addr, page, 1, pages);
+ if (pages)
+ pages[0] = page;
- return 1;
+ return nr;
}
/* FOLL_FORCE can write to even unwritable PMDs in COW mappings. */
@@ -719,13 +721,13 @@ static inline bool can_follow_write_pmd(pmd_t pmd, struct page *page,
}
static long follow_huge_pmd(struct vm_area_struct *vma,
- unsigned long addr, pmd_t *pmd,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long addr, unsigned long end, pmd_t *pmd,
+ unsigned int flags, struct page **pages)
{
struct mm_struct *mm = vma->vm_mm;
pmd_t pmdval = *pmd;
struct page *page;
+ unsigned long off, nr;
int ret;
assert_spin_locked(pmd_lockptr(mm, pmd));
@@ -748,7 +750,10 @@ static long follow_huge_pmd(struct vm_area_struct *vma,
VM_WARN_ON_ONCE_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
!PageAnonExclusive(page), page);
- ret = try_grab_folio(page_folio(page), 1, flags);
+ off = PFN_DOWN(addr & ~HPAGE_PMD_MASK);
+ nr = min(HPAGE_PMD_NR - off, PFN_DOWN(end - addr));
+
+ ret = try_grab_folio(page_folio(page), nr, flags);
if (ret)
return ret;
@@ -757,27 +762,25 @@ static long follow_huge_pmd(struct vm_area_struct *vma,
touch_pmd(vma, addr, pmd, flags & FOLL_WRITE);
#endif /* CONFIG_TRANSPARENT_HUGEPAGE */
- page += (addr & ~HPAGE_PMD_MASK) >> PAGE_SHIFT;
- *page_mask = HPAGE_PMD_NR - 1;
+ page += off;
- gup_fill_pages(vma, addr, page, 1, pages);
+ if (pages)
+ pages[0] = page;
- return 1;
+ return nr;
}
#else /* CONFIG_PGTABLE_HAS_HUGE_LEAVES */
static long follow_huge_pud(struct vm_area_struct *vma,
- unsigned long addr, pud_t *pudp,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long addr, unsigned long end, pud_t *pudp,
+ unsigned int flags, struct page **pages)
{
return 0;
}
static long follow_huge_pmd(struct vm_area_struct *vma,
- unsigned long addr, pmd_t *pmd,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long addr, unsigned long end, pmd_t *pmd,
+ unsigned int flags, struct page **pages)
{
return 0;
}
@@ -939,9 +942,8 @@ static long follow_page_pte(struct vm_area_struct *vma,
}
static long follow_pmd_mask(struct vm_area_struct *vma,
- unsigned long address, pud_t *pudp,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long address, unsigned long end, pud_t *pudp,
+ unsigned int flags, struct page **pages)
{
pmd_t *pmd, pmdval;
spinlock_t *ptl;
@@ -977,15 +979,23 @@ static long follow_pmd_mask(struct vm_area_struct *vma,
return pte_alloc(mm, pmd) ? -ENOMEM :
follow_page_pte(vma, address, pmd, flags, pages);
}
- ret = follow_huge_pmd(vma, address, pmd, flags, page_mask, pages);
+ ret = follow_huge_pmd(vma, address, end, pmd, flags, pages);
spin_unlock(ptl);
+
+ /*
+ * Refs are already grabbed above; the array fill and cache
+ * flushes only touch the now-pinned pages, so do them without
+ * the pmd lock held.
+ */
+ if (ret > 0 && pages)
+ gup_fill_pages(vma, address, pages[0], ret, pages);
+
return ret;
}
static long follow_pud_mask(struct vm_area_struct *vma,
- unsigned long address, p4d_t *p4dp,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long address, unsigned long end, p4d_t *p4dp,
+ unsigned int flags, struct page **pages)
{
pud_t *pudp, pud;
spinlock_t *ptl;
@@ -998,8 +1008,18 @@ static long follow_pud_mask(struct vm_area_struct *vma,
return no_page_table(vma, flags, address);
if (pud_leaf(pud)) {
ptl = pud_lock(mm, pudp);
- ret = follow_huge_pud(vma, address, pudp, flags, page_mask, pages);
+ ret = follow_huge_pud(vma, address, end, pudp, flags, pages);
spin_unlock(ptl);
+
+ /*
+ * Refs are already grabbed above; the array fill and cache
+ * flushes only touch the now-pinned pages, so do them
+ * without the pud lock held -- a 1 GB folio can be up to
+ * HPAGE_PUD_NR pages, too long to flush under a spinlock.
+ */
+ if (ret > 0 && pages)
+ gup_fill_pages(vma, address, pages[0], ret, pages);
+
if (ret)
return ret;
return no_page_table(vma, flags, address);
@@ -1007,13 +1027,12 @@ static long follow_pud_mask(struct vm_area_struct *vma,
if (unlikely(pud_bad(pud)))
return no_page_table(vma, flags, address);
- return follow_pmd_mask(vma, address, pudp, flags, page_mask, pages);
+ return follow_pmd_mask(vma, address, end, pudp, flags, pages);
}
static long follow_p4d_mask(struct vm_area_struct *vma,
- unsigned long address, pgd_t *pgdp,
- unsigned int flags, unsigned long *page_mask,
- struct page **pages)
+ unsigned long address, unsigned long end, pgd_t *pgdp,
+ unsigned int flags, struct page **pages)
{
p4d_t *p4dp, p4d;
@@ -1024,18 +1043,18 @@ static long follow_p4d_mask(struct vm_area_struct *vma,
if (!p4d_present(p4d) || p4d_bad(p4d))
return no_page_table(vma, flags, address);
- return follow_pud_mask(vma, address, p4dp, flags, page_mask, pages);
+ return follow_pud_mask(vma, address, end, p4dp, flags, pages);
}
/**
- * follow_page_mask - look up a page descriptor from a user-virtual address
+ * follow_page_mask - look up pages at a user-virtual address
* @vma: vm_area_struct mapping @address
* @address: virtual address to look up
+ * @end: virtual address at which to stop batching contiguous pages
* @flags: flags modifying lookup behaviour
- * @page_mask: a pointer to output page_mask
- * @pages: array to receive the page found, refcounted per @flags, or NULL
- * to walk the page tables (e.g. to fault pages in) without
- * collecting or refcounting them
+ * @pages: array to receive the pages, refcounted per @flags, or NULL to
+ * walk the page tables (e.g. to fault pages in) without collecting
+ * or refcounting them
*
* @flags can have FOLL_ flags set, defined in <linux/mm.h>
*
@@ -1044,15 +1063,15 @@ static long follow_p4d_mask(struct vm_area_struct *vma,
* trigger a fault with FAULT_FLAG_UNSHARE set. Note that unsharing is only
* relevant with FOLL_PIN and !FOLL_WRITE.
*
- * On output, @page_mask is set according to the size of the page.
- *
- * Return: 1 with @pages[0] filled in if a page was found, 0 if no mapping
- * exists at @address, or a negative errno for a mapping to something not
- * represented by a page descriptor (see also vm_normal_page()).
+ * Return: the number of contiguous pages starting at @address that were
+ * placed into @pages (if non-NULL), which may be fewer than the pages
+ * requested via @end; 0 if no mapping exists at @address; or a negative
+ * errno for a mapping to something not represented by a page descriptor
+ * (see also vm_normal_page()).
*/
static long follow_page_mask(struct vm_area_struct *vma,
- unsigned long address, unsigned int flags,
- unsigned long *page_mask, struct page **pages)
+ unsigned long address, unsigned long end,
+ unsigned int flags, struct page **pages)
{
pgd_t *pgd;
struct mm_struct *mm = vma->vm_mm;
@@ -1060,13 +1079,12 @@ static long follow_page_mask(struct vm_area_struct *vma,
vma_pgtable_walk_begin(vma);
- *page_mask = 0;
pgd = pgd_offset(mm, address);
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
ret = no_page_table(vma, flags, address);
else
- ret = follow_p4d_mask(vma, address, pgd, flags, page_mask, pages);
+ ret = follow_p4d_mask(vma, address, end, pgd, flags, pages);
vma_pgtable_walk_end(vma);
@@ -1404,7 +1422,6 @@ static long __get_user_pages(struct mm_struct *mm,
{
long ret = 0, i = 0;
struct vm_area_struct *vma = NULL;
- unsigned long page_mask = 0;
if (!nr_pages)
return 0;
@@ -1419,7 +1436,6 @@ static long __get_user_pages(struct mm_struct *mm,
do {
struct page *page;
- unsigned int page_increm;
long nr;
/* first iteration or cross vma bound */
@@ -1477,8 +1493,8 @@ static long __get_user_pages(struct mm_struct *mm,
}
cond_resched();
- nr = follow_page_mask(vma, start, gup_flags, &page_mask,
- pages ? &pages[i] : NULL);
+ nr = follow_page_mask(vma, start, start + nr_pages * PAGE_SIZE,
+ gup_flags, pages ? &pages[i] : NULL);
if (!nr || nr == -EMLINK) {
ret = faultin_page(vma, start, gup_flags,
nr == -EMLINK, locked);
@@ -1500,62 +1516,25 @@ static long __get_user_pages(struct mm_struct *mm,
* Proper page table entry exists, but no corresponding
* struct page. If the caller expects **pages to be
* filled in, bail out now, because that can't be done
- * for this page.
+ * for this page. Otherwise advance by the one page
+ * follow_page_mask() looked at.
*/
if (pages) {
ret = nr;
goto out;
}
+ nr = 1;
} else if (nr < 0) {
ret = nr;
goto out;
}
- page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
- if (page_increm > nr_pages)
- page_increm = nr_pages;
-
- /*
- * This must be a large folio (and doesn't need to
- * be the whole folio; it can be part of it), do
- * the refcount work for all the subpages too.
- *
- * NOTE: here the page may not be the head page
- * e.g. when start addr is not thp-size aligned.
- * try_grab_folio() should have taken care of tail
- * pages.
- */
- if (pages && page_increm > 1) {
- struct page *subpage;
- unsigned int j;
- struct folio *folio = page_folio(pages[i]);
-
- /*
- * Since we already hold refcount on the
- * large folio, this should never fail.
- */
- if (try_grab_folio(folio, page_increm - 1,
- gup_flags)) {
- /*
- * Release the 1st page ref if the
- * folio is problematic, fail hard.
- */
- gup_put_folio(folio, 1, gup_flags);
- ret = -EFAULT;
- goto out;
- }
-
- for (j = 1; j < page_increm; j++) {
- subpage = pages[i] + j;
- pages[i + j] = subpage;
- flush_anon_page(vma, subpage, start + j * PAGE_SIZE);
- flush_dcache_page(subpage);
- }
- }
+ /* Check that we didn't pin more pages than the caller will free. */
+ VM_WARN_ON_ONCE(nr > nr_pages);
- i += page_increm;
- start += page_increm * PAGE_SIZE;
- nr_pages -= page_increm;
+ i += nr;
+ start += nr * PAGE_SIZE;
+ nr_pages -= nr;
} while (nr_pages);
out:
return i ? i : ret;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 5/5] mm/gup: walk multiple PTEs per follow_page_pte() call
2026-08-01 3:15 [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask() Rik van Riel
` (3 preceding siblings ...)
2026-08-01 3:15 ` [PATCH 4/5] mm/gup: return a huge page's full count from follow_page_mask() Rik van Riel
@ 2026-08-01 3:15 ` Rik van Riel
4 siblings, 0 replies; 6+ messages in thread
From: Rik van Riel @ 2026-08-01 3:15 UTC (permalink / raw)
To: linux-kernel, Andrew Morton, David Hildenbrand
Cc: kernel-team, Rik van Riel, Jason Gunthorpe, John Hubbard,
Peter Xu, linux-mm, Lorenzo Stoakes
follow_page_pte() still looks at one PTE per call, so
__get_user_pages() calls it once per page even for a PTE-mapped large
folio (mTHP), restarting the pgd/p4d/pud/pmd descent and retaking the
PTE lock each time.
Walk every PTE from @address to the page-table/VMA/@end boundary in
one call instead, without stopping at folio boundaries -- adjacent
pages from different folios, or plain base pages with no folio
relationship at all, are covered by the same call under one lock.
Within that walk, contiguous same-folio pages still get one combined
refcount grab: each run starts with a full per-PTE resolve, then
follow_pte_batch() finds how many more PTEs extend it via a cheap
comparison scan, not a re-derivation of each page.
Measured with mm/gup_test.c (PIN_LONGTERM_BENCHMARK) on a 256 MB
MADV_HUGEPAGE region in a 4 CPU VM, median get time over 16
iterations, before/after back to back in the same VM. Each folio size
was confirmed through the per-size anon_fault_alloc counters (4096
folios for 64 kB, 128 for 2 MB):
gup_test -L -m 256 -n 65536 -r 16 -t
before after
64 kB mTHP 3000 us 207 us (14.5x)
2 MB THP (control) 70 us 69 us
4 kB base (control) 2801 us 1188 us (2.4x)
The 4 kB case shares no folio, so gets no refcount-batching benefit --
yet it still improves 2.4x purely from walking the page table once
instead of restarting per page. The 64 kB run gets that saving plus
refcount batching on top, reaching 14.5x. The 2 MB THP case doesn't
reach follow_page_pte() at all, so it stays flat.
Suggested-by: David Hildenbrand <david@kernel.org>
Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Rik van Riel <riel@surriel.com>
---
mm/gup.c | 209 ++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 145 insertions(+), 64 deletions(-)
diff --git a/mm/gup.c b/mm/gup.c
index ccbef9476ff6..55bbeaa52b13 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -827,18 +827,20 @@ static inline bool can_follow_write_pte(pte_t pte, struct page *page,
/*
* The caller has already run every per-PTE safety check (present,
- * write-fault, gup_must_unshare()) on the PTE, so this only does the
- * per-folio work: the refcount grab, the FOLL_PIN accessibility fault-in,
- * dirty/accessed marking, and the array fill with the cache flush.
+ * write-fault, gup_must_unshare()) on each PTE in the run, so this only
+ * does the per-folio work: the refcount grab, the FOLL_PIN accessibility
+ * fault-in, dirty/accessed marking, and the array fill with per-subpage
+ * cache flushes.
*/
static long follow_page_pte_commit(struct vm_area_struct *vma,
- unsigned long address, struct folio *folio, struct page *page,
- pte_t pte, unsigned int flags, struct page **pages)
+ unsigned long run_address, struct folio *folio,
+ struct page *run_page, pte_t run_pte, unsigned long run_len,
+ unsigned int flags, struct page **pages)
{
long ret;
/* try_grab_folio() does nothing unless FOLL_GET or FOLL_PIN is set. */
- ret = try_grab_folio(folio, 1, flags);
+ ret = try_grab_folio(folio, run_len, flags);
if (unlikely(ret))
return ret;
@@ -850,13 +852,13 @@ static long follow_page_pte_commit(struct vm_area_struct *vma,
if (flags & FOLL_PIN) {
ret = arch_make_folio_accessible(folio);
if (ret) {
- gup_put_folio(folio, 1, flags);
+ gup_put_folio(folio, run_len, flags);
return ret;
}
}
if (flags & FOLL_TOUCH) {
if ((flags & FOLL_WRITE) &&
- !pte_dirty(pte) && !folio_test_dirty(folio))
+ !pte_dirty(run_pte) && !folio_test_dirty(folio))
folio_mark_dirty(folio);
/*
* pte_mkyoung() would be more correct here, but atomic care
@@ -866,79 +868,158 @@ static long follow_page_pte_commit(struct vm_area_struct *vma,
folio_mark_accessed(folio);
}
- gup_fill_pages(vma, address, page, 1, pages);
+ gup_fill_pages(vma, run_address, run_page, run_len, pages);
return 0;
}
+/*
+ * Return how many PTEs from @ptep can batch with @pte's page:
+ * consecutive present, uniform-write PTEs of @folio, bounded by
+ * @walk_end. Returns at least 1. A cheap pte_same() scan, so a
+ * large folio's run costs one scan.
+ *
+ * gup_must_unshare()/write-fault checks are per PTE, but a writable
+ * run is always safe: a writable anon page is exclusive. A read-only
+ * run under FOLL_WRITE/FOLL_PIN needs a per-page check instead, so
+ * it falls back to one page at a time.
+ */
+static unsigned long follow_pte_batch(struct vm_area_struct *vma,
+ unsigned long address, unsigned long walk_end, struct folio *folio,
+ pte_t *ptep, pte_t pte, unsigned int flags)
+{
+ pte_t batch_pte = pte;
+ unsigned long max;
+
+ if (!folio_test_large(folio))
+ return 1;
+ if (!pte_write(pte) && (flags & (FOLL_WRITE | FOLL_PIN)))
+ return 1;
+
+ max = (walk_end - address) >> PAGE_SHIFT;
+ if (max <= 1)
+ return 1;
+
+ return folio_pte_batch_flags(folio, vma, ptep, &batch_pte, max,
+ FPB_RESPECT_WRITE);
+}
+
+/*
+ * Walk every PTE from @address to @end (this page table and VMA).
+ *
+ * If the first PTE can't be included (not present, a write/unshare
+ * fault, PFN-special, ...), that reason is returned directly.
+ * A failure in a subsequent page results in a short read;
+ * __get_user_pages retrying the read will get the error.
+ */
static long follow_page_pte(struct vm_area_struct *vma,
- unsigned long address, pmd_t *pmd, unsigned int flags,
- struct page **pages)
+ unsigned long address, unsigned long end, pmd_t *pmd,
+ unsigned int flags, struct page **pages)
{
struct mm_struct *mm = vma->vm_mm;
- struct folio *folio;
- struct page *page;
spinlock_t *ptl;
- pte_t *ptep, pte;
- long ret;
+ pte_t *ptep, *orig_ptep;
+ unsigned long walk_end;
+ unsigned long nr = 0;
+ bool need_no_page_table = false;
+ long ret = 0;
- ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
+ orig_ptep = ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!ptep)
return no_page_table(vma, flags, address);
- pte = ptep_get(ptep);
- if (!pte_present(pte))
- goto no_page;
- if (pte_protnone(pte) && !gup_can_follow_protnone(vma, flags))
- goto no_page;
- page = vm_normal_page(vma, address, pte);
+ walk_end = min(pmd_addr_end(address, end), vma->vm_end);
- /*
- * We only care about anon pages in can_follow_write_pte().
- */
- if ((flags & FOLL_WRITE) &&
- !can_follow_write_pte(pte, page, vma, flags)) {
- ret = 0;
- goto out;
- }
+ for (; address < walk_end; address += PAGE_SIZE, ptep++) {
+ pte_t pte = ptep_get(ptep);
+ struct page *page;
+ struct folio *folio;
+ unsigned long batch;
- if (unlikely(!page)) {
- if (flags & FOLL_DUMP) {
- /* Avoid special (like zero) pages in core dumps */
- ret = -EFAULT;
- goto out;
+ if (!pte_present(pte)) {
+ if (nr)
+ break;
+ if (pte_none(pte))
+ need_no_page_table = true;
+ goto unlock;
+ }
+ if (pte_protnone(pte) && !gup_can_follow_protnone(vma, flags)) {
+ if (nr)
+ break;
+ goto unlock;
}
- if (is_zero_pfn(pte_pfn(pte))) {
- page = pte_page(pte);
- } else {
- ret = follow_pfn_pte(vma, address, ptep, flags);
- goto out;
+ page = vm_normal_page(vma, address, pte);
+
+ /*
+ * We only care about anon pages in can_follow_write_pte().
+ */
+ if ((flags & FOLL_WRITE) &&
+ !can_follow_write_pte(pte, page, vma, flags)) {
+ if (nr)
+ break;
+ goto unlock;
}
- }
- folio = page_folio(page);
- if (!pte_write(pte) && gup_must_unshare(vma, flags, page)) {
- ret = -EMLINK;
- goto out;
- }
+ if (unlikely(!page)) {
+ if (flags & FOLL_DUMP) {
+ /* Avoid special (like zero) pages in core dumps */
+ if (nr)
+ break;
+ ret = -EFAULT;
+ goto unlock;
+ }
- VM_WARN_ON_ONCE_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
- !PageAnonExclusive(page), page);
+ if (is_zero_pfn(pte_pfn(pte))) {
+ page = pte_page(pte);
+ } else {
+ /*
+ * Proper page table entry exists, but no
+ * corresponding struct page: the caller decides
+ * whether that is fatal (see the -EEXIST handling
+ * in __get_user_pages()).
+ */
+ if (nr)
+ break;
+ ret = follow_pfn_pte(vma, address, ptep, flags);
+ goto unlock;
+ }
+ }
+ folio = page_folio(page);
- ret = follow_page_pte_commit(vma, address, folio, page, pte, flags,
- pages);
- if (ret)
- goto out;
- ret = 1;
-out:
- pte_unmap_unlock(ptep, ptl);
- return ret;
-no_page:
- pte_unmap_unlock(ptep, ptl);
- if (!pte_none(pte))
- return 0;
- return no_page_table(vma, flags, address);
+ if (!pte_write(pte) && gup_must_unshare(vma, flags, page)) {
+ if (nr)
+ break;
+ ret = -EMLINK;
+ goto unlock;
+ }
+
+ VM_WARN_ON_ONCE_PAGE((flags & FOLL_PIN) && PageAnon(page) &&
+ !PageAnonExclusive(page), page);
+
+ batch = follow_pte_batch(vma, address, walk_end, folio, ptep, pte,
+ flags);
+
+ ret = follow_page_pte_commit(vma, address, folio, page, pte,
+ batch, flags,
+ pages ? pages + nr : NULL);
+ if (ret) {
+ if (nr)
+ break;
+ goto unlock;
+ }
+ nr += batch;
+
+ /* The loop's own increment covers one PTE; skip the rest of the batch. */
+ ptep += batch - 1;
+ address += (batch - 1) * PAGE_SIZE;
+ }
+
+unlock:
+ pte_unmap_unlock(orig_ptep, ptl);
+ if (need_no_page_table)
+ ret = no_page_table(vma, flags, address);
+ return nr ? (long)nr : ret;
}
static long follow_pmd_mask(struct vm_area_struct *vma,
@@ -957,7 +1038,7 @@ static long follow_pmd_mask(struct vm_area_struct *vma,
if (!pmd_present(pmdval))
return no_page_table(vma, flags, address);
if (likely(!pmd_leaf(pmdval)))
- return follow_page_pte(vma, address, pmd, flags, pages);
+ return follow_page_pte(vma, address, end, pmd, flags, pages);
if (pmd_protnone(pmdval) && !gup_can_follow_protnone(vma, flags))
return no_page_table(vma, flags, address);
@@ -970,14 +1051,14 @@ static long follow_pmd_mask(struct vm_area_struct *vma,
}
if (unlikely(!pmd_leaf(pmdval))) {
spin_unlock(ptl);
- return follow_page_pte(vma, address, pmd, flags, pages);
+ return follow_page_pte(vma, address, end, pmd, flags, pages);
}
if (pmd_trans_huge(pmdval) && (flags & FOLL_SPLIT_PMD)) {
spin_unlock(ptl);
split_huge_pmd(vma, pmd, address);
/* If pmd was left empty, stuff a page table in there quickly */
return pte_alloc(mm, pmd) ? -ENOMEM :
- follow_page_pte(vma, address, pmd, flags, pages);
+ follow_page_pte(vma, address, end, pmd, flags, pages);
}
ret = follow_huge_pmd(vma, address, end, pmd, flags, pages);
spin_unlock(ptl);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-08-01 3:15 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-01 3:15 [PATCH 0/5] mm/gup: batch contiguous pages in follow_page_mask() Rik van Riel
2026-08-01 3:15 ` [PATCH 1/5] mm/gup: convert follow_page_mask() to return a long Rik van Riel
2026-08-01 3:15 ` [PATCH 2/5] mm/gup: split follow_page_pte_commit() out of follow_page_pte() Rik van Riel
2026-08-01 3:15 ` [PATCH 3/5] mm/gup: add gup_fill_pages() and use it Rik van Riel
2026-08-01 3:15 ` [PATCH 4/5] mm/gup: return a huge page's full count from follow_page_mask() Rik van Riel
2026-08-01 3:15 ` [PATCH 5/5] mm/gup: walk multiple PTEs per follow_page_pte() call Rik van Riel
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.