Re: [PATCH] mm/vma: don't remove VMA from rmap if pgoff unchanged

From: Lorenzo Stoakes (ARM)

Date: Mon Sep 28 2026 - 11:15:45 EST


On Fri, Sep 25, 2026 at 07:32:20PM +0100, Lorenzo Stoakes (ARM) wrote:
> When updating a VMA, vma_prepare() unconditionally removes it from its rmap
> interval trees under the rmap lock, and vma_complete() reinserts it before
> releasing the lock.
>
> This is wholly unnecessary if its page offset (file rmap) or anonymous page
> offset (anon rmap) is unchanged.
>
> So, track whether they will change in the newly introduced
> vp->anon_pgoff_unchanged and vp->pgoff_unchanged fields, and use them to
> determine whether to remove the VMA or not.
>
> The rmap lock keeps things safe as no rmap walks can concurrently occur
> during the operation.
>
> Additionally, some architectures (arm, parisc, nios2, csky) have dcache
> flush rmap walkers which take only flush_dcache_mmap_lock(), which is
> likewise held across the operation.
>
> It's also necessary to keep the rb_subtree_last field updated in the
> interval tree so implement anon_rmap_tree_update_inplace() and
> mapping_rmap_tree_update_inplace() to do that.
>
> This is done in vma_complete(), after the VMA's range has been updated, so
> in the interim the field may be invalid. However, given the locks described
> above, this cannot be observed until after the state is corrected.
>
> The anonymous rmap is keyed on anon_vma_chains not VMAs, so in those
> instances anon_rmap_tree_update_vma_inplace() iterates over
> vma->anon_vma_chain, invoking anon_rmap_tree_update_inplace() on each one.
>
> For the anon rmap case, with CONFIG_DEBUG_VM_RB set, avc->cached_vma_last
> is also updated in anon_rmap_tree_update_inplace().
>
> When performing a VMA shrink or a split where the VMA is the lower one, the
> page offset cannot change, so set the flags unconditionally in these cases.
>
> When merging VMAs the page offset is unchanged only in some cases, so
> update init_multi_vma_prep() to set the flags only if the page offsets
> remain the same.
>
> These changes ultimately result in less rmap lock contention.
>
> Reported-by: Pan Deng <pan.deng@xxxxxxxxx>
> Closes: https://lore.kernel.org/linux-mm/20260924054301.2330822-1-pan.deng@xxxxxxxxx/

Andrew - Can you switch these 2 tags to:

Suggested-by: Pan Deng <pan.deng@xxxxxxxxx>

Please, thanks! :)

