Both hints are non-destructive, but the tests so far only check that for
private anonymous memory. Shared memory reaches reclaim through different
paths: a dirty shared file page must be written back or left resident,
shared anonymous memory is paged out through shmem, and a SHM_LOCK'd
segment is unevictable without its VMA being VM_LOCKED.

Check that MADV_COLD and MADV_PAGEOUT preserve a dirty shared file page,
that MADV_PAGEOUT preserves a shared anonymous page, and that it leaves a
SHM_LOCK'd page resident. The SHM_LOCK test needs root to confirm through
/proc/kpageflags that the page is unevictable, and skips if it is not.

Signed-off-by: Gregory Price (Meta) <[email protected]>
---
 tools/testing/selftests/mm/madvise.c | 138 +++++++++++++++++++++++++++
 tools/testing/selftests/mm/vm_util.h |   1 +
 2 files changed, 139 insertions(+)

diff --git a/tools/testing/selftests/mm/madvise.c 
b/tools/testing/selftests/mm/madvise.c
index bd78f63f1314..0851f5428e3d 100644
--- a/tools/testing/selftests/mm/madvise.c
+++ b/tools/testing/selftests/mm/madvise.c
@@ -7,7 +7,10 @@
 #include <fcntl.h>
 #include <sched.h>
 #include <stdbool.h>
+#include <stdint.h>
+#include <sys/ipc.h>
 #include <sys/mman.h>
+#include <sys/shm.h>
 #include <unistd.h>
 
 #include "kselftest_harness.h"
@@ -21,6 +24,8 @@ static size_t page_size;
 FIXTURE(madvise)
 {
        int pagemap_fd;
+       /* Opening it needs CAP_SYS_ADMIN, -1 otherwise. */
+       int kpageflags_fd;
 };
 
 FIXTURE_SETUP(madvise)
@@ -29,10 +34,13 @@ FIXTURE_SETUP(madvise)
 
        self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
        ASSERT_GE(self->pagemap_fd, 0);
+       self->kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
 }
 
 FIXTURE_TEARDOWN(madvise)
 {
+       if (self->kpageflags_fd >= 0)
+               close(self->kpageflags_fd);
        close(self->pagemap_fd);
 }
 
@@ -52,6 +60,22 @@ static bool pageout_until_swapped(struct __test_metadata 
*_metadata,
        return false;
 }
 
