* [PATCH v2 1/3] Revert "mm/khugepaged: remove definition of struct khugepaged_mm_slot"
2026-11-05 20:57 [PATCH v2 0/3] mm/khugepaged: bound mmap_lock hold time when walking an mm's VMAs Lorenzo Stoakes (ARM)
@ 2026-11-05 20:57 ` Lorenzo Stoakes (ARM)
2026-11-05 21:47 ` Jose A. Perez de Azpillaga
2026-11-05 20:57 ` [PATCH v2 2/3] mm/khugepaged: scan mm's more efficiently under mmap_lock contention Lorenzo Stoakes (ARM)
2026-11-05 20:57 ` [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs Lorenzo Stoakes (ARM)
2 siblings, 1 reply; 8+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-11-05 20:57 UTC (permalink / raw)
To: David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton
Cc: linux-mm, linux-kernel, Lorenzo Stoakes (ARM)
This partially reverts commit b4c9ffb54b32 ("mm/khugepaged: remove
definition of struct khugepaged_mm_slot"), as a subsequent commit needs to
resurrect this type to store additional data alongside the mm_slot.
The stated intent of the commit was to address a NULL check in
__khugepaged_exit():
mm_slot = mm_slot_entry(slot, struct khugepaged_mm_slot, slot);
slot can be NULL and mm_slot_entry() is an alias for container_of(), so
this is container_of() a NULL value.
This was benign as there was only 1 field in khugepaged_mm_slot (offsetting
NULL at 0 results in NULL), however it was right to fix this, so retain the
fix.
It also unwrapped KMEM_CACHE() in khugepaged_init(). This was a mistake,
and was itself reverted in commit c14bdcc9f274 ("mm/khugepaged: use
KMEM_CACHE()"). Therefore, retain the KMEM_CACHE() form in this revert.
Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
mm/khugepaged.c | 57 ++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 38 insertions(+), 19 deletions(-)
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index da91762f6358..aad8797f8d5a 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -66,6 +66,14 @@ static DEFINE_READ_MOSTLY_HASHTABLE(mm_slots_hash, MM_SLOTS_HASH_BITS);
static struct kmem_cache *mm_slot_cache __ro_after_init;
+/**
+ * struct khugepaged_mm_slot - khugepaged information per mm that is being scanned
+ * @slot: hash lookup from mm to mm_slot
+ */
+struct khugepaged_mm_slot {
+ struct mm_slot slot;
+};
+
/**
* struct khugepaged_scan - cursor for scanning
* @mm_head: the head of the mm list to scan
@@ -76,7 +84,7 @@ static struct kmem_cache *mm_slot_cache __ro_after_init;
*/
struct khugepaged_scan {
struct list_head mm_head;
- struct mm_slot *mm_slot;
+ struct khugepaged_mm_slot *mm_slot;
unsigned long address;
};
@@ -386,7 +394,7 @@ int hugepage_madvise(struct vm_area_struct *vma,
int __init khugepaged_init(void)
{
- mm_slot_cache = KMEM_CACHE(mm_slot, 0);
+ mm_slot_cache = KMEM_CACHE(khugepaged_mm_slot, 0);
if (!mm_slot_cache)
return -ENOMEM;
@@ -446,20 +454,22 @@ static bool hugepage_enabled(void)
void __khugepaged_enter(struct mm_struct *mm)
{
+ struct khugepaged_mm_slot *mm_slot;
struct mm_slot *slot;
int wakeup;
/* __khugepaged_exit() must not run from under us */
VM_BUG_ON_MM(collapse_test_exit(mm), mm);
- slot = mm_slot_alloc(mm_slot_cache);
- if (!slot)
+ mm_slot = mm_slot_alloc(mm_slot_cache);
+ if (!mm_slot)
return;
if (unlikely(mm_flags_test_and_set(MMF_VM_HUGEPAGE, mm))) {
- mm_slot_free(mm_slot_cache, slot);
+ mm_slot_free(mm_slot_cache, mm_slot);
return;
}
+ slot = &mm_slot->slot;
spin_lock(&khugepaged_mm_lock);
mm_slot_insert(mm_slots_hash, mm, slot);
@@ -504,12 +514,15 @@ void khugepaged_enter_vma(struct vm_area_struct *vma,
void __khugepaged_exit(struct mm_struct *mm)
{
+ struct khugepaged_mm_slot *mm_slot;
struct mm_slot *slot;
int free = 0;
spin_lock(&khugepaged_mm_lock);
slot = mm_slot_lookup(mm_slots_hash, mm);
- if (slot && khugepaged_scan.mm_slot != slot) {
+
+ mm_slot = slot ? mm_slot_entry(slot, struct khugepaged_mm_slot, slot) : NULL;
+ if (mm_slot && khugepaged_scan.mm_slot != mm_slot) {
mm_slot_remove(slot);
free = 1;
}
@@ -517,9 +530,9 @@ void __khugepaged_exit(struct mm_struct *mm)
if (free) {
mm_flags_clear(MMF_VM_HUGEPAGE, mm);
- mm_slot_free(mm_slot_cache, slot);
+ mm_slot_free(mm_slot_cache, mm_slot);
mmdrop(mm);
- } else if (slot) {
+ } else if (mm_slot) {
/*
* This is required to serialize against
* collapse_test_exit() (which is guaranteed to run
@@ -1773,8 +1786,9 @@ static enum scan_result collapse_scan_anon(struct vm_area_struct *vma,
return result;
}
-static void collect_mm_slot(struct mm_slot *slot)
+static void collect_mm_slot(struct khugepaged_mm_slot *mm_slot)
{
+ struct mm_slot *slot = &mm_slot->slot;
struct mm_struct *mm = slot->mm;
lockdep_assert_held(&khugepaged_mm_lock);
@@ -1790,7 +1804,7 @@ static void collect_mm_slot(struct mm_slot *slot)
*/
/* khugepaged_mm_lock actually not necessary for the below */
- mm_slot_free(mm_slot_cache, slot);
+ mm_slot_free(mm_slot_cache, mm_slot);
mmdrop(mm);
}
}
@@ -2891,6 +2905,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
__acquires(&khugepaged_mm_lock)
{
struct vma_iterator vmi;
+ struct khugepaged_mm_slot *mm_slot;
struct mm_slot *slot;
struct mm_struct *mm;
struct vm_area_struct *vma;
@@ -2900,12 +2915,14 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
*result = SCAN_FAIL;
if (khugepaged_scan.mm_slot) {
- slot = khugepaged_scan.mm_slot;
+ mm_slot = khugepaged_scan.mm_slot;
+ slot = &mm_slot->slot;
} else {
slot = list_first_entry(&khugepaged_scan.mm_head,
struct mm_slot, mm_node);
+ mm_slot = mm_slot_entry(slot, struct khugepaged_mm_slot, slot);
khugepaged_scan.address = 0;
- khugepaged_scan.mm_slot = slot;
+ khugepaged_scan.mm_slot = mm_slot;
}
spin_unlock(&khugepaged_mm_lock);
@@ -2981,7 +2998,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
breakouterloop_mmap_lock:
spin_lock(&khugepaged_mm_lock);
- VM_BUG_ON(khugepaged_scan.mm_slot != slot);
+ VM_BUG_ON(khugepaged_scan.mm_slot != mm_slot);
/*
* Release the current mm_slot if this mm is about to die, or
* if we scanned all vmas of this mm, or THP got disabled.
@@ -2993,14 +3010,16 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
* mm_slot not pointing to the exiting mm.
*/
if (!list_is_last(&slot->mm_node, &khugepaged_scan.mm_head)) {
- khugepaged_scan.mm_slot = list_next_entry(slot, mm_node);
+ slot = list_next_entry(slot, mm_node);
+ khugepaged_scan.mm_slot =
+ mm_slot_entry(slot, struct khugepaged_mm_slot, slot);
khugepaged_scan.address = 0;
} else {
khugepaged_scan.mm_slot = NULL;
khugepaged_full_scans++;
}
- collect_mm_slot(slot);
+ collect_mm_slot(mm_slot);
}
trace_mm_khugepaged_scan(mm, cc->progress - progress_prev,
@@ -3090,7 +3109,7 @@ static void khugepaged_wait_work(void)
static int khugepaged(void *none)
{
- struct mm_slot *slot;
+ struct khugepaged_mm_slot *mm_slot;
set_freezable();
set_user_nice(current, MAX_NICE);
@@ -3101,10 +3120,10 @@ static int khugepaged(void *none)
}
spin_lock(&khugepaged_mm_lock);
- slot = khugepaged_scan.mm_slot;
+ mm_slot = khugepaged_scan.mm_slot;
khugepaged_scan.mm_slot = NULL;
- if (slot)
- collect_mm_slot(slot);
+ if (mm_slot)
+ collect_mm_slot(mm_slot);
spin_unlock(&khugepaged_mm_lock);
return 0;
}
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH v2 1/3] Revert "mm/khugepaged: remove definition of struct khugepaged_mm_slot"
2026-11-05 20:57 ` [PATCH v2 1/3] Revert "mm/khugepaged: remove definition of struct khugepaged_mm_slot" Lorenzo Stoakes (ARM)
@ 2026-11-05 21:47 ` Jose A. Perez de Azpillaga
0 siblings, 0 replies; 8+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-05 21:47 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM),
David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton
Cc: linux-mm, linux-kernel
On 11/5/26 9:57 PM, Lorenzo Stoakes (ARM) wrote:
> This partially reverts commit b4c9ffb54b32 ("mm/khugepaged: remove
> definition of struct khugepaged_mm_slot"), as a subsequent commit needs to
> resurrect this type to store additional data alongside the mm_slot.
>
> The stated intent of the commit was to address a NULL check in
> __khugepaged_exit():
>
> mm_slot = mm_slot_entry(slot, struct khugepaged_mm_slot, slot);
>
> slot can be NULL and mm_slot_entry() is an alias for container_of(), so
> this is container_of() a NULL value.
>
> This was benign as there was only 1 field in khugepaged_mm_slot (offsetting
> NULL at 0 results in NULL), however it was right to fix this, so retain the
> fix.
>
> It also unwrapped KMEM_CACHE() in khugepaged_init(). This was a mistake,
> and was itself reverted in commit c14bdcc9f274 ("mm/khugepaged: use
> KMEM_CACHE()"). Therefore, retain the KMEM_CACHE() form in this revert.
>
> Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
> Reviewed-by: Lance Yang <lance.yang@linux.dev>
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
LGTM:
Reviewed-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2 2/3] mm/khugepaged: scan mm's more efficiently under mmap_lock contention
2026-11-05 20:57 [PATCH v2 0/3] mm/khugepaged: bound mmap_lock hold time when walking an mm's VMAs Lorenzo Stoakes (ARM)
2026-11-05 20:57 ` [PATCH v2 1/3] Revert "mm/khugepaged: remove definition of struct khugepaged_mm_slot" Lorenzo Stoakes (ARM)
@ 2026-11-05 20:57 ` Lorenzo Stoakes (ARM)
2026-11-05 21:48 ` Jose A. Perez de Azpillaga
2026-11-05 20:57 ` [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs Lorenzo Stoakes (ARM)
2 siblings, 1 reply; 8+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-11-05 20:57 UTC (permalink / raw)
To: David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton
Cc: linux-mm, linux-kernel, Lorenzo Stoakes (ARM)
khugepaged scans VMAs in collapse_scan_mm_slot() in the same mm until it is
about to be torn down, prctl(PR_SET_THP_DISABLE) disables THP for it,
mm->mmap_lock contention occurs, or all its VMAs have been scanned.
When lock contention causes the function to bail out, khugepaged doesn't
keep track of where it left off, so when the mm is next scanned, it starts
from address 0 again.
If there's a lot of mmap_lock contention this can significantly slow down
collapse later in the mm, since the /sys/.../pages_to_scan budget can run
out before the end of the mm is reached.
The budget is incremented (as tracked in cc->progress) at a rate of 1 page
per ineligible VMA, but the budget is only checked once eligible VMAs are
encountered.
Then, it only takes the combined total of ineligible VMAs and eligible but
non-collapsible pages to exceed the /sys/.../pages_to_scan budget for the
rest of the mm to be skipped.
For a system with 4 KiB page size and default sysctl settings this only
requires 4096 ineligible VMAs/uncollapsible pages to be skipped before this
happens.
Resolve the issue by simply tracking the last examined address in the
khugepaged_mm_slot object, so when the mm is resumed, the scan starts from
where it left off.
Additionally, skip to the next candidate if a VMA cannot be collapsed.
This issue was noticed during some work in which an mm was instantiated
with a large number of small VMAs under significant mmap_lock contention,
during which collapse was observed to take 10.8s. With this patch applied
it took 3.6s to collapse the same mm.
Also, while here, update a couple VM_BUG_ON()'s to VM_WARN_ON_ONCE()'s.
This patch is also a prerequisite for the next patch in the series.
Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
Reviewed-by: Lance Yang <lance.yang@linux.dev>
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
mm/khugepaged.c | 38 ++++++++++++++++++++------------------
1 file changed, 20 insertions(+), 18 deletions(-)
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index aad8797f8d5a..216c4b8d28c4 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -69,23 +69,23 @@ static struct kmem_cache *mm_slot_cache __ro_after_init;
/**
* struct khugepaged_mm_slot - khugepaged information per mm that is being scanned
* @slot: hash lookup from mm to mm_slot
+ * @address: the next address inside that to be scanned
*/
struct khugepaged_mm_slot {
struct mm_slot slot;
+ unsigned long address;
};
/**
* struct khugepaged_scan - cursor for scanning
* @mm_head: the head of the mm list to scan
* @mm_slot: the current mm_slot we are scanning
- * @address: the next address inside that to be scanned
*
* There is only the one khugepaged_scan instance of this cursor structure.
*/
struct khugepaged_scan {
struct list_head mm_head;
struct khugepaged_mm_slot *mm_slot;
- unsigned long address;
};
static struct khugepaged_scan khugepaged_scan = {
@@ -2921,7 +2921,6 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
slot = list_first_entry(&khugepaged_scan.mm_head,
struct mm_slot, mm_node);
mm_slot = mm_slot_entry(slot, struct khugepaged_mm_slot, slot);
- khugepaged_scan.address = 0;
khugepaged_scan.mm_slot = mm_slot;
}
spin_unlock(&khugepaged_mm_lock);
@@ -2939,7 +2938,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
if (unlikely(collapse_test_exit_or_disable(mm)))
goto breakouterloop;
- vma_iter_init(&vmi, mm, khugepaged_scan.address);
+ vma_iter_init(&vmi, mm, mm_slot->address);
for_each_vma(vmi, vma) {
unsigned long hstart, hend;
@@ -2948,38 +2947,38 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
cc->progress++;
break;
}
- if (!collapse_possible_orders(vma, vma->vm_flags,
- TVA_KHUGEPAGED)) {
+ if (!collapse_possible_orders(vma, vma->vm_flags, TVA_KHUGEPAGED)) {
+ /* Unable to collapse, skip to the next candidate. */
+ mm_slot->address = ALIGN(vma->vm_end, HPAGE_PMD_SIZE);
cc->progress++;
continue;
}
hstart = ALIGN(vma->vm_start, HPAGE_PMD_SIZE);
hend = ALIGN_DOWN(vma->vm_end, HPAGE_PMD_SIZE);
- if (khugepaged_scan.address > hend) {
+ if (mm_slot->address > hend) {
cc->progress++;
continue;
}
- if (khugepaged_scan.address < hstart)
- khugepaged_scan.address = hstart;
- VM_BUG_ON(khugepaged_scan.address & ~HPAGE_PMD_MASK);
+ if (mm_slot->address < hstart)
+ mm_slot->address = hstart;
+ VM_WARN_ON_ONCE(mm_slot->address & ~HPAGE_PMD_MASK);
- while (khugepaged_scan.address < hend) {
+ while (mm_slot->address < hend) {
bool lock_dropped = false;
cond_resched();
if (unlikely(collapse_test_exit_or_disable(mm)))
goto breakouterloop;
- VM_WARN_ON_ONCE(khugepaged_scan.address < hstart ||
- khugepaged_scan.address + HPAGE_PMD_SIZE >
- hend);
+ VM_WARN_ON_ONCE(mm_slot->address < hstart ||
+ mm_slot->address + HPAGE_PMD_SIZE > hend);
- *result = collapse_single_pmd(khugepaged_scan.address,
+ *result = collapse_single_pmd(mm_slot->address,
vma, &lock_dropped, cc);
if (*result == SCAN_SUCCEED)
khugepaged_pages_collapsed++;
/* move to next address */
- khugepaged_scan.address += HPAGE_PMD_SIZE;
+ mm_slot->address += HPAGE_PMD_SIZE;
if (lock_dropped)
/*
* We released mmap_lock so break loop. Note
@@ -2993,12 +2992,16 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
goto breakouterloop;
}
}
+ /* Every VMA has been examined, so reset. */
+ if (!vma)
+ mm_slot->address = 0;
+
breakouterloop:
mmap_read_unlock(mm); /* exit_mmap will destroy ptes after this */
breakouterloop_mmap_lock:
spin_lock(&khugepaged_mm_lock);
- VM_BUG_ON(khugepaged_scan.mm_slot != mm_slot);
+ VM_WARN_ON_ONCE(khugepaged_scan.mm_slot != mm_slot);
/*
* Release the current mm_slot if this mm is about to die, or
* if we scanned all vmas of this mm, or THP got disabled.
@@ -3013,7 +3016,6 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
slot = list_next_entry(slot, mm_node);
khugepaged_scan.mm_slot =
mm_slot_entry(slot, struct khugepaged_mm_slot, slot);
- khugepaged_scan.address = 0;
} else {
khugepaged_scan.mm_slot = NULL;
khugepaged_full_scans++;
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH v2 2/3] mm/khugepaged: scan mm's more efficiently under mmap_lock contention
2026-11-05 20:57 ` [PATCH v2 2/3] mm/khugepaged: scan mm's more efficiently under mmap_lock contention Lorenzo Stoakes (ARM)
@ 2026-11-05 21:48 ` Jose A. Perez de Azpillaga
0 siblings, 0 replies; 8+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-05 21:48 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM),
David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton
Cc: linux-mm, linux-kernel
On 11/5/26 9:57 PM, Lorenzo Stoakes (ARM) wrote:
> khugepaged scans VMAs in collapse_scan_mm_slot() in the same mm until it is
> about to be torn down, prctl(PR_SET_THP_DISABLE) disables THP for it,
> mm->mmap_lock contention occurs, or all its VMAs have been scanned.
>
> When lock contention causes the function to bail out, khugepaged doesn't
> keep track of where it left off, so when the mm is next scanned, it starts
> from address 0 again.
>
> If there's a lot of mmap_lock contention this can significantly slow down
> collapse later in the mm, since the /sys/.../pages_to_scan budget can run
> out before the end of the mm is reached.
>
> The budget is incremented (as tracked in cc->progress) at a rate of 1 page
> per ineligible VMA, but the budget is only checked once eligible VMAs are
> encountered.
>
> Then, it only takes the combined total of ineligible VMAs and eligible but
> non-collapsible pages to exceed the /sys/.../pages_to_scan budget for the
> rest of the mm to be skipped.
>
> For a system with 4 KiB page size and default sysctl settings this only
> requires 4096 ineligible VMAs/uncollapsible pages to be skipped before this
> happens.
>
> Resolve the issue by simply tracking the last examined address in the
> khugepaged_mm_slot object, so when the mm is resumed, the scan starts from
> where it left off.
>
> Additionally, skip to the next candidate if a VMA cannot be collapsed.
>
> This issue was noticed during some work in which an mm was instantiated
> with a large number of small VMAs under significant mmap_lock contention,
> during which collapse was observed to take 10.8s. With this patch applied
> it took 3.6s to collapse the same mm.
>
> Also, while here, update a couple VM_BUG_ON()'s to VM_WARN_ON_ONCE()'s.
>
> This patch is also a prerequisite for the next patch in the series.
>
> Reviewed-by: David Hildenbrand (Arm) <david@kernel.org>
> Reviewed-by: Lance Yang <lance.yang@linux.dev>
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
LGTM:
Reviewed-by: Jose A. Perez de Azpillaga <azpijr@gmail.com>
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs
2026-11-05 20:57 [PATCH v2 0/3] mm/khugepaged: bound mmap_lock hold time when walking an mm's VMAs Lorenzo Stoakes (ARM)
2026-11-05 20:57 ` [PATCH v2 1/3] Revert "mm/khugepaged: remove definition of struct khugepaged_mm_slot" Lorenzo Stoakes (ARM)
2026-11-05 20:57 ` [PATCH v2 2/3] mm/khugepaged: scan mm's more efficiently under mmap_lock contention Lorenzo Stoakes (ARM)
@ 2026-11-05 20:57 ` Lorenzo Stoakes (ARM)
2026-11-05 21:53 ` Jose A. Perez de Azpillaga
2026-11-06 2:12 ` Vernon Yang
2 siblings, 2 replies; 8+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-11-05 20:57 UTC (permalink / raw)
To: David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton
Cc: linux-mm, linux-kernel, Lorenzo Stoakes (ARM)
khugepaged walks an mm's VMAs under the mmap read lock in
collapse_scan_mm_slot(), checking for ineligible VMAs first, and skipping
them if encountered.
It charges cc->progress 1 page against the /sys/.../pages_to_scan budget
when these are encountered, however only checking this budget once an
eligible PMD-spanning VMA is encountered.
It can therefore hold the mmap read lock for a long time if there are a
large number of ineligible VMAs present in an mm, over which time no mmap
write locks (and thus no VMA write locks) can be acquired.
Address this by checking whether the mmap lock is contended after every
KHUGEPAGED_MAX_VMA_SCAN_BATCH VMAs walked, that is a quarter of the
pages_to_scan budget, and if so dropping and re-acquiring the mmap read
lock before resuming the walk from the per-mm cursor.
As the walk is resumed from the cursor rather than the VMA that was current
when the lock was dropped, the cursor must never move backwards. A VMA
ending at the top of the address space causes ALIGN() to wrap the cursor to
zero, so treat such a VMA as the last in the mm rather than skipping past
it.
This was tested locally with ~1M ineligible VMAs mapped ahead of a
collapsible region of memory, and resulted in a reduced mmap_lock hold time
of 170us down from 95ms.
This was also observed to reduce mmap_lock stalls in concurrent threads of
a similar duration. Note that a reader re-acquiring the lock can currently
steal it from a writer that is queued but not yet running, so the full
benefit to writers depends on the separate rwsem change at [0].
Local measurements indicated no change for ordinary use cases, so this is
all upside.
This work came out of investigations into mmap stalls when getting latency
numbers for an unrelated patch series.
[0]: https://lore.kernel.org/all/20261105-rwsem-dont-steal-from-queued-writer-v1-1-275e070f426f@kernel.org/
Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
---
mm/khugepaged.c | 31 +++++++++++++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index 216c4b8d28c4..eaa01dae5a03 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -66,6 +66,12 @@ static DEFINE_READ_MOSTLY_HASHTABLE(mm_slots_hash, MM_SLOTS_HASH_BITS);
static struct kmem_cache *mm_slot_cache __ro_after_init;
+/*
+ * The number of VMAs to walk before checking for, and relieving, mmap lock
+ * contention, as a function of the pages_to_scan budget.
+ */
+#define KHUGEPAGED_MAX_VMA_SCAN_BATCH(pages_to_scan) ((pages_to_scan) / 4)
+
/**
* struct khugepaged_mm_slot - khugepaged information per mm that is being scanned
* @slot: hash lookup from mm to mm_slot
@@ -2910,6 +2916,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
struct mm_struct *mm;
struct vm_area_struct *vma;
unsigned int progress_prev = cc->progress;
+ unsigned int nr_vmas_batch = 0;
lockdep_assert_held(&khugepaged_mm_lock);
*result = SCAN_FAIL;
@@ -2942,15 +2949,35 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
for_each_vma(vmi, vma) {
unsigned long hstart, hend;
+ /* Relieve mmap lock contention after walking a batch of VMAs. */
+ if (nr_vmas_batch++ >= KHUGEPAGED_MAX_VMA_SCAN_BATCH(progress_max)) {
+ nr_vmas_batch = 0;
+ if (mmap_lock_is_contended(mm)) {
+ mmap_read_unlock(mm);
+ cond_resched();
+ mmap_read_lock(mm);
+ vma_iter_set(&vmi, mm_slot->address);
+ continue;
+ }
+ }
+
cond_resched();
if (unlikely(collapse_test_exit_or_disable(mm))) {
cc->progress++;
break;
}
if (!collapse_possible_orders(vma, vma->vm_flags, TVA_KHUGEPAGED)) {
- /* Unable to collapse, skip to the next candidate. */
- mm_slot->address = ALIGN(vma->vm_end, HPAGE_PMD_SIZE);
+ const unsigned long next =
+ ALIGN(vma->vm_end, HPAGE_PMD_SIZE);
+
cc->progress++;
+ /* Check for overflow. */
+ if (unlikely(next < vma->vm_end)) {
+ vma = NULL;
+ break;
+ }
+ /* Unable to collapse, skip to the next candidate. */
+ mm_slot->address = next;
continue;
}
hstart = ALIGN(vma->vm_start, HPAGE_PMD_SIZE);
--
2.56.0
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs
2026-11-05 20:57 ` [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs Lorenzo Stoakes (ARM)
@ 2026-11-05 21:53 ` Jose A. Perez de Azpillaga
2026-11-06 2:12 ` Vernon Yang
1 sibling, 0 replies; 8+ messages in thread
From: Jose A. Perez de Azpillaga @ 2026-11-05 21:53 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM),
David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton
Cc: linux-mm, linux-kernel
On 11/5/26 9:57 PM, Lorenzo Stoakes (ARM) wrote:
> khugepaged walks an mm's VMAs under the mmap read lock in
> collapse_scan_mm_slot(), checking for ineligible VMAs first, and skipping
> them if encountered.
>
> It charges cc->progress 1 page against the /sys/.../pages_to_scan budget
> when these are encountered, however only checking this budget once an
> eligible PMD-spanning VMA is encountered.
>
> It can therefore hold the mmap read lock for a long time if there are a
> large number of ineligible VMAs present in an mm, over which time no mmap
> write locks (and thus no VMA write locks) can be acquired.
>
> Address this by checking whether the mmap lock is contended after every
> KHUGEPAGED_MAX_VMA_SCAN_BATCH VMAs walked, that is a quarter of the
> pages_to_scan budget, and if so dropping and re-acquiring the mmap read
> lock before resuming the walk from the per-mm cursor.
>
> As the walk is resumed from the cursor rather than the VMA that was current
> when the lock was dropped, the cursor must never move backwards. A VMA
> ending at the top of the address space causes ALIGN() to wrap the cursor to
> zero, so treat such a VMA as the last in the mm rather than skipping past
> it.
>
> This was tested locally with ~1M ineligible VMAs mapped ahead of a
> collapsible region of memory, and resulted in a reduced mmap_lock hold time
> of 170us down from 95ms.
>
> This was also observed to reduce mmap_lock stalls in concurrent threads of
> a similar duration. Note that a reader re-acquiring the lock can currently
> steal it from a writer that is queued but not yet running, so the full
> benefit to writers depends on the separate rwsem change at [0].
>
> Local measurements indicated no change for ordinary use cases, so this is
> all upside.
>
> This work came out of investigations into mmap stalls when getting latency
> numbers for an unrelated patch series.
>
> [0]: https://lore.kernel.org/all/20261105-rwsem-dont-steal-from-queued-writer-v1-1-275e070f426f@kernel.org/
>
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> ---
> mm/khugepaged.c | 31 +++++++++++++++++++++++++++++--
> 1 file changed, 29 insertions(+), 2 deletions(-)
>
> diff --git a/mm/khugepaged.c b/mm/khugepaged.c
> index 216c4b8d28c4..eaa01dae5a03 100644
> --- a/mm/khugepaged.c
> +++ b/mm/khugepaged.c
> @@ -66,6 +66,12 @@ static DEFINE_READ_MOSTLY_HASHTABLE(mm_slots_hash, MM_SLOTS_HASH_BITS);
>
> static struct kmem_cache *mm_slot_cache __ro_after_init;
>
> +/*
> + * The number of VMAs to walk before checking for, and relieving, mmap lock
> + * contention, as a function of the pages_to_scan budget.
> + */
> +#define KHUGEPAGED_MAX_VMA_SCAN_BATCH(pages_to_scan) ((pages_to_scan) / 4)
> +
> /**
> * struct khugepaged_mm_slot - khugepaged information per mm that is being scanned
> * @slot: hash lookup from mm to mm_slot
> @@ -2910,6 +2916,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
> struct mm_struct *mm;
> struct vm_area_struct *vma;
> unsigned int progress_prev = cc->progress;
> + unsigned int nr_vmas_batch = 0;
>
> lockdep_assert_held(&khugepaged_mm_lock);
> *result = SCAN_FAIL;
> @@ -2942,15 +2949,35 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
> for_each_vma(vmi, vma) {
> unsigned long hstart, hend;
>
> + /* Relieve mmap lock contention after walking a batch of VMAs. */
> + if (nr_vmas_batch++ >= KHUGEPAGED_MAX_VMA_SCAN_BATCH(progress_max)) {
> + nr_vmas_batch = 0;
> + if (mmap_lock_is_contended(mm)) {
> + mmap_read_unlock(mm);
> + cond_resched();
> + mmap_read_lock(mm);
> + vma_iter_set(&vmi, mm_slot->address);
> + continue;
> + }
> + }
> +
with pages_to_scan below 4 the batch is 0, so under constant contention
we keep dropping the lock at the cursor and never scan anything, don't we?
--
cheers,
jose a. p-a
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs
2026-11-05 20:57 ` [PATCH v2 3/3] mm/khugepaged: release contended mmap read lock after scanning enough VMAs Lorenzo Stoakes (ARM)
2026-11-05 21:53 ` Jose A. Perez de Azpillaga
@ 2026-11-06 2:12 ` Vernon Yang
1 sibling, 0 replies; 8+ messages in thread
From: Vernon Yang @ 2026-11-06 2:12 UTC (permalink / raw)
To: Lorenzo Stoakes (ARM)
Cc: David Hildenbrand, Zi Yan, Baolin Wang, Liam R. Howlett,
Nico Pache, Ryan Roberts, Dev Jain, Barry Song, Lance Yang,
Usama Arif, Kiryl Shutsemau, Andrew Morton, linux-mm,
linux-kernel
On Thu, Nov 05, 2026 at 08:57:24PM +0000, Lorenzo Stoakes (ARM) wrote:
> khugepaged walks an mm's VMAs under the mmap read lock in
> collapse_scan_mm_slot(), checking for ineligible VMAs first, and skipping
> them if encountered.
>
> It charges cc->progress 1 page against the /sys/.../pages_to_scan budget
> when these are encountered, however only checking this budget once an
> eligible PMD-spanning VMA is encountered.
>
> It can therefore hold the mmap read lock for a long time if there are a
> large number of ineligible VMAs present in an mm, over which time no mmap
> write locks (and thus no VMA write locks) can be acquired.
>
> Address this by checking whether the mmap lock is contended after every
> KHUGEPAGED_MAX_VMA_SCAN_BATCH VMAs walked, that is a quarter of the
> pages_to_scan budget, and if so dropping and re-acquiring the mmap read
> lock before resuming the walk from the per-mm cursor.
>
> As the walk is resumed from the cursor rather than the VMA that was current
> when the lock was dropped, the cursor must never move backwards. A VMA
> ending at the top of the address space causes ALIGN() to wrap the cursor to
> zero, so treat such a VMA as the last in the mm rather than skipping past
> it.
>
> This was tested locally with ~1M ineligible VMAs mapped ahead of a
> collapsible region of memory, and resulted in a reduced mmap_lock hold time
> of 170us down from 95ms.
>
> This was also observed to reduce mmap_lock stalls in concurrent threads of
> a similar duration. Note that a reader re-acquiring the lock can currently
> steal it from a writer that is queued but not yet running, so the full
> benefit to writers depends on the separate rwsem change at [0].
>
> Local measurements indicated no change for ordinary use cases, so this is
> all upside.
>
> This work came out of investigations into mmap stalls when getting latency
> numbers for an unrelated patch series.
>
> [0]: https://lore.kernel.org/all/20261105-rwsem-dont-steal-from-queued-writer-v1-1-275e070f426f@kernel.org/
>
> Signed-off-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>
> ---
> mm/khugepaged.c | 31 +++++++++++++++++++++++++++++--
> 1 file changed, 29 insertions(+), 2 deletions(-)
>
> diff --git a/mm/khugepaged.c b/mm/khugepaged.c
> index 216c4b8d28c4..eaa01dae5a03 100644
> --- a/mm/khugepaged.c
> +++ b/mm/khugepaged.c
> @@ -66,6 +66,12 @@ static DEFINE_READ_MOSTLY_HASHTABLE(mm_slots_hash, MM_SLOTS_HASH_BITS);
>
> static struct kmem_cache *mm_slot_cache __ro_after_init;
>
> +/*
> + * The number of VMAs to walk before checking for, and relieving, mmap lock
> + * contention, as a function of the pages_to_scan budget.
> + */
> +#define KHUGEPAGED_MAX_VMA_SCAN_BATCH(pages_to_scan) ((pages_to_scan) / 4)
> +
When pages_to_scan to equate 1/2/3, KHUGEPAGED_MAX_VMA_SCAN_BATCH(progress_max)
always to 0, we will never scan anything.
The following code will solve this problem.
#define KHUGEPAGED_MAX_VMA_SCAN_BATCH(pages_to_scan) max((pages_to_scan) / 4, 1)
> /**
> * struct khugepaged_mm_slot - khugepaged information per mm that is being scanned
> * @slot: hash lookup from mm to mm_slot
> @@ -2910,6 +2916,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
> struct mm_struct *mm;
> struct vm_area_struct *vma;
> unsigned int progress_prev = cc->progress;
> + unsigned int nr_vmas_batch = 0;
>
> lockdep_assert_held(&khugepaged_mm_lock);
> *result = SCAN_FAIL;
> @@ -2942,15 +2949,35 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
> for_each_vma(vmi, vma) {
> unsigned long hstart, hend;
>
> + /* Relieve mmap lock contention after walking a batch of VMAs. */
> + if (nr_vmas_batch++ >= KHUGEPAGED_MAX_VMA_SCAN_BATCH(progress_max)) {
> + nr_vmas_batch = 0;
> + if (mmap_lock_is_contended(mm)) {
> + mmap_read_unlock(mm);
> + cond_resched();
> + mmap_read_lock(mm);
> + vma_iter_set(&vmi, mm_slot->address);
> + continue;
> + }
> + }
> +
> cond_resched();
> if (unlikely(collapse_test_exit_or_disable(mm))) {
> cc->progress++;
> break;
> }
> if (!collapse_possible_orders(vma, vma->vm_flags, TVA_KHUGEPAGED)) {
> - /* Unable to collapse, skip to the next candidate. */
> - mm_slot->address = ALIGN(vma->vm_end, HPAGE_PMD_SIZE);
> + const unsigned long next =
> + ALIGN(vma->vm_end, HPAGE_PMD_SIZE);
> +
> cc->progress++;
> + /* Check for overflow. */
> + if (unlikely(next < vma->vm_end)) {
I think you want it to be like that.
if (unlikely(next >= vma->vm_end)) {
^^
> + vma = NULL;
> + break;
> + }
> + /* Unable to collapse, skip to the next candidate. */
> + mm_slot->address = next;
> continue;
> }
> hstart = ALIGN(vma->vm_start, HPAGE_PMD_SIZE);
>
> --
> 2.56.0
>
>
--
Cheers,
Vernon
^ permalink raw reply [flat|nested] 8+ messages in thread