MADV_COLD and MADV_PAGEOUT share a page-table walker covering ordinary PTEs, PTE-mapped large folios and huge PMDs. Existing selftests provide little coverage of its range, folio-state and permission decisions.
Add a kselftest harness test for both hints, starting with the most basic behavior userspace relies on: MADV_COLD neither populates a PTE hole nor faults a swapped page back in, and neither hint may be applied to a locked VMA. The swapped-page test skips when swap is unavailable. Signed-off-by: Gregory Price (Meta) <[email protected]> --- tools/testing/selftests/mm/Makefile | 2 + tools/testing/selftests/mm/ksft_madvise.sh | 4 + tools/testing/selftests/mm/madvise.c | 134 +++++++++++++++++++++ tools/testing/selftests/mm/run_vmtests.sh | 5 + 4 files changed, 145 insertions(+) create mode 100755 tools/testing/selftests/mm/ksft_madvise.sh create mode 100644 tools/testing/selftests/mm/madvise.c diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile index beacc0f87304..07d61110eb24 100644 --- a/tools/testing/selftests/mm/Makefile +++ b/tools/testing/selftests/mm/Makefile @@ -69,6 +69,7 @@ TEST_GEN_FILES += hugetlb-shm TEST_GEN_FILES += hugetlb-soft-offline TEST_GEN_FILES += khugepaged TEST_GEN_FILES += madv_populate +TEST_GEN_FILES += madvise TEST_GEN_FILES += map_fixed_noreplace TEST_GEN_FILES += map_populate ifneq (,$(filter $(ARCH),arm64 riscv riscv64 x86 x86_64 loongarch32 loongarch64)) @@ -156,6 +157,7 @@ TEST_PROGS += ksft_ksm.sh TEST_PROGS += ksft_ksm_numa.sh TEST_PROGS += ksft_madv_guard.sh TEST_PROGS += ksft_madv_populate.sh +TEST_PROGS += ksft_madvise.sh TEST_PROGS += ksft_memfd_secret.sh TEST_PROGS += ksft_memory_failure.sh TEST_PROGS += ksft_migration.sh diff --git a/tools/testing/selftests/mm/ksft_madvise.sh b/tools/testing/selftests/mm/ksft_madvise.sh new file mode 100755 index 000000000000..29cbbca800f2 --- /dev/null +++ b/tools/testing/selftests/mm/ksft_madvise.sh @@ -0,0 +1,4 @@ +#!/bin/sh -e +# SPDX-License-Identifier: GPL-2.0 + +./run_vmtests.sh -t madvise diff --git a/tools/testing/selftests/mm/madvise.c b/tools/testing/selftests/mm/madvise.c new file mode 100644 index 000000000000..bb4efb92e6da --- /dev/null +++ b/tools/testing/selftests/mm/madvise.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * MADV_COLD and MADV_PAGEOUT tests. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <sys/mman.h> +#include <unistd.h> + +#include "kselftest_harness.h" +#include "vm_util.h" + +/* Reclaim can fail transiently, so retry for up to a second. */ +#define PAGEOUT_RETRIES 100 + +static size_t page_size; + +FIXTURE(madvise) +{ + int pagemap_fd; +}; + +FIXTURE_SETUP(madvise) +{ + page_size = getpagesize(); + + self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY); + ASSERT_GE(self->pagemap_fd, 0); +} + +FIXTURE_TEARDOWN(madvise) +{ + close(self->pagemap_fd); +} + +/* Page out the range until its first page is swapped. */ +static bool pageout_until_swapped(struct __test_metadata *_metadata, + char *mapping, size_t size, int pagemap_fd) +{ + int retry; + + for (retry = 0; retry < PAGEOUT_RETRIES; retry++) { + ASSERT_EQ(madvise(mapping, size, MADV_PAGEOUT), 0); + if (pagemap_is_swapped(pagemap_fd, mapping)) + return true; + usleep(10000); + } + + return false; +} + +/* + * Both hints are best effort, so these tests check what userspace can rely + * on: which mappings are accepted, which pages must never be reclaimed, and + * that neither hint changes memory contents or populates the range. A test + * that needs a page to be reclaimed first checks that reclaim works at all, + * and skips if it does not. + */ + +/* Neither hint may populate a hole between two populated pages. */ +static void check_skips_pte_hole(struct __test_metadata *_metadata, + int pagemap_fd, int advice) +{ + char *mapping; + + mapping = mmap(NULL, 3 * page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(mapping, MAP_FAILED); + mapping[0] = 1; + mapping[2 * page_size] = 1; + ASSERT_FALSE(pagemap_is_populated(pagemap_fd, mapping + page_size)); + + ASSERT_EQ(madvise(mapping, 3 * page_size, advice), 0); + ASSERT_FALSE(pagemap_is_populated(pagemap_fd, mapping + page_size)); + ASSERT_EQ(mapping[0], 1); + ASSERT_EQ(mapping[2 * page_size], 1); + + ASSERT_EQ(munmap(mapping, 3 * page_size), 0); +} + +TEST_F(madvise, cold_skips_pte_hole) +{ + check_skips_pte_hole(_metadata, self->pagemap_fd, MADV_COLD); +} + +/* Neither hint may be applied to locked memory. */ +TEST_F(madvise, locked_vma_is_rejected) +{ + char *mapping; + + mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(mapping, MAP_FAILED); + mapping[0] = 1; + if (mlock(mapping, page_size)) + SKIP(return, "could not lock a page"); + + ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), -1); + ASSERT_EQ(errno, EINVAL); + ASSERT_EQ(madvise(mapping, page_size, MADV_PAGEOUT), -1); + ASSERT_EQ(errno, EINVAL); + ASSERT_EQ(mapping[0], 1); + + ASSERT_EQ(munmap(mapping, page_size), 0); +} + +/* Neither hint may fault a swapped page back in. */ +static void check_skips_swapped_pte(struct __test_metadata *_metadata, + int pagemap_fd, int advice) +{ + char *mapping; + + mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(mapping, MAP_FAILED); + mapping[0] = 1; + if (!pageout_until_swapped(_metadata, mapping, page_size, pagemap_fd)) + SKIP(return, "MADV_PAGEOUT did not swap the page"); + + ASSERT_EQ(madvise(mapping, page_size, advice), 0); + ASSERT_TRUE(pagemap_is_swapped(pagemap_fd, mapping)); + ASSERT_EQ(mapping[0], 1); + + ASSERT_EQ(munmap(mapping, page_size), 0); +} + +TEST_F(madvise, cold_skips_swapped_pte) +{ + check_skips_swapped_pte(_metadata, self->pagemap_fd, MADV_COLD); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh index a1b45a3dedae..0e6a24659d12 100755 --- a/tools/testing/selftests/mm/run_vmtests.sh +++ b/tools/testing/selftests/mm/run_vmtests.sh @@ -53,6 +53,8 @@ separated by spaces: test madvise(2) MADV_GUARD_INSTALL and MADV_GUARD_REMOVE options - madv_populate test memadvise(2) MADV_POPULATE_{READ,WRITE} options +- madvise + test madvise(2) MADV_COLD and MADV_PAGEOUT - memfd_secret test memfd_secret(2) - process_mrelease @@ -330,6 +332,9 @@ CATEGORY="madv_guard" run_test ./guard-regions # MADV_POPULATE_READ and MADV_POPULATE_WRITE tests CATEGORY="madv_populate" run_test ./madv_populate +# MADV_COLD and MADV_PAGEOUT tests +CATEGORY="madvise" run_test ./madvise + # PROCESS_MADV test CATEGORY="process_madv" run_test ./process_madv -- 2.56.0