> Signed-off-by: Lorenzo Stoakes <ljs@xxxxxxxxxx>
> ---
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@xxxxxxxxxx>
> ---
> include/linux/mm.h | 3 +++
> mm/interval_tree.c | 33 ++++++++++++++++++++++++++++++++
> mm/vma.c | 40 ++++++++++++++++++++++++++++++++++-----
> mm/vma.h | 2 ++
> tools/testing/vma/include/stubs.h | 8 ++++++++
> 5 files changed, 81 insertions(+), 5 deletions(-)
>
> diff --git a/include/linux/mm.h b/include/linux/mm.h
> index 6e71eaa4af3f..94c2eb055716 100644
> --- a/include/linux/mm.h
> +++ b/include/linux/mm.h
> @@ -4357,6 +4357,8 @@ void mapping_rmap_tree_insert_after(struct vm_area_struct *vma,
> struct address_space *mapping);
> void mapping_rmap_tree_remove(struct vm_area_struct *vma,
> struct address_space *mapping);
> +void mapping_rmap_tree_update_inplace(struct vm_area_struct *vma);
> +
> struct vm_area_struct *
> mapping_rmap_tree_iter_first(struct address_space *mapping,
> pgoff_t pgoff_start, pgoff_t pgoff_last);
> @@ -4374,6 +4376,7 @@ void anon_rmap_tree_insert(struct anon_vma_chain *avc,
> struct anon_vma *anon_vma);
> void anon_rmap_tree_remove(struct anon_vma_chain *avc,
> struct anon_vma *anon_vma);
> +void anon_rmap_tree_update_inplace(struct anon_vma_chain *avc);
> struct anon_vma_chain *
> anon_rmap_tree_iter_first(struct anon_vma *anon_vma,
> pgoff_t pgoff_start, pgoff_t pgoff_last);
> diff --git a/mm/interval_tree.c b/mm/interval_tree.c
> index 7bbbf15cfbf0..eafde5d12ef5 100644
> --- a/mm/interval_tree.c
> +++ b/mm/interval_tree.c
> @@ -64,6 +64,21 @@ void mapping_rmap_tree_remove(struct vm_area_struct *vma,
> __mapping_rmap_tree_remove(vma, &mapping->i_mmap);
> }
>
> +/**
> + * mapping_rmap_tree_update_inplace() - Update file rmap tree to reflect an
> + * in-place change in a VMA's size.
> + * @vma: The VMA whose size has changed.
> + *
> + * The file rmap lock must be held.
> + *
> + * Invalid to do so if @vma->vm_pgoff has changed.
> + */
> +void mapping_rmap_tree_update_inplace(struct vm_area_struct *vma)
> +{
> + /* Propagate all the way up the tree. */
> + __mapping_rmap_tree_augment.propagate(&vma->shared.rb, NULL);
> +}
> +
> struct vm_area_struct *
> mapping_rmap_tree_iter_first(struct address_space *mapping,
> pgoff_t pgoff_start, pgoff_t pgoff_last)
> @@ -111,6 +126,24 @@ void anon_rmap_tree_remove(struct anon_vma_chain *avc,
> __anon_rmap_tree_remove(avc, &anon_vma->rb_root);
> }
>
> +/**
> + * anon_rmap_tree_update_inplace() - Update anon rmap tree to reflect an
> + * in-place change in the size of @avc's VMA.
> + * @avc: The anon_vma_chain whose VMA's size has changed.
> + *
> + * The anon rmap root lock must be held.
> + *
> + * Invalid to do so if the VMA's anonymous pgoff has changed.
> + */
> +void anon_rmap_tree_update_inplace(struct anon_vma_chain *avc)
> +{
> +#ifdef CONFIG_DEBUG_VM_RB
> + avc->cached_vma_last = avc_last_pgoff(avc);
> +#endif
> + /* Propagate all the way up the tree. */
> + __anon_rmap_tree_augment.propagate(&avc->rb, NULL);
> +}
> +
> struct anon_vma_chain *
> anon_rmap_tree_iter_first(struct anon_vma *anon_vma,
> pgoff_t pgoff_start, pgoff_t pgoff_last)
> diff --git a/mm/vma.c b/mm/vma.c
> index 077e23694143..8b333ec0c958 100644
> --- a/mm/vma.c
> +++ b/mm/vma.c
> @@ -201,8 +201,15 @@ static void init_multi_vma_prep(struct vma_prepare *vp,
> if (vp->file)
> vp->mapping = vma->vm_file->f_mapping;
>
> - if (vmg && vmg->skip_vma_uprobe)
> + if (!vmg)
> + return;
> +
> + if (vmg->skip_vma_uprobe)
> vp->skip_vma_uprobe = true;
> + if (vma_start_pgoff(vma) == vmg_start_pgoff(vmg))
> + vp->pgoff_unchanged = true;
> + if (vma_start_anon_pgoff(vma) == vmg_start_anon_pgoff(vmg))
> + vp->anon_pgoff_unchanged = true;
> }
>
> /*
> @@ -331,6 +338,15 @@ anon_rmap_tree_post_update_vma(struct vm_area_struct *vma)
> anon_rmap_tree_insert(avc, avc->anon_vma);
> }
>
> +static void
> +anon_rmap_tree_update_vma_inplace(struct vm_area_struct *vma)
> +{
> + struct anon_vma_chain *avc;
> +
> + list_for_each_entry(avc, &vma->anon_vma_chain, same_vma)
> + anon_rmap_tree_update_inplace(avc);
> +}
> +
> /*
> * vma_prepare() - Helper function for handling locking VMAs prior to altering
> * @vp: The initialized vma_prepare struct
> @@ -359,14 +375,16 @@ static void vma_prepare(struct vma_prepare *vp)
>
> if (vp->anon_vma) {
> anon_vma_lock_write(vp->anon_vma);
> - anon_rmap_tree_pre_update_vma(vp->vma);
> + if (!vp->anon_pgoff_unchanged)
> + anon_rmap_tree_pre_update_vma(vp->vma);
> if (vp->adj_next)
> anon_rmap_tree_pre_update_vma(vp->adj_next);
> }
>
> if (vp->file) {
> flush_dcache_mmap_lock(vp->mapping);
> - mapping_rmap_tree_remove(vp->vma, vp->mapping);
> + if (!vp->pgoff_unchanged)
> + mapping_rmap_tree_remove(vp->vma, vp->mapping);
> if (vp->adj_next)
> mapping_rmap_tree_remove(vp->adj_next, vp->mapping);
> }
> @@ -387,7 +405,11 @@ static void vma_complete(struct vma_prepare *vp, struct vma_iterator *vmi,
> if (vp->file) {
> if (vp->adj_next)
> mapping_rmap_tree_insert(vp->adj_next, vp->mapping);
> - mapping_rmap_tree_insert(vp->vma, vp->mapping);
> + /* Need only propagate the change inplace. */
> + if (vp->pgoff_unchanged)
> + mapping_rmap_tree_update_inplace(vp->vma);
> + else
> + mapping_rmap_tree_insert(vp->vma, vp->mapping);
> flush_dcache_mmap_unlock(vp->mapping);
> }
>
> @@ -406,7 +428,11 @@ static void vma_complete(struct vma_prepare *vp, struct vma_iterator *vmi,
> }
>
> if (vp->anon_vma) {
> - anon_rmap_tree_post_update_vma(vp->vma);
> + /* Need only propagate the change inplace. */
> + if (vp->anon_pgoff_unchanged)
> + anon_rmap_tree_update_vma_inplace(vp->vma);
> + else
> + anon_rmap_tree_post_update_vma(vp->vma);
> if (vp->adj_next)
> anon_rmap_tree_post_update_vma(vp->adj_next);
> anon_vma_unlock_write(vp->anon_vma);
> @@ -593,6 +619,8 @@ __split_vma(struct vma_iterator *vmi, struct vm_area_struct *vma,
>
> init_vma_prep(&vp, vma);
> vp.insert = new;
> + vp.pgoff_unchanged = !new_below;
> + vp.anon_pgoff_unchanged = !new_below;
> vma_prepare(&vp);
>
> /*
> @@ -1346,6 +1374,8 @@ int vma_shrink(struct vma_iterator *vmi, struct vm_area_struct *vma,
> vma_start_write(vma);
>
> init_vma_prep(&vp, vma);
> + vp.pgoff_unchanged = true;
> + vp.anon_pgoff_unchanged = true;
> vma_prepare(&vp);
> vma_adjust_trans_huge(vma, vma->vm_start, end, NULL);
>
> diff --git a/mm/vma.h b/mm/vma.h
> index 7a683272c0a8..336b4ced82c9 100644
> --- a/mm/vma.h
> +++ b/mm/vma.h
> @@ -28,6 +28,8 @@ struct vma_prepare {
> struct vm_area_struct *remove2;
>
> bool skip_vma_uprobe :1;
> + bool pgoff_unchanged :1;
> + bool anon_pgoff_unchanged :1;
> };
>
> struct unlink_vma_file_batch {
> diff --git a/tools/testing/vma/include/stubs.h b/tools/testing/vma/include/stubs.h
> index e4acc6f1fe7b..f0a69393c02a 100644
> --- a/tools/testing/vma/include/stubs.h
> +++ b/tools/testing/vma/include/stubs.h
> @@ -267,6 +267,10 @@ static inline void mapping_rmap_tree_remove(struct vm_area_struct *vma,
> {
> }
>
> +static inline void mapping_rmap_tree_update_inplace(struct vm_area_struct *vma)
> +{
> +}
> +
> static inline void flush_dcache_mmap_unlock(struct address_space *mapping)
> {
> }
> @@ -281,6 +285,10 @@ static inline void anon_rmap_tree_remove(struct anon_vma_chain *avc,
> {
> }
>
> +static inline void anon_rmap_tree_update_inplace(struct anon_vma_chain *avc)
> +{
> +}
> +
> static inline void uprobe_mmap(struct vm_area_struct *vma)
> {
> }
>
> ---
> base-commit: e8d0f6a1b2a447d02845984fa6288787543cb03c
> change-id: 20260925-speed-up-inplace-rmap-808fbb3848f5
>
> Best regards,
> --
> Lorenzo Stoakes (ARM) <ljs@xxxxxxxxxx>
>

--
Cheers, Lorenzo