[PATCH v5 15/19] selftests/mm: check that one khugepaged pass collapses one window

From: Kiryl Shutsemau

Date: Tue Sep 08 2026 - 09:03:16 EST


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

khugepaged_full_pass() drives the daemon through sysfs: a store to
scan_sleep_millisecs wakes it, and full_scans advancing by two marks one
pass that started after setup. Every mTHP collapse result in the suite
rests on that pair, and nothing checks it.

Add khugepaged_sync_check. Each step:

- prepare one aligned window
- record its source PFNs from pagemap
- run one khugepaged_full_pass() barrier
- require the window came out collapsed, with exactly one collapse
attempt attributed to it

The anon events carry no virtual address, so an attempt is matched by the
source folio PFN and order that the mm_collapse_huge_page_isolate
tracepoint reports.

Reading the trace buffer takes four small helpers in vm_util: open an
event subsystem's enable file, flip it, clear the buffer, and open it for
reading.

scan_sleep_millisecs is set to a minute, so a step that took a sleep
instead of a wake would blow the budget.

Passes 5/5 on x86-64 4K and arm64 64K.

Assisted-by: LLM
Tested-by: Muhammad Usama Anjum <usama.anjum@xxxxxxx>
Signed-off-by: Kiryl Shutsemau (Meta) <kas@xxxxxxxxxx>
---
tools/testing/selftests/mm/Makefile | 1 +
.../selftests/mm/khugepaged_sync_check.c | 179 ++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 2 +
tools/testing/selftests/mm/vm_util.c | 38 ++++
tools/testing/selftests/mm/vm_util.h | 4 +
5 files changed, 224 insertions(+)
create mode 100644 tools/testing/selftests/mm/khugepaged_sync_check.c

diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 2093fcf6e915..b2d6e5c12934 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -105,6 +105,7 @@ TEST_GEN_FILES += merge
TEST_GEN_FILES += rmap
TEST_GEN_FILES += folio_split_race_test
TEST_GEN_FILES += folio_order_check
+TEST_GEN_FILES += khugepaged_sync_check

