[PATCH 12/16] selftests/mm: add memory-pressure threads to the khugepaged race harness

Kiryl Shutsemau <[email protected]>
Newsgroups gmane.linux.kernel,gmane.linux.kernel.mm
Message-ID <[email protected]>
From: "Kiryl Shutsemau (Meta)" <[email protected]>

The race harness exercises collapse against faults, pins, fork, mremap and
MADV_DONTNEED, but nothing in it ever elevates a source folio's refcount
from the reclaim or compaction side: LRU isolation, migration of a source
folio out from under the collapse, or swap traffic churning the LRU while
a collapse is in progress.

Add -p, which starts two more racing threads:

 - pageout: cycles MADV_PAGEOUT over a dedicated neighbor region
   (4 PMD areas, clamped to [16M, 64M]), faulting everything back in and
   verifying content each round -- swap traffic and LRU churn with an
   exact correctness check, since a page's pattern must survive the round
   trip through swap. Disabled with a note when the host has no swap:
   without it there is no anon reclaim to drive.

 - compactor: writes to /proc/sys/vm/compact_memory in a loop. Compaction
   isolates and migrates folios, so it competes with a collapse for the
   very pages it is trying to gather, with transient refcount elevations
   and migration entries of its own.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]>
---
 tools/testing/selftests/mm/khugepaged_race.c | 109 ++++++++++++++++++-
 tools/testing/selftests/mm/run_vmtests.sh    |   2 +
 2 files changed, 107 insertions(+), 4 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index 2e36e242caa7..304c72b4ee3c 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -22,6 +22,12 @@
  * clear, zero-filled copy, install-time pte_none() verify and abort)
  * race the faulters directly.
  *
+ * -p adds memory pressure to any of the above: MADV_PAGEOUT cycling
+ * on a dedicated neighbor region (swap traffic and LRU churn; skipped
+ * with a note when the host has no swap) and a compact_memory trigger
+ * loop (compaction migrates source folios, racing collapse's freeze
+ * with refcount elevation and migration entries of its own).
+ *
  * Correctness signals: every racing page must read as its pattern or
  * zero (MADV_DONTNEED), never anything else — checked continuously by
  * the faulters and the fork children and once at the end — plus
@@ -64,6 +70,8 @@ static unsigned long page_size;
 static char *region;		/* NR_AREAS * hpage_pmd_size */
 static char *mremap_area;	/* region + NR_SHARED_AREAS areas */
 static char *mremap_scratch;	/* well above the region */
+static char *pageout_area;	/* -p: dedicated pressure region */
+static size_t pageout_size;
 static int gup_fd = -1;
 static volatile int stop;
 static volatile int corrupted;
@@ -189,6 +197,70 @@ static void *mremapper_fn(void *arg)
 	return NULL;
 }
 
+/*
+ * -p: swap traffic and LRU churn on a region of our own. The content
+ * check is exact — a page out and back through swap must preserve the
+ * pattern, and nothing else ever writes here.
+ */
+static void *pageout_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+	unsigned long nr = pageout_size / page_size;
+	unsigned long i;
+
+	for (i = 0; i < nr; i++)
+		*(unsigned int *)(pageout_area + i * page_size) = pattern(i);
+
+	while (!stop) {
+		madvise(pageout_area, pageout_size, MADV_PAGEOUT);
+		for (i = 0; i < nr && !stop; i++) {
+			unsigned int val = *(unsigned int *)(pageout_area +
+							     i * page_size);
+
+			if (val != pattern(i)) {
+				corrupted = 1;
+				ksft_print_msg("Pageout corruption at page %lu: %#x != %#x\n",
+					       i, val, pattern(i));
+			}
+		}
+		usleep(rand_r(&seed) % 2000);
+	}
+	return NULL;
+}
+
+/* -p: compaction migrates the collapse sources out from under us. */
+static void *compactor_fn(void *arg)
+{
+	unsigned int seed = (unsigned long)arg;
+	int fd = open("/proc/sys/vm/compact_memory", O_WRONLY);
+
+	if (fd < 0) {
+		ksft_print_msg("No compact_memory; compactor idle\n");
+		return NULL;
+	}
+	while (!stop) {
+		if (write(fd, "1", 1) < 0)
+			break;
+		usleep(10000 + rand_r(&seed) % 100000);
+	}
+	close(fd);
+	return NULL;
+}
+
+static bool swap_available(void)
+{
+	char line[256];
+	int lines = 0;
+	FILE *fp = fopen("/proc/swaps", "r");
+
+	if (!fp)
+		return false;
+	while (fgets(line, sizeof(line), fp))
+		lines++;
+	fclose(fp);
+	return lines > 1;
+}
+
 static unsigned long now_ms(void)
 {
 	struct timeval tv;
@@ -200,8 +272,9 @@ static unsigned long now_ms(void)
 static void usage(void)
 {
 	fprintf(stderr,
-		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-z] [-a areas]\n"
+		"Usage: khugepaged_race [-d seconds] [-m stepped|free|madvise] [-z] [-p] [-a areas]\n"
 		"\t-z: permissive max_ptes_none (hole-heavy windows)\n"
+		"\t-p: memory pressure (pageout + compaction) threads\n"
 		"\t-a: number of shared PMD-sized playground areas (default 3)\n");
 	exit(1);
 }
