Thread (119 messages) flat view 119 messages, 9 authors, 9d ago
COOLING9d

[RFC PATCH 44/57] mm/khugepaged: hold the address space open across a scan

From: Kiryl Shutsemau <hidden>
Date: 2026-08-16 22:47:40
Also in: bpf, linux-kselftest, linux-mm, lkml
Subsystem: memory management, memory management - thp (transparent huge page), the rest · Maintainers: Andrew Morton, David Hildenbrand, Linus Torvalds

From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>

Preparation for taking a per-VMA read lock instead of mmap_lock.

What tells khugepaged an address space is going away is the barrier in
__khugepaged_exit(): it runs before exit_mmap() and takes mmap_lock for
writing, which waits for a scan holding it for reading.  A scan under a
per-VMA lock holds no mmap_lock, so nothing waits for it and exit_mmap()
frees the page tables it is walking.

Take a reference on mm_users for the pass instead.  __mmput() cannot start
while one is held, so neither can exit_mmap(), whatever lock the pass
uses.

Drop it with mmput_async(), so the last reference does not tear an address
space down inside khugepaged.  Drop it before the exiting mm is judged,
too: that judgement needs the true count to release the slot.

The reference is also what the exiting-mm checks were reading, so an
address space whose owner has gone now shows as one reference rather than
none.  The three checks inside the pass ask collapse_test_exit_mmref()
instead; the slot-release judgement keeps the old test, running after the
reference is dropped.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
 mm/collapse.h   | 19 +++++++++++++++++--
 mm/khugepaged.c | 31 +++++++++++++++++++++++++++----
 2 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/mm/collapse.h b/mm/collapse.h
index 9e2cec1f250b..74e513c5c76c 100644
--- a/mm/collapse.h
+++ b/mm/collapse.h
@@ -170,6 +170,11 @@ struct collapse_control {
 	pte_t *saved_ptes;
 };
 
+static inline int collapse_disabled(struct mm_struct *mm)
+{
+	return mm_flags_test(MMF_DISABLE_THP_COMPLETELY, mm);
+}
+
 static inline int collapse_test_exit(struct mm_struct *mm)
 {
 	return atomic_read(&mm->mm_users) == 0;
@@ -177,8 +182,18 @@ static inline int collapse_test_exit(struct mm_struct *mm)
 
 static inline int collapse_test_exit_or_disable(struct mm_struct *mm)
 {
-	return collapse_test_exit(mm) ||
-		mm_flags_test(MMF_DISABLE_THP_COMPLETELY, mm);
+	return collapse_test_exit(mm) || collapse_disabled(mm);
+}
+
+/* The owner has gone: the caller's own reference is the only one left */
+static inline int collapse_test_exit_mmref(struct mm_struct *mm)
+{
+	return atomic_read(&mm->mm_users) == 1;
+}
+
+static inline int collapse_test_exit_or_disable_mmref(struct mm_struct *mm)
+{
+	return collapse_test_exit_mmref(mm) || collapse_disabled(mm);
 }
 
 /*
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index cc5ff429d811..f3ea1846990e 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -531,16 +531,31 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
 	spin_unlock(&khugepaged_mm_lock);
 
 	mm = slot->mm;
+	vma = NULL;
+
+	/*
+	 * A reference on mm_users for as long as the pass works on this address
+	 * space.  __mmput() cannot start while one is held, so neither can
+	 * exit_mmap(), and the VMAs and page tables stay where they are.
+	 *
+	 * Once per pass, not once per table: the reference is what makes the
+	 * address space safe to work on, and a pass is how long that is wanted
+	 * for.  Nothing else in mm takes it per unit of work -- DAMON takes one
+	 * per target and walks every region under it, swapoff one per mm across
+	 * the whole address space, userfaultfd one per call.
+	 */
+	if (!mmget_not_zero(mm))
+		goto breakouterloop_no_mmput;
+
 	/*
 	 * Don't wait for semaphore (to avoid long wait times).  Just move to
 	 * the next mm on the list.
 	 */
-	vma = NULL;
 	if (unlikely(!mmap_read_trylock(mm)))
 		goto breakouterloop_mmap_lock;
 
 	cc->progress++;
-	if (unlikely(collapse_test_exit_or_disable(mm)))
+	if (unlikely(collapse_test_exit_or_disable_mmref(mm)))
 		goto breakouterloop;
 
 	vma_iter_init(&vmi, mm, khugepaged_scan.address);
@@ -549,7 +564,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
 		unsigned long orders;
 
 		cond_resched();
-		if (unlikely(collapse_test_exit_or_disable(mm))) {
+		if (unlikely(collapse_test_exit_or_disable_mmref(mm))) {
 			cc->progress++;
 			break;
 		}
@@ -595,7 +610,7 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
 			range_end = min(hend, pmd_addr + HPAGE_PMD_SIZE);
 
 			cond_resched();
-			if (unlikely(collapse_test_exit_or_disable(mm)) ||
+			if (unlikely(collapse_test_exit_or_disable_mmref(mm)) ||
 			    cc->progress >= progress_max)
 				goto breakouterloop;
 
@@ -622,6 +637,14 @@ static void collapse_scan_mm_slot(unsigned int progress_max,
 breakouterloop:
 	mmap_read_unlock(mm); /* exit_mmap will destroy ptes after this */
 breakouterloop_mmap_lock:
+	/*
+	 * Not mmput(): the last reference would run exit_mmap() here, and
+	 * khugepaged is not the thread that should tear an address space down.
+	 * Dropped before the exiting mm is judged below, so that judgement still
+	 * sees the true count.
+	 */
+	mmput_async(mm);
+breakouterloop_no_mmput:
 
 	spin_lock(&khugepaged_mm_lock);
 	VM_BUG_ON(khugepaged_scan.mm_slot != slot);
-- 
2.54.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help