* [LTP] [PATCH v2] cpuset_memory_spread: count only the test file's page cache
@ 2026-09-04 12:50 Changwei Zou via ltp
2026-09-04 14:52 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 6+ messages in thread
From: Changwei Zou via ltp @ 2026-09-04 12:50 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, checks the
cpuset.memory_spread_page policy by having cpuset_mem_hog read a 100 MB
DATAFILE and then comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical
thresholds (upperlimit/lowerlimit) become unreliable and the test fails
spuriously, e.g.:
cpuset_memory_spread 5 TFAIL: hog the memory on the unexpected
node(FilePages_For_Nodes(KB): _0: 7592 _1: 108328, Expect Nodes: 1).
Here 108328 KB even exceeds the 100 MB file size, showing the counter
includes unrelated cache.
Instead of measuring the noisy global counters, account for only DATAFILE's
own page-cache pages: after reading the file, cpuset_mem_hog mmaps it,
faults every page in and uses move_pages(2) (with a NULL node array, so
nothing is migrated) to learn the NUMA node each of the file's pages
resides on. It writes the per-node totals (in KB) to a result file that
the shell reads.
result_check() then simply verifies that the file's cache landed on the
expected node(s) and that the other nodes hold at most a small fraction
(UNEXPECTED_TOLERANCE, 5%) of it. Because only this file's pages are
counted, unrelated page-cache activity can no longer perturb the result,
and the check is page-size independent.
On non-NUMA machines the test is still skipped as before.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
---
.../cpuset_mem_hog.c | 130 +++++++++++++++++-
.../cpuset_memory_spread_testset.sh | 106 +++++++-------
2 files changed, 175 insertions(+), 61 deletions(-)
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
index 56e039eee..c04aabd48 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
@@ -27,17 +27,143 @@
#include <ctype.h>
#include <getopt.h>
#include <err.h>
+#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
#include <fcntl.h>
#include "../cpuset_lib/common.h"
+#include "lapi/syscalls.h"
#define BUFFER_SIZE 100
+/* The file whose page cache placement we test and where we report it. */
+#define DATAFILE "DATAFILE"
+#define RESULTFILE "cpuset_mem_hog_nodes"
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/* Upper bound on the number of NUMA nodes we account for. */
+#define MAX_NODES 1024
+
volatile int end;
+/*
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static long query_pages_node(unsigned long count, void **pages, int *status)
+{
+ return syscall(__NR_move_pages, 0, count, pages, NULL, status, 0);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node and write the
+ * result (node id and size in KB) to RESULTFILE. Because we look exclusively
+ * at this file's pages -- rather than the global per-node FilePages counters
+ * in /sys -- unrelated page-cache activity on the system cannot perturb the
+ * measurement.
+ *
+ * return 0 on success, -1 on failure.
+ */
+static int count_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr = MAP_FAILED;
+ long page_size = sysconf(_SC_PAGESIZE);
+ unsigned long npages, i, off;
+ void **pages = NULL;
+ int *status = NULL;
+ unsigned long *counts = NULL;
+ FILE *fp;
+ int ret = -1;
+
+ fd = open(DATAFILE, O_RDONLY);
+ if (fd == -1) {
+ warn("open %s failed", DATAFILE);
+ return -1;
+ }
+ if (fstat(fd, &st) == -1) {
+ warn("fstat %s failed", DATAFILE);
+ close(fd);
+ return -1;
+ }
+ npages = (st.st_size + page_size - 1) / page_size;
+ if (npages == 0) {
+ close(fd);
+ return -1;
+ }
+
+ addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (addr == MAP_FAILED) {
+ warn("mmap %s failed", DATAFILE);
+ return -1;
+ }
+
+ pages = calloc(npages, sizeof(*pages));
+ status = calloc(npages, sizeof(*status));
+ counts = calloc(MAX_NODES, sizeof(*counts));
+ if (!pages || !status || !counts) {
+ warn("calloc failed");
+ goto out;
+ }
+
+ /*
+ * Fault in every page so it maps the page-cache page already
+ * populated by page_cache_hog(); query_pages_node() then reports
+ * the node that cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+ if (query_pages_node(n, pages + off, status + off) == -1) {
+ warn("move_pages failed");
+ goto out;
+ }
+ }
+
+ for (i = 0; i < npages; i++) {
+ if (status[i] >= 0 && status[i] < MAX_NODES)
+ counts[status[i]]++;
+ }
+
+ fp = fopen(RESULTFILE, "w");
+ if (!fp) {
+ warn("open %s failed", RESULTFILE);
+ goto out;
+ }
+ for (i = 0; i < MAX_NODES; i++) {
+ if (counts[i])
+ fprintf(fp, "%lu %lu\n", i,
+ counts[i] * (unsigned long)page_size / 1024);
+ }
+ fclose(fp);
+ ret = 0;
+out:
+ if (addr != MAP_FAILED)
+ munmap(addr, st.st_size);
+ free(pages);
+ free(status);
+ free(counts);
+ return ret;
+}
+
void sighandler1(UNUSED int signo)
{
}
@@ -54,7 +180,7 @@ int page_cache_hog(void)
char path[BUFFER_SIZE];
int ret = 0;
- sprintf(path, "%s", "DATAFILE");
+ sprintf(path, "%s", DATAFILE);
fd = open(path, O_RDONLY);
if (fd == -1) {
warn("open %s failed", path);
@@ -81,6 +207,8 @@ int mem_hog(void)
while (!end) {
ret = page_cache_hog();
+ if (ret == 0)
+ ret = count_file_pages();
fd = open("./myfifo", O_WRONLY);
if (fd == -1)
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
index 4c49bb8fd..3d0aff602 100755
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
@@ -34,27 +34,22 @@ exit_status=0
nr_cpus=$NR_CPUS
nr_mems=$N_NODES
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
-
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
+# The cache hog (cpuset_mem_hog) reads DATAFILE and reports, per NUMA node,
+# how much of DATAFILE's own page cache resides on that node (in KB). Because
+# only this file's pages are accounted, unrelated page-cache activity on the
+# system does not affect the result. The pages must land on the expected
+# node(s); the other nodes may hold at most the following fraction of them.
+UNEXPECTED_TOLERANCE=5
cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
nodedir="/sys/devices/system/node"
FIFO="./myfifo"
+# per-node page-cache accounting of DATAFILE written by cpuset_mem_hog
+HOG_RESULT="./cpuset_mem_hog_nodes"
+
# memsinfo is an array implementation of the form of a multi-line string
# _0: value0
# _1: value1
@@ -131,69 +126,60 @@ freemem_check()
done
}
-# get_memsinfo
-get_memsinfo()
+# load_hog_result
+# Load the per-node page-cache accounting of DATAFILE that cpuset_mem_hog
+# wrote to $HOG_RESULT into the memsinfo array. Each line is "node kb".
+load_hog_result()
{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
-{
- local i=
+ local node= kb=
- for i in `seq 0 $((nr_mems-1))`
+ init_memsinfo_array
+ while read node kb
do
- account_meminfo $i
- done
+ set_memsinfo_val "$node" "$kb"
+ done < "$HOG_RESULT"
}
-# result_check <nodelist>
+# result_check <expect_nodes>
+# All of DATAFILE's page cache should reside on the expected node(s); the
+# other nodes should hold at most UNEXPECTED_TOLERANCE percent of it. Only
+# DATAFILE's own pages are accounted (see cpuset_mem_hog), so unrelated
+# page-cache activity no longer perturbs the result.
# return 0: success
# 1: fail
result_check()
{
local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
+ local i= total=0 expected_sum=0 unexpected_sum=0
- for i in $nodelist
+ for i in `seq 0 $((nr_mems-1))`
do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
+ total=$((total + $(get_memsinfo_val $i)))
done
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
+ # nothing was cached: the hog did not populate the page cache
+ [ $total -gt 0 ] || return 1
- local othernodelist="$allnodelist"
for i in $nodelist
do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
- done
-
- for i in $othernodelist
- do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
+ # every expected node must hold a share of the cache
+ if [ $(get_memsinfo_val $i) -eq 0 ]; then
return 1
fi
+ expected_sum=$((expected_sum + $(get_memsinfo_val $i)))
done
+
+ unexpected_sum=$((total - expected_sum))
+
+ # report the actual fraction landing on the unexpected node(s)
+ tst_resm TINFO "unexpected nodes hold $(awk -v u=$unexpected_sum -v t=$total \
+ 'BEGIN { printf "%.2f", u * 100 / t }')% of DATAFILE's page cache (tolerance ${UNEXPECTED_TOLERANCE}%)."
+
+ # the unexpected nodes must hold only a negligible fraction
+ if [ $((unexpected_sum * 100)) -ge $((total * UNEXPECTED_TOLERANCE)) ]; then
+ return 1
+ fi
}
# general_memory_spread_test <cpusetpath> <is_spread> <cpu_list> <node_list> \
@@ -238,7 +224,7 @@ general_memory_spread_test()
return 1
fi
- get_memsinfo
+ rm -f $HOG_RESULT
/bin/kill -s SIGUSR1 $test_pid
read exit_num < $FIFO
if [ $exit_num -eq 0 ]; then
@@ -246,10 +232,10 @@ general_memory_spread_test()
return 1
fi
- account_memsinfo
+ load_hog_result
result_check $expect_nodes
if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
+ tst_resm TFAIL "hog the memory on the unexpected node(DATAFILE_Pages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
return 1
fi
}
@@ -357,6 +343,6 @@ fi
test_spread_page1
test_spread_page2
-rm -f DATAFILE $FIFO
+rm -f DATAFILE $FIFO $HOG_RESULT
exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [LTP] cpuset_memory_spread: count only the test file's page cache
2026-09-04 12:50 [LTP] [PATCH v2] cpuset_memory_spread: count only the test file's page cache Changwei Zou via ltp
@ 2026-09-04 14:52 ` linuxtestproject.agent
0 siblings, 0 replies; 6+ messages in thread
From: linuxtestproject.agent @ 2026-09-04 14:52 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei,
On Sep 4, 2026, Changwei Zou wrote:
> cpuset_memory_spread: count only the test file's page cache
> + if (query_pages_node(n, pages + off, status + off) == -1) {
> + warn("move_pages failed");
> + goto out;
> + }
Medium: In Linux 7.3, move_pages() is built only when
CONFIG_NUMA_MIGRATION=y. With it disabled this returns ENOSYS, the helper
writes "0" to the FIFO, and every case reports TFAIL; require the config and
report missing syscall support as TCONF.
> + # the unexpected nodes must hold only a negligible fraction
> + if [ $((unexpected_sum * 100)) -ge $((total * UNEXPECTED_TOLERANCE)) ]; then
> + return 1
> + fi
Low: This rejects an unexpected share of exactly 5%, although the commit
message and comments say that "at most" 5% is allowed. Use -gt, or document
5% as an exclusive limit.
Verdict - Needs revision
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 6+ messages in thread
* [LTP] [PATCH v3] cpuset_memory_spread: count only the test file's page cache
@ 2026-09-05 1:51 Changwei Zou via ltp
2026-09-05 3:06 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 6+ messages in thread
From: Changwei Zou via ltp @ 2026-09-05 1:51 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, checks the
cpuset.memory_spread_page policy by having cpuset_mem_hog read a 100 MB
DATAFILE and then comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical
thresholds (upperlimit/lowerlimit) become unreliable and the test fails
spuriously, e.g.:
cpuset_memory_spread 5 TFAIL: hog the memory on the unexpected
node(FilePages_For_Nodes(KB): _0: 7592 _1: 108328, Expect Nodes: 1).
Here 108328 KB even exceeds the 100 MB file size, showing the counter
includes unrelated cache.
Instead of measuring the noisy global counters, account for only DATAFILE's
own page-cache pages: after reading the file, cpuset_mem_hog mmaps it,
faults every page in and uses move_pages(2) (with a NULL node array, so
nothing is migrated) to learn the NUMA node each of the file's pages
resides on. It writes the per-node totals (in KB) to a result file that
the shell reads.
result_check() then simply verifies that the file's cache landed on the
expected node(s) and that the other nodes hold at most a small fraction
(UNEXPECTED_TOLERANCE, 5%) of it. Because only this file's pages are
counted, unrelated page-cache activity can no longer perturb the result,
and the check is page-size independent.
"cpuset_mem_hog check" probes for move_pages(2) up front and the
test is skipped with TCONF when it is unavailable.
On non-NUMA machines the test is still skipped as before.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
---
.../cpuset_mem_hog.c | 156 +++++++++++++++++-
.../cpuset_memory_spread_testset.sh | 112 ++++++-------
2 files changed, 206 insertions(+), 62 deletions(-)
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
index 56e039eee..2602ecbe5 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
@@ -27,17 +27,159 @@
#include <ctype.h>
#include <getopt.h>
#include <err.h>
+#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
#include <fcntl.h>
#include "../cpuset_lib/common.h"
+#include "lapi/syscalls.h"
#define BUFFER_SIZE 100
+/* The file whose page cache placement we test and where we report it. */
+#define DATAFILE "DATAFILE"
+#define RESULTFILE "cpuset_mem_hog_nodes"
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/* Upper bound on the number of NUMA nodes we count. */
+#define MAX_NODES 1024
+
volatile int end;
+/*
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static long query_pages_node(unsigned long count, void **pages, int *status)
+{
+ return syscall(__NR_move_pages, 0, count, pages, NULL, status, 0);
+}
+
+/*
+ * An unimplemented syscall returns ENOSYS before its arguments are dereferenced,
+ * so a dummy page pointer is enough to probe for support.
+ * return 1 if supported, 0 if not.
+ */
+static int move_pages_supported(void)
+{
+ void *page = NULL;
+ int status = -1;
+ long ret;
+
+ ret = query_pages_node(1, &page, &status);
+
+ return !(ret == -1 && errno == ENOSYS);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node and write the
+ * result (node id and size in KB) to RESULTFILE. Because we look exclusively
+ * at this file's pages -- rather than the global per-node FilePages counters
+ * in /sys -- unrelated page-cache activity on the system cannot perturb the
+ * measurement.
+ *
+ * return 0 on success, -1 on failure.
+ */
+static int count_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr = MAP_FAILED;
+ long page_size = sysconf(_SC_PAGESIZE);
+ unsigned long npages, i, off;
+ void **pages = NULL;
+ int *status = NULL;
+ unsigned long *counts = NULL;
+ FILE *fp;
+ int ret = -1;
+
+ fd = open(DATAFILE, O_RDONLY);
+ if (fd == -1) {
+ warn("open %s failed", DATAFILE);
+ return -1;
+ }
+ if (fstat(fd, &st) == -1) {
+ warn("fstat %s failed", DATAFILE);
+ close(fd);
+ return -1;
+ }
+ npages = (st.st_size + page_size - 1) / page_size;
+ if (npages == 0) {
+ close(fd);
+ return -1;
+ }
+
+ addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (addr == MAP_FAILED) {
+ warn("mmap %s failed", DATAFILE);
+ return -1;
+ }
+
+ pages = calloc(npages, sizeof(*pages));
+ status = calloc(npages, sizeof(*status));
+ counts = calloc(MAX_NODES, sizeof(*counts));
+ if (!pages || !status || !counts) {
+ warn("calloc failed");
+ goto out;
+ }
+
+ /*
+ * Fault in every page so it maps the page-cache page already
+ * populated by page_cache_hog(); query_pages_node() then reports
+ * the node that cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+ if (query_pages_node(n, pages + off, status + off) == -1) {
+ warn("move_pages failed");
+ goto out;
+ }
+ }
+
+ for (i = 0; i < npages; i++) {
+ if (status[i] >= 0 && status[i] < MAX_NODES)
+ counts[status[i]]++;
+ }
+
+ fp = fopen(RESULTFILE, "w");
+ if (!fp) {
+ warn("open %s failed", RESULTFILE);
+ goto out;
+ }
+ for (i = 0; i < MAX_NODES; i++) {
+ if (counts[i])
+ fprintf(fp, "%lu %lu\n", i,
+ counts[i] * (unsigned long)page_size / 1024);
+ }
+ fclose(fp);
+ ret = 0;
+out:
+ if (addr != MAP_FAILED)
+ munmap(addr, st.st_size);
+ free(pages);
+ free(status);
+ free(counts);
+ return ret;
+}
+
void sighandler1(UNUSED int signo)
{
}
@@ -54,7 +196,7 @@ int page_cache_hog(void)
char path[BUFFER_SIZE];
int ret = 0;
- sprintf(path, "%s", "DATAFILE");
+ sprintf(path, "%s", DATAFILE);
fd = open(path, O_RDONLY);
if (fd == -1) {
warn("open %s failed", path);
@@ -81,6 +223,8 @@ int mem_hog(void)
while (!end) {
ret = page_cache_hog();
+ if (ret == 0)
+ ret = count_file_pages();
fd = open("./myfifo", O_WRONLY);
if (fd == -1)
@@ -102,10 +246,18 @@ int mem_hog(void)
return ret;
}
-int main(void)
+int main(int argc, char *argv[])
{
struct sigaction sa1, sa2;
+ /*
+ * "cpuset_mem_hog check" only probes move_pages(2) support and exits:
+ * 0 if supported, 1 if not. The shell uses it to skip (TCONF) on
+ * kernels built without CONFIG_NUMA_MIGRATION.
+ */
+ if (argc > 1 && !strcmp(argv[1], "check"))
+ return move_pages_supported() ? 0 : 1;
+
sa1.sa_handler = sighandler1;
if (sigemptyset(&sa1.sa_mask) < 0)
err(1, "sigemptyset()");
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
index 4c49bb8fd..771dfa39f 100755
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
@@ -30,31 +30,32 @@ export TST_COUNT=1
check
+# cpuset_mem_hog counts DATAFILE's page cache via move_pages(2)
+cpuset_mem_hog check
+if [ $? -ne 0 ]; then
+ tst_brkm TCONF "move_pages() is not supported, CONFIG_NUMA_MIGRATION disabled?"
+fi
+
exit_status=0
nr_cpus=$NR_CPUS
nr_mems=$N_NODES
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
-
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
+# The cache hog (cpuset_mem_hog) reads DATAFILE and reports, per NUMA node,
+# how much of DATAFILE's own page cache resides on that node (in KB). Because
+# only this file's pages are counted, unrelated page-cache activity on the
+# system does not affect the result. The pages must land on the expected
+# node(s); the other nodes may hold at most the following fraction of them.
+UNEXPECTED_TOLERANCE=5
cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
nodedir="/sys/devices/system/node"
FIFO="./myfifo"
+# per-node page-cache count of DATAFILE written by cpuset_mem_hog
+HOG_RESULT="./cpuset_mem_hog_nodes"
+
# memsinfo is an array implementation of the form of a multi-line string
# _0: value0
# _1: value1
@@ -131,69 +132,60 @@ freemem_check()
done
}
-# get_memsinfo
-get_memsinfo()
+# load_hog_result
+# Load the per-node page-cache count of DATAFILE that cpuset_mem_hog
+# wrote to $HOG_RESULT into the memsinfo array. Each line is "node kb".
+load_hog_result()
{
- local i=
+ local node= kb=
- for i in `seq 0 $((nr_mems-1))`
+ init_memsinfo_array
+ while read node kb
do
- get_meminfo $i "FilePages"
- done
+ set_memsinfo_val "$node" "$kb"
+ done < "$HOG_RESULT"
}
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
-{
- local i=
- for i in `seq 0 $((nr_mems-1))`
- do
- account_meminfo $i
- done
-}
-
-
-# result_check <nodelist>
+# result_check <expect_nodes>
+# All of DATAFILE's page cache should reside on the expected node(s); the
+# other nodes should hold at most UNEXPECTED_TOLERANCE percent of it. Only
+# DATAFILE's own pages are counted (see cpuset_mem_hog), so unrelated
+# page-cache activity no longer perturbs the result.
# return 0: success
# 1: fail
result_check()
{
local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
+ local i= total=0 expected_sum=0 unexpected_sum=0
- for i in $nodelist
+ for i in `seq 0 $((nr_mems-1))`
do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
+ total=$((total + $(get_memsinfo_val $i)))
done
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
+ # nothing was cached: the hog did not populate the page cache
+ [ $total -gt 0 ] || return 1
- local othernodelist="$allnodelist"
for i in $nodelist
do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
- done
-
- for i in $othernodelist
- do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
+ # every expected node must hold a share of the cache
+ if [ $(get_memsinfo_val $i) -eq 0 ]; then
return 1
fi
+ expected_sum=$((expected_sum + $(get_memsinfo_val $i)))
done
+
+ unexpected_sum=$((total - expected_sum))
+
+ # report the actual fraction landing on the unexpected node(s)
+ tst_resm TINFO "unexpected nodes hold $(awk -v u=$unexpected_sum -v t=$total \
+ 'BEGIN { printf "%.2f", u * 100 / t }')% of DATAFILE's page cache (tolerance ${UNEXPECTED_TOLERANCE}%)."
+
+ # the unexpected nodes must hold at most UNEXPECTED_TOLERANCE percent
+ if [ $((unexpected_sum * 100)) -gt $((total * UNEXPECTED_TOLERANCE)) ]; then
+ return 1
+ fi
}
# general_memory_spread_test <cpusetpath> <is_spread> <cpu_list> <node_list> \
@@ -238,7 +230,7 @@ general_memory_spread_test()
return 1
fi
- get_memsinfo
+ rm -f $HOG_RESULT
/bin/kill -s SIGUSR1 $test_pid
read exit_num < $FIFO
if [ $exit_num -eq 0 ]; then
@@ -246,10 +238,10 @@ general_memory_spread_test()
return 1
fi
- account_memsinfo
+ load_hog_result
result_check $expect_nodes
if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
+ tst_resm TFAIL "hog the memory on the unexpected node(DATAFILE_Pages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
return 1
fi
}
@@ -357,6 +349,6 @@ fi
test_spread_page1
test_spread_page2
-rm -f DATAFILE $FIFO
+rm -f DATAFILE $FIFO $HOG_RESULT
exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [LTP] cpuset_memory_spread: count only the test file's page cache
2026-09-05 1:51 [LTP] [PATCH v3] " Changwei Zou via ltp
@ 2026-09-05 3:06 ` linuxtestproject.agent
0 siblings, 0 replies; 6+ messages in thread
From: linuxtestproject.agent @ 2026-09-05 3:06 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei,
On Sat, Sep 5, 2026, Changwei Zou wrote:
> cpuset_memory_spread: count only the test file's page cache
> for i in $nodelist
> do
> # every expected node must hold a share of the cache
> if [ $(get_memsinfo_val $i) -eq 0 ]; then
> return 1
> fi
> expected_sum=$((expected_sum + $(get_memsinfo_val $i)))
> done
This accepts any nonzero share on each expected node. In a two-node spread
case, one page on one node and the rest on the other still passes, although
the kernel contract requires an even spread; check the balance among expected
nodes against an appropriate tolerance.
Verdict - Needs revision
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 6+ messages in thread
* [LTP] [PATCH v4] cpuset_memory_spread: count only the test file's page cache
@ 2026-09-05 6:18 Changwei Zou via ltp
2026-09-05 8:07 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 6+ messages in thread
From: Changwei Zou via ltp @ 2026-09-05 6:18 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, checks the
cpuset.memory_spread_page policy by having cpuset_mem_hog read a 100 MB
DATAFILE and then comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical
thresholds (upperlimit/lowerlimit) become unreliable and the test fails
spuriously, e.g.:
cpuset_memory_spread 5 TFAIL: hog the memory on the unexpected
node(FilePages_For_Nodes(KB): _0: 7592 _1: 108328, Expect Nodes: 1).
Here 108328 KB even exceeds the 100 MB file size, showing the counter
includes unrelated cache.
Instead of measuring the noisy global counters, account for only DATAFILE's
own page-cache pages: after reading the file, cpuset_mem_hog mmaps it,
faults every page in and uses move_pages(2) (with a NULL node array, so
nothing is migrated) to learn the NUMA node each of the file's pages
resides on. It writes the per-node totals (in KB) to a result file that
the shell reads.
result_check() then verifies that the file's cache landed on the expected
node(s) and that the other nodes hold at most a small fraction
(UNEXPECTED_TOLERANCE, 5%) of it. When several expected nodes are given,
the kernel spreads the cache evenly, so each expected node must also hold
at least (100 - BALANCE_TOLERANCE, 80%) of its even share. Because only
this file's pages are counted, unrelated page-cache activity can no longer
perturb the result, and the check is page-size independent.
"cpuset_mem_hog check" probes for move_pages(2) up front and the
test is skipped with TCONF when it is unavailable.
On non-NUMA machines the test is still skipped as before.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
---
.../cpuset_mem_hog.c | 156 +++++++++++++++++-
.../cpuset_memory_spread_testset.sh | 119 ++++++-------
2 files changed, 217 insertions(+), 58 deletions(-)
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
index 56e039eee..2602ecbe5 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
@@ -27,17 +27,159 @@
#include <ctype.h>
#include <getopt.h>
#include <err.h>
+#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
#include <fcntl.h>
#include "../cpuset_lib/common.h"
+#include "lapi/syscalls.h"
#define BUFFER_SIZE 100
+/* The file whose page cache placement we test and where we report it. */
+#define DATAFILE "DATAFILE"
+#define RESULTFILE "cpuset_mem_hog_nodes"
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/* Upper bound on the number of NUMA nodes we count. */
+#define MAX_NODES 1024
+
volatile int end;
+/*
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static long query_pages_node(unsigned long count, void **pages, int *status)
+{
+ return syscall(__NR_move_pages, 0, count, pages, NULL, status, 0);
+}
+
+/*
+ * An unimplemented syscall returns ENOSYS before its arguments are dereferenced,
+ * so a dummy page pointer is enough to probe for support.
+ * return 1 if supported, 0 if not.
+ */
+static int move_pages_supported(void)
+{
+ void *page = NULL;
+ int status = -1;
+ long ret;
+
+ ret = query_pages_node(1, &page, &status);
+
+ return !(ret == -1 && errno == ENOSYS);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node and write the
+ * result (node id and size in KB) to RESULTFILE. Because we look exclusively
+ * at this file's pages -- rather than the global per-node FilePages counters
+ * in /sys -- unrelated page-cache activity on the system cannot perturb the
+ * measurement.
+ *
+ * return 0 on success, -1 on failure.
+ */
+static int count_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr = MAP_FAILED;
+ long page_size = sysconf(_SC_PAGESIZE);
+ unsigned long npages, i, off;
+ void **pages = NULL;
+ int *status = NULL;
+ unsigned long *counts = NULL;
+ FILE *fp;
+ int ret = -1;
+
+ fd = open(DATAFILE, O_RDONLY);
+ if (fd == -1) {
+ warn("open %s failed", DATAFILE);
+ return -1;
+ }
+ if (fstat(fd, &st) == -1) {
+ warn("fstat %s failed", DATAFILE);
+ close(fd);
+ return -1;
+ }
+ npages = (st.st_size + page_size - 1) / page_size;
+ if (npages == 0) {
+ close(fd);
+ return -1;
+ }
+
+ addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (addr == MAP_FAILED) {
+ warn("mmap %s failed", DATAFILE);
+ return -1;
+ }
+
+ pages = calloc(npages, sizeof(*pages));
+ status = calloc(npages, sizeof(*status));
+ counts = calloc(MAX_NODES, sizeof(*counts));
+ if (!pages || !status || !counts) {
+ warn("calloc failed");
+ goto out;
+ }
+
+ /*
+ * Fault in every page so it maps the page-cache page already
+ * populated by page_cache_hog(); query_pages_node() then reports
+ * the node that cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+ if (query_pages_node(n, pages + off, status + off) == -1) {
+ warn("move_pages failed");
+ goto out;
+ }
+ }
+
+ for (i = 0; i < npages; i++) {
+ if (status[i] >= 0 && status[i] < MAX_NODES)
+ counts[status[i]]++;
+ }
+
+ fp = fopen(RESULTFILE, "w");
+ if (!fp) {
+ warn("open %s failed", RESULTFILE);
+ goto out;
+ }
+ for (i = 0; i < MAX_NODES; i++) {
+ if (counts[i])
+ fprintf(fp, "%lu %lu\n", i,
+ counts[i] * (unsigned long)page_size / 1024);
+ }
+ fclose(fp);
+ ret = 0;
+out:
+ if (addr != MAP_FAILED)
+ munmap(addr, st.st_size);
+ free(pages);
+ free(status);
+ free(counts);
+ return ret;
+}
+
void sighandler1(UNUSED int signo)
{
}
@@ -54,7 +196,7 @@ int page_cache_hog(void)
char path[BUFFER_SIZE];
int ret = 0;
- sprintf(path, "%s", "DATAFILE");
+ sprintf(path, "%s", DATAFILE);
fd = open(path, O_RDONLY);
if (fd == -1) {
warn("open %s failed", path);
@@ -81,6 +223,8 @@ int mem_hog(void)
while (!end) {
ret = page_cache_hog();
+ if (ret == 0)
+ ret = count_file_pages();
fd = open("./myfifo", O_WRONLY);
if (fd == -1)
@@ -102,10 +246,18 @@ int mem_hog(void)
return ret;
}
-int main(void)
+int main(int argc, char *argv[])
{
struct sigaction sa1, sa2;
+ /*
+ * "cpuset_mem_hog check" only probes move_pages(2) support and exits:
+ * 0 if supported, 1 if not. The shell uses it to skip (TCONF) on
+ * kernels built without CONFIG_NUMA_MIGRATION.
+ */
+ if (argc > 1 && !strcmp(argv[1], "check"))
+ return move_pages_supported() ? 0 : 1;
+
sa1.sa_handler = sighandler1;
if (sigemptyset(&sa1.sa_mask) < 0)
err(1, "sigemptyset()");
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
index 4c49bb8fd..6ab40ba83 100755
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
@@ -30,31 +30,37 @@ export TST_COUNT=1
check
+# cpuset_mem_hog counts DATAFILE's page cache via move_pages(2)
+cpuset_mem_hog check
+if [ $? -ne 0 ]; then
+ tst_brkm TCONF "move_pages() is not supported, CONFIG_NUMA_MIGRATION disabled?"
+fi
+
exit_status=0
nr_cpus=$NR_CPUS
nr_mems=$N_NODES
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
+# The cache hog (cpuset_mem_hog) reads DATAFILE and reports, per NUMA node,
+# how much of DATAFILE's own page cache resides on that node (in KB). Because
+# only this file's pages are counted, unrelated page-cache activity on the
+# system does not affect the result. The pages must land on the expected
+# node(s); the other nodes may hold at most the following fraction of them.
+UNEXPECTED_TOLERANCE=5
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
+# When the page cache is spread across several expected nodes, the kernel
+# spreads it evenly. Every expected node must hold at least
+# (100 - BALANCE_TOLERANCE) percent of its even share.
+BALANCE_TOLERANCE=20
cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
nodedir="/sys/devices/system/node"
FIFO="./myfifo"
+# per-node page-cache count of DATAFILE written by cpuset_mem_hog
+HOG_RESULT="./cpuset_mem_hog_nodes"
+
# memsinfo is an array implementation of the form of a multi-line string
# _0: value0
# _1: value1
@@ -131,66 +137,67 @@ freemem_check()
done
}
-# get_memsinfo
-get_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
+# load_hog_result
+# Load the per-node page-cache count of DATAFILE that cpuset_mem_hog
+# wrote to $HOG_RESULT into the memsinfo array. Each line is "node kb".
+load_hog_result()
{
- local i=
+ local node= kb=
- for i in `seq 0 $((nr_mems-1))`
+ init_memsinfo_array
+ while read node kb
do
- account_meminfo $i
- done
+ set_memsinfo_val "$node" "$kb"
+ done < "$HOG_RESULT"
}
-# result_check <nodelist>
+# result_check <expect_nodes>
+# All of DATAFILE's page cache should reside on the expected node(s); the
+# other nodes should hold at most UNEXPECTED_TOLERANCE percent of it. When
+# several expected nodes are given the cache must be spread evenly among
+# them. Only DATAFILE's own pages are counted (see cpuset_mem_hog), so
+# unrelated page-cache activity no longer perturbs the result.
# return 0: success
# 1: fail
result_check()
{
local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
+ local i= total=0 expected_sum=0 unexpected_sum=0 n_expected=0
- for i in $nodelist
+ for i in `seq 0 $((nr_mems-1))`
do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
+ total=$((total + $(get_memsinfo_val $i)))
done
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
+ # nothing was cached: the hog did not populate the page cache
+ [ $total -gt 0 ] || return 1
- local othernodelist="$allnodelist"
for i in $nodelist
do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
+ expected_sum=$((expected_sum + $(get_memsinfo_val $i)))
+ n_expected=$((n_expected + 1))
done
- for i in $othernodelist
+ unexpected_sum=$((total - expected_sum))
+
+ # report the actual fraction landing on the unexpected node(s)
+ tst_resm TINFO "unexpected nodes hold $(awk -v u=$unexpected_sum -v t=$total \
+ 'BEGIN { printf "%.2f", u * 100 / t }')% of DATAFILE's page cache (tolerance ${UNEXPECTED_TOLERANCE}%)."
+
+ # the unexpected nodes must hold at most UNEXPECTED_TOLERANCE percent
+ if [ $((unexpected_sum * 100)) -gt $((total * UNEXPECTED_TOLERANCE)) ]; then
+ return 1
+ fi
+
+ # every expected node must hold at least (100 - BALANCE_TOLERANCE)% of
+ # its even share (expected_sum / n_expected). This enforces an even
+ # spread across several expected nodes and also rejects an expected
+ # node that received nothing. A single expected node always passes.
+ for i in $nodelist
do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
+ if [ $(($(get_memsinfo_val $i) * n_expected * 100)) -lt \
+ $((expected_sum * (100 - BALANCE_TOLERANCE))) ]; then
return 1
fi
done
@@ -238,7 +245,7 @@ general_memory_spread_test()
return 1
fi
- get_memsinfo
+ rm -f $HOG_RESULT
/bin/kill -s SIGUSR1 $test_pid
read exit_num < $FIFO
if [ $exit_num -eq 0 ]; then
@@ -246,10 +253,10 @@ general_memory_spread_test()
return 1
fi
- account_memsinfo
+ load_hog_result
result_check $expect_nodes
if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
+ tst_resm TFAIL "hog the memory on the unexpected node(DATAFILE_Pages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
return 1
fi
}
@@ -357,6 +364,6 @@ fi
test_spread_page1
test_spread_page2
-rm -f DATAFILE $FIFO
+rm -f DATAFILE $FIFO $HOG_RESULT
exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [LTP] cpuset_memory_spread: count only the test file's page cache
2026-09-05 6:18 [LTP] [PATCH v4] " Changwei Zou via ltp
@ 2026-09-05 8:07 ` linuxtestproject.agent
0 siblings, 0 replies; 6+ messages in thread
From: linuxtestproject.agent @ 2026-09-05 8:07 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei,
On Sat, 5 Sep 2026, Changwei Zou wrote:
> cpuset_memory_spread: count only the test file's page cache
> for (i = 0; i < npages; i++) {
> if (status[i] >= 0 && status[i] < MAX_NODES)
> counts[status[i]]++;
> }
Medium: Per-page -ENOENT and -EFAULT statuses are silently omitted even
though move_pages() itself returns success, so result_check() can compute its
percentages from only a subset of DATAFILE and falsely pass. Fail on any
negative status and verify that all npages were counted.
Verdict - Needs revision
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 6+ messages in thread
* [LTP] [PATCH v5] cpuset_memory_spread: count only the test file's page cache
@ 2026-09-05 10:19 Changwei Zou via ltp
2026-09-05 12:27 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 6+ messages in thread
From: Changwei Zou via ltp @ 2026-09-05 10:19 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, checks the
cpuset.memory_spread_page policy by having cpuset_mem_hog read a 100 MB
DATAFILE and then comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical
thresholds (upperlimit/lowerlimit) become unreliable and the test fails
spuriously, e.g.:
cpuset_memory_spread 5 TFAIL: hog the memory on the unexpected
node(FilePages_For_Nodes(KB): _0: 7592 _1: 108328, Expect Nodes: 1).
Here 108328 KB even exceeds the 100 MB file size, showing the counter
includes unrelated cache.
Instead of measuring the noisy global counters, account for only DATAFILE's
own page-cache pages: after reading the file, cpuset_mem_hog mmaps it,
faults every page in and uses move_pages(2) (with a NULL node array, so
nothing is migrated) to learn the NUMA node each of the file's pages
resides on. Every page must be accounted -- a per-page error (negative
status) or a short count fails the run -- so a partial measurement cannot
pass. It writes the per-node totals (in KB) to a result file that the
shell reads.
result_check() then verifies that the file's cache landed on the expected
node(s) and that the other nodes hold at most a small fraction
(UNEXPECTED_TOLERANCE, 5%) of it. When several expected nodes are given,
the kernel spreads the cache evenly, so each expected node must also hold
at least (100 - BALANCE_TOLERANCE, 80%) of its even share. Because only
this file's pages are counted, unrelated page-cache activity can no longer
perturb the result, and the check is page-size independent.
"cpuset_mem_hog check" probes for move_pages(2) up front and the
test is skipped with TCONF when it is unavailable.
On non-NUMA machines the test is still skipped as before.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
---
.../cpuset_mem_hog.c | 172 +++++++++++++++++-
.../cpuset_memory_spread_testset.sh | 119 ++++++------
2 files changed, 233 insertions(+), 58 deletions(-)
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
index 56e039eee..0bd60ecd1 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
@@ -27,17 +27,175 @@
#include <ctype.h>
#include <getopt.h>
#include <err.h>
+#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
#include <fcntl.h>
#include "../cpuset_lib/common.h"
+#include "lapi/syscalls.h"
#define BUFFER_SIZE 100
+/* The file whose page cache placement we test and where we report it. */
+#define DATAFILE "DATAFILE"
+#define RESULTFILE "cpuset_mem_hog_nodes"
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/* Upper bound on the number of NUMA nodes we count. */
+#define MAX_NODES 1024
+
volatile int end;
+/*
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static long query_pages_node(unsigned long count, void **pages, int *status)
+{
+ return syscall(__NR_move_pages, 0, count, pages, NULL, status, 0);
+}
+
+/*
+ * An unimplemented syscall returns ENOSYS before its arguments are dereferenced,
+ * so a dummy page pointer is enough to probe for support.
+ * return 1 if supported, 0 if not.
+ */
+static int move_pages_supported(void)
+{
+ void *page = NULL;
+ int status = -1;
+ long ret;
+
+ ret = query_pages_node(1, &page, &status);
+
+ return !(ret == -1 && errno == ENOSYS);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node and write the
+ * result (node id and size in KB) to RESULTFILE. Because we look exclusively
+ * at this file's pages -- rather than the global per-node FilePages counters
+ * in /sys -- unrelated page-cache activity on the system cannot perturb the
+ * measurement.
+ *
+ * return 0 on success, -1 on failure.
+ */
+static int count_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr = MAP_FAILED;
+ long page_size = sysconf(_SC_PAGESIZE);
+ unsigned long npages, i, off, counted = 0;
+ void **pages = NULL;
+ int *status = NULL;
+ unsigned long *counts = NULL;
+ FILE *fp;
+ int ret = -1;
+
+ fd = open(DATAFILE, O_RDONLY);
+ if (fd == -1) {
+ warn("open %s failed", DATAFILE);
+ return -1;
+ }
+ if (fstat(fd, &st) == -1) {
+ warn("fstat %s failed", DATAFILE);
+ close(fd);
+ return -1;
+ }
+ npages = (st.st_size + page_size - 1) / page_size;
+ if (npages == 0) {
+ close(fd);
+ return -1;
+ }
+
+ addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (addr == MAP_FAILED) {
+ warn("mmap %s failed", DATAFILE);
+ return -1;
+ }
+
+ pages = calloc(npages, sizeof(*pages));
+ status = calloc(npages, sizeof(*status));
+ counts = calloc(MAX_NODES, sizeof(*counts));
+ if (!pages || !status || !counts) {
+ warn("calloc failed");
+ goto out;
+ }
+
+ /*
+ * Fault in every page so it maps the page-cache page already
+ * populated by page_cache_hog(); query_pages_node() then reports
+ * the node that cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+ if (query_pages_node(n, pages + off, status + off) == -1) {
+ warn("move_pages failed");
+ goto out;
+ }
+ }
+
+ /*
+ * move_pages() can return success while reporting a per-page error
+ * (e.g. -ENOENT or -EFAULT) in status[]. Treat any such page as a
+ * failure: otherwise the shell would compute its percentages from
+ * only a subset of DATAFILE and could pass incorrectly.
+ */
+ for (i = 0; i < npages; i++) {
+ if (status[i] < 0 || status[i] >= MAX_NODES) {
+ warnx("page %lu not accounted (status %d)", i,
+ status[i]);
+ goto out;
+ }
+ counts[status[i]]++;
+ counted++;
+ }
+
+ if (counted != npages) {
+ warnx("counted %lu of %lu pages", counted, npages);
+ goto out;
+ }
+
+ fp = fopen(RESULTFILE, "w");
+ if (!fp) {
+ warn("open %s failed", RESULTFILE);
+ goto out;
+ }
+ for (i = 0; i < MAX_NODES; i++) {
+ if (counts[i])
+ fprintf(fp, "%lu %lu\n", i,
+ counts[i] * (unsigned long)page_size / 1024);
+ }
+ fclose(fp);
+ ret = 0;
+out:
+ if (addr != MAP_FAILED)
+ munmap(addr, st.st_size);
+ free(pages);
+ free(status);
+ free(counts);
+ return ret;
+}
+
void sighandler1(UNUSED int signo)
{
}
@@ -54,7 +212,7 @@ int page_cache_hog(void)
char path[BUFFER_SIZE];
int ret = 0;
- sprintf(path, "%s", "DATAFILE");
+ sprintf(path, "%s", DATAFILE);
fd = open(path, O_RDONLY);
if (fd == -1) {
warn("open %s failed", path);
@@ -81,6 +239,8 @@ int mem_hog(void)
while (!end) {
ret = page_cache_hog();
+ if (ret == 0)
+ ret = count_file_pages();
fd = open("./myfifo", O_WRONLY);
if (fd == -1)
@@ -102,10 +262,18 @@ int mem_hog(void)
return ret;
}
-int main(void)
+int main(int argc, char *argv[])
{
struct sigaction sa1, sa2;
+ /*
+ * "cpuset_mem_hog check" only probes move_pages(2) support and exits:
+ * 0 if supported, 1 if not. The shell uses it to skip (TCONF) on
+ * kernels built without CONFIG_NUMA_MIGRATION.
+ */
+ if (argc > 1 && !strcmp(argv[1], "check"))
+ return move_pages_supported() ? 0 : 1;
+
sa1.sa_handler = sighandler1;
if (sigemptyset(&sa1.sa_mask) < 0)
err(1, "sigemptyset()");
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
index 4c49bb8fd..6ab40ba83 100755
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
@@ -30,31 +30,37 @@ export TST_COUNT=1
check
+# cpuset_mem_hog counts DATAFILE's page cache via move_pages(2)
+cpuset_mem_hog check
+if [ $? -ne 0 ]; then
+ tst_brkm TCONF "move_pages() is not supported, CONFIG_NUMA_MIGRATION disabled?"
+fi
+
exit_status=0
nr_cpus=$NR_CPUS
nr_mems=$N_NODES
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
+# The cache hog (cpuset_mem_hog) reads DATAFILE and reports, per NUMA node,
+# how much of DATAFILE's own page cache resides on that node (in KB). Because
+# only this file's pages are counted, unrelated page-cache activity on the
+# system does not affect the result. The pages must land on the expected
+# node(s); the other nodes may hold at most the following fraction of them.
+UNEXPECTED_TOLERANCE=5
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
+# When the page cache is spread across several expected nodes, the kernel
+# spreads it evenly. Every expected node must hold at least
+# (100 - BALANCE_TOLERANCE) percent of its even share.
+BALANCE_TOLERANCE=20
cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
nodedir="/sys/devices/system/node"
FIFO="./myfifo"
+# per-node page-cache count of DATAFILE written by cpuset_mem_hog
+HOG_RESULT="./cpuset_mem_hog_nodes"
+
# memsinfo is an array implementation of the form of a multi-line string
# _0: value0
# _1: value1
@@ -131,66 +137,67 @@ freemem_check()
done
}
-# get_memsinfo
-get_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
+# load_hog_result
+# Load the per-node page-cache count of DATAFILE that cpuset_mem_hog
+# wrote to $HOG_RESULT into the memsinfo array. Each line is "node kb".
+load_hog_result()
{
- local i=
+ local node= kb=
- for i in `seq 0 $((nr_mems-1))`
+ init_memsinfo_array
+ while read node kb
do
- account_meminfo $i
- done
+ set_memsinfo_val "$node" "$kb"
+ done < "$HOG_RESULT"
}
-# result_check <nodelist>
+# result_check <expect_nodes>
+# All of DATAFILE's page cache should reside on the expected node(s); the
+# other nodes should hold at most UNEXPECTED_TOLERANCE percent of it. When
+# several expected nodes are given the cache must be spread evenly among
+# them. Only DATAFILE's own pages are counted (see cpuset_mem_hog), so
+# unrelated page-cache activity no longer perturbs the result.
# return 0: success
# 1: fail
result_check()
{
local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
+ local i= total=0 expected_sum=0 unexpected_sum=0 n_expected=0
- for i in $nodelist
+ for i in `seq 0 $((nr_mems-1))`
do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
+ total=$((total + $(get_memsinfo_val $i)))
done
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
+ # nothing was cached: the hog did not populate the page cache
+ [ $total -gt 0 ] || return 1
- local othernodelist="$allnodelist"
for i in $nodelist
do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
+ expected_sum=$((expected_sum + $(get_memsinfo_val $i)))
+ n_expected=$((n_expected + 1))
done
- for i in $othernodelist
+ unexpected_sum=$((total - expected_sum))
+
+ # report the actual fraction landing on the unexpected node(s)
+ tst_resm TINFO "unexpected nodes hold $(awk -v u=$unexpected_sum -v t=$total \
+ 'BEGIN { printf "%.2f", u * 100 / t }')% of DATAFILE's page cache (tolerance ${UNEXPECTED_TOLERANCE}%)."
+
+ # the unexpected nodes must hold at most UNEXPECTED_TOLERANCE percent
+ if [ $((unexpected_sum * 100)) -gt $((total * UNEXPECTED_TOLERANCE)) ]; then
+ return 1
+ fi
+
+ # every expected node must hold at least (100 - BALANCE_TOLERANCE)% of
+ # its even share (expected_sum / n_expected). This enforces an even
+ # spread across several expected nodes and also rejects an expected
+ # node that received nothing. A single expected node always passes.
+ for i in $nodelist
do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
+ if [ $(($(get_memsinfo_val $i) * n_expected * 100)) -lt \
+ $((expected_sum * (100 - BALANCE_TOLERANCE))) ]; then
return 1
fi
done
@@ -238,7 +245,7 @@ general_memory_spread_test()
return 1
fi
- get_memsinfo
+ rm -f $HOG_RESULT
/bin/kill -s SIGUSR1 $test_pid
read exit_num < $FIFO
if [ $exit_num -eq 0 ]; then
@@ -246,10 +253,10 @@ general_memory_spread_test()
return 1
fi
- account_memsinfo
+ load_hog_result
result_check $expect_nodes
if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
+ tst_resm TFAIL "hog the memory on the unexpected node(DATAFILE_Pages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
return 1
fi
}
@@ -357,6 +364,6 @@ fi
test_spread_page1
test_spread_page2
-rm -f DATAFILE $FIFO
+rm -f DATAFILE $FIFO $HOG_RESULT
exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread* [LTP] [PATCH v6] cpuset_memory_spread: count only the test file's page cache
@ 2026-09-05 12:50 Changwei Zou via ltp
2026-09-05 15:50 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 6+ messages in thread
From: Changwei Zou via ltp @ 2026-09-05 12:50 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, checks the
cpuset.memory_spread_page policy by having cpuset_mem_hog read a 100 MB
DATAFILE and then comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical
thresholds (upperlimit/lowerlimit) become unreliable and the test fails
spuriously, e.g.:
cpuset_memory_spread 5 TFAIL: hog the memory on the unexpected
node(FilePages_For_Nodes(KB): _0: 7592 _1: 108328, Expect Nodes: 1).
Here 108328 KB even exceeds the 100 MB file size, showing the counter
includes unrelated cache.
Instead of measuring the noisy global counters, account for only DATAFILE's
own page-cache pages: after reading the file, cpuset_mem_hog mmaps it,
faults every page in and uses move_pages(2) (with a NULL node array, so
nothing is migrated) to learn the NUMA node each of the file's pages
resides on. Every page must be accounted -- a per-page error (negative
status) fails the run -- so a partial measurement cannot pass. It writes
the per-node totals (in KB) to a result file that the shell reads.
result_check() then verifies that the file's cache landed on the expected
node(s) and that the other nodes hold at most a small fraction
(UNEXPECTED_TOLERANCE, 5%) of it. When several expected nodes are given,
the kernel spreads the cache evenly, so each expected node must also hold
at least (100 - BALANCE_TOLERANCE, 80%) of its even share. Because only
this file's pages are counted, unrelated page-cache activity can no longer
perturb the result, and the check is page-size independent.
"cpuset_mem_hog check" probes for move_pages(2) up front and the
test is skipped with TCONF when it is unavailable.
On non-NUMA machines the test is still skipped as before.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
---
.../cpuset_mem_hog.c | 166 +++++++++++++++++-
.../cpuset_memory_spread_testset.sh | 119 +++++++------
2 files changed, 227 insertions(+), 58 deletions(-)
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
index 56e039eee..59cf81649 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
@@ -27,17 +27,169 @@
#include <ctype.h>
#include <getopt.h>
#include <err.h>
+#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
#include <fcntl.h>
#include "../cpuset_lib/common.h"
+#include "lapi/syscalls.h"
#define BUFFER_SIZE 100
+/* The file whose page cache placement we test and where we report it. */
+#define DATAFILE "DATAFILE"
+#define RESULTFILE "cpuset_mem_hog_nodes"
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/* Upper bound on the number of NUMA nodes we count. */
+#define MAX_NODES 1024
+
volatile int end;
+/*
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static long query_pages_node(unsigned long count, void **pages, int *status)
+{
+ return syscall(__NR_move_pages, 0, count, pages, NULL, status, 0);
+}
+
+/*
+ * An unimplemented syscall returns ENOSYS before its arguments are dereferenced,
+ * so a dummy page pointer is enough to probe for support.
+ * return 1 if supported, 0 if not.
+ */
+static int move_pages_supported(void)
+{
+ void *page = NULL;
+ int status = -1;
+ long ret;
+
+ ret = query_pages_node(1, &page, &status);
+
+ return !(ret == -1 && errno == ENOSYS);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node and write the
+ * result (node id and size in KB) to RESULTFILE. Because we look exclusively
+ * at this file's pages -- rather than the global per-node FilePages counters
+ * in /sys -- unrelated page-cache activity on the system cannot perturb the
+ * measurement.
+ *
+ * return 0 on success, -1 on failure.
+ */
+static int count_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr = MAP_FAILED;
+ long page_size = sysconf(_SC_PAGESIZE);
+ unsigned long npages, i, off;
+ void **pages = NULL;
+ int *status = NULL;
+ unsigned long *counts = NULL;
+ FILE *fp;
+ int ret = -1;
+
+ fd = open(DATAFILE, O_RDONLY);
+ if (fd == -1) {
+ warn("open %s failed", DATAFILE);
+ return -1;
+ }
+ if (fstat(fd, &st) == -1) {
+ warn("fstat %s failed", DATAFILE);
+ close(fd);
+ return -1;
+ }
+ npages = (st.st_size + page_size - 1) / page_size;
+ if (npages == 0) {
+ close(fd);
+ return -1;
+ }
+
+ addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (addr == MAP_FAILED) {
+ warn("mmap %s failed", DATAFILE);
+ return -1;
+ }
+
+ pages = calloc(npages, sizeof(*pages));
+ status = calloc(npages, sizeof(*status));
+ counts = calloc(MAX_NODES, sizeof(*counts));
+ if (!pages || !status || !counts) {
+ warn("calloc failed");
+ goto out;
+ }
+
+ /*
+ * Fault in every page so it maps the page-cache page already
+ * populated by page_cache_hog(); query_pages_node() then reports
+ * the node that cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+ if (query_pages_node(n, pages + off, status + off) == -1) {
+ warn("move_pages failed");
+ goto out;
+ }
+ }
+
+ /*
+ * move_pages() can return success while reporting a per-page error
+ * (e.g. -ENOENT or -EFAULT) in status[]. Treat any such page as a
+ * failure: otherwise the shell would compute its percentages from
+ * only a subset of DATAFILE and could pass incorrectly.
+ */
+ for (i = 0; i < npages; i++) {
+ if (status[i] < 0 || status[i] >= MAX_NODES) {
+ warnx("page %lu not accounted (status %d)", i,
+ status[i]);
+ goto out;
+ }
+ counts[status[i]]++;
+ }
+
+ fp = fopen(RESULTFILE, "w");
+ if (!fp) {
+ warn("open %s failed", RESULTFILE);
+ goto out;
+ }
+ for (i = 0; i < MAX_NODES; i++) {
+ if (counts[i])
+ fprintf(fp, "%lu %lu\n", i,
+ counts[i] * (unsigned long)page_size / 1024);
+ }
+ fclose(fp);
+ ret = 0;
+out:
+ if (addr != MAP_FAILED)
+ munmap(addr, st.st_size);
+ free(pages);
+ free(status);
+ free(counts);
+ return ret;
+}
+
void sighandler1(UNUSED int signo)
{
}
@@ -54,7 +206,7 @@ int page_cache_hog(void)
char path[BUFFER_SIZE];
int ret = 0;
- sprintf(path, "%s", "DATAFILE");
+ sprintf(path, "%s", DATAFILE);
fd = open(path, O_RDONLY);
if (fd == -1) {
warn("open %s failed", path);
@@ -81,6 +233,8 @@ int mem_hog(void)
while (!end) {
ret = page_cache_hog();
+ if (ret == 0)
+ ret = count_file_pages();
fd = open("./myfifo", O_WRONLY);
if (fd == -1)
@@ -102,10 +256,18 @@ int mem_hog(void)
return ret;
}
-int main(void)
+int main(int argc, char *argv[])
{
struct sigaction sa1, sa2;
+ /*
+ * "cpuset_mem_hog check" only probes move_pages(2) support and exits:
+ * 0 if supported, 1 if not. The shell uses it to skip (TCONF) on
+ * kernels built without CONFIG_NUMA_MIGRATION.
+ */
+ if (argc > 1 && !strcmp(argv[1], "check"))
+ return move_pages_supported() ? 0 : 1;
+
sa1.sa_handler = sighandler1;
if (sigemptyset(&sa1.sa_mask) < 0)
err(1, "sigemptyset()");
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
index 4c49bb8fd..6ab40ba83 100755
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
@@ -30,31 +30,37 @@ export TST_COUNT=1
check
+# cpuset_mem_hog counts DATAFILE's page cache via move_pages(2)
+cpuset_mem_hog check
+if [ $? -ne 0 ]; then
+ tst_brkm TCONF "move_pages() is not supported, CONFIG_NUMA_MIGRATION disabled?"
+fi
+
exit_status=0
nr_cpus=$NR_CPUS
nr_mems=$N_NODES
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
+# The cache hog (cpuset_mem_hog) reads DATAFILE and reports, per NUMA node,
+# how much of DATAFILE's own page cache resides on that node (in KB). Because
+# only this file's pages are counted, unrelated page-cache activity on the
+# system does not affect the result. The pages must land on the expected
+# node(s); the other nodes may hold at most the following fraction of them.
+UNEXPECTED_TOLERANCE=5
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
+# When the page cache is spread across several expected nodes, the kernel
+# spreads it evenly. Every expected node must hold at least
+# (100 - BALANCE_TOLERANCE) percent of its even share.
+BALANCE_TOLERANCE=20
cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
nodedir="/sys/devices/system/node"
FIFO="./myfifo"
+# per-node page-cache count of DATAFILE written by cpuset_mem_hog
+HOG_RESULT="./cpuset_mem_hog_nodes"
+
# memsinfo is an array implementation of the form of a multi-line string
# _0: value0
# _1: value1
@@ -131,66 +137,67 @@ freemem_check()
done
}
-# get_memsinfo
-get_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
+# load_hog_result
+# Load the per-node page-cache count of DATAFILE that cpuset_mem_hog
+# wrote to $HOG_RESULT into the memsinfo array. Each line is "node kb".
+load_hog_result()
{
- local i=
+ local node= kb=
- for i in `seq 0 $((nr_mems-1))`
+ init_memsinfo_array
+ while read node kb
do
- account_meminfo $i
- done
+ set_memsinfo_val "$node" "$kb"
+ done < "$HOG_RESULT"
}
-# result_check <nodelist>
+# result_check <expect_nodes>
+# All of DATAFILE's page cache should reside on the expected node(s); the
+# other nodes should hold at most UNEXPECTED_TOLERANCE percent of it. When
+# several expected nodes are given the cache must be spread evenly among
+# them. Only DATAFILE's own pages are counted (see cpuset_mem_hog), so
+# unrelated page-cache activity no longer perturbs the result.
# return 0: success
# 1: fail
result_check()
{
local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
+ local i= total=0 expected_sum=0 unexpected_sum=0 n_expected=0
- for i in $nodelist
+ for i in `seq 0 $((nr_mems-1))`
do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
+ total=$((total + $(get_memsinfo_val $i)))
done
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
+ # nothing was cached: the hog did not populate the page cache
+ [ $total -gt 0 ] || return 1
- local othernodelist="$allnodelist"
for i in $nodelist
do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
+ expected_sum=$((expected_sum + $(get_memsinfo_val $i)))
+ n_expected=$((n_expected + 1))
done
- for i in $othernodelist
+ unexpected_sum=$((total - expected_sum))
+
+ # report the actual fraction landing on the unexpected node(s)
+ tst_resm TINFO "unexpected nodes hold $(awk -v u=$unexpected_sum -v t=$total \
+ 'BEGIN { printf "%.2f", u * 100 / t }')% of DATAFILE's page cache (tolerance ${UNEXPECTED_TOLERANCE}%)."
+
+ # the unexpected nodes must hold at most UNEXPECTED_TOLERANCE percent
+ if [ $((unexpected_sum * 100)) -gt $((total * UNEXPECTED_TOLERANCE)) ]; then
+ return 1
+ fi
+
+ # every expected node must hold at least (100 - BALANCE_TOLERANCE)% of
+ # its even share (expected_sum / n_expected). This enforces an even
+ # spread across several expected nodes and also rejects an expected
+ # node that received nothing. A single expected node always passes.
+ for i in $nodelist
do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
+ if [ $(($(get_memsinfo_val $i) * n_expected * 100)) -lt \
+ $((expected_sum * (100 - BALANCE_TOLERANCE))) ]; then
return 1
fi
done
@@ -238,7 +245,7 @@ general_memory_spread_test()
return 1
fi
- get_memsinfo
+ rm -f $HOG_RESULT
/bin/kill -s SIGUSR1 $test_pid
read exit_num < $FIFO
if [ $exit_num -eq 0 ]; then
@@ -246,10 +253,10 @@ general_memory_spread_test()
return 1
fi
- account_memsinfo
+ load_hog_result
result_check $expect_nodes
if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
+ tst_resm TFAIL "hog the memory on the unexpected node(DATAFILE_Pages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
return 1
fi
}
@@ -357,6 +364,6 @@ fi
test_spread_page1
test_spread_page2
-rm -f DATAFILE $FIFO
+rm -f DATAFILE $FIFO $HOG_RESULT
exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-09-05 15:50 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-04 12:50 [LTP] [PATCH v2] cpuset_memory_spread: count only the test file's page cache Changwei Zou via ltp
2026-09-04 14:52 ` [LTP] " linuxtestproject.agent
-- strict thread matches above, loose matches on Subject: below --
2026-09-05 1:51 [LTP] [PATCH v3] " Changwei Zou via ltp
2026-09-05 3:06 ` [LTP] " linuxtestproject.agent
2026-09-05 6:18 [LTP] [PATCH v4] " Changwei Zou via ltp
2026-09-05 8:07 ` [LTP] " linuxtestproject.agent
2026-09-05 10:19 [LTP] [PATCH v5] " Changwei Zou via ltp
2026-09-05 12:27 ` [LTP] " linuxtestproject.agent
2026-09-05 12:50 [LTP] [PATCH v6] " Changwei Zou via ltp
2026-09-05 15:50 ` [LTP] " linuxtestproject.agent
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.