+/* Page out the range until its first page is no longer mapped. */
+static bool pageout_until_evicted(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_populated(pagemap_fd, mapping))
+                       return true;
+               usleep(10000);
+       }
+
+       return false;
+}
+
 /*
  * Keep this task on one CPU, so that draining that CPU's LRU batches drains
  * the folios this task faulted in.
@@ -67,6 +91,25 @@ static void pin_to_current_cpu(struct __test_metadata 
*_metadata)
        ASSERT_EQ(sched_setaffinity(0, sizeof(mask), &mask), 0);
 }
 
+/* Map a shared mapping of an unlinked, zero-filled temporary file. */
+static char *map_temp_file(struct __test_metadata *_metadata, size_t size)
+{
+       char template[] = "/tmp/madvise-pageout-XXXXXX";
+       char *mapping;
+       int fd;
+
+       fd = mkstemp(template);
+       ASSERT_GE(fd, 0);
+       ASSERT_EQ(unlink(template), 0);
+       ASSERT_EQ(ftruncate(fd, size), 0);
+
+       mapping = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+       ASSERT_NE(mapping, MAP_FAILED);
+       close(fd);
+
+       return mapping;
+}
+
 /*
  * 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
@@ -267,4 +310,99 @@ TEST_F(madvise, pageout_skips_swapped_pte)
        check_skips_swapped_pte(_metadata, self->pagemap_fd, MADV_PAGEOUT);
 }
 
+/* MADV_COLD must not discard a dirty page of a shared file mapping. */
+TEST_F(madvise, cold_preserves_dirty_shared_file_page)
+{
+       char *mapping;
+
+       mapping = map_temp_file(_metadata, page_size);
+       mapping[0] = 1;
+
+       ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), 0);
+       ASSERT_EQ(mapping[0], 1);
+
+       ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/*
+ * MADV_PAGEOUT may write a dirty shared file page back or leave it resident,
+ * but must not discard it.
+ */
+TEST_F(madvise, pageout_preserves_dirty_shared_file_page)
+{
+       char *mapping;
+
+       mapping = map_temp_file(_metadata, page_size);
+       mapping[0] = 1;
+
+       if (!pageout_until_evicted(_metadata, mapping, page_size,
+                                  self->pagemap_fd))
+               SKIP(return, "MADV_PAGEOUT did not unmap the page");
+       ASSERT_EQ(mapping[0], 1);
+
+       ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/* MADV_PAGEOUT must not lose the contents of shared anonymous memory. */
+TEST_F(madvise, pageout_preserves_shared_anon_page)
+{
+       char *mapping;
+
+       mapping = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+                      MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+       ASSERT_NE(mapping, MAP_FAILED);
+       mapping[0] = 1;
+
+       if (!pageout_until_evicted(_metadata, mapping, page_size,
+                                  self->pagemap_fd))
+               SKIP(return, "MADV_PAGEOUT did not evict the page");
+       ASSERT_EQ(mapping[0], 1);
+
+       ASSERT_EQ(munmap(mapping, page_size), 0);
+}
+
+/*
+ * SHM_LOCK makes shmem pages unevictable without setting VM_LOCKED on the
+ * VMA, so MADV_PAGEOUT accepts the range but must not reclaim the page.
+ */
+TEST_F(madvise, pageout_skips_unevictable_page)
+{
+       unsigned long pfn;
+       uint64_t flags;
+       char *mapping;
+       int shmid;
+
+       if (self->kpageflags_fd < 0)
+               SKIP(return, "requires root to read page flags");
+       shmid = shmget(IPC_PRIVATE, page_size, IPC_CREAT | 0600);
+       ASSERT_GE(shmid, 0);
+       if (shmctl(shmid, SHM_LOCK, NULL)) {
+               shmctl(shmid, IPC_RMID, NULL);
+               SKIP(return, "could not lock a shmem segment");
+       }
+       mapping = shmat(shmid, NULL, 0);
+       /* The segment is destroyed once it is detached. */
+       shmctl(shmid, IPC_RMID, NULL);
+       ASSERT_NE(mapping, (void *)-1);
+       pin_to_current_cpu(_metadata);
+       mapping[0] = 1;
+
+       /*
+        * MADV_COLD drains this CPU's LRU add batch, which makes the page
+        * unevictable.
+        */
+       ASSERT_EQ(madvise(mapping, page_size, MADV_COLD), 0);
+       pfn = pagemap_get_pfn(self->pagemap_fd, mapping);
+       ASSERT_NE(pfn, -1UL);
+       ASSERT_EQ(pageflags_get(pfn, self->kpageflags_fd, &flags), 0);
+       if (!(flags & KPF_UNEVICTABLE))
+               SKIP(return, "could not make the page unevictable");
+
+       ASSERT_EQ(madvise(mapping, page_size, MADV_PAGEOUT), 0);
+       ASSERT_TRUE(pagemap_is_populated(self->pagemap_fd, mapping));
+       ASSERT_EQ(mapping[0], 1);
+
+       ASSERT_EQ(shmdt(mapping), 0);
+}
+
 TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/mm/vm_util.h 
b/tools/testing/selftests/mm/vm_util.h
index b5d59729d432..95898e045bac 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -24,6 +24,7 @@
 
 #define KPF_COMPOUND_HEAD             BIT_ULL(15)
 #define KPF_COMPOUND_TAIL             BIT_ULL(16)
+#define KPF_UNEVICTABLE               BIT_ULL(18)
 #define KPF_HWPOISON                  BIT_ULL(19)
 #define KPF_THP                       BIT_ULL(22)
 /*
-- 
2.56.0


Reply via email to