[PATCH v3 17/18] selftests/mm: add memory-pressure threads to the khugepaged race harness

From: Kiryl Shutsemau

Date: Wed Aug 12 2026 - 09:35:01 EST


From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>

The harness races collapse against faults, pins, fork, mremap and
MADV_DONTNEED, but nothing in it elevates a source folio's refcount from
the reclaim or compaction side.

Add two more threads, and run every mode and occupancy limit both with and
without them:

- pageout: cycles MADV_PAGEOUT over a dedicated neighbour region, faults
it back in and checks the content each round, since a page's pattern
must survive the trip through swap. Idle 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.

-p narrows a run to the combinations that have them, the way -z narrows
the occupancy and -m the mode. Each result says which it ran:

ok 2 stepped/strict/pressure: 5s, 88 steps, no corruption

Assisted-by: Claude-Code:claude-opus-5
Tested-by: Muhammad Usama Anjum <usama.anjum@xxxxxxx>
Signed-off-by: Kiryl Shutsemau (Meta) <kas@xxxxxxxxxx>
---
tools/testing/selftests/mm/khugepaged_race.c | 158 +++++++++++++++++--
1 file changed, 144 insertions(+), 14 deletions(-)

diff --git a/tools/testing/selftests/mm/khugepaged_race.c b/tools/testing/selftests/mm/khugepaged_race.c
index ba4f5c36bf88..bde36af03e49 100644
--- a/tools/testing/selftests/mm/khugepaged_race.c
+++ b/tools/testing/selftests/mm/khugepaged_race.c
@@ -18,6 +18,12 @@
* zero-filled, and re-checked under the page table lock at install time in
* case a racing fault got there first.
*
+ * -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. The faulters and the fork
* children check that continuously, a final sweep checks it once more, plus
@@ -61,6 +67,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;
@@ -219,6 +227,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;
@@ -230,11 +302,14 @@ 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"
"\tWithout -m, every mode runs in turn.\n"
"\t-d: seconds per mode (default 5)\n"
"\tBoth occupancy limits run unless -z asks for holes only.\n"
"\t-z: only max_ptes_none = HPAGE_PMD_NR - 1 (hole-heavy)\n"
+ "\tRuns with and without memory pressure unless -p asks for\n"
+ "\tpressure only.\n"
+ "\t-p: only with the pageout and compaction threads\n"
"\t-a: number of shared PMD-sized playground areas (default 3)\n");
exit(1);
}
@@ -243,18 +318,22 @@ 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)];
static const char * const all_modes[] = { "stepped", "free", "madvise" };
static const int all_nones[] = { 0, 1 }; /* strict, holes */
+ static const int all_press[] = { 0, 1 }; /* quiet, under pressure */
const int *nones = all_nones;
+ const int *press = all_press;
int nr_nones = ARRAY_SIZE(all_nones);
+ int nr_press = ARRAY_SIZE(all_press);
const char *one_mode[1];
const char * const *modes = all_modes;
int nr_modes = ARRAY_SIZE(all_modes);
@@ -263,13 +342,15 @@ int main(int argc, char **argv)
unsigned long end_ms;
int duration_s = 5;
unsigned long thread_mask = ~0UL;
+ unsigned long base_mask;
int nr_areas_arg = 0;
bool holes_only = false;
+ bool pressure_only = 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);
@@ -287,6 +368,9 @@ int main(int argc, char **argv)
case 'z':
holes_only = true;
break;
+ case 'p':
+ pressure_only = true;
+ break;
default:
usage();
}
@@ -296,6 +380,11 @@ int main(int argc, char **argv)
nr_nones = 1;
}

+ if (pressure_only) {
+ press = all_press + 1;
+ nr_press = 1;
+ }
+
if (mode_arg) {
if (strcmp(mode_arg, "stepped") && strcmp(mode_arg, "free") &&
strcmp(mode_arg, "madvise"))
@@ -335,7 +424,12 @@ int main(int argc, char **argv)
-1, 0) != (void *)mremap_scratch)
ksft_exit_fail_perror("mmap() mremap scratch");

- ksft_set_plan(nr_modes * nr_nones);
+ base_mask = thread_mask;
+ if (!swap_available())
+ /* No swap, no anon reclaim: compaction-only pressure. */
+ ksft_print_msg("no swap: the pageout thread stays idle\n");
+
+ ksft_set_plan(nr_modes * nr_nones * nr_press);

thp_save_settings();
thp_read_settings(&settings);
@@ -347,9 +441,17 @@ int main(int argc, char **argv)
*/
thp_push_settings(&settings);

- for (int mn = 0; mn < nr_modes * nr_nones; mn++) {
- const char *mode = modes[mn / nr_nones];
- bool holes = nones[mn % nr_nones];
+ for (int run = 0; run < nr_modes * nr_nones * nr_press; run++) {
+ int rem = run % (nr_nones * nr_press);
+ const char *mode = modes[run / (nr_nones * nr_press)];
+ bool holes = nones[rem / nr_press];
+ bool pressure = press[rem % nr_press];
+
+ thread_mask = base_mask;
+ if (!pressure)
+ thread_mask &= ~(pageout_bit | compactor_bit);
+ else if (!swap_available())
+ thread_mask &= ~pageout_bit;

thp_read_settings(&settings);
settings.thp_enabled = THP_MADVISE;
@@ -385,6 +487,24 @@ 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) {
+ /*
+ * 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);
@@ -434,8 +554,9 @@ int main(int argc, char **argv)
check_page(i);

ksft_test_result(!corrupted,
- "%s/%s: %ds, %d steps, no corruption\n",
+ "%s/%s%s: %ds, %d steps, no corruption\n",
mode, holes ? "holes" : "strict",
+ pressure ? "/pressure" : "",
duration_s, steps);

/*
@@ -444,17 +565,26 @@ int main(int argc, char **argv)
* and its scan cadence differs.
*/
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 (++mn < nr_modes * nr_nones)
- ksft_test_result_skip("%s/%s: skipped after corruption\n",
- modes[mn / nr_nones],
- nones[mn % nr_nones] ?
- "holes" : "strict");
+ while (++run < nr_modes * nr_nones * nr_press) {
+ rem = run % (nr_nones * nr_press);
+
+ ksft_test_result_skip("%s/%s%s: skipped after corruption\n",
+ modes[run / (nr_nones * nr_press)],
+ nones[rem / nr_press] ?
+ "holes" : "strict",
+ press[rem % nr_press] ?
+ "/pressure" : "");
+ }
break;
}
}
--
2.54.0