From: "Kiryl Shutsemau (Meta)" 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) --- 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