[PATCH v5 18/19] selftests/mm: add memory-pressure threads to the khugepaged race harness
From: Kiryl Shutsemau
Date: Tue Sep 08 2026 - 10:21:36 EST
From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>
The harness races collapse against faults, pins, fork, mremap and
MADV_DONTNEED, but nothing in it runs reclaim or compaction against the
collapse.
Add two more threads, and run every mode and occupancy limit both with and
without them:
- pageout: cycles MADV_PAGEOUT over a region of its own, faults it back
in and checks the content each round, since a page's pattern must
survive the trip through swap. Left out when the host has no swap,
because then there is no anon reclaim to drive.
- compactor: writes /proc/sys/vm/compact_memory in a loop. Compaction
isolates and migrates folios, so it competes with a collapse for the
pages it is gathering, with refcount elevations and migration entries
of its own.
Each result says whether it ran under pressure:
ok 2 stepped/strict/pressure: 5s, 88 steps, no corruption
A full run is now twelve combinations; -m picks one mode, -d shortens
each run.
Assisted-by: LLM
Tested-by: Muhammad Usama Anjum <usama.anjum@xxxxxxx>
Signed-off-by: Kiryl Shutsemau (Meta) <kas@xxxxxxxxxx>
---
tools/testing/selftests/mm/khugepaged_race.c | 142 ++++++++++++++++---
1 file changed, 122 insertions(+), 20 deletions(-)
diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index f31f12fc0390..1f4aa23834db 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -44,6 +44,8 @@ static unsigned long page_size;
static char *region;
static char *mremap_area;
static char *mremap_scratch;
+static char *pageout_area;
+static size_t pageout_size;
static int gup_fd = -1;
static volatile int stop;
static volatile int corrupted;
@@ -199,6 +201,69 @@ static void *mremapper_fn(void *arg)
return NULL;
}
+/*
+ * Swap traffic and LRU churn on a region nothing else writes, so a page's
+ * pattern must survive the trip through swap exactly.
+ */
+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;
+}
+
+/* Compaction migrates the collapse sources while they are being gathered */
+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;
@@ -222,17 +287,23 @@ 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,
};
+ enum { T_FAULTER, T_FAULTER2, T_DONTNEED, T_PINNER, T_FORKER,
+ T_MREMAPPER, T_PAGEOUT, T_COMPACTOR };
+ const unsigned long pageout_bit = 1UL << T_PAGEOUT;
+ const unsigned long compactor_bit = 1UL << T_COMPACTOR;
const int nr_threads = ARRAY_SIZE(thread_names);
pthread_t threads[ARRAY_SIZE(thread_names)];
static const char * const all_modes[] = { "stepped", "free", "madvise" };
static const bool occupancies[] = { false, true }; /* strict, holes */
+ static const bool pressures[] = { false, true }; /* quiet, under pressure */
const int nr_occupancies = ARRAY_SIZE(occupancies);
+ const int nr_pressures = ARRAY_SIZE(pressures);
const char *one_mode[1];
const char * const *modes = all_modes;
int nr_modes = ARRAY_SIZE(all_modes);
@@ -241,6 +312,9 @@ int main(int argc, char **argv)
unsigned long end_ms;
int duration_s = 5;
unsigned long thread_mask = ~0UL;
+ unsigned long base_mask;
+ bool have_swap;
+ char label[64];
int nr_areas_arg = 0;
unsigned long i;
int steps = 0;
@@ -300,7 +374,13 @@ int main(int argc, char **argv)
-1, 0) != (void *)mremap_scratch)
ksft_exit_fail_perror("mmap() mremap scratch");
- ksft_set_plan(nr_modes * nr_occupancies);
+ base_mask = thread_mask;
+ have_swap = swap_available();
+ if (!have_swap)
+ /* No swap, no anon reclaim: compaction-only pressure */
+ ksft_print_msg("no swap: the pageout thread is not started\n");
+
+ ksft_set_plan(nr_modes * nr_occupancies * nr_pressures);
thp_save_settings();
thp_read_settings(&settings);
@@ -308,9 +388,25 @@ int main(int argc, char **argv)
/* Base of the settings stack; the bottom entry is never popped */
thp_push_settings(&settings);
- for (int run = 0; run < nr_modes * nr_occupancies; run++) {
- const char *mode = modes[run / nr_occupancies];
- bool holes = occupancies[run % nr_occupancies];
+ for (int run = 0; run < nr_modes * nr_occupancies * nr_pressures; run++) {
+ int rem = run % (nr_occupancies * nr_pressures);
+ const char *mode = modes[run / (nr_occupancies * nr_pressures)];
+ bool holes = occupancies[rem / nr_pressures];
+ bool pressure = pressures[rem % nr_pressures];
+
+ snprintf(label, sizeof(label), "%s/%s%s", mode,
+ holes ? "holes" : "strict", pressure ? "/pressure" : "");
+ if (corrupted) {
+ /* Memory is suspect; the rest would prove nothing */
+ ksft_test_result_skip("%s: skipped after corruption\n", label);
+ continue;
+ }
+
+ thread_mask = base_mask;
+ if (!pressure)
+ thread_mask &= ~(pageout_bit | compactor_bit);
+ else if (!have_swap)
+ thread_mask &= ~pageout_bit;
thp_read_settings(&settings);
settings.thp_enabled = THP_MADVISE;
@@ -344,6 +440,20 @@ int main(int argc, char **argv)
ksft_exit_fail_perror("mmap() playground");
mremap_area = region + nr_shared_areas * hpage_pmd_size;
+ if (thread_mask & pageout_bit) {
+ /* Enough to drive real reclaim without swamping a small guest */
+ pageout_size = 4 * hpage_pmd_size;
+ if (pageout_size < 16UL << 20)
+ pageout_size = 16UL << 20;
+ if (pageout_size > 64UL << 20)
+ pageout_size = 64UL << 20;
+ 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);
@@ -392,26 +502,18 @@ int main(int argc, char **argv)
for (i = 0; i < nr_shared_areas * hpage_pmd_size / page_size; i++)
check_page(i);
- ksft_test_result(!corrupted,
- "%s/%s: %ds, %d steps, no corruption\n",
- mode, holes ? "holes" : "strict",
- duration_s, steps);
+ ksft_test_result(!corrupted, "%s: %ds, %d steps, no corruption\n",
+ label, duration_s, steps);
/* The next mode maps the same fixed address with its own settings */
munmap(region, nr_areas * hpage_pmd_size);
+ if (pageout_area) {
+ munmap(pageout_area, pageout_size);
+ pageout_area = NULL;
+ }
thp_pop_settings();
stop = 0;
steps = 0;
-
- if (corrupted) {
- /* Memory is suspect; the rest would prove nothing */
- while (++run < nr_modes * nr_occupancies)
- ksft_test_result_skip("%s/%s: skipped after corruption\n",
- modes[run / nr_occupancies],
- occupancies[run % nr_occupancies] ?
- "holes" : "strict");
- break;
- }
}
ksft_finished();
--
2.54.0