[PATCH 05/16] selftests/mm: add khugepaged completion barrier helper

Kiryl Shutsemau <[email protected]> Sun, 2 Aug 2026 20:52:37 +0100
Newsgroups org.kernel.vger.linux-kselftest,org.kernel.vger.linux-kernel,org.kvack.linux-mm
Message-ID <[email protected]>
From: "Kiryl Shutsemau (Meta)" <[email protected]>

Race and functional tests need to drive khugepaged synchronously: set
up a layout, let exactly one full scan pass over it, check the result.
The khugepaged selftest already waits on full_scans advancing by two —
a completion barrier for one pass that started after setup — but it
relies on a short configured scan_sleep_millisecs to make progress.

Lift the pattern into a library helper, khugepaged_full_pass(), and
drive it by the sysfs wake path: any store to scan_sleep_millisecs
wakes the daemon, so the barrier completes promptly regardless of the
configured scan cadence. Wake exactly once per missing pass:
over-waking would queue a straggler pass behind the barrier that
overlaps and perturbs whatever the caller sets up next. One wake
completes one full pass only when the whole mm list fits in a single
scan batch, so callers must pair the helper with a large pages_to_scan.

Settings pushes and pops must not start passes nobody asked for
either, so thp_write_settings() now writes each khugepaged knob only
when it changes. Switch the khugepaged selftest order-parameterized
cases to the helper.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]>
---
 .../testing/selftests/mm/hugepage_settings.c  | 66 ++++++++++++++++---
 .../testing/selftests/mm/hugepage_settings.h  |  2 +
 tools/testing/selftests/mm/khugepaged.c       | 17 +----
 3 files changed, 61 insertions(+), 24 deletions(-)

diff --git a/tools/testing/selftests/mm/hugepage_settings.c b/tools/testing/selftests/mm/hugepage_settings.c
index d7917dce3aba..a26a0cffa9c5 100644
--- a/tools/testing/selftests/mm/hugepage_settings.c
+++ b/tools/testing/selftests/mm/hugepage_settings.c
@@ -183,6 +183,17 @@ void thp_read_settings(struct thp_settings *settings)
 	}
 }
 
+/*
+ * Write only on change: any store to a khugepaged sysfs knob wakes the
+ * daemon, and settings pushes/pops must not start scan passes nobody
+ * asked for — khugepaged_full_pass() is the only sanctioned wake.
+ */
+static void thp_update_num(const char *name, unsigned long num)
+{
+	if (thp_read_num(name) != num)
+		thp_write_num(name, num);
+}
+
 void thp_write_settings(struct thp_settings *settings)
 {
 	struct khugepaged_settings *khugepaged = &settings->khugepaged;
@@ -198,15 +209,15 @@ void thp_write_settings(struct thp_settings *settings)
 			shmem_enabled_strings[settings->shmem_enabled]);
 	thp_write_num("use_zero_page", settings->use_zero_page);
 
-	thp_write_num("khugepaged/defrag", khugepaged->defrag);
-	thp_write_num("khugepaged/alloc_sleep_millisecs",
-			khugepaged->alloc_sleep_millisecs);
-	thp_write_num("khugepaged/scan_sleep_millisecs",
-			khugepaged->scan_sleep_millisecs);
-	thp_write_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
-	thp_write_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
-	thp_write_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
-	thp_write_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);
+	thp_update_num("khugepaged/defrag", khugepaged->defrag);
+	thp_update_num("khugepaged/alloc_sleep_millisecs",
+		       khugepaged->alloc_sleep_millisecs);
+	thp_update_num("khugepaged/scan_sleep_millisecs",
+		       khugepaged->scan_sleep_millisecs);
+	thp_update_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
+	thp_update_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
+	thp_update_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
+	thp_update_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);
 
 	if (dev_queue_read_ahead_path[0])
 		write_num(dev_queue_read_ahead_path, settings->read_ahead_kb);
@@ -230,6 +241,43 @@ void thp_write_settings(struct thp_settings *settings)
 	}
 }
 
+/*
+ * Completion barrier for khugepaged: wait until a full scan pass that
+ * started after this call has finished. full_scans must advance by two;
+ * a +1 step may complete a pass that examined this mm before the
+ * caller's setup was in place.
+ *
+ * Any store to scan_sleep_millisecs wakes the daemon, so the barrier
+ * works regardless of the configured scan cadence. It wakes exactly
+ * once per missing pass — over-waking would queue a straggler pass
+ * behind the barrier, perturbing whatever the caller sets up next.
+ * One wake completes one full pass only if the whole mm list fits in
+ * one scan batch, so callers must pair this with a large
+ * pages_to_scan.
+ */
+bool khugepaged_full_pass(unsigned int timeout_s)
+{
+	unsigned long deadline_ms = timeout_s * 1000UL;
+	unsigned long sleep_ms =
+		thp_read_num("khugepaged/scan_sleep_millisecs");
+	unsigned long elapsed_ms = 0;
+	int pass;
+
+	for (pass = 0; pass < 2; pass++) {
+		unsigned long target =
+			thp_read_num("khugepaged/full_scans") + 1;
+
+		thp_write_num("khugepaged/scan_sleep_millisecs", sleep_ms);
+		while (thp_read_num("khugepaged/full_scans") < target) {
+			if (elapsed_ms >= deadline_ms)
+				return false;
+			usleep(10 * 1000);
+			elapsed_ms += 10;
+		}
+	}
+	return true;
+}
+
 struct thp_settings *thp_current_settings(void)
 {
 	if (!settings_index) {
diff --git a/tools/testing/selftests/mm/hugepage_settings.h b/tools/testing/selftests/mm/hugepage_settings.h
index 726c73c43c05..8de446affeec 100644
--- a/tools/testing/selftests/mm/hugepage_settings.h
+++ b/tools/testing/selftests/mm/hugepage_settings.h
@@ -83,6 +83,8 @@ static inline void thp_save_settings(void)
 	hugepage_save_settings(/* thp = */ true, /* hugetlb = */ false);
 }
 
+bool khugepaged_full_pass(unsigned int timeout_s);
+
 void thp_set_read_ahead_path(char *path);
 unsigned long thp_supported_orders(void);
 unsigned long thp_shmem_supported_orders(void);
diff --git a/tools/testing/selftests/mm/khugepaged.c b/tools/testing/selftests/mm/khugepaged.c
index 971e97a7330a..65fafab06410 100644
--- a/tools/testing/selftests/mm/khugepaged.c
+++ b/tools/testing/selftests/mm/khugepaged.c
@@ -1126,23 +1126,10 @@ static bool range_not_collapsed(void *p, size_t len)
 	return true;
 }
 
-/*
- * Completion barrier: one full khugepaged pass that started after this
- * call. Waiting for full_scans to advance by two guarantees it; a +1
- * step might complete a pass that scanned our mm before the setup.
- */
 static bool khugepaged_wait_full_pass(void)
 {
-	int full_scans = thp_read_num("khugepaged/full_scans") + 2;
-	int timeout = 60; /* 30 seconds */
-
-	while (timeout--) {
-		if (thp_read_num("khugepaged/full_scans") >= full_scans)
-			return true;
-		printf(".");
-		usleep(TICK);
-	}
-	return false;
+	/* Wait up to 30 seconds for the pass to complete. */
+	return khugepaged_full_pass(30);
 }
 
 /*
-- 
2.54.0