@@ -210,12 +283,13 @@ int main(int argc, char **argv)
 {
 	static const char * const thread_names[] = {
 		"faulter", "faulter2", "dontneed", "pinner", "forker",
-		"mremapper",
+		"mremapper", "pageout", "compactor",
 	};
 	void *(*const thread_fns[])(void *) = {
 		faulter_fn, faulter_fn, dontneed_fn, pinner_fn, forker_fn,
-		mremapper_fn,
+		mremapper_fn, pageout_fn, compactor_fn,
 	};
+	const unsigned long pageout_bit = 1UL << 6, compactor_bit = 1UL << 7;
 	const int nr_threads = ARRAY_SIZE(thread_names);
 	pthread_t threads[ARRAY_SIZE(thread_names)];
 	const char *mode = "stepped";
@@ -225,11 +299,12 @@ int main(int argc, char **argv)
 	unsigned long thread_mask = ~0UL;
 	int nr_areas_arg = 0;
 	bool permissive_none = false;
+	bool pressure = false;
 	unsigned long i;
 	int steps = 0;
 	int opt;
 
-	while ((opt = getopt(argc, argv, "a:d:m:t:zh")) != -1) {
+	while ((opt = getopt(argc, argv, "a:d:m:t:zph")) != -1) {
 		switch (opt) {
 		case 'a':
 			nr_areas_arg = atoi(optarg);
@@ -247,6 +322,9 @@ int main(int argc, char **argv)
 		case 'z':
 			permissive_none = true;
 			break;
+		case 'p':
+			pressure = true;
+			break;
 		default:
 			usage();
 		}
@@ -271,6 +349,14 @@ int main(int argc, char **argv)
 	nr_shared_areas = nr_areas_arg > 0 ? nr_areas_arg : DEFAULT_SHARED_AREAS;
 	nr_areas = nr_shared_areas + 1;
 
+	if (!pressure) {
+		thread_mask &= ~(pageout_bit | compactor_bit);
+	} else if (!swap_available()) {
+		/* No swap, no anon reclaim: compaction-only pressure. */
+		ksft_print_msg("-p without swap: pageout thread disabled\n");
+		thread_mask &= ~pageout_bit;
+	}
+
 	ksft_set_plan(1);
 
 	thp_save_settings();
@@ -311,6 +397,21 @@ int main(int argc, char **argv)
 	mremap_area = region + nr_shared_areas * hpage_pmd_size;
 	mremap_scratch = (char *)BASE_ADDR + 2 * nr_areas * hpage_pmd_size;
 
+	if (thread_mask & pageout_bit) {
+		/*
+		 * Big enough to cycle real reclaim, small enough not to
+		 * dominate a TCG guest: 4 PMD areas, clamped to [16M, 64M].
+		 */
+		pageout_size = 4 * hpage_pmd_size;
+		pageout_size = pageout_size < (16UL << 20) ? (16UL << 20) :
+			       pageout_size > (64UL << 20) ? (64UL << 20) :
+			       pageout_size;
+		pageout_area = mmap(NULL, pageout_size, PROT_READ | PROT_WRITE,
+				    MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+		if (pageout_area == MAP_FAILED)
+			ksft_exit_fail_perror("mmap() pageout area");
+	}
+
 	/* Populate so the first pass has something to collapse. */
 	for (i = 0; i < nr_shared_areas * hpage_pmd_size / page_size; i++)
 		*(unsigned int *)(region + i * page_size) = pattern(i);
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 83a04b1e2520..f826cf940c3d 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -415,6 +415,8 @@ CATEGORY="thp" run_test ./khugepaged_race -d 5 -m madvise
 
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped -z
 
+CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped -p
+
 CATEGORY="thp" run_test ./khugepaged
 
 CATEGORY="thp" run_test ./khugepaged -s 2
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.