ifneq ($(ARCH),arm64)
TEST_GEN_FILES += soft-dirty
diff --git a/tools/testing/selftests/mm/khugepaged_sync_check.c b/tools/testing/selftests/mm/khugepaged_sync_check.c
new file mode 100644
index 000000000000..1c1b942ac325
--- /dev/null
+++ b/tools/testing/selftests/mm/khugepaged_sync_check.c
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Check that khugepaged_full_pass() drives khugepaged in step: one barrier
+ * over one prepared window must collapse it with exactly one collapse
+ * attempt attributed to its source pages, step after step.
+ *
+ * scan_sleep_millisecs is a minute so that a step which slept instead of
+ * being woken blows the budget.
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+#include "hugepage_settings.h"
+
+#define BASE_ADDR ((void *)(1UL << 30))
+/* Smallest order khugepaged considers */
+#define TARGET_ORDER 2
+#define NR_ITERATIONS 5
+#define PASS_TIMEOUT_S 30
+
+static int pagemap_fd;
+static int kpageflags_fd;
+static int trace_events_fd = -1;
+static unsigned long hpage_pmd_size;
+
+/*
+ * The events are system-wide: switch them off however the test ends,
+ * including from inside a helper that gives up.
+ */
+static void trace_events_off(void)
+{
+ if (trace_events_fd >= 0)
+ tracing_events_enable(trace_events_fd, false);
+}
+
+/* Count the isolate events whose scan_pfn is one of the window's source PFNs */
+static int count_attributed(unsigned long *pfns, int nr_pfns,
+ unsigned int order)
+{
+ char line[1024];
+ int count = 0;
+ FILE *fp;
+
+ fp = tracing_open_trace();
+ if (!fp)
+ ksft_exit_fail_msg("Cannot open trace buffer\n");
+
+ while (fgets(line, sizeof(line), fp)) {
+ unsigned long val;
+ unsigned int ord;
+ char *s, *o;
+ int i;
+
+ s = strstr(line, "mm_collapse_huge_page_isolate:");
+ if (!s)
+ continue;
+ if (sscanf(s, "mm_collapse_huge_page_isolate: scan_pfn=0x%lx",
+ &val) != 1)
+ continue;
+ o = strstr(s, "order=");
+ if (!o || sscanf(o, "order=%u", &ord) != 1 || ord != order)
+ continue;
+ for (i = 0; i < nr_pfns; i++) {
+ if (val == pfns[i]) {
+ count++;
+ break;
+ }
+ }
+ }
+ fclose(fp);
+ return count;
+}
+
+static void one_step(int iteration)
+{
+ const size_t window = getpagesize() << TARGET_ORDER;
+ const int nr_pages = 1 << TARGET_ORDER;
+ unsigned long pfns[1 << TARGET_ORDER];
+ bool collapsed, passed;
+ int attributed;
+ char *p;
+ int i;
+
+ p = mmap(BASE_ADDR, hpage_pmd_size, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED_NOREPLACE, -1, 0);
+ if (p != BASE_ADDR)
+ ksft_exit_fail_perror("mmap() window");
+
+ for (i = 0; i < nr_pages; i++) {
+ p[i * getpagesize()] = i + 1;
+ pfns[i] = pagemap_get_pfn(pagemap_fd, p + i * getpagesize());
+ if (pfns[i] == -1UL)
+ ksft_exit_fail_msg("Source page not present\n");
+ }
+
+ /* Clear before enabling so the buffer holds only this step's events */
+ if (tracing_clear_trace())
+ ksft_exit_fail_msg("Cannot clear the trace buffer\n");
+ if (tracing_events_enable(trace_events_fd, true))
+ ksft_exit_fail_msg("Cannot enable huge_memory events\n");
+
+ if (madvise(p, hpage_pmd_size, MADV_HUGEPAGE))
+ ksft_exit_fail_perror("madvise(MADV_HUGEPAGE)");
+ passed = khugepaged_full_pass(PASS_TIMEOUT_S);
+
+ /* Off before anything that can give up: the events are system-wide */
+ if (tracing_events_enable(trace_events_fd, false))
+ ksft_exit_fail_msg("Cannot disable huge_memory events\n");
+ if (!passed)
+ ksft_exit_fail_msg("khugepaged did not complete a full pass\n");
+
+ collapsed = is_range_backed_by_order(p, window, TARGET_ORDER,
+ pagemap_fd, kpageflags_fd);
+ attributed = count_attributed(pfns, nr_pages, TARGET_ORDER);
+
+ ksft_test_result(collapsed && attributed == 1,
+ "step %d: window collapsed, %d attributed result(s)\n",
+ iteration, attributed);
+
+ munmap(p, hpage_pmd_size);
+}
+
+int main(void)
+{
+ struct thp_settings settings;
+ int i;
+
+ ksft_print_header();
+
+ if (!thp_available())
+ ksft_exit_skip("Transparent Hugepages not available\n");
+ if (!(thp_supported_orders() & (1UL << TARGET_ORDER)))
+ ksft_exit_skip("Order %d is not a supported anon THP order\n",
+ TARGET_ORDER);
+
+ hpage_pmd_size = read_pmd_pagesize();
+ if (!hpage_pmd_size)
+ ksft_exit_fail_msg("Reading PMD pagesize failed\n");
+ pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+ if (pagemap_fd < 0)
+ ksft_exit_fail_perror("open(/proc/self/pagemap)");
+ kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+ if (kpageflags_fd < 0)
+ ksft_exit_skip("open(/proc/kpageflags) requires root\n");
+ trace_events_fd = tracing_events_open("huge_memory");
+ if (trace_events_fd < 0)
+ ksft_exit_skip("huge_memory events require tracefs and root\n");
+ atexit(trace_events_off);
+
+ ksft_set_plan(NR_ITERATIONS);
+
+ thp_save_settings();
+ thp_read_settings(&settings);
+ settings.thp_enabled = THP_MADVISE;
+ settings.thp_defrag = THP_DEFRAG_ALWAYS;
+ settings.khugepaged.defrag = 1;
+ settings.khugepaged.scan_sleep_millisecs = 60 * 1000;
+ settings.khugepaged.alloc_sleep_millisecs = 60 * 1000;
+ settings.khugepaged.max_ptes_none = (hpage_pmd_size / getpagesize()) - 1;
+ /* One wake must complete one full pass; see khugepaged_full_pass() */
+ settings.khugepaged.pages_to_scan = 1UL << 24;
+ for (i = 0; i < NR_ORDERS; i++)
+ settings.hugepages[i].enabled = THP_NEVER;
+ settings.hugepages[TARGET_ORDER].enabled = THP_INHERIT;
+ /* Base of the settings stack; the bottom entry is never popped */
+ thp_push_settings(&settings);
+
+ for (i = 0; i < NR_ITERATIONS; i++)
+ one_step(i);
+
+ ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 8bf898b71350..c0f69da3fd3b 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -404,6 +404,8 @@ CATEGORY="cow" run_test ./cow

CATEGORY="thp" run_test ./folio_order_check

+CATEGORY="thp" run_test ./khugepaged_sync_check
+
CATEGORY="thp" run_test ./khugepaged

CATEGORY="thp" run_test ./khugepaged -s 2
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index 4947612e8b3d..af8324e1e8f2 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -599,6 +599,44 @@ bool is_range_backed_by_order(char *start, size_t len, int order,
return true;
}

+#define TRACEFS_ROOT "/sys/kernel/tracing"
+
+/*
+ * Returns -1 without tracefs or the subsystem. The events are system-wide:
+ * whoever switches them on has to switch them off again, on every exit path.
+ */
+int tracing_events_open(const char *subsys)
+{
+ char path[256];
+
+ snprintf(path, sizeof(path), TRACEFS_ROOT "/events/%s/enable",
+ subsys);
+ return open(path, O_WRONLY);
+}
+
+int tracing_events_enable(int fd, bool enable)
+{
+ if (pwrite(fd, enable ? "1" : "0", 1, 0) != 1)
+ return -1;
+ return 0;
+}
+
+/* Drop what the trace buffer holds so far */
+int tracing_clear_trace(void)
+{
+ int fd = open(TRACEFS_ROOT "/trace", O_WRONLY | O_TRUNC);
+
+ if (fd < 0)
+ return -1;
+ close(fd);
+ return 0;
+}
+
+FILE *tracing_open_trace(void)
+{
+ return fopen(TRACEFS_ROOT "/trace", "r");
+}
+
/* If `ioctls' non-NULL, the allowed ioctls will be returned into the var */
int uffd_register_with_ioctls(int uffd, void *addr, uint64_t len,
bool miss, bool wp, bool minor, uint64_t *ioctls)
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index 3be430e01901..5a91b9676ec5 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -119,6 +119,10 @@ int close_procmap(struct procmap_fd *procmap);
int write_sysfs(const char *file_path, unsigned long val);
int read_sysfs(const char *file_path, unsigned long *val);
bool softdirty_supported(void);
+int tracing_events_open(const char *subsys);
+int tracing_events_enable(int fd, bool enable);
+int tracing_clear_trace(void);
+FILE *tracing_open_trace(void);

static inline int open_self_procmap(struct procmap_fd *procmap_out)
{
--
2.54.0