From: Rik van Riel <riel@surriel.com>
To: Andrew Morton <akpm@linux-foundation.org>
Cc: linux-kernel@vger.kernel.org, linux-mm@kvack.org,
kernel-team@meta.com, Dave Hansen <dave.hansen@linux.intel.com>,
Peter Zijlstra <peterz@infradead.org>,
Suren Baghdasaryan <surenb@google.com>,
Lorenzo Stoakes <ljs@kernel.org>,
Vlastimil Babka <vbabka@kernel.org>,
David Hildenbrand <david@kernel.org>,
"Liam R. Howlett" <liam@infradead.org>,
Mike Rapoport <rppt@kernel.org>, Michal Hocko <mhocko@suse.com>,
Jason Gunthorpe <jgg@ziepe.ca>,
John Hubbard <jhubbard@nvidia.com>, Peter Xu <peterx@redhat.com>,
Matthew Wilcox <willy@infradead.org>,
Usama Arif <usamaarif642@gmail.com>,
Rik van Riel <riel@surriel.com>
Subject: [PATCH RFC v4 06/12] mm: use per-VMA lock in __access_remote_vm() for single-VMA accesses
Date: Fri, 24 Jul 2026 18:29:28 -0400 [thread overview]
Message-ID: <20260724222934.1463812-7-riel@surriel.com> (raw)
In-Reply-To: <20260724222934.1463812-1-riel@surriel.com>
__access_remote_vm() holds mmap_read_lock() for the whole transfer. On
large machines running big multi-threaded applications, that lock is
contended between readers and writers: an mmap() or munmap() stalls readers
like /proc/PID/cmdline, /proc/PID/environ, or /proc/PID/mem, even though the
memory they read is almost always resident.
Use the per-VMA lock to access a remote process's memory when the access
fits in one VMA. Fall back to the mmap_lock if the access crosses VMA
boundaries, or when get_user_page_vma() cannot finish the access under the
per-VMA lock.
Factor the walk into remote_vm_walk(): it selects the lock, faults in each
page with get_user_page_vma(), and hands the page to a per-page action.
__access_remote_vm() passes access_vm_page(), which copies it; a later
patch shares the walk for the remote string reader.
remote_access_lock() and remote_access_unlock() keep the lock selection out
of the walk. remote_access_lock() takes the per-VMA lock when the range
fits one VMA whose flags permit the access, and the mmap lock otherwise,
returning an ERR_PTR() when the mmap lock cannot be taken.
Looking up the VMA first untags the remote address with
untagged_addr_remote_unlocked(), added earlier in this series, so the untag
needs no mmap lock.
Walking the page tables under only the per-VMA lock is safe against both
page table freeing and THP collapse.
munmap() frees a VMA's page tables through free_pgtables(), which uses
neither the page table lock nor RCU. But it runs only after the VMA is
detached under the VMA write lock, which our per-VMA read lock excludes, so
no page table of this VMA is torn down under the walk.
THP collapse instead needs neither lock: file-backed collapse retracts a
page table under i_mmap_lock and the page table lock alone. It frees the
retracted PTE page by RCU, and pte_offset_map() holds the RCU read lock, so
the page stays valid for the walk.
follow_page_pte() takes that same page table lock through
pte_offset_map_lock() and rechecks the pmd, so it either walks an intact
table or sees the collapsed pmd and faults the THP in.
get_user_page_vma() returns -EFAULT for memory with no struct page: the raw
PFNs of a VM_IO/VM_PFNMAP VMA, ioremapped device memory reached through
ptrace and /proc/PID/mem.
access_vm_page() reaches that memory under the mmap lock through
vma->vm_ops->access(), via a new access_remote_vma_ops() helper, as
get_user_pages_remote() and the old ->access() fallback did before.
A COWed page in such a VMA now reads normally through get_user_page_vma();
previously it was routed to ->access(), whose generic_access_phys() rejects
the ioremap of a RAM page.
A single-threaded microbenchmark reading a remote process's memory through
/proc/PID/mem shows the per-VMA path costs less than the old
mmap_read_lock() plus get_user_pages_remote() route. The gain grows with
the read size as the per-page VMA re-lookup drops away. Median of three
pinned repetitions in a VM:
read size baseline per-VMA throughput
8 B 201.7 ns 170.6 ns ~15% faster
4 KB 495 ns 442 ns +12% (8266 -> 9265 MB/s)
64 KB 4115 ns 3355 ns +23% (15927 -> 19536 MB/s)
1 MB 75902 ns 63641 ns +19% (13815 -> 16477 MB/s)
This does not measure the multi-threaded reader-versus-writer contention
that motivates the per-VMA lock; that case remains to be quantified.
Assisted-by: Claude:claude-opus-4.8
Suggested-by: Suren Baghdasaryan <surenb@google.com>
Signed-off-by: Rik van Riel <riel@surriel.com>
---
mm/memory.c | 268 +++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 213 insertions(+), 55 deletions(-)
diff --git a/mm/memory.c b/mm/memory.c
index 3b86eeaf084f..273dfe12bc6d 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -7015,86 +7015,244 @@ EXPORT_SYMBOL_GPL(generic_access_phys);
#endif
/*
- * Access another process' address space as given in mm.
+ * VM_IO / VM_PFNMAP memory, such as an ioremapped device mapping, maps
+ * PFNs that have no struct page, so get_user_page_vma() cannot fetch it
+ * even though the page tables are populated. It can still be reached
+ * through vma->vm_ops->access().
+ *
+ * Returns the number of bytes transferred, or <= 0 if @vma cannot be
+ * accessed this way.
*/
-static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,
- void *buf, int len, unsigned int gup_flags)
+static int access_remote_vma_ops(struct vm_area_struct *vma, unsigned long addr,
+ void *buf, int len, int write)
+{
+#ifdef CONFIG_HAVE_IOREMAP_PROT
+ if (vma->vm_ops && vma->vm_ops->access)
+ return vma->vm_ops->access(vma, addr, buf, len, write);
+#endif
+ return 0;
+}
+
+/*
+ * Lock @mm to reach the remote range [@addr, @addr + @len).
+ *
+ * Take the per-VMA lock when the whole range fits in a single VMA whose
+ * flags permit the access. The RCU freed page tables then keep page table
+ * memory from being reused with unexpected contents while the lock is held.
+ * Otherwise fall back to the mmap lock, which also covers multi-VMA ranges,
+ * stack expansion, and ->access() memory.
+ *
+ * Return whether the mmap lock is held. The per-VMA locked VMA, when one is
+ * taken, is stored in *@vmap; it is NULL on the mmap lock path. *@vmap is
+ * set to an ERR_PTR() when the mmap lock could not be taken, so callers must
+ * check IS_ERR(*@vmap) before using either result.
+ */
+static bool remote_access_lock(struct mm_struct *mm, unsigned long addr,
+ int len, unsigned int gup_flags,
+ struct vm_area_struct **vmap)
+{
+ struct vm_area_struct *vma = NULL;
+
+#if defined(CONFIG_PER_VMA_LOCK) && defined(CONFIG_MMU_GATHER_RCU_TABLE_FREE)
+ vma = lock_vma_under_rcu(mm, addr);
+ if (vma) {
+ /* addr + len must not wrap, and must fit within the one VMA. */
+ if (addr + len < addr || addr + len > vma->vm_end ||
+ check_vma_flags(vma, gup_flags, 0)) {
+ vma_end_read(vma);
+ vma = NULL;
+ }
+ }
+#endif
+
+ if (!vma) {
+ if (mmap_read_lock_killable(mm)) {
+ *vmap = ERR_PTR(-EINTR);
+ return false;
+ }
+ *vmap = NULL;
+ return true;
+ }
+
+ *vmap = vma;
+ return false;
+}
+
+/* Release the lock taken by remote_access_lock(). */
+static void remote_access_unlock(struct mm_struct *mm,
+ struct vm_area_struct *vma, bool have_mmap_lock)
+{
+ if (have_mmap_lock)
+ mmap_read_unlock(mm);
+ else if (vma)
+ vma_end_read(vma);
+}
+
+/*
+ * Per-page action for a remote VM walk. Handle up to @len bytes at @addr on
+ * @page, advancing *@buf past the bytes read from or written to it. @page is
+ * NULL for struct-page-less memory (VM_IO / VM_PFNMAP) reached under the mmap
+ * lock.
+ *
+ * Return the number of source bytes handled at @addr, 0 to end the walk (a
+ * string reached its NUL, or ->access() memory could not be reached), or a
+ * negative errno to abort.
+ */
+typedef int (*remote_vm_action)(struct vm_area_struct *vma, struct page *page,
+ unsigned long addr, void **buf, int len,
+ int write);
+
+/*
+ * Walk the remote range [@addr, @addr + @len) of @mm, handing each page to
+ * @action. Use the per-VMA lock when the range fits one VMA, and fall back to
+ * the mmap lock for multi-VMA ranges, stack expansion (when @can_expand_stack
+ * is set), or when the per-VMA lock cannot finish a fault.
+ *
+ * Each page is faulted in with get_user_page_vma() under whichever lock is
+ * held. Return the number of bytes @action consumed; *@err is a negative
+ * errno when the walk aborted, else 0.
+ */
+static int remote_vm_walk(struct mm_struct *mm, unsigned long addr, void *buf,
+ int len, unsigned int gup_flags, bool can_expand_stack,
+ remote_vm_action action, int *err)
{
void *old_buf = buf;
int write = gup_flags & FOLL_WRITE;
+ bool have_mmap_lock;
+ struct vm_area_struct *vma;
- if (mmap_read_lock_killable(mm))
- return 0;
+ *err = 0;
- /* Untag the address before looking up the VMA */
- addr = untagged_addr_remote(mm, addr);
+ /*
+ * Set FOLL_REMOTE so check_vma_flags() applies the same protection key
+ * rules as get_user_pages_remote() did: the current PKRU is not checked
+ * against a VMA reached on @mm's behalf.
+ */
+ gup_flags |= FOLL_REMOTE;
- /* Avoid triggering the temporary warning in __get_user_pages */
- if (!vma_lookup(mm, addr) && !expand_stack(mm, addr))
+ addr = untagged_addr_remote_unlocked(mm, addr);
+
+ have_mmap_lock = remote_access_lock(mm, addr, len, gup_flags, &vma);
+ if (IS_ERR(vma)) {
+ *err = -EFAULT;
return 0;
+ }
- /* ignore errors, just check how much was successfully transferred */
while (len) {
- int bytes, offset;
- void *maddr;
- struct folio *folio;
- struct vm_area_struct *vma = NULL;
- struct page *page = get_user_page_lookup_vma(mm, addr,
- gup_flags, &vma);
+ unsigned int foll_flags = gup_flags;
+ struct page *page;
+ int ret;
- if (IS_ERR(page)) {
- /* We might need to expand the stack to access it */
+ if (!vma || addr >= vma->vm_end) {
+ /* Any lookup here holds the mmap lock. */
+ VM_BUG_ON(!have_mmap_lock);
vma = vma_lookup(mm, addr);
- if (!vma) {
+ if (!vma && can_expand_stack) {
+ /* expand_stack() drops the mmap lock if it fails */
vma = expand_stack(mm, addr);
-
- /* mmap_lock was dropped on failure */
if (!vma)
- return buf - old_buf;
-
- /* Try again if stack expansion worked */
- continue;
+ have_mmap_lock = false;
}
+ if (!vma) {
+ *err = -EFAULT;
+ break;
+ }
+ }
+ /*
+ * FOLL_UNLOCKABLE lets the per-VMA fault retry, dropping the
+ * lock, so the walk can fall back to the mmap lock.
+ */
+ if (!have_mmap_lock)
+ foll_flags |= FOLL_VMA_LOCK | FOLL_UNLOCKABLE;
+
+ page = get_user_page_vma(vma, addr, foll_flags);
+ if (IS_ERR(page)) {
/*
- * Check if this is a VM_IO | VM_PFNMAP VMA, which
- * we can access using slightly different code.
+ * get_user_page_vma() returns -EAGAIN, with the per-VMA
+ * lock released, for anything it could not finish under
+ * it; retake the mmap lock and retry. A different error
+ * therefore only arrives under the mmap lock, where
+ * struct-page-less memory can be reached via ->access().
*/
- bytes = 0;
-#ifdef CONFIG_HAVE_IOREMAP_PROT
- if (vma->vm_ops && vma->vm_ops->access)
- bytes = vma->vm_ops->access(vma, addr, buf,
- len, write);
-#endif
- if (bytes <= 0)
- break;
- } else {
- folio = page_folio(page);
- bytes = len;
- offset = addr & (PAGE_SIZE-1);
- if (bytes > PAGE_SIZE-offset)
- bytes = PAGE_SIZE-offset;
-
- maddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);
- if (write) {
- copy_to_user_page(vma, page, addr,
- maddr + offset, buf, bytes);
- folio_mark_dirty_lock(folio);
- } else {
- copy_from_user_page(vma, page, addr,
- buf, maddr + offset, bytes);
+ if (PTR_ERR(page) == -EAGAIN) {
+ vma = NULL;
+ if (mmap_read_lock_killable(mm)) {
+ *err = -EFAULT;
+ break;
+ }
+ have_mmap_lock = true;
+ continue;
}
- folio_release_kmap(folio, maddr);
+ if (WARN_ON_ONCE(!have_mmap_lock))
+ break;
+ page = NULL;
}
- len -= bytes;
- buf += bytes;
- addr += bytes;
+
+ ret = action(vma, page, addr, &buf, len, write);
+ if (ret <= 0) {
+ if (ret < 0)
+ *err = ret;
+ break;
+ }
+ addr += ret;
+ len -= ret;
}
- mmap_read_unlock(mm);
+
+ remote_access_unlock(mm, vma, have_mmap_lock);
return buf - old_buf;
}
+/*
+ * Copy one page's worth of [@addr, @addr + @len) to or from *@buf. Reaches
+ * struct-page-less VM_IO / VM_PFNMAP memory through vma->vm_ops->access().
+ */
+static int access_vm_page(struct vm_area_struct *vma, struct page *page,
+ unsigned long addr, void **buf, int len, int write)
+{
+ struct folio *folio;
+ int bytes, offset;
+ void *maddr;
+
+ if (!page) {
+ bytes = access_remote_vma_ops(vma, addr, *buf, len, write);
+ if (bytes > 0)
+ *buf += bytes;
+ return bytes;
+ }
+
+ bytes = len;
+ offset = addr & (PAGE_SIZE - 1);
+ if (bytes > PAGE_SIZE - offset)
+ bytes = PAGE_SIZE - offset;
+
+ folio = page_folio(page);
+ maddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);
+ if (write) {
+ copy_to_user_page(vma, page, addr, maddr + offset, *buf, bytes);
+ folio_mark_dirty_lock(folio);
+ } else {
+ copy_from_user_page(vma, page, addr, *buf, maddr + offset, bytes);
+ }
+ folio_release_kmap(folio, maddr);
+
+ *buf += bytes;
+ return bytes;
+}
+
+/*
+ * Access another process' address space as given in mm.
+ */
+static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,
+ void *buf, int len, unsigned int gup_flags)
+{
+ int err;
+
+ return remote_vm_walk(mm, addr, buf, len, gup_flags, true,
+ access_vm_page, &err);
+}
+
/**
* access_remote_vm - access another process' address space
* @mm: the mm_struct of the target address space
--
2.53.0-Meta
next prev parent reply other threads:[~2026-07-24 22:30 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-24 22:29 [PATCH RFC v4 0/12] mm: use per-VMA lock in __access_remote_vm for improved monitoring reliability Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 01/12] x86/mm: add untagged_addr_remote_unlocked() Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 02/12] riscv/mm: " Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 03/12] mm: rename get_user_page_vma_remote() to get_user_page_lookup_vma() Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 04/12] mm/gup: let check_vma_flags() ignore selected VMA flags Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 05/12] mm/gup: add get_user_page_vma() to fault in a page under a held lock Rik van Riel
2026-07-24 22:29 ` Rik van Riel [this message]
2026-07-24 22:29 ` [PATCH RFC v4 07/12] mm: read remote strings under the per-VMA lock Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 08/12] selftests/mm: cover /proc/pid/mem access to VM_PFNMAP memory Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 09/12] mm/gup: build get_user_page_lookup_vma() on get_user_page_vma() Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 10/12] mm/gup: pass an end address to follow_page_mask() and return a page count Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 11/12] mm/gup: batch contiguous PTE-mapped large folios in follow_page_mask() Rik van Riel
2026-07-24 22:29 ` [PATCH RFC v4 12/12] selftests/mm: add a slow-GUP content and COW test for mTHP Rik van Riel
2026-07-26 11:56 ` Mike Rapoport
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260724222934.1463812-7-riel@surriel.com \
--to=riel@surriel.com \
--cc=akpm@linux-foundation.org \
--cc=dave.hansen@linux.intel.com \
--cc=david@kernel.org \
--cc=jgg@ziepe.ca \
--cc=jhubbard@nvidia.com \
--cc=kernel-team@meta.com \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=mhocko@suse.com \
--cc=peterx@redhat.com \
--cc=peterz@infradead.org \
--cc=rppt@kernel.org \
--cc=surenb@google.com \
--cc=usamaarif642@gmail.com \
--cc=vbabka@kernel.org \
--cc=willy@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox