The tests validate memory.cache file functionality in terms of limiting
the amount of page cache used and being able to correctly account it.

The test makes use of a supplementary program that maps the test file
and accesses it with a random pattern.

https://virtuozzo.atlassian.net/browse/VSTOR-112174

Signed-off-by: Dmitry Sepp <[email protected]>

Feature: mm: Memory cgroup page cache limit
Signed-off-by: Dmitry Sepp <[email protected]>
---
 tools/testing/selftests/cgroup/Makefile      |   4 +-
 tools/testing/selftests/cgroup/test_cache.sh | 254 +++++++++++++++++++
 tools/testing/selftests/cgroup/touch_pages.c |  97 +++++++
 3 files changed, 354 insertions(+), 1 deletion(-)
 create mode 100755 tools/testing/selftests/cgroup/test_cache.sh
 create mode 100644 tools/testing/selftests/cgroup/touch_pages.c

diff --git a/tools/testing/selftests/cgroup/Makefile 
b/tools/testing/selftests/cgroup/Makefile
index 1b897152bab6..05c6f6785ff0 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -5,7 +5,8 @@ all: ${HELPER_PROGS}
 
 TEST_FILES     := with_stress.sh
 TEST_PROGS     := test_stress.sh test_cpuset_prs.sh test_cpuset_v1_hp.sh
-TEST_GEN_FILES := wait_inotify
+TEST_PROGS     += test_cache.sh
+TEST_GEN_FILES := wait_inotify touch_pages
 # Keep the lists lexicographically sorted
 TEST_GEN_PROGS  = test_core
 TEST_GEN_PROGS += test_cpu
@@ -32,3 +33,4 @@ $(OUTPUT)/test_kmem: cgroup_util.c
 $(OUTPUT)/test_memcontrol: cgroup_util.c
 $(OUTPUT)/test_pids: cgroup_util.c
 $(OUTPUT)/test_zswap: cgroup_util.c
+$(OUTPUT)/touch_pages: LDLIBS += -lz
diff --git a/tools/testing/selftests/cgroup/test_cache.sh 
b/tools/testing/selftests/cgroup/test_cache.sh
new file mode 100755
index 000000000000..20b9f7fa239d
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_cache.sh
@@ -0,0 +1,254 @@
+#!/bin/bash
+#
+# Copyright (c) 2025 Virtuozzo International GmbH. All rights reserved.
+#
+# Output directory handling is derived from 
tools/testing/selftests/bpf/vmtest.sh
+#
+
+# shellcheck disable=SC2329
+
+skip_test() {
+       echo "$1"
+       echo "Test SKIPPED"
+       exit 4 # ksft_skip
+}
+
+[[ $(id -u) -eq 0 ]] || skip_test "Test must be run as root!"
+
+cgroup2=$(grep cgroup2 /proc/mounts | cut -d ' ' -f 2)
+
+[[ ! -z "$cgroup2" ]] || skip_test "cgroup v2 isn't mounted"
+
+is_rel_path()
+{
+       local path="$1"
+
+       [[ ${path:0:1} != "/" ]]
+}
+
+out_dir="kselftest/cgroup"
+# Figure out where the kernel is being built.
+# O takes precedence over KBUILD_OUTPUT.
+if [[ "${O:=""}" != "" ]]; then
+       if is_rel_path "${O}"; then
+               O="$(realpath "${PWD}/${O}")"
+       fi
+       touch_pages="${O}/${out_dir}/touch_pages"
+elif [[ "${KBUILD_OUTPUT:=""}" != "" ]]; then
+       if is_rel_path "${KBUILD_OUTPUT}"; then
+               KBUILD_OUTPUT="$(realpath "${PWD}/${KBUILD_OUTPUT}")"
+       fi
+       touch_pages="${KBUILD_OUTPUT}/${out_dir}/touch_pages"
+else
+       touch_pages="./touch_pages"
+fi
+
+tmp_dir=$(mktemp -d)
+bin_file="$tmp_dir/testfile.bin"
+group0_max=$((2**25))
+group1_max=$((2**22))
+ts=$(date +%s%N)
+group0="kselftest0-$ts"
+group1="kselftest1-$ts"
+
+dd if=/dev/urandom of="$bin_file" bs=1M count=1000 2>/dev/null
+sync
+
+mkdir -p "$cgroup2"/{"$group0","$group1"}
+
+cache_get_current() {
+       local group="$1"
+       cat "$cgroup2/$group/memory.cache.current"
+}
+
+cache_get_max() {
+       local group="$1"
+       cat "$cgroup2/$group/memory.cache.max"
+}
+
+cache_set_max() {
+       local group="$1"
+       local value="$2"
+       echo "$value" > "$cgroup2/$group/memory.cache.max"
+}
+
+group_set() {
+       local group="$1"
+       local proc="$2"
+       echo "$proc" > "$cgroup2/$group/cgroup.procs"
+}
+
+check_usage() {
+       local cache_curr
+       local deviation
+       local upper
+       local lower
+       local group="$1"
+       local caller=${FUNCNAME[1]}
+       cache_curr=$(cache_get_current "$group")
+       cache_max=$(cache_get_max "$group")
+       deviation=$((cache_max * 15 / 100))
+       upper=$((cache_max + deviation))
+       lower=$((cache_max - deviation))
+       if [[ $cache_curr -gt $upper || $cache_curr -lt $lower ]]; then
+               >&2 echo -n "$caller: Failed to limit cache usage: "
+               >&2 echo "current=$cache_curr max=$cache_max"
+               return 1
+       fi
+       return 0
+}
+
+check_accounting() {
+       local res
+       local cache_curr
+       local group="$1"
+       local caller=${FUNCNAME[1]}
+       cache_curr=$(cache_get_current "$group")
+       res=$(fincore -brno res "$bin_file")
+       if [[ $cache_curr -lt $res ]]; then
+               >&2 echo -n "$caller: Incorrect cache accounting: "
+               >&2 echo "cgroup=$cache_curr res=$res"
+               return 1
+       fi
+       return 0
+}
+
+limit_before_read() {
+       echo 3 > /proc/sys/vm/drop_caches
+       cache_set_max "$group0" "$group0_max"
+       (
+               local cache_curr
+               local res
+               local self=$BASHPID
+               group_set "$group0" "$self"
+
+               cat "$bin_file" > /dev/null
+               sleep 2
+               if ! check_usage "$group0"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               if ! check_accounting "$group0"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               group_set "" "$self"
+               exit 0
+       )
+       local result=$?
+       cache_set_max "$group0" "max"
+       return "$result"
+}
+
+limit_after_read() {
+       echo 3 > /proc/sys/vm/drop_caches
+       (
+               local cache_curr
+               local res
+               local self=$BASHPID
+               group_set "$group0" "$self"
+               cat "$bin_file" > /dev/null
+               sleep 2
+               if ! check_accounting "$group0"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               cache_set_max "$group0" "$group0_max"
+               if ! check_usage "$group0"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               group_set "" "$self"
+               exit 0
+       )
+       local result=$?
+       cache_set_max "$group0" "max"
+       return "$result"
+}
+
+cgroup_migrate() {
+       echo 3 > /proc/sys/vm/drop_caches
+       cache_set_max "$group0" "$group0_max"
+       cache_set_max "$group1" "$group1_max"
+       (
+               local cache_curr
+               local res
+               local self=$BASHPID
+               group_set "$group0" "$self"
+               cat "$bin_file" > /dev/null
+               sleep 2
+               group_set "$group1" "$self"
+               sleep 2
+               if ! check_usage "$group1"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               if ! check_accounting "$group1"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               group_set "" "$self"
+               exit 0
+       )
+       local result=$?
+       cache_set_max "$group0" "max"
+       return "$result"
+}
+
+numa_migrate() {
+       nodes=$(numactl -H | head -1 | cut -d ' ' -f 2)
+       if [[ $nodes -lt 2 ]]; then
+               >&2 echo "${FUNCNAME[0]}:At least 2 nodes are required, 
skipping!"
+               return 4
+       fi
+       cache_set_max "$group0" "$group0_max"
+       (
+               local self=$BASHPID
+               group_set "$group0" "$self"
+               numactl --membind=0 "$touch_pages" "$bin_file" &
+               pid=$!
+               sleep 1
+               migratepages "$pid" 0 1
+               sleep 1
+               if ! check_usage "$group0"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               if ! check_accounting "$group0"; then
+                       group_set "" "$self"
+                       exit 1
+               fi
+               group_set "" "$self"
+               exit 0
+       )
+       local result=$?
+       cache_set_max "$group0" "max"
+       return "$result"
+}
+
+cache_tests=(
+       limit_before_read
+       limit_after_read
+       cgroup_migrate
+       numa_migrate
+)
+
+result=0
+for test in "${cache_tests[@]}"; do
+       $test
+       test_result=$?
+       if [[ $test_result -eq 0 ]]; then
+               echo -n "ok "
+       elif [[ $test_result -eq 4 ]]; then
+               echo -n "skip "
+       else
+               echo -n "not ok "
+               result=1
+       fi
+       echo "$test"
+done
+
+rmdir "$cgroup2"/{"$group0","$group1"}
+rm -rf "$tmp_dir"
+
+exit $result
diff --git a/tools/testing/selftests/cgroup/touch_pages.c 
b/tools/testing/selftests/cgroup/touch_pages.c
new file mode 100644
index 000000000000..05bd5427ba1c
--- /dev/null
+++ b/tools/testing/selftests/cgroup/touch_pages.c
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2025 Virtuozzo International GmbH. All rights reserved.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <zlib.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <linux/prctl.h>
+
+int main(int argc, char *argv[])
+{
+       int fd;
+       int ret;
+       void *mmap_ptr;
+       volatile uint8_t *ptr;
+       struct stat st;
+       uint32_t crc;
+       size_t file_size;
+       size_t data_size;
+       uint32_t *crc_ptr;
+       bool op_is_store;
+       char *file_path;
+
+       if (argc < 2) {
+               fprintf(stderr, "no input file provided\n");
+               return -1;
+       }
+
+       file_path = argv[1];
+       fd = open(file_path, O_RDWR);
+       if (fd < 0) {
+               perror("failed to open input file");
+               return -1;
+       }
+
+       ret = fstat(fd, &st);
+       if (ret < 0) {
+               perror("failed to stat input file");
+               goto err_close;
+       }
+       file_size = st.st_size;
+
+       mmap_ptr = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, 
fd, 0);
+       if (mmap_ptr == MAP_FAILED) {
+               perror("failed to map input file");
+               ret = -1;
+               goto err_close;
+       }
+
+       ret = prctl(PR_SET_PDEATHSIG, SIGTERM);
+       if (ret < 0) {
+               perror("failed to set the parent-death signal");
+               goto err_unmap;
+       }
+
+       srand(getpid());
+
+       data_size = file_size - sizeof(uint32_t);
+       crc_ptr = mmap_ptr + data_size;
+
+       crc = crc32_z(0, mmap_ptr, data_size);
+       *crc_ptr = crc;
+       while (1) {
+               crc = crc32_z(0, mmap_ptr, data_size);
+               if (crc != *crc_ptr) {
+                       fprintf(stderr, "crc32 mismatch: calc=%08x read=%08x\n",
+                               crc, *crc_ptr);
+                       ret = -1;
+                       goto err_unmap;
+               }
+
+               ptr = mmap_ptr + rand() % data_size;
+               op_is_store = !!(rand() % 2);
+               if (op_is_store) {
+                       *ptr = rand() % UINT8_MAX;
+                       crc = crc32_z(0, mmap_ptr, data_size);
+                       *crc_ptr = crc;
+               } else {
+                       *ptr;
+               }
+       }
+
+err_unmap:
+       munmap(mmap_ptr, file_size);
+err_close:
+       close(fd);
+
+       return ret;
+}
-- 
2.51.0

_______________________________________________
Devel mailing list
[email protected]
https://lists.openvz.org/mailman/listinfo/devel

Reply via email to